Browse Source

Merge pull request #1195 from rafalp/sync-frontend-improvements-with-019

Move frontend improvements frm 0.19.5 release to master branch
Rafał Pitoń 6 years ago
parent
commit
5f835f5ffb
400 changed files with 17822 additions and 17451 deletions
  1. 30 40
      frontend/gulpfile.js
  2. 5447 4760
      frontend/package-lock.json
  3. 3 2
      frontend/package.json
  4. 28 30
      frontend/src/components/RegisterLegalFootnote.js
  5. 18 17
      frontend/src/components/StartSocialAuth.js
  6. 16 13
      frontend/src/components/accept-agreement.js
  7. 27 28
      frontend/src/components/add-participant.js
  8. 20 14
      frontend/src/components/auth-message.js
  9. 15 16
      frontend/src/components/avatar.js
  10. 38 36
      frontend/src/components/banned-page.js
  11. 9 12
      frontend/src/components/button.js
  12. 6 5
      frontend/src/components/categories/blankslate.js
  13. 6 12
      frontend/src/components/categories/categories-list.js
  14. 11 19
      frontend/src/components/categories/category/index.js
  15. 4 5
      frontend/src/components/categories/category/list-item/description.js
  16. 15 21
      frontend/src/components/categories/category/list-item/icon.js
  17. 14 18
      frontend/src/components/categories/category/list-item/index.js
  18. 38 52
      frontend/src/components/categories/category/list-item/last-thread.js
  19. 6 9
      frontend/src/components/categories/category/list-item/main.js
  20. 22 15
      frontend/src/components/categories/category/list-item/stats.js
  21. 8 14
      frontend/src/components/categories/category/list-item/subcategories/index.js
  22. 12 20
      frontend/src/components/categories/category/list-item/subcategories/list-item.js
  23. 23 28
      frontend/src/components/categories/index.js
  24. 7 8
      frontend/src/components/category-select.js
  25. 80 83
      frontend/src/components/change-avatar/crop.js
  26. 115 104
      frontend/src/components/change-avatar/gallery.js
  27. 82 99
      frontend/src/components/change-avatar/index.js
  28. 101 102
      frontend/src/components/change-avatar/root.js
  29. 99 98
      frontend/src/components/change-avatar/upload.js
  30. 15 15
      frontend/src/components/dropdown-toggle.js
  31. 5 6
      frontend/src/components/edit-details/blankslate.js
  32. 15 16
      frontend/src/components/edit-details/field-input.js
  33. 6 7
      frontend/src/components/edit-details/fieldset.js
  34. 34 36
      frontend/src/components/edit-details/form.js
  35. 21 29
      frontend/src/components/edit-details/index.js
  36. 5 6
      frontend/src/components/edit-details/loader.js
  37. 6 7
      frontend/src/components/editor/actions/action.js
  38. 11 16
      frontend/src/components/editor/actions/code.js
  39. 6 9
      frontend/src/components/editor/actions/emphasis.js
  40. 6 9
      frontend/src/components/editor/actions/hr.js
  41. 15 22
      frontend/src/components/editor/actions/image.js
  42. 16 23
      frontend/src/components/editor/actions/link.js
  43. 12 17
      frontend/src/components/editor/actions/quote.js
  44. 6 9
      frontend/src/components/editor/actions/striketrough.js
  45. 6 9
      frontend/src/components/editor/actions/strong.js
  46. 102 75
      frontend/src/components/editor/attachments/attachment/complete.js
  47. 27 22
      frontend/src/components/editor/attachments/attachment/error.js
  48. 10 17
      frontend/src/components/editor/attachments/attachment/index.js
  49. 22 15
      frontend/src/components/editor/attachments/attachment/upload.js
  50. 8 9
      frontend/src/components/editor/attachments/index.js
  51. 6 9
      frontend/src/components/editor/attachments/list.js
  52. 11 14
      frontend/src/components/editor/attachments/upload-button.js
  53. 38 38
      frontend/src/components/editor/attachments/uploader.js
  54. 63 65
      frontend/src/components/editor/index.js
  55. 4 5
      frontend/src/components/editor/markup-preview.js
  56. 35 30
      frontend/src/components/editor/textutils.js
  57. 39 39
      frontend/src/components/form-group.js
  58. 69 64
      frontend/src/components/form.js
  59. 12 12
      frontend/src/components/li.js
  60. 3 4
      frontend/src/components/loader.js
  61. 47 42
      frontend/src/components/merge-conflict.js
  62. 11 11
      frontend/src/components/misago-markup.js
  63. 7 8
      frontend/src/components/modal-loader.js
  64. 24 30
      frontend/src/components/modal-message.js
  65. 8 8
      frontend/src/components/navbar-search/clean-results.js
  66. 3 3
      frontend/src/components/navbar-search/dropdown/constants.js
  67. 5 9
      frontend/src/components/navbar-search/dropdown/dropdown-menu.js
  68. 3 4
      frontend/src/components/navbar-search/dropdown/empty.js
  69. 13 13
      frontend/src/components/navbar-search/dropdown/flatten-results.js
  70. 19 22
      frontend/src/components/navbar-search/dropdown/index.js
  71. 3 4
      frontend/src/components/navbar-search/dropdown/input.js
  72. 4 5
      frontend/src/components/navbar-search/dropdown/loader.js
  73. 13 10
      frontend/src/components/navbar-search/dropdown/result/footer.js
  74. 3 8
      frontend/src/components/navbar-search/dropdown/result/header.js
  75. 9 16
      frontend/src/components/navbar-search/dropdown/result/index.js
  76. 7 12
      frontend/src/components/navbar-search/dropdown/result/result.js
  77. 17 12
      frontend/src/components/navbar-search/dropdown/result/thread.js
  78. 16 13
      frontend/src/components/navbar-search/dropdown/result/user.js
  79. 72 75
      frontend/src/components/navbar-search/index.js
  80. 5 6
      frontend/src/components/options/change-username/form-loading.js
  81. 8 8
      frontend/src/components/options/change-username/form-locked.js
  82. 80 62
      frontend/src/components/options/change-username/form.js
  83. 34 48
      frontend/src/components/options/change-username/root.js
  84. 62 42
      frontend/src/components/options/delete-account.js
  85. 51 44
      frontend/src/components/options/download-data.js
  86. 10 14
      frontend/src/components/options/edit-details.js
  87. 65 64
      frontend/src/components/options/forum-options.js
  88. 20 22
      frontend/src/components/options/navs.js
  89. 34 46
      frontend/src/components/options/root.js
  90. 12 10
      frontend/src/components/options/sign-in-credentials/UnusablePasswordMessage.js
  91. 72 67
      frontend/src/components/options/sign-in-credentials/change-email.js
  92. 86 72
      frontend/src/components/options/sign-in-credentials/change-password.js
  93. 11 14
      frontend/src/components/options/sign-in-credentials/root.js
  94. 18 12
      frontend/src/components/page-lead.js
  95. 7 8
      frontend/src/components/panel-loader.js
  96. 6 14
      frontend/src/components/panel-message.js
  97. 7 12
      frontend/src/components/participants/add-participant.js
  98. 70 43
      frontend/src/components/participants/cards-list/actions.js
  99. 17 31
      frontend/src/components/participants/cards-list/card.js
  100. 6 7
      frontend/src/components/participants/cards-list/index.js
  101. 28 23
      frontend/src/components/participants/cards-list/make-owner.js
  102. 29 24
      frontend/src/components/participants/cards-list/remove.js
  103. 7 8
      frontend/src/components/participants/index.js
  104. 11 6
      frontend/src/components/participants/utils.js
  105. 44 46
      frontend/src/components/password-strength.js
  106. 37 36
      frontend/src/components/poll/form/choices-control.js
  107. 64 63
      frontend/src/components/poll/form/index.js
  108. 4 4
      frontend/src/components/poll/index.js
  109. 69 46
      frontend/src/components/poll/info.js
  110. 24 23
      frontend/src/components/poll/poll.js
  111. 26 30
      frontend/src/components/poll/results/chart.js
  112. 6 7
      frontend/src/components/poll/results/index.js
  113. 65 89
      frontend/src/components/poll/results/modal.js
  114. 71 75
      frontend/src/components/poll/results/options.js
  115. 25 23
      frontend/src/components/poll/voting/help.js
  116. 52 55
      frontend/src/components/poll/voting/index.js
  117. 13 14
      frontend/src/components/poll/voting/select.js
  118. 8 8
      frontend/src/components/poll/voting/utils.js
  119. 14 19
      frontend/src/components/post-changelog/diff.js
  120. 6 6
      frontend/src/components/post-changelog/footer.js
  121. 72 68
      frontend/src/components/post-changelog/index.js
  122. 52 45
      frontend/src/components/post-changelog/toolbar.js
  123. 3 3
      frontend/src/components/post-changelog/utils.js
  124. 8 17
      frontend/src/components/post-feed/index.js
  125. 16 13
      frontend/src/components/post-feed/post/body.js
  126. 13 16
      frontend/src/components/post-feed/post/header.js
  127. 11 18
      frontend/src/components/post-feed/post/index.js
  128. 7 13
      frontend/src/components/post-feed/post/post-side/anonymous.js
  129. 6 14
      frontend/src/components/post-feed/post/post-side/button.js
  130. 6 14
      frontend/src/components/post-feed/post/post-side/index.js
  131. 9 20
      frontend/src/components/post-feed/post/post-side/registered.js
  132. 7 12
      frontend/src/components/post-feed/post/post-side/user-title.js
  133. 12 16
      frontend/src/components/post-feed/preview.js
  134. 50 81
      frontend/src/components/post-likes.js
  135. 51 61
      frontend/src/components/posting/edit.js
  136. 15 24
      frontend/src/components/posting/index.js
  137. 69 69
      frontend/src/components/posting/reply.js
  138. 58 64
      frontend/src/components/posting/start-private.js
  139. 103 110
      frontend/src/components/posting/start.js
  140. 11 9
      frontend/src/components/posting/utils/attachments.js
  141. 5 8
      frontend/src/components/posting/utils/container.js
  142. 5 6
      frontend/src/components/posting/utils/loader.js
  143. 11 10
      frontend/src/components/posting/utils/message.js
  144. 47 56
      frontend/src/components/posting/utils/options.js
  145. 6 6
      frontend/src/components/posting/utils/usernames.js
  146. 79 55
      frontend/src/components/posting/utils/validators.js
  147. 101 77
      frontend/src/components/posts-list/event/controls.js
  148. 21 24
      frontend/src/components/posts-list/event/icon.js
  149. 12 13
      frontend/src/components/posts-list/event/index.js
  150. 82 48
      frontend/src/components/posts-list/event/info.js
  151. 154 99
      frontend/src/components/posts-list/event/message.js
  152. 5 10
      frontend/src/components/posts-list/event/unread-label.js
  153. 10 21
      frontend/src/components/posts-list/index.js
  154. 59 37
      frontend/src/components/posts-list/post/attachments/attachment.js
  155. 17 14
      frontend/src/components/posts-list/post/attachments/index.js
  156. 59 37
      frontend/src/components/posts-list/post/body.js
  157. 170 148
      frontend/src/components/posts-list/post/controls/actions.js
  158. 114 194
      frontend/src/components/posts-list/post/controls/dropdown.js
  159. 5 8
      frontend/src/components/posts-list/post/controls/index.js
  160. 34 30
      frontend/src/components/posts-list/post/controls/move.js
  161. 193 170
      frontend/src/components/posts-list/post/controls/split.js
  162. 39 24
      frontend/src/components/posts-list/post/flags.js
  163. 98 97
      frontend/src/components/posts-list/post/footer.js
  164. 67 51
      frontend/src/components/posts-list/post/header.js
  165. 20 18
      frontend/src/components/posts-list/post/index.js
  166. 16 27
      frontend/src/components/posts-list/post/post-side/anonymous.js
  167. 2 2
      frontend/src/components/posts-list/post/post-side/has-visible-title.js
  168. 6 11
      frontend/src/components/posts-list/post/post-side/index.js
  169. 16 35
      frontend/src/components/posts-list/post/post-side/registered.js
  170. 14 15
      frontend/src/components/posts-list/post/post-side/user-postcount.js
  171. 8 13
      frontend/src/components/posts-list/post/post-side/user-status.js
  172. 10 17
      frontend/src/components/posts-list/post/post-side/user-title.js
  173. 35 11
      frontend/src/components/posts-list/post/preview.js
  174. 17 14
      frontend/src/components/posts-list/post/select.js
  175. 40 31
      frontend/src/components/posts-list/waypoint.js
  176. 110 102
      frontend/src/components/profile/ban-details.js
  177. 8 11
      frontend/src/components/profile/details/empty-message.js
  178. 9 15
      frontend/src/components/profile/details/field-value.js
  179. 5 8
      frontend/src/components/profile/details/field.js
  180. 5 12
      frontend/src/components/profile/details/form.js
  181. 5 6
      frontend/src/components/profile/details/group.js
  182. 17 25
      frontend/src/components/profile/details/groups-list.js
  183. 7 13
      frontend/src/components/profile/details/header.js
  184. 29 30
      frontend/src/components/profile/details/index.js
  185. 65 38
      frontend/src/components/profile/feed/index.js
  186. 51 47
      frontend/src/components/profile/feed/route.js
  187. 53 48
      frontend/src/components/profile/follow-button.js
  188. 136 116
      frontend/src/components/profile/followers.js
  189. 44 25
      frontend/src/components/profile/follows.js
  190. 72 109
      frontend/src/components/profile/header.js
  191. 12 15
      frontend/src/components/profile/message-button.js
  192. 133 110
      frontend/src/components/profile/moderation/avatar-controls.js
  193. 95 90
      frontend/src/components/profile/moderation/change-username.js
  194. 139 121
      frontend/src/components/profile/moderation/delete-account.js
  195. 26 48
      frontend/src/components/profile/moderation/nav.js
  196. 16 24
      frontend/src/components/profile/navs.js
  197. 50 59
      frontend/src/components/profile/root.js
  198. 137 107
      frontend/src/components/profile/username-history.js
  199. 16 16
      frontend/src/components/quick-search.js
  200. 36 43
      frontend/src/components/register-button.js
  201. 232 211
      frontend/src/components/register.js
  202. 59 67
      frontend/src/components/request-activation-link.js
  203. 108 122
      frontend/src/components/request-password-reset.js
  204. 89 87
      frontend/src/components/reset-password-form.js
  205. 51 42
      frontend/src/components/search/form.js
  206. 14 14
      frontend/src/components/search/index.js
  207. 14 23
      frontend/src/components/search/page.js
  208. 12 19
      frontend/src/components/search/sidenav.js
  209. 42 25
      frontend/src/components/search/threads/footer.js
  210. 9 16
      frontend/src/components/search/threads/index.js
  211. 14 11
      frontend/src/components/search/threads/post.js
  212. 50 40
      frontend/src/components/search/threads/results.js
  213. 9 21
      frontend/src/components/search/users/index.js
  214. 47 48
      frontend/src/components/select.js
  215. 57 66
      frontend/src/components/sign-in.js
  216. 18 20
      frontend/src/components/snackbar.js
  217. 26 22
      frontend/src/components/social-auth/complete.js
  218. 6 9
      frontend/src/components/social-auth/header.js
  219. 14 15
      frontend/src/components/social-auth/index.js
  220. 86 82
      frontend/src/components/social-auth/register.js
  221. 8 17
      frontend/src/components/thread/header/breadcrumbs.js
  222. 56 62
      frontend/src/components/thread/header/index.js
  223. 98 87
      frontend/src/components/thread/header/stats.js
  224. 167 156
      frontend/src/components/thread/moderation/posts/actions.js
  225. 81 102
      frontend/src/components/thread/moderation/posts/dropdown.js
  226. 9 14
      frontend/src/components/thread/moderation/posts/errors-list.js
  227. 15 17
      frontend/src/components/thread/moderation/posts/index.js
  228. 37 33
      frontend/src/components/thread/moderation/posts/move.js
  229. 197 178
      frontend/src/components/thread/moderation/posts/split.js
  230. 184 218
      frontend/src/components/thread/moderation/thread/controls.js
  231. 3 3
      frontend/src/components/thread/moderation/thread/index.js
  232. 2 2
      frontend/src/components/thread/moderation/thread/is-visible.js
  233. 47 48
      frontend/src/components/thread/moderation/thread/merge.js
  234. 82 85
      frontend/src/components/thread/moderation/thread/move.js
  235. 24 24
      frontend/src/components/thread/paginator.js
  236. 4 7
      frontend/src/components/thread/reply-button.js
  237. 18 15
      frontend/src/components/thread/root.js
  238. 83 84
      frontend/src/components/thread/route.js
  239. 52 52
      frontend/src/components/thread/subscription.js
  240. 18 29
      frontend/src/components/thread/toolbar-bottom.js
  241. 49 70
      frontend/src/components/thread/toolbar-top.js
  242. 9 18
      frontend/src/components/threads-list/index.js
  243. 16 16
      frontend/src/components/threads-list/list/diff-message.js
  244. 7 11
      frontend/src/components/threads-list/list/empty.js
  245. 6 8
      frontend/src/components/threads-list/list/preview.js
  246. 8 14
      frontend/src/components/threads-list/list/ready.js
  247. 48 81
      frontend/src/components/threads-list/thread/details/bottom.js
  248. 6 10
      frontend/src/components/threads-list/thread/details/category.js
  249. 4 5
      frontend/src/components/threads-list/thread/details/index.js
  250. 33 54
      frontend/src/components/threads-list/thread/details/top.js
  251. 8 16
      frontend/src/components/threads-list/thread/last-action.js
  252. 28 49
      frontend/src/components/threads-list/thread/options.js
  253. 15 17
      frontend/src/components/threads-list/thread/preview.js
  254. 27 40
      frontend/src/components/threads-list/thread/ready.js
  255. 14 20
      frontend/src/components/threads-list/thread/subscription/compact.js
  256. 21 32
      frontend/src/components/threads-list/thread/subscription/full.js
  257. 20 18
      frontend/src/components/threads-list/thread/subscription/modal.js
  258. 74 75
      frontend/src/components/threads-list/thread/subscription/options.js
  259. 6 14
      frontend/src/components/threads-list/thread/user-url.js
  260. 11 17
      frontend/src/components/threads/category-picker.js
  261. 10 10
      frontend/src/components/threads/compare.js
  262. 14 25
      frontend/src/components/threads/container.js
  263. 41 57
      frontend/src/components/threads/header.js
  264. 14 24
      frontend/src/components/threads/list-empty.js
  265. 244 292
      frontend/src/components/threads/moderation/controls.js
  266. 6 14
      frontend/src/components/threads/moderation/errors-list.js
  267. 229 212
      frontend/src/components/threads/moderation/merge.js
  268. 120 106
      frontend/src/components/threads/moderation/move.js
  269. 15 15
      frontend/src/components/threads/moderation/selection.js
  270. 10 13
      frontend/src/components/threads/nav.js
  271. 40 40
      frontend/src/components/threads/root.js
  272. 138 138
      frontend/src/components/threads/route.js
  273. 18 30
      frontend/src/components/threads/toolbar.js
  274. 47 43
      frontend/src/components/threads/utils.js
  275. 15 23
      frontend/src/components/user-menu/guest-nav.js
  276. 8 20
      frontend/src/components/user-menu/root.js
  277. 35 61
      frontend/src/components/user-menu/user-nav.js
  278. 86 67
      frontend/src/components/user-status.js
  279. 47 29
      frontend/src/components/username-history/change-preview.js
  280. 42 49
      frontend/src/components/username-history/change.js
  281. 13 13
      frontend/src/components/username-history/list-empty.js
  282. 14 14
      frontend/src/components/username-history/list-preview.js
  283. 12 12
      frontend/src/components/username-history/list-ready.js
  284. 8 14
      frontend/src/components/username-history/root.js
  285. 14 33
      frontend/src/components/users-list/card/index.js
  286. 57 50
      frontend/src/components/users-list/card/stats.js
  287. 7 12
      frontend/src/components/users-list/card/user-title.js
  288. 11 24
      frontend/src/components/users-list/index.js
  289. 14 23
      frontend/src/components/users-list/preview/card.js
  290. 11 16
      frontend/src/components/users-list/preview/index.js
  291. 14 12
      frontend/src/components/users/active-posters/list-empty.js
  292. 18 22
      frontend/src/components/users/active-posters/list-item-preview.js
  293. 29 42
      frontend/src/components/users/active-posters/list-item.js
  294. 24 22
      frontend/src/components/users/active-posters/list-preview.js
  295. 34 27
      frontend/src/components/users/active-posters/list-ready.js
  296. 37 41
      frontend/src/components/users/active-posters/root.js
  297. 15 18
      frontend/src/components/users/nav.js
  298. 5 10
      frontend/src/components/users/rank/list-loading.js
  299. 5 6
      frontend/src/components/users/rank/list.js
  300. 26 26
      frontend/src/components/users/rank/pager.js
  301. 58 63
      frontend/src/components/users/rank/root.js
  302. 27 31
      frontend/src/components/users/root.js
  303. 10 12
      frontend/src/components/with-dropdown.js
  304. 14 22
      frontend/src/components/yes-no-switch.js
  305. 12 13
      frontend/src/data/profile-details.js
  306. 18 18
      frontend/src/index.js
  307. 5 5
      frontend/src/initializers/ajax.js
  308. 19 16
      frontend/src/initializers/auth-sync.js
  309. 9 9
      frontend/src/initializers/auth.js
  310. 8 8
      frontend/src/initializers/captcha.js
  311. 11 12
      frontend/src/initializers/components/accept-agreement.js
  312. 8 8
      frontend/src/initializers/components/auth-message.js
  313. 7 7
      frontend/src/initializers/components/banned-page.js
  314. 9 9
      frontend/src/initializers/components/categories.js
  315. 9 9
      frontend/src/initializers/components/options.js
  316. 10 10
      frontend/src/initializers/components/profile.js
  317. 8 8
      frontend/src/initializers/components/request-activation-link.js
  318. 8 8
      frontend/src/initializers/components/request-password-reset.js
  319. 8 8
      frontend/src/initializers/components/reset-password-form.js
  320. 9 9
      frontend/src/initializers/components/search.js
  321. 8 8
      frontend/src/initializers/components/snackbar.js
  322. 10 11
      frontend/src/initializers/components/social-auth.js
  323. 8 8
      frontend/src/initializers/components/thread.js
  324. 23 19
      frontend/src/initializers/components/threads.js
  325. 13 9
      frontend/src/initializers/components/user-menu.js
  326. 9 9
      frontend/src/initializers/components/users.js
  327. 5 5
      frontend/src/initializers/include.js
  328. 5 5
      frontend/src/initializers/local-storage.js
  329. 7 7
      frontend/src/initializers/mobile-navbar-dropdown.js
  330. 7 7
      frontend/src/initializers/modal.js
  331. 5 5
      frontend/src/initializers/moment-locale.js
  332. 7 7
      frontend/src/initializers/page-title.js
  333. 7 7
      frontend/src/initializers/polls.js
  334. 7 7
      frontend/src/initializers/posting.js
  335. 18 11
      frontend/src/initializers/reducers/auth.js
  336. 10 10
      frontend/src/initializers/reducers/participants.js
  337. 11 11
      frontend/src/initializers/reducers/poll.js
  338. 11 11
      frontend/src/initializers/reducers/posts.js
  339. 10 10
      frontend/src/initializers/reducers/profile-details.js
  340. 8 8
      frontend/src/initializers/reducers/profile-hydrate.js
  341. 7 7
      frontend/src/initializers/reducers/profile.js
  342. 14 10
      frontend/src/initializers/reducers/search.js
  343. 7 7
      frontend/src/initializers/reducers/selection.js
  344. 7 7
      frontend/src/initializers/reducers/snackbar.js
  345. 11 11
      frontend/src/initializers/reducers/thread.js
  346. 7 7
      frontend/src/initializers/reducers/threads.js
  347. 7 7
      frontend/src/initializers/reducers/tick.js
  348. 7 7
      frontend/src/initializers/reducers/username-history.js
  349. 7 7
      frontend/src/initializers/reducers/users.js
  350. 7 7
      frontend/src/initializers/snackbar.js
  351. 6 6
      frontend/src/initializers/store.js
  352. 9 9
      frontend/src/initializers/tick-start.js
  353. 6 6
      frontend/src/initializers/zxcvbn.js
  354. 25 25
      frontend/src/reducers/auth.js
  355. 6 6
      frontend/src/reducers/participants.js
  356. 28 26
      frontend/src/reducers/poll.js
  357. 14 12
      frontend/src/reducers/post.js
  358. 56 53
      frontend/src/reducers/posts.js
  359. 6 6
      frontend/src/reducers/profile-details.js
  360. 18 14
      frontend/src/reducers/profile.js
  361. 17 17
      frontend/src/reducers/search.js
  362. 13 13
      frontend/src/reducers/selection.js
  363. 12 12
      frontend/src/reducers/snackbar.js
  364. 28 26
      frontend/src/reducers/thread.js
  365. 53 50
      frontend/src/reducers/threads.js
  366. 7 7
      frontend/src/reducers/tick.js
  367. 26 26
      frontend/src/reducers/username-history.js
  368. 22 22
      frontend/src/reducers/users.js
  369. 109 97
      frontend/src/services/ajax.js
  370. 34 32
      frontend/src/services/auth.js
  371. 63 66
      frontend/src/services/captcha.js
  372. 9 9
      frontend/src/services/include.js
  373. 14 14
      frontend/src/services/local-storage.js
  374. 14 14
      frontend/src/services/mobile-navbar-dropdown.js
  375. 11 11
      frontend/src/services/modal.js
  376. 62 58
      frontend/src/services/one-box.js
  377. 19 15
      frontend/src/services/page-title.js
  378. 29 23
      frontend/src/services/polls.js
  379. 47 46
      frontend/src/services/posting.js
  380. 24 24
      frontend/src/services/snackbar.js
  381. 13 11
      frontend/src/services/store.js
  382. 21 21
      frontend/src/services/zxcvbn.js
  383. 80 77
      frontend/src/test-setup.js
  384. 22 23
      frontend/src/utils/banned-page.js
  385. 12 12
      frontend/src/utils/batch.js
  386. 6 6
      frontend/src/utils/concat-unique.js
  387. 5 5
      frontend/src/utils/countdown.js
  388. 10 8
      frontend/src/utils/escape-html.js
  389. 6 6
      frontend/src/utils/file-size.js
  390. 6 3
      frontend/src/utils/is-url.js
  391. 11 18
      frontend/src/utils/mount-component.js
  392. 93 93
      frontend/src/utils/ordered-list.js
  393. 6 6
      frontend/src/utils/random.js
  394. 2 2
      frontend/src/utils/reset-scroll.js
  395. 11 12
      frontend/src/utils/routed-component.js
  396. 13 13
      frontend/src/utils/sets.js
  397. 12 12
      frontend/src/utils/string-count.js
  398. 59 48
      frontend/src/utils/test-utils.js
  399. 63 44
      frontend/src/utils/validators.js
  400. 14 14
      frontend/src/vendor.js

+ 30 - 40
frontend/gulpfile.js

@@ -6,8 +6,8 @@ var gutil = require('gulp-util');
 var babelify = require('babelify');
 var browserify = require('browserify');
 var buffer = require('vinyl-buffer');
+var eslint = require('gulp-eslint');
 var image = require('gulp-image');
-var jshint = require('gulp-jshint');
 var less = require('gulp-less');
 var minify = require('gulp-minify-css');
 var rename = require('gulp-rename');
@@ -74,8 +74,35 @@ function getSources() {
 
 gulp.task('lintsource', function() {
   return gulp.src('src/**/*.js')
-    .pipe(jshint())
-    .pipe(jshint.reporter('default'));
+    .pipe(eslint({
+        'parser': 'babel-eslint',
+        'parserOptions': {
+            'ecmaVersion': 7,
+            'sourceType': 'module',
+            'ecmaFeatures': {
+                'jsx': true
+            }
+        },
+        rules: {
+          "semi": ["error", "never"],
+          "no-undef": "error",
+          "strict": 2
+        },
+        globals: [
+          "gettext",
+          "ngettext",
+          "interpolate",
+          "misago",
+          "hljs"
+        ],
+        envs: [
+            "browser",
+            "jquery",
+            "node",
+            "es6"
+        ]
+    }))
+    .pipe(eslint.format());
 });
 
 gulp.task('fastsource', ['lintsource'], function() {
@@ -274,40 +301,3 @@ gulp.task('copypolyfill', function() {
     .pipe(sourcemaps.write('.'))
     .pipe(gulp.dest(misago + 'js'));
 });
-
-// Test task
-
-var tests = (function() {
-  var flag = process.argv.indexOf('--limit');
-  var value = process.argv[flag + 1];
-
-  var tests = ['src/test-setup.js'];
-  if (flag !== -1 && value) {
-    var pattern = value.trim();
-    glob.sync('tests/**/*.js').map(function(path) {
-      if (path.indexOf(pattern) !== -1) {
-        tests.push(path);
-      }
-    });
-  } else {
-    tests.push('tests/**/*.js');
-  }
-
-  return tests;
-})();
-
-gulp.task('linttests', function() {
-  return gulp.src(tests)
-    .pipe(jshint())
-    .pipe(jshint.reporter('default'));
-});
-
-gulp.task('test', ['linttests', 'lintsource'], function() {
-  var mochify = require('mochify');
-
-  mochify(tests.join(" "), {
-      reporter: 'spec'
-    })
-    .transform(babelify)
-    .bundle();
-});

File diff suppressed because it is too large
+ 5447 - 4760
frontend/package-lock.json


+ 3 - 2
frontend/package.json

@@ -27,6 +27,7 @@
   "dependencies": {
     "at.js": "^1.5.3",
     "babel-core": "6.7.x",
+    "babel-eslint": "^10.0.1",
     "babel-plugin-module-alias": "^1.0.0",
     "babel-plugin-transform-class-properties": "^6.3.13",
     "babel-polyfill": "^6.3.14",
@@ -42,7 +43,8 @@
     "del": "^2.1.0",
     "dropzone": "^4.2.0",
     "glob": "^7.0.3",
-    "gulp": "^3.9.0",
+    "gulp": "^3.9.1",
+    "gulp-eslint": "^5.0.0",
     "gulp-image": "^2.7.2",
     "gulp-jshint": "^2.0.0",
     "gulp-less": "^3.0.5",
@@ -70,7 +72,6 @@
     "zxcvbn": "^4.2.0"
   },
   "devDependencies": {
-    "jshint": "^2.8.0",
     "mochify": "^2.14.3"
   }
 }

+ 28 - 30
frontend/src/components/RegisterLegalFootnote.js

@@ -1,26 +1,25 @@
-/* jshint ignore:start */
-import React from 'react';
-import misago from 'misago';
-import escapeHtml from 'misago/utils/escape-html';
+import React from "react"
+import misago from "misago"
+import escapeHtml from "misago/utils/escape-html"
 
 const AGREEMENT_URL = '<a href="%(url)s" target="_blank">%(agreement)s</a>'
 
-const RegisterLegalFootnote = (props) => {
+const RegisterLegalFootnote = props => {
   const {
     errors,
     privacyPolicy,
     termsOfService,
     onPrivacyPolicyChange,
     onTermsOfServiceChange
-  } = props;
-  
-  const termsOfServiceId = misago.get('TERMS_OF_SERVICE_ID');
-  const termsOfServiceUrl = misago.get('TERMS_OF_SERVICE_URL');
+  } = props
 
-  const privacyPolicyId = misago.get('PRIVACY_POLICY_ID');
-  const privacyPolicyUrl = misago.get('PRIVACY_POLICY_URL');
+  const termsOfServiceId = misago.get("TERMS_OF_SERVICE_ID")
+  const termsOfServiceUrl = misago.get("TERMS_OF_SERVICE_URL")
 
-  if (!termsOfServiceId && !privacyPolicyId) return null;
+  const privacyPolicyId = misago.get("PRIVACY_POLICY_ID")
+  const privacyPolicyUrl = misago.get("PRIVACY_POLICY_URL")
+
+  if (!termsOfServiceId && !privacyPolicyId) return null
 
   return (
     <div>
@@ -41,13 +40,13 @@ const RegisterLegalFootnote = (props) => {
         onChange={onPrivacyPolicyChange}
       />
     </div>
-  );
+  )
 }
 
-const LegalAgreement = (props) => {
-  const { agreement, checked, errors, url, value, onChange } = props;
+const LegalAgreement = props => {
+  const { agreement, checked, errors, url, value, onChange } = props
 
-  if (!url) return;
+  if (!url) return null
 
   const agreementHtml = interpolate(
     AGREEMENT_URL,
@@ -55,10 +54,10 @@ const LegalAgreement = (props) => {
     true
   )
   const label = interpolate(
-      gettext("I have read and accept %(agreement)s."),
-      { agreement: agreementHtml },
-      true
-  );
+    gettext("I have read and accept %(agreement)s."),
+    { agreement: agreementHtml },
+    true
+  )
 
   return (
     <div className="checkbox legal-footnote">
@@ -69,17 +68,16 @@ const LegalAgreement = (props) => {
           value={value}
           onChange={onChange}
         />
-        <span
-          dangerouslySetInnerHTML={{ __html: label}}
-        />
+        <span dangerouslySetInnerHTML={{ __html: label }} />
       </label>
-      {errors && errors.map((error, i) => (
-        <div className="help-block errors" key={i}>
-          {error}
-        </div>
-      ))}
+      {errors &&
+        errors.map((error, i) => (
+          <div className="help-block errors" key={i}>
+            {error}
+          </div>
+        ))}
     </div>
-  );
+  )
 }
 
-export default RegisterLegalFootnote;
+export default RegisterLegalFootnote

+ 18 - 17
frontend/src/components/StartSocialAuth.js

@@ -1,34 +1,37 @@
-/* jshint ignore:start */
-import React from 'react'
-import misago from 'misago'
+import React from "react"
+import misago from "misago"
 
-const StartSocialAuth = (props) => {
+const StartSocialAuth = props => {
   const {
     buttonClassName,
     buttonLabel,
     formLabel,
     header,
-    labelClassName,
-  } = props;
-  const socialAuth = misago.get('SETTINGS').SOCIAL_AUTH;
+    labelClassName
+  } = props
+  const socialAuth = misago.get("SETTINGS").SOCIAL_AUTH
 
-  if (socialAuth.length === 0) return null;
+  if (socialAuth.length === 0) return null
 
   return (
     <div className="form-group form-social-auth">
       <FormHeader className={labelClassName} text={header} />
       <div className="row">
         {socialAuth.map(({ id, name, url }) => {
-          const className = 'btn btn-block btn-default btn-social-' + id;
-          const finalButtonLabel = interpolate(buttonLabel, { site: name }, true);
+          const className = "btn btn-block btn-default btn-social-" + id
+          const finalButtonLabel = interpolate(
+            buttonLabel,
+            { site: name },
+            true
+          )
 
           return (
-            <div className={buttonClassName || 'col-xs-12'} key={id}>
+            <div className={buttonClassName || "col-xs-12"} key={id}>
               <a className={className} href={url}>
                 {finalButtonLabel}
               </a>
             </div>
-          );
+          )
         })}
       </div>
       <hr />
@@ -38,10 +41,8 @@ const StartSocialAuth = (props) => {
 }
 
 const FormHeader = ({ className, text }) => {
-  if (!text) return null;
-  return (
-    <h5 className={className || ""}>{text}</h5>
-  );
+  if (!text) return null
+  return <h5 className={className || ""}>{text}</h5>
 }
 
-export default StartSocialAuth
+export default StartSocialAuth

+ 16 - 13
frontend/src/components/accept-agreement.js

@@ -1,21 +1,24 @@
-/* jshint ignore:start */
-import React from 'react';
-import ajax from 'misago/services/ajax'
+import React from "react"
+import ajax from "misago/services/ajax"
 
 export default class AcceptAgreement extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
-    this.state = { submiting: false };
+    this.state = { submiting: false }
   }
 
   handleDecline = () => {
-    if (this.state.submiting) return;
+    if (this.state.submiting) return
 
-    const confirmation = confirm(gettext("Declining will result in immediate deactivation and deletion of your account. This action is not reversible."));
-    if (!confirmation) return;
+    const confirmation = confirm(
+      gettext(
+        "Declining will result in immediate deactivation and deletion of your account. This action is not reversible."
+      )
+    )
+    if (!confirmation) return
 
-    this.setState({ submiting: true });
+    this.setState({ submiting: true })
 
     ajax.post(this.props.api, { accept: false }).then(() => {
       location.reload(true)
@@ -23,9 +26,9 @@ export default class AcceptAgreement extends React.Component {
   }
 
   handleAccept = () => {
-    if (this.state.submiting) return;
+    if (this.state.submiting) return
 
-    this.setState({ submiting: true });
+    this.setState({ submiting: true })
 
     ajax.post(this.props.api, { accept: true }).then(() => {
       location.reload(true)
@@ -52,6 +55,6 @@ export default class AcceptAgreement extends React.Component {
           {gettext("Accept and continue")}
         </button>
       </div>
-    );
+    )
   }
-}
+}

+ 27 - 28
frontend/src/components/add-participant.js

@@ -1,52 +1,51 @@
-/* jshint ignore:start */
-import React from 'react';
-import Form from './form';
-import FormGroup from 'misago/components/form-group';
-import * as participants from 'misago/reducers/participants';
-import { updateAcl } from 'misago/reducers/thread';
-import ajax from 'misago/services/ajax';
-import modal from 'misago/services/modal';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
+import React from "react"
+import Form from "./form"
+import FormGroup from "misago/components/form-group"
+import * as participants from "misago/reducers/participants"
+import { updateAcl } from "misago/reducers/thread"
+import ajax from "misago/services/ajax"
+import modal from "misago/services/modal"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoading: false,
 
-      username: ''
-    };
+      username: ""
+    }
   }
 
-  onUsernameChange = (event) => {
-    this.changeValue('username', event.target.value);
-  };
+  onUsernameChange = event => {
+    this.changeValue("username", event.target.value)
+  }
 
   clean() {
     if (!this.state.username.trim().length) {
-      snackbar.error(gettext("You have to enter user name."));
-      return false;
+      snackbar.error(gettext("You have to enter user name."))
+      return false
     }
 
-    return true;
+    return true
   }
 
   send() {
     return ajax.patch(this.props.thread.api.index, [
-      {op: 'add', path: 'participants', value: this.state.username},
-      {op: 'add', path: 'acl', value: 1}
-    ]);
+      { op: "add", path: "participants", value: this.state.username },
+      { op: "add", path: "acl", value: 1 }
+    ])
   }
 
   handleSuccess(data) {
-    store.dispatch(updateAcl(data));
-    store.dispatch(participants.replace(data.participants));
+    store.dispatch(updateAcl(data))
+    store.dispatch(participants.replace(data.participants))
 
-    snackbar.success(gettext("New participant has been added to thread."));
+    snackbar.success(gettext("New participant has been added to thread."))
 
-    modal.hide();
+    modal.hide()
   }
 
   render() {
@@ -86,7 +85,7 @@ export default class extends Form {
           </div>
         </form>
       </div>
-    );
+    )
   }
 }
 
@@ -103,5 +102,5 @@ export function ModalHeader(props) {
       </button>
       <h4 className="modal-title">{gettext("Add participant")}</h4>
     </div>
-  );
+  )
 }

+ 20 - 14
frontend/src/components/auth-message.js

@@ -1,27 +1,34 @@
-import React from 'react';
+import React from "react"
 
 export default class extends React.Component {
   refresh() {
-    window.location.reload();
+    window.location.reload()
   }
 
   getMessage() {
     if (this.props.signedIn) {
       return interpolate(
-        gettext("You have signed in as %(username)s. Please refresh the page before continuing."),
-        {username: this.props.signedIn.username}, true);
+        gettext(
+          "You have signed in as %(username)s. Please refresh the page before continuing."
+        ),
+        { username: this.props.signedIn.username },
+        true
+      )
     } else if (this.props.signedOut) {
       return interpolate(
-        gettext("%(username)s, you have been signed out. Please refresh the page before continuing."),
-        {username: this.props.user.username}, true);
+        gettext(
+          "%(username)s, you have been signed out. Please refresh the page before continuing."
+        ),
+        { username: this.props.user.username },
+        true
+      )
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    let className = 'auth-message';
+    let className = "auth-message"
     if (this.props.signedIn || this.props.signedOut) {
-      className += ' show';
+      className += " show"
     }
 
     return (
@@ -37,13 +44,12 @@ export default class extends React.Component {
               {gettext("Reload page")}
             </button>
             <span className="hidden-xs hidden-sm">
-              {' ' + gettext("or press F5 key.")}
+              {" " + gettext("or press F5 key.")}
             </span>
           </p>
         </div>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 }
 
@@ -52,5 +58,5 @@ export function select(state) {
     user: state.auth.user,
     signedIn: state.auth.signedIn,
     signedOut: state.auth.signedOut
-  };
-}
+  }
+}

+ 15 - 16
frontend/src/components/avatar.js

@@ -1,39 +1,38 @@
-/* jshint ignore:start */
-import React from 'react';
-import misago from 'misago';
+import React from "react"
+import misago from "misago"
 
 export default function(props) {
-  const size = props.size || 100;
-  const size2x = props.size2x || size;
+  const size = props.size || 100
+  const size2x = props.size2x || size
 
   return (
     <img
-      alt=''
-      className={props.className || 'user-avatar'}
+      alt=""
+      className={props.className || "user-avatar"}
       src={getSrc(props.user, size)}
       srcSet={getSrc(props.user, size2x)}
       width={size}
       height={size}
     />
-  );
+  )
 }
 
 export function getSrc(user, size) {
   if (user && user.id) {
     // just avatar hash, size and user id
-    return resolveAvatarForSize(user.avatars, size).url;
+    return resolveAvatarForSize(user.avatars, size).url
   } else {
     // just append avatar size to file to produce no-avatar placeholder
-    return misago.get('BLANK_AVATAR_URL');
+    return misago.get("BLANK_AVATAR_URL")
   }
 }
 
 export function resolveAvatarForSize(avatars, size) {
-  let avatar = avatars[0];
-  avatars.forEach((av) => {
+  let avatar = avatars[0]
+  avatars.forEach(av => {
     if (av.size >= size) {
-      avatar = av;
+      avatar = av
     }
-  });
-  return avatar;
-}
+  })
+  return avatar
+}

+ 38 - 36
frontend/src/components/banned-page.js

@@ -1,63 +1,65 @@
-import moment from 'moment';
-import React from 'react';
+import moment from "moment"
+import React from "react"
 
 export default class extends React.Component {
   getReasonMessage() {
-    /* jshint ignore:start */
     if (this.props.message.html) {
-      return <div className="lead" dangerouslySetInnerHTML={{
-          __html: this.props.message.html
-        }} />;
+      return (
+        <div
+          className="lead"
+          dangerouslySetInnerHTML={{
+            __html: this.props.message.html
+          }}
+        />
+      )
     } else {
-      return <p className="lead">{this.props.message.plain}</p>;
+      return <p className="lead">{this.props.message.plain}</p>
     }
-    /* jshint ignore:end */
   }
 
   getExpirationMessage() {
     if (this.props.expires) {
       if (this.props.expires.isAfter(moment())) {
-        /* jshint ignore:start */
         let title = interpolate(
-          gettext("This ban expires on %(expires_on)s."), {
-            'expires_on': this.props.expires.format('LL, LT')
-          }, true);
+          gettext("This ban expires on %(expires_on)s."),
+          {
+            expires_on: this.props.expires.format("LL, LT")
+          },
+          true
+        )
 
         let message = interpolate(
-          gettext("This ban expires %(expires_on)s."), {
-            'expires_on': this.props.expires.fromNow()
-          }, true);
+          gettext("This ban expires %(expires_on)s."),
+          {
+            expires_on: this.props.expires.fromNow()
+          },
+          true
+        )
 
-        return <abbr title={title}>
-          {message}
-        </abbr>;
-        /* jshint ignore:end */
+        return <abbr title={title}>{message}</abbr>
       } else {
-        return gettext("This ban has expired.");
+        return gettext("This ban has expired.")
       }
     } else {
-      return gettext("This ban is permanent.");
+      return gettext("This ban is permanent.")
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className="page page-error page-error-banned">
-      <div className="container">
-        <div className="message-panel">
-
-          <div className="message-icon">
-            <span className="material-icon">highlight_off</span>
-          </div>
-          <div className="message-body">
-            {this.getReasonMessage()}
-            <p className="message-footnote">
-              {this.getExpirationMessage()}
-            </p>
+    return (
+      <div className="page page-error page-error-banned">
+        <div className="container">
+          <div className="message-panel">
+            <div className="message-icon">
+              <span className="material-icon">highlight_off</span>
+            </div>
+            <div className="message-body">
+              {this.getReasonMessage()}
+              <p className="message-footnote">{this.getExpirationMessage()}</p>
+            </div>
           </div>
         </div>
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
 }

+ 9 - 12
frontend/src/components/button.js

@@ -1,33 +1,30 @@
-import React from 'react';
-import Loader from './loader'; // jshint ignore:line
+import React from "react"
+import Loader from "./loader"
 
 export default class Button extends React.Component {
   render() {
-    let className = 'btn ' + this.props.className;
-    let disabled = this.props.disabled;
+    let className = "btn " + this.props.className
+    let disabled = this.props.disabled
 
     if (this.props.loading) {
-      className += ' btn-loading';
-      disabled = true;
+      className += " btn-loading"
+      disabled = true
     }
 
-    /* jshint ignore:start */
     return (
       <button
         className={className}
         disabled={disabled}
         onClick={this.props.onClick}
-        type={this.props.onClick ? 'button' : 'submit'}
+        type={this.props.onClick ? "button" : "submit"}
       >
         {this.props.children}
         {this.props.loading ? <Loader /> : null}
       </button>
-    );
-    /* jshint ignore:end */
+    )
   }
 }
 
-
 Button.defaultProps = {
   className: "btn-default",
 
@@ -37,4 +34,4 @@ Button.defaultProps = {
   disabled: false,
 
   onClick: null
-};
+}

+ 6 - 5
frontend/src/components/categories/blankslate.js

@@ -1,5 +1,4 @@
-// jshint ignore:start
-import React from 'react';
+import React from "react"
 
 export default function(props) {
   return (
@@ -7,10 +6,12 @@ export default function(props) {
       <ul className="list-group">
         <li className="list-group-item empty-message">
           <p className="lead">
-            {gettext("No categories exist or you don't have permission to see them.")}
+            {gettext(
+              "No categories exist or you don't have permission to see them."
+            )}
           </p>
         </li>
       </ul>
     </div>
-  );
-}
+  )
+}

+ 6 - 12
frontend/src/components/categories/categories-list.js

@@ -1,18 +1,12 @@
-// jshint ignore:start
-import React from 'react';
-import Category from './category';
+import React from "react"
+import Category from "./category"
 
 export default function({ categories }) {
   return (
     <div className="categories-list">
-      {categories.map((category) => {
-        return (
-          <Category
-            category={category}
-            key={category.id}
-          />
-        );
+      {categories.map(category => {
+        return <Category category={category} key={category.id} />
       })}
     </div>
-  );
-}
+  )
+}

+ 11 - 19
frontend/src/components/categories/category/index.js

@@ -1,29 +1,21 @@
-// jshint ignore:start
-import React from 'react';
-import ListItem from './list-item';
+import React from "react"
+import ListItem from "./list-item"
 
 export default function({ category }) {
-  let className = 'list-group list-group-category';
+  let className = "list-group list-group-category"
   if (category.css_class) {
-    className += ' list-group-category-has-flavor';
-    className += ' list-group-category-' + category.css_class;
+    className += " list-group-category-has-flavor"
+    className += " list-group-category-" + category.css_class
   }
 
   return (
     <ul className={className}>
-      <ListItem
-        category={category}
-        isFirst={true}
-      />
-      {category.subcategories.map((category) => {
+      <ListItem category={category} isFirst={true} />
+      {category.subcategories.map(category => {
         return (
-          <ListItem
-            category={category}
-            isFirst={false}
-            key={category.id}
-          />
-        );
+          <ListItem category={category} isFirst={false} key={category.id} />
+        )
       })}
     </ul>
-  );
-}
+  )
+}

+ 4 - 5
frontend/src/components/categories/category/list-item/description.js

@@ -1,8 +1,7 @@
-// jshint ignore:start
-import React from 'react';
+import React from "react"
 
 export default function({ category }) {
-  if (!category.description) return null;
+  if (!category.description) return null
 
   return (
     <div
@@ -11,5 +10,5 @@ export default function({ category }) {
         __html: category.description.html
       }}
     />
-  );
-}
+  )
+}

+ 15 - 21
frontend/src/components/categories/category/list-item/icon.js

@@ -1,55 +1,49 @@
-// jshint ignore:start
-import React from 'react';
+import React from "react"
 
 export default function({ category }) {
   return (
-    <div
-      className={getClassName(category)}
-      title={getTitle(category)}
-    >
-      <span className="material-icon">
-        {getIcon(category)}
-      </span>
+    <div className={getClassName(category)} title={getTitle(category)}>
+      <span className="material-icon">{getIcon(category)}</span>
     </div>
-  );
+  )
 }
 
 export function getClassName(category) {
   if (category.is_read) {
-    return 'read-status item-read';
+    return "read-status item-read"
   }
 
-  return 'read-status item-new';
+  return "read-status item-new"
 }
 
 export function getTitle(category) {
   if (category.is_closed) {
     if (category.is_read) {
-      return gettext("This category has no new posts. (closed)");
+      return gettext("This category has no new posts. (closed)")
     }
 
-    return gettext("This category has new posts. (closed)");
+    return gettext("This category has new posts. (closed)")
   }
 
   if (category.is_read) {
-    return gettext("This category has no new posts.");
+    return gettext("This category has no new posts.")
   }
 
-  return gettext("This category has new posts.");
+  return gettext("This category has new posts.")
 }
 
 export function getIcon(category) {
   if (category.is_closed) {
     if (category.is_read) {
-      return 'lock_outline';
+      return "lock_outline"
     }
 
-    return 'lock';
+    return "lock"
   }
 
   if (category.is_read) {
-    return 'chat_bubble_outline';
+    return "chat_bubble_outline"
   }
 
-  return 'chat_bubble';
-}
+  return "chat_bubble"
+}

+ 14 - 18
frontend/src/components/categories/category/list-item/index.js

@@ -1,25 +1,24 @@
-// jshint ignore:start
-import React from 'react';
-import Main from './main';
-import LastThread from './last-thread';
-import Stats from './stats';
-import Subcategories from './subcategories';
+import React from "react"
+import Main from "./main"
+import LastThread from "./last-thread"
+import Stats from "./stats"
+import Subcategories from "./subcategories"
 
 export default function({ category, isFirst }) {
-  let className = 'list-group-item';
+  let className = "list-group-item"
 
   if (category.description) {
-    className += ' list-group-category-has-description';
+    className += " list-group-category-has-description"
   } else {
-    className += ' list-group-category-no-description';
+    className += " list-group-category-no-description"
   }
 
   if (isFirst) {
-    className += ' list-group-item-first';
+    className += " list-group-item-first"
   }
   if (category.css_class) {
-    className += ' list-group-category-has-flavor';
-    className += ' list-group-item-category-' + category.css_class;
+    className += " list-group-category-has-flavor"
+    className += " list-group-item-category-" + category.css_class
   }
 
   return (
@@ -29,10 +28,7 @@ export default function({ category, isFirst }) {
         <Stats category={category} />
         <LastThread category={category} />
       </div>
-      <Subcategories
-        category={category}
-        isFirst={isFirst}
-      />
+      <Subcategories category={category} isFirst={isFirst} />
     </li>
-  );
-}
+  )
+}

+ 38 - 52
frontend/src/components/categories/category/list-item/last-thread.js

@@ -1,6 +1,5 @@
-// jshint ignore:start
-import React from 'react';
-import Avatar from 'misago/components/avatar';
+import React from "react"
+import Avatar from "misago/components/avatar"
 
 export default function({ category }) {
   return (
@@ -10,13 +9,13 @@ export default function({ category }) {
       <Private category={category} />
       <Protected category={category} />
     </div>
-  );
+  )
 }
 
 export function LastThread({ category }) {
-  if (!category.acl.can_browse) return null;
-  if (!category.acl.can_see_all_threads) return null;
-  if (!category.last_thread_title) return null;
+  if (!category.acl.can_browse) return null
+  if (!category.acl.can_see_all_threads) return null
+  if (!category.last_thread_title) return null
 
   return (
     <div className="media">
@@ -37,9 +36,7 @@ export function LastThread({ category }) {
           <li className="category-last-thread-poster">
             <LastPosterName category={category} />
           </li>
-          <li className="divider">
-            &#8212;
-          </li>
+          <li className="divider">&#8212;</li>
           <li className="category-last-thread-date">
             <a href={category.url.last_post}>
               {category.last_post_on.fromNow()}
@@ -48,7 +45,7 @@ export function LastThread({ category }) {
         </ul>
       </div>
     </div>
-  );
+  )
 }
 
 export function LastPosterAvatar({ category }) {
@@ -65,87 +62,76 @@ export function LastPosterAvatar({ category }) {
           user={category.last_poster}
         />
       </a>
-    );
+    )
   }
 
   return (
-      <span
-        className="last-poster-avatar"
-        title={category.last_poster_name}
-      >
-        <Avatar
-          className="media-object"
-          size={40}
-        />
-      </span>
-  );
+    <span className="last-poster-avatar" title={category.last_poster_name}>
+      <Avatar className="media-object" size={40} />
+    </span>
+  )
 }
 
 export function LastPosterName({ category }) {
   if (category.last_poster) {
     return (
-      <a
-        className="item-title"
-        href={category.last_poster.url}
-      >
+      <a className="item-title" href={category.last_poster.url}>
         {category.last_poster_name}
       </a>
-    );
+    )
   }
 
-  return (
-    <span className="item-title">
-      {category.last_poster_name}
-    </span>
-  );
+  return <span className="item-title">{category.last_poster_name}</span>
 }
 
 export function Empty({ category }) {
-  if (!category.acl.can_browse) return null;
-  if (!category.acl.can_see_all_threads) return null;
-  if (category.last_thread_title) return null;
+  if (!category.acl.can_browse) return null
+  if (!category.acl.can_see_all_threads) return null
+  if (category.last_thread_title) return null
 
   return (
     <Message
-      message={gettext("This category is empty. No threads were posted within it so far.")}
+      message={gettext(
+        "This category is empty. No threads were posted within it so far."
+      )}
     />
-  );
+  )
 }
 
 export function Private({ category }) {
-  if (!category.acl.can_browse) return null;
-  if (category.acl.can_see_all_threads) return null;
+  if (!category.acl.can_browse) return null
+  if (category.acl.can_see_all_threads) return null
 
   return (
     <Message
-      message={gettext("This category is private. You can see only your own threads within it.")}
+      message={gettext(
+        "This category is private. You can see only your own threads within it."
+      )}
     />
-  );
+  )
 }
 
 export function Protected({ category }) {
-  if (category.acl.can_browse) return null;
+  if (category.acl.can_browse) return null
 
   return (
     <Message
-      message={gettext("This category is protected. You can't browse it's contents.")}
+      message={gettext(
+        "This category is protected. You can't browse it's contents."
+      )}
     />
-  );
+  )
 }
 
 export function Message({ message }) {
   return (
     <div className="media category-thread-message">
       <div className="media-left">
-        <span className="material-icon">
-          info_outline
-        </span>
+        <span className="material-icon">info_outline</span>
       </div>
       <div className="media-body">
-        <p>
-          {message}
-        </p>
+        <p>{message}</p>
       </div>
     </div>
-  );
-}
+  )
+}

+ 6 - 9
frontend/src/components/categories/category/list-item/main.js

@@ -1,7 +1,6 @@
-// jshint ignore:start
-import React from 'react';
-import Description from './description';
-import Icon from './icon';
+import React from "react"
+import Description from "./description"
+import Icon from "./icon"
 
 export default function({ category }) {
   return (
@@ -12,13 +11,11 @@ export default function({ category }) {
         </div>
         <div className="media-body">
           <h4 className="media-heading">
-            <a href={category.url.index}>
-              {category.name}
-            </a>
+            <a href={category.url.index}>{category.name}</a>
           </h4>
           <Description category={category} />
         </div>
       </div>
     </div>
-  );
-}
+  )
+}

+ 22 - 15
frontend/src/components/categories/category/list-item/stats.js

@@ -1,6 +1,5 @@
-// jshint ignore:start
-import React from 'react';
-import Avatar from 'misago/components/avatar';
+import React from "react"
+import Avatar from "misago/components/avatar"
 
 export default function({ category }) {
   return (
@@ -10,29 +9,37 @@ export default function({ category }) {
         <Posts posts={category.posts} />
       </ul>
     </div>
-  );
+  )
 }
 
 export function Threads({ threads }) {
-  const message = ngettext("%(threads)s thread", "%(threads)s threads", threads);
+  const message = ngettext("%(threads)s thread", "%(threads)s threads", threads)
 
   return (
     <li className="category-stat-threads">
-      {interpolate(message, {
-        'threads': threads
-      }, true)}
+      {interpolate(
+        message,
+        {
+          threads: threads
+        },
+        true
+      )}
     </li>
-  );
+  )
 }
 
 export function Posts({ posts }) {
-  const message = ngettext("%(posts)s post", "%(posts)s posts", posts);
+  const message = ngettext("%(posts)s post", "%(posts)s posts", posts)
 
   return (
     <li className="category-stat-posts">
-      {interpolate(message, {
-        'posts': posts
-      }, true)}
+      {interpolate(
+        message,
+        {
+          posts: posts
+        },
+        true
+      )}
     </li>
-  );
-}
+  )
+}

+ 8 - 14
frontend/src/components/categories/category/list-item/subcategories/index.js

@@ -1,21 +1,15 @@
-// jshint ignore:start
-import React from 'react';
-import ListItem from './list-item';
+import React from "react"
+import ListItem from "./list-item"
 
 export default function({ category, isFirst }) {
-  if (isFirst) return null;
-  if (category.subcategories.length === 0) return null;
+  if (isFirst) return null
+  if (category.subcategories.length === 0) return null
 
   return (
     <div className="row subcategories-list">
-      {category.subcategories.map((category) => {
-        return (
-          <ListItem
-            category={category}
-            key={category.id}
-          />
-        );
+      {category.subcategories.map(category => {
+        return <ListItem category={category} key={category.id} />
       })}
     </div>
-  );
-}
+  )
+}

+ 12 - 20
frontend/src/components/categories/category/list-item/subcategories/list-item.js

@@ -1,41 +1,33 @@
-// jshint ignore:start
-import React from 'react';
+import React from "react"
 
 export default function({ category }) {
-  let className = 'btn btn-default btn-block btn-sm btn-subcategory';
+  let className = "btn btn-default btn-block btn-sm btn-subcategory"
   if (!category.is_read) {
-    className += ' btn-subcategory-new';
+    className += " btn-subcategory-new"
   }
 
   return (
     <div className="col-xs-12 col-sm-4 col-md-3">
-      <a
-        className={className}
-        href={category.url.index}
-      >
-        <span className="material-icon">
-          {getIcon(category)}
-        </span>
-        <span className="icon-text">
-          {category.name}
-        </span>
+      <a className={className} href={category.url.index}>
+        <span className="material-icon">{getIcon(category)}</span>
+        <span className="icon-text">{category.name}</span>
       </a>
     </div>
-  );
+  )
 }
 
 export function getIcon(category) {
   if (category.is_closed) {
     if (category.is_read) {
-      return 'lock_outline';
+      return "lock_outline"
     }
 
-    return 'lock';
+    return "lock"
   }
 
   if (category.is_read) {
-    return 'chat_bubble_outline';
+    return "chat_bubble_outline"
   }
 
-  return 'chat_bubble';
-}
+  return "chat_bubble"
+}

+ 23 - 28
frontend/src/components/categories/index.js

@@ -1,61 +1,56 @@
-// jshint ignore:start
-import moment from 'moment';
-import React from 'react';
-import Blankslate from './blankslate';
-import CategoriesList from './categories-list';
-import misago from 'misago/index';
-import polls from 'misago/services/polls';
+import moment from "moment"
+import React from "react"
+import Blankslate from "./blankslate"
+import CategoriesList from "./categories-list"
+import misago from "misago/index"
+import polls from "misago/services/polls"
 
 const hydrate = function(category) {
   return Object.assign({}, category, {
     last_post_on: category.last_post_on ? moment(category.last_post_on) : null,
     subcategories: category.subcategories.map(hydrate)
-  });
-};
+  })
+}
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
-      categories: misago.get('CATEGORIES').map(hydrate)
-    };
+      categories: misago.get("CATEGORIES").map(hydrate)
+    }
 
-    this.startPolling(misago.get('CATEGORIES_API'));
+    this.startPolling(misago.get("CATEGORIES_API"))
   }
 
   startPolling(api) {
     polls.start({
-      poll: 'categories',
+      poll: "categories",
       url: api,
       frequency: 180 * 1000,
       update: this.update
-    });
+    })
   }
 
-  update = (data) => {
+  update = data => {
     this.setState({
       categories: data.map(hydrate)
-    });
-  };
+    })
+  }
 
   render() {
-    const { categories } = this.state;
+    const { categories } = this.state
 
     if (categories.length === 0) {
-      return (
-        <Blankslate />
-      );
+      return <Blankslate />
     }
 
-    return (
-      <CategoriesList categories={categories} />
-    );
+    return <CategoriesList categories={categories} />
   }
 }
 
 export function select(store) {
   return {
-    'tick': store.tick.tick,
-  };
-}
+    tick: store.tick.tick
+  }
+}

+ 7 - 8
frontend/src/components/category-select.js

@@ -1,26 +1,25 @@
-// jshint ignore:start
-import React from 'react';
+import React from "react"
 
 export default function(props) {
   return (
     <select
-      className={props.className || 'form-control'}
+      className={props.className || "form-control"}
       disabled={props.disabled || false}
       id={props.id || null}
       onChange={props.onChange}
       value={props.value}
     >
-      {props.choices.map((item) => {
+      {props.choices.map(item => {
         return (
           <option
             disabled={item.disabled || false}
             key={item.value}
             value={item.value}
           >
-            {'- - '.repeat(item.level) + item.label}
+            {"- - ".repeat(item.level) + item.label}
           </option>
-        );
+        )
       })}
     </select>
-  );
-}
+  )
+}

+ 80 - 83
frontend/src/components/change-avatar/crop.js

@@ -1,47 +1,47 @@
-import React from 'react';
-import Avatar from 'misago/components/avatar'; // jshint ignore:line
-import Button from 'misago/components/button'; // jshint ignore:line
-import ajax from 'misago/services/ajax'; // jshint ignore:line
-import snackbar from 'misago/services/snackbar'; // jshint ignore:line
+import React from "react"
+import Avatar from "misago/components/avatar"
+import Button from "misago/components/button"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoading: false,
-      deviceRatio: 1,
-    };
+      deviceRatio: 1
+    }
   }
 
   getAvatarSize() {
     if (this.props.upload) {
-      return this.props.options.crop_tmp.size;
+      return this.props.options.crop_tmp.size
     } else {
-      return this.props.options.crop_src.size;
+      return this.props.options.crop_src.size
     }
   }
 
   getImagePath() {
     if (this.props.upload) {
-      return this.props.dataUrl;
+      return this.props.dataUrl
     } else {
-      return this.props.options.crop_src.url;
+      return this.props.options.crop_src.url
     }
   }
 
   componentDidMount() {
-    let cropit = $('.crop-form');
-    let cropperWidth = this.getAvatarSize();
+    let cropit = $(".crop-form")
+    let cropperWidth = this.getAvatarSize()
 
-    const initialWidth = cropit.width();
+    const initialWidth = cropit.width()
     while (initialWidth < cropperWidth) {
-      cropperWidth = cropperWidth / 2;
+      cropperWidth = cropperWidth / 2
     }
 
-    const deviceRatio = this.getAvatarSize() / cropperWidth;
+    const deviceRatio = this.getAvatarSize() / cropperWidth
 
-    cropit.width(cropperWidth);
+    cropit.width(cropperWidth)
 
     cropit.cropit({
       width: cropperWidth,
@@ -53,116 +53,115 @@ export default class extends React.Component {
       onImageLoaded: () => {
         if (this.props.upload) {
           // center uploaded image
-          let zoomLevel = cropit.cropit('zoom');
-          let imageSize = cropit.cropit('imageSize');
+          let zoomLevel = cropit.cropit("zoom")
+          let imageSize = cropit.cropit("imageSize")
 
           // is it wider than taller?
           if (imageSize.width > imageSize.height) {
-            let displayedWidth = (imageSize.width * zoomLevel);
-            let offsetX = (displayedWidth - this.getAvatarSize()) / -2;
+            let displayedWidth = imageSize.width * zoomLevel
+            let offsetX = (displayedWidth - this.getAvatarSize()) / -2
 
-            cropit.cropit('offset', {
+            cropit.cropit("offset", {
               x: offsetX,
               y: 0
-            });
+            })
           } else if (imageSize.width < imageSize.height) {
-            let displayedHeight = (imageSize.height * zoomLevel);
-            let offsetY = (displayedHeight - this.getAvatarSize()) / -2;
+            let displayedHeight = imageSize.height * zoomLevel
+            let offsetY = (displayedHeight - this.getAvatarSize()) / -2
 
-            cropit.cropit('offset', {
+            cropit.cropit("offset", {
               x: 0,
               y: offsetY
-            });
+            })
           } else {
-            cropit.cropit('offset', {
+            cropit.cropit("offset", {
               x: 0,
               y: 0
-            });
+            })
           }
         } else {
           // use preserved crop
-          let crop = this.props.options.crop_src.crop;
+          let crop = this.props.options.crop_src.crop
 
           if (crop) {
-            cropit.cropit('zoom', crop.zoom);
-            cropit.cropit('offset', {
+            cropit.cropit("zoom", crop.zoom)
+            cropit.cropit("offset", {
               x: crop.x,
               y: crop.y
-            });
+            })
           }
         }
       }
-    });
+    })
   }
 
   componentWillUnmount() {
-    $('.crop-form').cropit('disable');
+    $(".crop-form").cropit("disable")
   }
 
-  /* jshint ignore:start */
   cropAvatar = () => {
     if (this.state.isLoading) {
-      return false;
+      return false
     }
 
     this.setState({
-      'isLoading': true
-    });
-
-    let avatarType = this.props.upload ? 'crop_tmp' : 'crop_src';
-    let cropit = $('.crop-form');
-
-    const deviceRatio = cropit.cropit('exportZoom');
-    const cropitOffset = cropit.cropit('offset');
-
-    ajax.post(this.props.user.api.avatar, {
-      avatar: avatarType,
-      crop: {
-        offset: {
-          x: cropitOffset.x * deviceRatio,
-          y: cropitOffset.y * deviceRatio,
+      isLoading: true
+    })
+
+    let avatarType = this.props.upload ? "crop_tmp" : "crop_src"
+    let cropit = $(".crop-form")
+
+    const deviceRatio = cropit.cropit("exportZoom")
+    const cropitOffset = cropit.cropit("offset")
+
+    ajax
+      .post(this.props.user.api.avatar, {
+        avatar: avatarType,
+        crop: {
+          offset: {
+            x: cropitOffset.x * deviceRatio,
+            y: cropitOffset.y * deviceRatio
+          },
+          zoom: cropit.cropit("zoom") * deviceRatio
+        }
+      })
+      .then(
+        data => {
+          this.props.onComplete(data)
+          snackbar.success(data.detail)
         },
-        zoom: cropit.cropit('zoom') * deviceRatio,
-      }
-    }).then((data) => {
-      this.props.onComplete(data);
-      snackbar.success(data.detail);
-    }, (rejection) => {
-      if (rejection.status === 400) {
-        snackbar.error(rejection.detail);
-        this.setState({
-          isLoading: false
-        });
-      } else {
-        this.props.showError(rejection);
-      }
-    });
-  };
-  /* jshint ignore:end */
+        rejection => {
+          if (rejection.status === 400) {
+            snackbar.error(rejection.detail)
+            this.setState({
+              isLoading: false
+            })
+          } else {
+            this.props.showError(rejection)
+          }
+        }
+      )
+  }
 
   render() {
-    /* jshint ignore:start */
     return (
       <div>
         <div className="modal-body modal-avatar-crop">
           <div className="crop-form">
-            <div className="cropit-preview"></div>
-            <input
-              type="range"
-              className="cropit-image-zoom-input"
-            />
+            <div className="cropit-preview" />
+            <input type="range" className="cropit-image-zoom-input" />
           </div>
         </div>
         <div className="modal-footer">
           <div className="col-md-6 col-md-offset-3">
-
             <Button
               onClick={this.cropAvatar}
               loading={this.state.isLoading}
               className="btn-primary btn-block"
             >
-              {this.props.upload ? gettext("Set avatar")
-                                 : gettext("Crop image")}
+              {this.props.upload
+                ? gettext("Set avatar")
+                : gettext("Crop image")}
             </Button>
 
             <Button
@@ -172,11 +171,9 @@ export default class extends React.Component {
             >
               {gettext("Cancel")}
             </Button>
-
           </div>
         </div>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 115 - 104
frontend/src/components/change-avatar/gallery.js

@@ -1,34 +1,31 @@
-import React from 'react';
-import Avatar from 'misago/components/avatar'; // jshint ignore:line
-import Button from 'misago/components/button'; // jshint ignore:line
-import misago from 'misago/index'; // jshint ignore:line
-import ajax from 'misago/services/ajax'; // jshint ignore:line
-import snackbar from 'misago/services/snackbar'; // jshint ignore:line
-import batch from 'misago/utils/batch'; // jshint ignore:line
+import React from "react"
+import Avatar from "misago/components/avatar"
+import Button from "misago/components/button"
+import misago from "misago/index"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import batch from "misago/utils/batch"
 
 export class GalleryItem extends React.Component {
-  /* jshint ignore:start */
   select = () => {
-    this.props.select(this.props.id);
-  };
-  /* jshint ignore:end */
+    this.props.select(this.props.id)
+  }
 
   getClassName() {
     if (this.props.selection === this.props.id) {
       if (this.props.disabled) {
-        return 'btn btn-avatar btn-disabled avatar-selected';
+        return "btn btn-avatar btn-disabled avatar-selected"
       } else {
-        return 'btn btn-avatar avatar-selected';
+        return "btn btn-avatar avatar-selected"
       }
     } else if (this.props.disabled) {
-      return 'btn btn-avatar btn-disabled';
+      return "btn btn-avatar btn-disabled"
     } else {
-      return 'btn btn-avatar';
+      return "btn btn-avatar"
     }
   }
 
   render() {
-    /* jshint ignore:start */
     return (
       <button
         type="button"
@@ -38,126 +35,140 @@ export class GalleryItem extends React.Component {
       >
         <img src={this.props.url} />
       </button>
-    );
-    /* jshint ignore:end */
+    )
   }
 }
 
 export class Gallery extends React.Component {
   render() {
-    /* jshint ignore:start */
-    return <div className="avatars-gallery">
-      <h3>{this.props.name}</h3>
-
-      <div className="avatars-gallery-images">
-        {batch(this.props.images, 4, null).map((row, i) => {
-          return <div className="row" key={i}>
-            {row.map((item, i) => {
-              return <div className="col-xs-3" key={i}>
-                {item ? <GalleryItem
+    return (
+      <div className="avatars-gallery">
+        <h3>{this.props.name}</h3>
+
+        <div className="avatars-gallery-images">
+          {batch(this.props.images, 4, null).map((row, i) => {
+            return (
+              <div className="row" key={i}>
+                {row.map((item, i) => {
+                  return (
+                    <div className="col-xs-3" key={i}>
+                      {item ? (
+                        <GalleryItem
                           disabled={this.props.disabled}
                           select={this.props.select}
                           selection={this.props.selection}
                           {...item}
                         />
-                      : <div className="blank-avatar" />}
+                      ) : (
+                        <div className="blank-avatar" />
+                      )}
+                    </div>
+                  )
+                })}
               </div>
-            })}
-          </div>
-        })}
+            )
+          })}
+        </div>
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
 }
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
-      'selection': null,
-      'isLoading': false
-    };
+      selection: null,
+      isLoading: false
+    }
   }
 
-  /* jshint ignore:start */
-  select = (image) => {
+  select = image => {
     this.setState({
       selection: image
-    });
-  };
+    })
+  }
 
   save = () => {
     if (this.state.isLoading) {
-      return false;
+      return false
     }
 
     this.setState({
-      'isLoading': true
-    });
-
-    ajax.post(this.props.user.api.avatar, {
-      avatar: 'galleries',
-      image: this.state.selection
-    }).then((response) => {
-      this.setState({
-        'isLoading': false
-      });
-
-      snackbar.success(response.detail);
-      this.props.onComplete(response);
-      this.props.showIndex();
-    }, (rejection) => {
-      if (rejection.status === 400) {
-        snackbar.error(rejection.detail);
-        this.setState({
-          'isLoading': false
-        });
-      } else {
-        this.props.showError(rejection);
-      }
-    });
-  };
-  /* jshint ignore:end */
+      isLoading: true
+    })
+
+    ajax
+      .post(this.props.user.api.avatar, {
+        avatar: "galleries",
+        image: this.state.selection
+      })
+      .then(
+        response => {
+          this.setState({
+            isLoading: false
+          })
+
+          snackbar.success(response.detail)
+          this.props.onComplete(response)
+          this.props.showIndex()
+        },
+        rejection => {
+          if (rejection.status === 400) {
+            snackbar.error(rejection.detail)
+            this.setState({
+              isLoading: false
+            })
+          } else {
+            this.props.showError(rejection)
+          }
+        }
+      )
+  }
 
   render() {
-    /* jshint ignore:start */
-    return <div>
-      <div className="modal-body modal-avatar-gallery">
-
-        {this.props.options.galleries.map((item, i) => {
-          return <Gallery name={item.name}
-                          images={item.images}
-                          selection={this.state.selection}
-                          disabled={this.state.isLoading}
-                          select={this.select}
-                          key={i} />;
-        })}
-
-      </div>
-      <div className="modal-footer">
-        <div className="row">
-          <div className="col-md-6 col-md-offset-3">
-
-            <Button onClick={this.save}
-                    loading={this.state.isLoading}
-                    disabled={!this.state.selection}
-                    className="btn-primary btn-block">
-              {this.state.selection ? gettext("Save choice")
-                                    : gettext("Select avatar")}
-            </Button>
-
-            <Button onClick={this.props.showIndex}
-                    disabled={this.state.isLoading}
-                    className="btn-default btn-block">
-              {gettext("Cancel")}
-            </Button>
-
+    return (
+      <div>
+        <div className="modal-body modal-avatar-gallery">
+          {this.props.options.galleries.map((item, i) => {
+            return (
+              <Gallery
+                name={item.name}
+                images={item.images}
+                selection={this.state.selection}
+                disabled={this.state.isLoading}
+                select={this.select}
+                key={i}
+              />
+            )
+          })}
+        </div>
+        <div className="modal-footer">
+          <div className="row">
+            <div className="col-md-6 col-md-offset-3">
+              <Button
+                onClick={this.save}
+                loading={this.state.isLoading}
+                disabled={!this.state.selection}
+                className="btn-primary btn-block"
+              >
+                {this.state.selection
+                  ? gettext("Save choice")
+                  : gettext("Select avatar")}
+              </Button>
+
+              <Button
+                onClick={this.props.showIndex}
+                disabled={this.state.isLoading}
+                className="btn-default btn-block"
+              >
+                {gettext("Cancel")}
+              </Button>
+            </div>
           </div>
         </div>
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 82 - 99
frontend/src/components/change-avatar/index.js

@@ -1,77 +1,81 @@
-import React from 'react';
-import Avatar from 'misago/components/avatar'; // jshint ignore:line
-import Button from 'misago/components/button'; // jshint ignore:line
-import Loader from 'misago/components/loader'; // jshint ignore:line
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
+import React from "react"
+import Avatar from "misago/components/avatar"
+import Button from "misago/components/button"
+import Loader from "misago/components/loader"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
-      'isLoading': false
-    };
+      isLoading: false
+    }
   }
 
   callApi(avatarType) {
     if (this.state.isLoading) {
-      return false;
+      return false
     }
 
     this.setState({
-      'isLoading': true
-    });
-
-    ajax.post(this.props.user.api.avatar, {
-      avatar: avatarType
-    }).then((response) => {
-      this.setState({
-        'isLoading': false
-      });
-
-      snackbar.success(response.detail);
-      this.props.onComplete(response);
-    }, (rejection) => {
-      if (rejection.status === 400) {
-        snackbar.error(rejection.detail);
-        this.setState({
-          'isLoading': false
-        });
-      } else {
-        this.props.showError(rejection);
-      }
-    });
+      isLoading: true
+    })
+
+    ajax
+      .post(this.props.user.api.avatar, {
+        avatar: avatarType
+      })
+      .then(
+        response => {
+          this.setState({
+            isLoading: false
+          })
+
+          snackbar.success(response.detail)
+          this.props.onComplete(response)
+        },
+        rejection => {
+          if (rejection.status === 400) {
+            snackbar.error(rejection.detail)
+            this.setState({
+              isLoading: false
+            })
+          } else {
+            this.props.showError(rejection)
+          }
+        }
+      )
   }
 
-  /* jshint ignore:start */
   setGravatar = () => {
-    this.callApi('gravatar');
-  };
+    this.callApi("gravatar")
+  }
 
   setGenerated = () => {
-    this.callApi('generated');
-  };
-  /* jshint ignore:end */
+    this.callApi("generated")
+  }
 
   getGravatarButton() {
     if (this.props.options.gravatar) {
-      /* jshint ignore:start */
-      return <Button onClick={this.setGravatar}
-              disabled={this.state.isLoading}
-              className="btn-default btn-block btn-avatar-gravatar">
-        {gettext("Download my Gravatar")}
-      </Button>;
-      /* jshint ignore:end */
+      return (
+        <Button
+          onClick={this.setGravatar}
+          disabled={this.state.isLoading}
+          className="btn-default btn-block btn-avatar-gravatar"
+        >
+          {gettext("Download my Gravatar")}
+        </Button>
+      )
     } else {
-      return null;
+      return null
     }
   }
 
   getCropButton() {
-    if (!this.props.options.crop_src) return null;
+    if (!this.props.options.crop_src) return null
 
-    /* jshint ignore:start */
     return (
       <Button
         className="btn-default btn-block btn-avatar-crop"
@@ -80,14 +84,12 @@ export default class extends React.Component {
       >
         {gettext("Re-crop uploaded image")}
       </Button>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   getUploadButton() {
-    if (!this.props.options.upload) return null;
+    if (!this.props.options.upload) return null
 
-    /* jshint ignore:start */
     return (
       <Button
         className="btn-default btn-block btn-avatar-upload"
@@ -96,14 +98,12 @@ export default class extends React.Component {
       >
         {gettext("Upload new image")}
       </Button>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   getGalleryButton() {
-    if (!this.props.options.galleries) return null;
+    if (!this.props.options.galleries) return null
 
-    /* jshint ignore:start */
     return (
       <Button
         className="btn-default btn-block btn-avatar-gallery"
@@ -112,70 +112,53 @@ export default class extends React.Component {
       >
         {gettext("Pick avatar from gallery")}
       </Button>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   getAvatarPreview() {
-    /* jshint ignore:start */
     let userPeview = {
       id: this.props.user.id,
       avatars: this.props.options.avatars
     }
-    /* jshint ignore:end */
 
     if (this.state.isLoading) {
-      /* jshint ignore:start */
       return (
         <div className="avatar-preview preview-loading">
-          <Avatar
-            size="200"
-            user={userPeview}
-          />
+          <Avatar size="200" user={userPeview} />
           <Loader />
         </div>
-      );
-      /* jshint ignore:end */
+      )
     }
 
-    /* jshint ignore:start */
     return (
       <div className="avatar-preview">
-        <Avatar
-          size="200"
-          user={userPeview}
-        />
+        <Avatar size="200" user={userPeview} />
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className="modal-body modal-avatar-index">
-      <div className="row">
-        <div className="col-md-5">
-
-          {this.getAvatarPreview()}
-
-        </div>
-        <div className="col-md-7">
-
-          {this.getGravatarButton()}
-
-          <Button onClick={this.setGenerated}
-                  disabled={this.state.isLoading}
-                  className="btn-default btn-block btn-avatar-generate">
-            {gettext("Generate my individual avatar")}
-          </Button>
-
-          {this.getCropButton()}
-          {this.getUploadButton()}
-          {this.getGalleryButton()}
-
+    return (
+      <div className="modal-body modal-avatar-index">
+        <div className="row">
+          <div className="col-md-5">{this.getAvatarPreview()}</div>
+          <div className="col-md-7">
+            {this.getGravatarButton()}
+
+            <Button
+              onClick={this.setGenerated}
+              disabled={this.state.isLoading}
+              className="btn-default btn-block btn-avatar-generate"
+            >
+              {gettext("Generate my individual avatar")}
+            </Button>
+
+            {this.getCropButton()}
+            {this.getUploadButton()}
+            {this.getGalleryButton()}
+          </div>
         </div>
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 101 - 102
frontend/src/components/change-avatar/root.js

@@ -1,161 +1,160 @@
-import React from 'react';
-import AvatarIndex from 'misago/components/change-avatar/index'; // jshint ignore:line
-import AvatarCrop from 'misago/components/change-avatar/crop'; // jshint ignore:line
-import AvatarUpload from 'misago/components/change-avatar/upload'; // jshint ignore:line
-import AvatarGallery from 'misago/components/change-avatar/gallery'; // jshint ignore:line
-import Loader from 'misago/components/modal-loader'; // jshint ignore:line
-import { updateAvatar } from 'misago/reducers/users'; // jshint ignore:line
-import ajax from 'misago/services/ajax';
-import store from 'misago/services/store'; // jshint ignore:line
+import React from "react"
+import AvatarIndex from "misago/components/change-avatar/index"
+import AvatarCrop from "misago/components/change-avatar/crop"
+import AvatarUpload from "misago/components/change-avatar/upload"
+import AvatarGallery from "misago/components/change-avatar/gallery"
+import Loader from "misago/components/modal-loader"
+import { updateAvatar } from "misago/reducers/users"
+import ajax from "misago/services/ajax"
+import store from "misago/services/store"
 
 export class ChangeAvatarError extends React.Component {
   getErrorReason() {
     if (this.props.reason) {
-      /* jshint ignore:start */
-      return <p dangerouslySetInnerHTML={{__html: this.props.reason}} />;
-      /* jshint ignore:end */
+      return <p dangerouslySetInnerHTML={{ __html: this.props.reason }} />
     } else {
-      return null;
+      return null
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className="modal-body">
-      <div className="message-icon">
-        <span className="material-icon">
-          remove_circle_outline
-        </span>
-      </div>
-      <div className="message-body">
-        <p className="lead">
-          {this.props.message}
-        </p>
-        {this.getErrorReason()}
-        <button
-          className="btn btn-default"
-          data-dismiss="modal"
-          type="button"
-        >
-          {gettext("Ok")}
-        </button>
+    return (
+      <div className="modal-body">
+        <div className="message-icon">
+          <span className="material-icon">remove_circle_outline</span>
+        </div>
+        <div className="message-body">
+          <p className="lead">{this.props.message}</p>
+          {this.getErrorReason()}
+          <button
+            className="btn btn-default"
+            data-dismiss="modal"
+            type="button"
+          >
+            {gettext("Ok")}
+          </button>
+        </div>
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
 }
 
 export default class extends React.Component {
   componentDidMount() {
-    ajax.get(this.props.user.api.avatar).then((options) => {
-      this.setState({
-        'component': AvatarIndex,
-        'options': options,
-        'error': null
-      });
-    }, (rejection) => {
-      this.showError(rejection);
-    });
+    ajax.get(this.props.user.api.avatar).then(
+      options => {
+        this.setState({
+          component: AvatarIndex,
+          options: options,
+          error: null
+        })
+      },
+      rejection => {
+        this.showError(rejection)
+      }
+    )
   }
 
-  /* jshint ignore:start */
-  showError = (error) => {
+  showError = error => {
     this.setState({
       error
-    });
-  };
+    })
+  }
 
   showIndex = () => {
     this.setState({
-      'component': AvatarIndex
-    });
-  };
+      component: AvatarIndex
+    })
+  }
 
   showUpload = () => {
     this.setState({
-      'component': AvatarUpload
-    });
-  };
+      component: AvatarUpload
+    })
+  }
 
   showCrop = () => {
     this.setState({
-      'component': AvatarCrop
-    });
-  };
+      component: AvatarCrop
+    })
+  }
 
   showGallery = () => {
     this.setState({
-      'component': AvatarGallery
-    });
-  };
+      component: AvatarGallery
+    })
+  }
 
-  completeFlow = (options) => {
-    store.dispatch(updateAvatar(this.props.user, options.avatars));
+  completeFlow = options => {
+    store.dispatch(updateAvatar(this.props.user, options.avatars))
 
     this.setState({
-      'component': AvatarIndex,
+      component: AvatarIndex,
       options
-    });
-  };
-  /* jshint ignore:end */
+    })
+  }
 
   getBody() {
     if (this.state) {
       if (this.state.error) {
-        /* jshint ignore:start */
-        return <ChangeAvatarError message={this.state.error.detail}
-                                  reason={this.state.error.reason} />;
-        /* jshint ignore:end */
+        return (
+          <ChangeAvatarError
+            message={this.state.error.detail}
+            reason={this.state.error.reason}
+          />
+        )
       } else {
-        /* jshint ignore:start */
-        return <this.state.component options={this.state.options}
-                                     user={this.props.user}
-                                     onComplete={this.completeFlow}
-                                     showError={this.showError}
-                                     showIndex={this.showIndex}
-                                     showCrop={this.showCrop}
-                                     showUpload={this.showUpload}
-                                     showGallery={this.showGallery} />;
-        /* jshint ignore:end */
+        return (
+          <this.state.component
+            options={this.state.options}
+            user={this.props.user}
+            onComplete={this.completeFlow}
+            showError={this.showError}
+            showIndex={this.showIndex}
+            showCrop={this.showCrop}
+            showUpload={this.showUpload}
+            showGallery={this.showGallery}
+          />
+        )
       }
     } else {
-      /* jshint ignore:start */
-      return <Loader />;
-      /* jshint ignore:end */
+      return <Loader />
     }
   }
 
   getClassName() {
-   if (this.state && this.state.error) {
-      return "modal-dialog modal-message modal-change-avatar";
+    if (this.state && this.state.error) {
+      return "modal-dialog modal-message modal-change-avatar"
     } else {
-      return "modal-dialog modal-change-avatar";
+      return "modal-dialog modal-change-avatar"
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className={this.getClassName()}
-                role="document">
-      <div className="modal-content">
-        <div className="modal-header">
-          <button type="button" className="close" data-dismiss="modal"
-                  aria-label={gettext("Close")}>
-            <span aria-hidden="true">&times;</span>
-          </button>
-          <h4 className="modal-title">{gettext("Change your avatar")}</h4>
+    return (
+      <div className={this.getClassName()} role="document">
+        <div className="modal-content">
+          <div className="modal-header">
+            <button
+              type="button"
+              className="close"
+              data-dismiss="modal"
+              aria-label={gettext("Close")}
+            >
+              <span aria-hidden="true">&times;</span>
+            </button>
+            <h4 className="modal-title">{gettext("Change your avatar")}</h4>
+          </div>
+
+          {this.getBody()}
         </div>
-
-        {this.getBody()}
-
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
 }
 
 export function select(state) {
   return {
-    'user': state.auth.user
-  };
+    user: state.auth.user
+  }
 }

+ 99 - 98
frontend/src/components/change-avatar/upload.js

@@ -1,141 +1,152 @@
-import React from 'react';
-import AvatarCrop from 'misago/components/change-avatar/crop'; // jshint ignore:line
-import Button from 'misago/components/button'; // jshint ignore:line
-import ajax from 'misago/services/ajax'; // jshint ignore:line
-import snackbar from 'misago/services/snackbar'; // jshint ignore:line
-import fileSize from 'misago/utils/file-size';
+import React from "react"
+import AvatarCrop from "misago/components/change-avatar/crop"
+import Button from "misago/components/button"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import fileSize from "misago/utils/file-size"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       image: null,
       preview: null,
       progress: 0,
       uploaded: null,
-      dataUrl: null,
-    };
+      dataUrl: null
+    }
   }
 
   validateFile(image) {
     if (image.size > this.props.options.upload.limit) {
-      return interpolate(gettext("Selected file is too big. (%(filesize)s)"), {
-        'filesize': fileSize(image.size)
-      }, true);
+      return interpolate(
+        gettext("Selected file is too big. (%(filesize)s)"),
+        {
+          filesize: fileSize(image.size)
+        },
+        true
+      )
     }
 
-    let invalidTypeMsg = gettext("Selected file type is not supported.");
-    if (this.props.options.upload.allowed_mime_types.indexOf(image.type) === -1) {
-      return invalidTypeMsg;
+    let invalidTypeMsg = gettext("Selected file type is not supported.")
+    if (
+      this.props.options.upload.allowed_mime_types.indexOf(image.type) === -1
+    ) {
+      return invalidTypeMsg
     }
 
-    let extensionFound = false;
-    let loweredFilename = image.name.toLowerCase();
+    let extensionFound = false
+    let loweredFilename = image.name.toLowerCase()
     this.props.options.upload.allowed_extensions.map(function(extension) {
       if (loweredFilename.substr(extension.length * -1) === extension) {
-        extensionFound = true;
+        extensionFound = true
       }
-    });
+    })
 
     if (!extensionFound) {
-      return invalidTypeMsg;
+      return invalidTypeMsg
     }
 
-    return false;
+    return false
   }
 
-  /* jshint ignore:start */
   pickFile = () => {
-    document.getElementById('avatar-hidden-upload').click();
-  };
+    document.getElementById("avatar-hidden-upload").click()
+  }
 
   uploadFile = () => {
-    let image = document.getElementById('avatar-hidden-upload').files[0];
-    if (!image) return;
+    let image = document.getElementById("avatar-hidden-upload").files[0]
+    if (!image) return
 
-    let validationError = this.validateFile(image);
+    let validationError = this.validateFile(image)
     if (validationError) {
-      snackbar.error(validationError);
-      return;
+      snackbar.error(validationError)
+      return
     }
 
     this.setState({
       image,
       preview: URL.createObjectURL(image),
       progress: 0
-    });
-
-    let data = new FormData();
-    data.append('avatar', 'upload');
-    data.append('image', image);
-
-    ajax.upload(this.props.user.api.avatar, data, (progress) => {
-      this.setState({
-        progress
-      });
-    }).then((data) => {
-      this.setState({
-        options: data,
-        uploaded: data.detail,
-      });
-
-      snackbar.info(gettext("Your image has been uploaded and you may now crop it."));
-    }, (rejection) => {
-      if (rejection.status === 400 || rejection.status === 413) {
-        snackbar.error(rejection.detail);
+    })
+
+    let data = new FormData()
+    data.append("avatar", "upload")
+    data.append("image", image)
+
+    ajax
+      .upload(this.props.user.api.avatar, data, progress => {
         this.setState({
-          isLoading: false,
-          image: null,
-          progress: 0
-        });
-      } else {
-        this.props.showError(rejection);
-      }
-    });
-  };
-  /* jshint ignore:end */
+          progress
+        })
+      })
+      .then(
+        data => {
+          this.setState({
+            options: data,
+            uploaded: data.detail
+          })
+
+          snackbar.info(
+            gettext("Your image has been uploaded and you may now crop it.")
+          )
+        },
+        rejection => {
+          if (rejection.status === 400 || rejection.status === 413) {
+            snackbar.error(rejection.detail)
+            this.setState({
+              isLoading: false,
+              image: null,
+              progress: 0
+            })
+          } else {
+            this.props.showError(rejection)
+          }
+        }
+      )
+  }
 
   getUploadRequirements(options) {
     let extensions = options.allowed_extensions.map(function(extension) {
-      return extension.substr(1);
-    });
-
-    return interpolate(gettext("%(files)s files smaller than %(limit)s"), {
-        'files': extensions.join(', '),
-        'limit': fileSize(options.limit)
-      }, true);
+      return extension.substr(1)
+    })
+
+    return interpolate(
+      gettext("%(files)s files smaller than %(limit)s"),
+      {
+        files: extensions.join(", "),
+        limit: fileSize(options.limit)
+      },
+      true
+    )
   }
 
   getUploadButton() {
-    /* jshint ignore:start */
     return (
       <div className="modal-body modal-avatar-upload">
-        <Button
-          className="btn-pick-file"
-          onClick={this.pickFile}
-        >
-          <div className="material-icon">
-            input
-          </div>
+        <Button className="btn-pick-file" onClick={this.pickFile}>
+          <div className="material-icon">input</div>
           {gettext("Select file")}
         </Button>
         <p className="text-muted">
           {this.getUploadRequirements(this.props.options.upload)}
         </p>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   getUploadProgressLabel() {
-    return interpolate(gettext("%(progress)s % complete"), {
-        'progress': this.state.progress
-      }, true);
+    return interpolate(
+      gettext("%(progress)s % complete"),
+      {
+        progress: this.state.progress
+      },
+      true
+    )
   }
 
   getUploadProgress() {
-    /* jshint ignore:start */
     return (
       <div className="modal-body modal-avatar-upload">
         <div className="upload-progress">
@@ -148,19 +159,17 @@ export default class extends React.Component {
               aria-valuenow="{this.state.progress}"
               aria-valuemin="0"
               aria-valuemax="100"
-              style={{width: this.state.progress + '%'}}
+              style={{ width: this.state.progress + "%" }}
             >
               <span className="sr-only">{this.getUploadProgressLabel()}</span>
             </div>
           </div>
         </div>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   renderUpload() {
-    /* jshint ignore:start */
     return (
       <div>
         <input
@@ -169,11 +178,9 @@ export default class extends React.Component {
           className="hidden-file-upload"
           onChange={this.uploadFile}
         />
-        {this.state.image ? this.getUploadProgress()
-                          : this.getUploadButton()}
+        {this.state.image ? this.getUploadProgress() : this.getUploadButton()}
         <div className="modal-footer">
           <div className="col-md-6 col-md-offset-3">
-
             <Button
               onClick={this.props.showIndex}
               disabled={!!this.state.image}
@@ -181,16 +188,13 @@ export default class extends React.Component {
             >
               {gettext("Cancel")}
             </Button>
-
           </div>
         </div>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   renderCrop() {
-    /* jshint ignore:start */
     return (
       <AvatarCrop
         options={this.state.options}
@@ -201,15 +205,12 @@ export default class extends React.Component {
         showError={this.props.showError}
         showIndex={this.props.showIndex}
       />
-    );
-    /* jshint ignore:end */
+    )
   }
 
   render() {
-    /* jshint ignore:start */
-    if (this.state.uploaded) return this.renderCrop();
+    if (this.state.uploaded) return this.renderCrop()
 
-    return this.renderUpload();
-    /* jshint ignore:end */
+    return this.renderUpload()
   }
-}
+}

+ 15 - 15
frontend/src/components/dropdown-toggle.js

@@ -1,25 +1,25 @@
-import React from 'react';
+import React from "react"
 
 export default class extends React.Component {
   getClassName() {
     if (this.props.dropdown) {
-      return "btn btn-default btn-aligned btn-icon btn-dropdown-toggle open hidden-md hidden-lg";
+      return "btn btn-default btn-aligned btn-icon btn-dropdown-toggle open hidden-md hidden-lg"
     } else {
-      return "btn btn-default btn-aligned btn-icon btn-dropdown-toggle hidden-md hidden-lg";
+      return "btn btn-default btn-aligned btn-icon btn-dropdown-toggle hidden-md hidden-lg"
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <button className={this.getClassName()}
-                   type="button"
-                   onClick={this.props.toggleNav}
-                   aria-haspopup="true"
-                   aria-expanded={this.props.dropdown ? 'true' : 'false'}>
-      <i className="material-icon">
-        menu
-      </i>
-    </button>;
-    /* jshint ignore:end */
+    return (
+      <button
+        className={this.getClassName()}
+        type="button"
+        onClick={this.props.toggleNav}
+        aria-haspopup="true"
+        aria-expanded={this.props.dropdown ? "true" : "false"}
+      >
+        <i className="material-icon">menu</i>
+      </button>
+    )
   }
-}
+}

+ 5 - 6
frontend/src/components/edit-details/blankslate.js

@@ -1,14 +1,13 @@
-/* jshint ignore:start */
-import React from 'react';
-import PanelMessage from 'misago/components/panel-message';
+import React from "react"
+import PanelMessage from "misago/components/panel-message"
 
 export default function({ display }) {
-  if (!display) return null;
+  if (!display) return null
 
   return (
     <PanelMessage
       helpText={gettext("No profile details are editable at this time.")}
       message={gettext("This option is currently unavailable.")}
     />
-  );
-}
+  )
+}

+ 15 - 16
frontend/src/components/edit-details/field-input.js

@@ -1,18 +1,17 @@
-/* jshint ignore:start */
-import React from 'react';
-import Select from 'misago/components/select';
+import React from "react"
+import Select from "misago/components/select"
 
 export default class extends React.Component {
-  onChange = (ev) => {
-    const { field, onChange } = this.props;
-    onChange(field.fieldname, ev.target.value);
+  onChange = ev => {
+    const { field, onChange } = this.props
+    onChange(field.fieldname, ev.target.value)
   }
 
   render() {
-    const { disabled, field, value } = this.props;
-    const { input } = field;
+    const { disabled, field, value } = this.props
+    const { input } = field
 
-    if (input.type === 'select') {
+    if (input.type === "select") {
       return (
         <Select
           choices={input.choices}
@@ -21,10 +20,10 @@ export default class extends React.Component {
           onChange={this.onChange}
           value={value}
         />
-      );
+      )
     }
 
-    if (input.type === 'textarea') {
+    if (input.type === "textarea") {
       return (
         <textarea
           className="form-control"
@@ -35,10 +34,10 @@ export default class extends React.Component {
           type="text"
           value={value}
         />
-      );
+      )
     }
 
-    if (input.type === 'text') {
+    if (input.type === "text") {
       return (
         <input
           className="form-control"
@@ -48,9 +47,9 @@ export default class extends React.Component {
           type="text"
           value={value}
         />
-      );
+      )
     }
 
-    return null;
+    return null
   }
-}
+}

+ 6 - 7
frontend/src/components/edit-details/fieldset.js

@@ -1,13 +1,12 @@
-/* jshint ignore:start */
-import React from 'react';
-import FieldInput from './field-input';
-import FormGroup from 'misago/components/form-group';
+import React from "react"
+import FieldInput from "./field-input"
+import FormGroup from "misago/components/form-group"
 
 export default function({ disabled, errors, fields, name, onChange, value }) {
   return (
     <fieldset>
       <legend>{name}</legend>
-      {fields.map((field) => {
+      {fields.map(field => {
         return (
           <FormGroup
             for={"id_" + field.fieldname}
@@ -23,8 +22,8 @@ export default function({ disabled, errors, fields, name, onChange, value }) {
               value={value[field.fieldname]}
             />
           </FormGroup>
-        );
+        )
       })}
     </fieldset>
-  );
+  )
 }

+ 34 - 36
frontend/src/components/edit-details/form.js

@@ -1,29 +1,28 @@
-/* jshint ignore:start */
-import React from 'react';
-import Fieldset from './fieldset';
-import Button from 'misago/components/button';
-import Form from 'misago/components/form';
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
+import React from "react"
+import Fieldset from "./fieldset"
+import Button from "misago/components/button"
+import Form from "misago/components/form"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoading: false,
 
       errors: {}
-    };
+    }
 
-    const groups = props.groups.length;
+    const groups = props.groups.length
     for (let i = 0; i < groups; i++) {
-      const group = props.groups[i];
-      const fields = group.fields.length;
+      const group = props.groups[i]
+      const fields = group.fields.length
       for (let f = 0; f < fields; f++) {
-        const fieldname = group.fields[f].fieldname;
-        const initial = group.fields[f].initial;
-        this.state[fieldname] = initial;
+        const fieldname = group.fields[f].fieldname
+        const initial = group.fields[f].initial
+        this.state[fieldname] = initial
       }
     }
   }
@@ -32,29 +31,29 @@ export default class extends Form {
     const data = Object.assign({}, this.state, {
       errors: null,
       isLoading: null
-    });
+    })
 
     return ajax.post(this.props.api, data)
   }
 
   handleSuccess(data) {
-    this.props.onSuccess(data);
+    this.props.onSuccess(data)
   }
 
   handleError(rejection) {
     if (rejection.status === 400) {
-      snackbar.error(gettext("Form contains errors."));
+      snackbar.error(gettext("Form contains errors."))
       this.setState({ errors: rejection })
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
   }
 
   onChange = (name, value) => {
     this.setState({
       [name]: value
-    });
-  };
+    })
+  }
 
   render() {
     return (
@@ -71,35 +70,34 @@ export default class extends Form {
                 onChange={this.onChange}
                 value={this.state}
               />
-            );
+            )
           })}
         </div>
         <div className="panel-footer text-right">
           <CancelButton
             disabled={this.state.isLoading}
             onCancel={this.props.onCancel}
-          />
-          {' '}
+          />{" "}
           <Button className="btn-primary" loading={this.state.isLoading}>
             {gettext("Save changes")}
           </Button>
         </div>
       </form>
-    );
+    )
   }
 }
 
 export function CancelButton({ onCancel, disabled }) {
-  if (!onCancel) return null;
+  if (!onCancel) return null
 
   return (
-      <button
-        className="btn btn-default"
-        disabled={disabled}
-        onClick={onCancel}
-        type="button"
-      >
-        {gettext("Cancel")}
-      </button>
-  );
-}
+    <button
+      className="btn btn-default"
+      disabled={disabled}
+      onClick={onCancel}
+      type="button"
+    >
+      {gettext("Cancel")}
+    </button>
+  )
+}

+ 21 - 29
frontend/src/components/edit-details/index.js

@@ -1,48 +1,45 @@
-/* jshint ignore:start */
-import React from 'react';
-import Blankslate from './blankslate';
-import Loader from './loader';
-import Form from './form';
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
+import React from "react"
+import Blankslate from "./blankslate"
+import Loader from "./loader"
+import Form from "./form"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       loading: true,
       groups: null
-    };
+    }
   }
 
   componentDidMount() {
     ajax.get(this.props.api).then(
-      (groups) => {
+      groups => {
         this.setState({
           loading: false,
 
           groups
-        });
+        })
       },
-      (rejection) => {
-        snackbar.apiError(rejection);
+      rejection => {
+        snackbar.apiError(rejection)
         if (this.props.cancel) {
-          this.props.cancel();
+          this.props.cancel()
         }
       }
-    );
+    )
   }
 
   render() {
-    const { groups, loading } = this.state;
+    const { groups, loading } = this.state
 
     return (
       <div className="panel panel-default panel-form">
         <div className="panel-heading">
-          <h3 className="panel-title">
-            {gettext("Edit details")}
-          </h3>
+          <h3 className="panel-title">{gettext("Edit details")}</h3>
         </div>
         <Loader display={loading} />
         <Blankslate display={!loading && !groups.length} />
@@ -54,19 +51,14 @@ export default class extends React.Component {
           onSuccess={this.props.onSuccess}
         />
       </div>
-    );
+    )
   }
 }
 
 export function FormDisplay({ api, display, groups, onCancel, onSuccess }) {
-  if (!display) return null;
+  if (!display) return null
 
   return (
-    <Form
-      api={api}
-      groups={groups}
-      onCancel={onCancel}
-      onSuccess={onSuccess}
-    />
-  );
-}
+    <Form api={api} groups={groups} onCancel={onCancel} onSuccess={onSuccess} />
+  )
+}

+ 5 - 6
frontend/src/components/edit-details/loader.js

@@ -1,13 +1,12 @@
-/* jshint ignore:start */
-import React from 'react';
-import Loader from 'misago/components/loader';
+import React from "react"
+import Loader from "misago/components/loader"
 
 export default function({ display }) {
-  if (!display) return null;
+  if (!display) return null
 
   return (
     <div className="panel-body">
       <Loader />
     </div>
-  );
-}
+  )
+}

+ 6 - 7
frontend/src/components/editor/actions/action.js

@@ -1,15 +1,14 @@
-// jshint ignore:start
-import React from 'react';
+import React from "react"
 
 export default class extends React.Component {
   onClick = () => {
-    this.props.replaceSelection(this.props.execAction);
-  };
+    this.props.replaceSelection(this.props.execAction)
+  }
 
   render() {
     return (
       <button
-        className={'btn btn-icon ' + this.props.className}
+        className={"btn btn-icon " + this.props.className}
         disabled={this.props.disabled}
         onClick={this.onClick}
         title={this.props.title}
@@ -17,6 +16,6 @@ export default class extends React.Component {
       >
         {this.props.children}
       </button>
-    );
+    )
   }
-}
+}

+ 11 - 16
frontend/src/components/editor/actions/code.js

@@ -1,23 +1,18 @@
-// jshint ignore:start
-import React from 'react';
-import Action from './action';
-import isUrl from 'misago/utils/is-url';
+import React from "react"
+import Action from "./action"
+import isUrl from "misago/utils/is-url"
 
 export default function(props) {
   return (
-    <Action
-      execAction={insertCode}
-      title={gettext("Insert code")}
-      {...props}
-    >
-      <span className="material-icon">
-        functions
-      </span>
+    <Action execAction={insertCode} title={gettext("Insert code")} {...props}>
+      <span className="material-icon">functions</span>
     </Action>
-  );
+  )
 }
 
 export function insertCode(selection, replace) {
-  const syntax = $.trim(prompt(gettext("Enter name of syntax of your code (optional)") + ':'));
-  replace("\n\n```" + syntax + '\n' + selection + "\n```\n\n");
-}
+  const syntax = $.trim(
+    prompt(gettext("Enter name of syntax of your code (optional)") + ":")
+  )
+  replace("\n\n```" + syntax + "\n" + selection + "\n```\n\n")
+}

+ 6 - 9
frontend/src/components/editor/actions/emphasis.js

@@ -1,6 +1,5 @@
-// jshint ignore:start
-import React from 'react';
-import Action from './action';
+import React from "react"
+import Action from "./action"
 
 export default function(props) {
   return (
@@ -9,15 +8,13 @@ export default function(props) {
       title={gettext("Emphase selection")}
       {...props}
     >
-      <span className="material-icon">
-        format_italic
-      </span>
+      <span className="material-icon">format_italic</span>
     </Action>
-  );
+  )
 }
 
 export function makeEmphasis(selection, replace) {
   if (selection.length) {
-    replace('*' + selection + '*');
+    replace("*" + selection + "*")
   }
-}
+}

+ 6 - 9
frontend/src/components/editor/actions/hr.js

@@ -1,6 +1,5 @@
-// jshint ignore:start
-import React from 'react';
-import Action from './action';
+import React from "react"
+import Action from "./action"
 
 export default function(props) {
   return (
@@ -9,13 +8,11 @@ export default function(props) {
       title={gettext("Insert horizontal ruler")}
       {...props}
     >
-      <span className="material-icon">
-        remove
-      </span>
+      <span className="material-icon">remove</span>
     </Action>
-  );
+  )
 }
 
 export function insertHr(selection, replace) {
-  replace('\n\n- - - - -\n\n');
-}
+  replace("\n\n- - - - -\n\n")
+}

+ 15 - 22
frontend/src/components/editor/actions/image.js

@@ -1,42 +1,35 @@
-// jshint ignore:start
-import React from 'react';
-import Action from './action';
-import isUrl from 'misago/utils/is-url';
+import React from "react"
+import Action from "./action"
+import isUrl from "misago/utils/is-url"
 
 export default function(props) {
   return (
-    <Action
-      execAction={insertImage}
-      title={gettext("Insert image")}
-      {...props}
-    >
-      <span className="material-icon">
-        insert_photo
-      </span>
+    <Action execAction={insertImage} title={gettext("Insert image")} {...props}>
+      <span className="material-icon">insert_photo</span>
     </Action>
-  );
+  )
 }
 
 export function insertImage(selection, replace) {
-  let url = '';
-  let label = '';
+  let url = ""
+  let label = ""
 
   if (selection.length) {
     if (isUrl(selection)) {
-      url = selection;
+      url = selection
     } else {
-      label = selection;
+      label = selection
     }
   }
 
-  url = $.trim(prompt(gettext("Enter link to image") + ':', url));
-  label = $.trim(prompt(gettext("Enter image label (optional)") + ':', label));
+  url = $.trim(prompt(gettext("Enter link to image") + ":", url))
+  label = $.trim(prompt(gettext("Enter image label (optional)") + ":", label))
 
   if (url.length) {
     if (label.length > 0) {
-      replace('![' + label + '](' + url + ')');
+      replace("![" + label + "](" + url + ")")
     } else {
-      replace('!(' + url + ')');
+      replace("!(" + url + ")")
     }
   }
-}
+}

+ 16 - 23
frontend/src/components/editor/actions/link.js

@@ -1,43 +1,36 @@
-// jshint ignore:start
-import React from 'react';
-import Action from './action';
-import isUrl from 'misago/utils/is-url';
+import React from "react"
+import Action from "./action"
+import isUrl from "misago/utils/is-url"
 
 export default function(props) {
   return (
-    <Action
-      execAction={insertLink}
-      title={gettext("Insert link")}
-      {...props}
-    >
-      <span className="material-icon">
-        insert_link
-      </span>
+    <Action execAction={insertLink} title={gettext("Insert link")} {...props}>
+      <span className="material-icon">insert_link</span>
     </Action>
-  );
+  )
 }
 
 export function insertLink(selection, replace) {
-  let url = '';
-  let label = '';
+  let url = ""
+  let label = ""
 
   if (selection.length) {
     if (isUrl(selection)) {
-      url = selection;
+      url = selection
     } else {
-      label = selection;
+      label = selection
     }
   }
 
-  url = $.trim(prompt(gettext("Enter link address") + ':', url) || '');
-  if (url.length === 0) return false;
-  label = $.trim(prompt(gettext("Enter link label (optional)") + ':', label));
+  url = $.trim(prompt(gettext("Enter link address") + ":", url) || "")
+  if (url.length === 0) return false
+  label = $.trim(prompt(gettext("Enter link label (optional)") + ":", label))
 
   if (url.length) {
     if (label.length > 0) {
-      replace('[' + label + '](' + url + ')');
+      replace("[" + label + "](" + url + ")")
     } else {
-      replace(url);
+      replace(url)
     }
   }
-}
+}

+ 12 - 17
frontend/src/components/editor/actions/quote.js

@@ -1,28 +1,23 @@
-// jshint ignore:start
-import React from 'react';
-import Action from './action';
-import isUrl from 'misago/utils/is-url';
+import React from "react"
+import Action from "./action"
+import isUrl from "misago/utils/is-url"
 
 export default function(props) {
   return (
-    <Action
-      execAction={insertQuote}
-      title={gettext("Insert quote")}
-      {...props}
-    >
-      <span className="material-icon">
-        format_quote
-      </span>
+    <Action execAction={insertQuote} title={gettext("Insert quote")} {...props}>
+      <span className="material-icon">format_quote</span>
     </Action>
-  );
+  )
 }
 
 export function insertQuote(selection, replace) {
-  let title = $.trim(prompt(gettext("Enter quote autor, prefix usernames with @") + ':', title));
+  let title = $.trim(
+    prompt(gettext("Enter quote autor, prefix usernames with @") + ":", title)
+  )
 
   if (title) {
-    replace('\n\n[quote="' + title + '"]\n' + selection + '\n[/quote]\n\n');
+    replace('\n\n[quote="' + title + '"]\n' + selection + "\n[/quote]\n\n")
   } else {
-    replace('\n\n[quote]\n' + selection + '\n[/quote]\n\n');
+    replace("\n\n[quote]\n" + selection + "\n[/quote]\n\n")
   }
-}
+}

+ 6 - 9
frontend/src/components/editor/actions/striketrough.js

@@ -1,6 +1,5 @@
-// jshint ignore:start
-import React from 'react';
-import Action from './action';
+import React from "react"
+import Action from "./action"
 
 export default function(props) {
   return (
@@ -9,15 +8,13 @@ export default function(props) {
       title={gettext("Striketrough selection")}
       {...props}
     >
-      <span className="material-icon">
-        format_strikethrough
-      </span>
+      <span className="material-icon">format_strikethrough</span>
     </Action>
-  );
+  )
 }
 
 export function makeStriketrough(selection, replace) {
   if (selection.length) {
-    replace('~~' + selection + '~~');
+    replace("~~" + selection + "~~")
   }
-}
+}

+ 6 - 9
frontend/src/components/editor/actions/strong.js

@@ -1,6 +1,5 @@
-// jshint ignore:start
-import React from 'react';
-import Action from './action';
+import React from "react"
+import Action from "./action"
 
 export default function(props) {
   return (
@@ -9,15 +8,13 @@ export default function(props) {
       title={gettext("Bolder selection")}
       {...props}
     >
-      <span className="material-icon">
-        format_bold
-      </span>
+      <span className="material-icon">format_bold</span>
     </Action>
-  );
+  )
 }
 
 export function makeStrong(selection, replace) {
   if (selection.length) {
-    replace('**' + selection + '**');
+    replace("**" + selection + "**")
   }
-}
+}

+ 102 - 75
frontend/src/components/editor/attachments/attachment/complete.js

@@ -1,54 +1,69 @@
-// jshint ignore:start
-import React from 'react';
-import misago from 'misago';
-import escapeHtml from 'misago/utils/escape-html';
-import formatFilesize from 'misago/utils/file-size';
+import React from "react"
+import misago from "misago"
+import escapeHtml from "misago/utils/escape-html"
+import formatFilesize from "misago/utils/file-size"
 
-const DATE_ABBR = '<abbr title="%(absolute)s">%(relative)s</abbr>';
-const USER_SPAN = '<span class="item-title">%(user)s</span>';
-const USER_URL = '<a href="%(url)s" class="item-title">%(user)s</a>';
+const DATE_ABBR = '<abbr title="%(absolute)s">%(relative)s</abbr>'
+const USER_SPAN = '<span class="item-title">%(user)s</span>'
+const USER_URL = '<a href="%(url)s" class="item-title">%(user)s</a>'
 
 export default class extends React.Component {
   onInsert = () => {
-    this.props.replaceSelection(this.insertAttachment);
-  };
+    this.props.replaceSelection(this.insertAttachment)
+  }
 
   insertAttachment = (selection, replace) => {
-    const item = this.props.item;
+    const item = this.props.item
 
     if (item.is_image) {
       if (item.url.thumb) {
-        replace('[![' + item.filename + '](' + item.url.thumb + ')](' + item.url.index + ')');
+        replace(
+          "[![" +
+            item.filename +
+            "](" +
+            item.url.thumb +
+            ")](" +
+            item.url.index +
+            ")"
+        )
       } else {
-        replace('[![' + item.filename + '](' + item.url.index + ')](' + item.url.index + ')');
+        replace(
+          "[![" +
+            item.filename +
+            "](" +
+            item.url.index +
+            ")](" +
+            item.url.index +
+            ")"
+        )
       }
     } else {
-      replace('[' + item.filename + '](' + item.url.index + ')');
+      replace("[" + item.filename + "](" + item.url.index + ")")
     }
-  };
+  }
 
   onRemove = () => {
     this.updateItem({
       isRemoved: true
-    });
-  };
+    })
+  }
 
   onUndo = () => {
     this.updateItem({
       isRemoved: false
-    });
-  };
+    })
+  }
 
-  updateItem = (newState) => {
-    const updatedAttachments = this.props.attachments.map((item) => {
+  updateItem = newState => {
+    const updatedAttachments = this.props.attachments.map(item => {
       if (item.id === this.props.item.id) {
-        return Object.assign({}, item, newState);
+        return Object.assign({}, item, newState)
       } else {
-        return item;
+        return item
       }
-    });
-    this.props.onAttachmentsChange(updatedAttachments);
-  };
+    })
+    this.props.onAttachmentsChange(updatedAttachments)
+  }
 
   render() {
     return (
@@ -71,44 +86,38 @@ export default class extends React.Component {
           </div>
         </div>
       </li>
-    );
+    )
   }
-};
+}
 
 export function Preview(props) {
   if (props.item.is_image) {
-    return (
-      <Image {...props} />
-    );
+    return <Image {...props} />
   } else {
-    return (
-      <Icon {...props} />
-    );
+    return <Icon {...props} />
   }
 }
 
 export function Image(props) {
-  const thumbnailUrl = props.item.url.thumb || props.item.url.index;
+  const thumbnailUrl = props.item.url.thumb || props.item.url.index
 
   return (
     <div className="editor-attachment-image">
       <a
-        href={props.item.url.index + '?shva=1'}
-        style={{backgroundImage: "url('" + thumbnailUrl + "?shva=1')"}}
+        href={props.item.url.index + "?shva=1"}
+        style={{ backgroundImage: "url('" + thumbnailUrl + "?shva=1')" }}
         target="_blank"
       />
     </div>
-  );
-};
+  )
+}
 
 export function Icon(props) {
   return (
     <div className="editor-attachment-icon">
-      <span className="material-icon">
-        insert_drive_file
-      </span>
+      <span className="material-icon">insert_drive_file</span>
     </div>
-  );
+  )
 }
 
 export function Filename(props) {
@@ -116,43 +125,61 @@ export function Filename(props) {
     <h4>
       <a
         className="item-title"
-        href={props.item.url.index + '?shva=1'}
+        href={props.item.url.index + "?shva=1"}
         target="_blank"
       >
         {props.item.filename}
       </a>
     </h4>
-  );
+  )
 }
 
 export function Details(props) {
-  let user = null;
+  let user = null
   if (props.item.url.uploader) {
-    user = interpolate(USER_URL, {
-      url: escapeHtml(props.item.url.uploader),
-      user: escapeHtml(props.item.uploader_name)
-    }, true);
+    user = interpolate(
+      USER_URL,
+      {
+        url: escapeHtml(props.item.url.uploader),
+        user: escapeHtml(props.item.uploader_name)
+      },
+      true
+    )
   } else {
-    user = interpolate(USER_SPAN, {
-      user: escapeHtml(props.item.uploader_name)
-    }, true);
+    user = interpolate(
+      USER_SPAN,
+      {
+        user: escapeHtml(props.item.uploader_name)
+      },
+      true
+    )
   }
 
-  const date = interpolate(DATE_ABBR, {
-    absolute: escapeHtml(props.item.uploaded_on.format('LLL')),
-    relative: escapeHtml(props.item.uploaded_on.fromNow())
-  }, true);
-
-  const message = interpolate(escapeHtml(gettext("%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s.")), {
-    filetype: props.item.filetype,
-    size: formatFilesize(props.item.size),
-    uploader: user,
-    uploaded_on: date
-  }, true);
-
-  return (
-    <p dangerouslySetInnerHTML={{__html: message}} />
-  );
+  const date = interpolate(
+    DATE_ABBR,
+    {
+      absolute: escapeHtml(props.item.uploaded_on.format("LLL")),
+      relative: escapeHtml(props.item.uploaded_on.fromNow())
+    },
+    true
+  )
+
+  const message = interpolate(
+    escapeHtml(
+      gettext(
+        "%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s."
+      )
+    ),
+    {
+      filetype: props.item.filetype,
+      size: formatFilesize(props.item.size),
+      uploader: user,
+      uploaded_on: date
+    },
+    true
+  )
+
+  return <p dangerouslySetInnerHTML={{ __html: message }} />
 }
 
 export function Actions(props) {
@@ -164,12 +191,12 @@ export function Actions(props) {
         <Undo {...props} />
       </div>
     </div>
-  );
+  )
 }
 
 export function Insert(props) {
   if (!!props.item.isRemoved) {
-    return null;
+    return null
   }
 
   return (
@@ -182,12 +209,12 @@ export function Insert(props) {
         {gettext("Insert")}
       </button>
     </div>
-  );
+  )
 }
 
 export function Remove(props) {
   if (!!props.item.isRemoved && props.item.acl.can_delete) {
-    return null;
+    return null
   }
 
   return (
@@ -200,12 +227,12 @@ export function Remove(props) {
         {gettext("Remove")}
       </button>
     </div>
-  );
+  )
 }
 
 export function Undo(props) {
   if (!props.item.isRemoved) {
-    return null;
+    return null
   }
 
   return (
@@ -218,5 +245,5 @@ export function Undo(props) {
         {gettext("Undo removal")}
       </button>
     </div>
-  );
+  )
 }

+ 27 - 22
frontend/src/components/editor/attachments/attachment/error.js

@@ -1,36 +1,41 @@
-// jshint ignore:start
-import React from 'react';
-import escapeHtml from 'misago/utils/escape-html';
+import React from "react"
+import escapeHtml from "misago/utils/escape-html"
 
-const STRONG = '<strong>%(name)s</strong>';
+const STRONG = "<strong>%(name)s</strong>"
 
 export default class extends React.Component {
   onClick = () => {
-    const filteredAttachments = this.props.attachments.filter((item) => {
-      return item.key !== this.props.item.key;
-    });
-    this.props.onAttachmentsChange(filteredAttachments);
-  };
+    const filteredAttachments = this.props.attachments.filter(item => {
+      return item.key !== this.props.item.key
+    })
+    this.props.onAttachmentsChange(filteredAttachments)
+  }
 
   render() {
-    const filename = interpolate(STRONG, {
-      name: escapeHtml(this.props.item.filename)
-    }, true);
+    const filename = interpolate(
+      STRONG,
+      {
+        name: escapeHtml(this.props.item.filename)
+      },
+      true
+    )
 
-    const title = interpolate(gettext("Error uploading %(filename)s"), {
-      filename,
-      progress: this.props.item.progress + '%'
-    }, true);
+    const title = interpolate(
+      gettext("Error uploading %(filename)s"),
+      {
+        filename,
+        progress: this.props.item.progress + "%"
+      },
+      true
+    )
 
     return (
       <li className="editor-attachment-error">
         <div className="editor-attachment-error-icon">
-          <span className="material-icon">
-            warning
-          </span>
+          <span className="material-icon">warning</span>
         </div>
         <div className="editor-attachment-error-message">
-          <h4 dangerouslySetInnerHTML={{__html: title + ':'}} />
+          <h4 dangerouslySetInnerHTML={{ __html: title + ":" }} />
           <p>{this.props.item.error}</p>
           <button
             className="btn btn-default btn-sm"
@@ -41,6 +46,6 @@ export default class extends React.Component {
           </button>
         </div>
       </li>
-    );
+    )
   }
-};
+}

+ 10 - 17
frontend/src/components/editor/attachments/attachment/index.js

@@ -1,25 +1,18 @@
-// jshint ignore:start
-import React from 'react';
-import AttachmentComplete from './complete';
-import AttachmentError from './error';
-import AttachmentUpload from './upload';
-import misago from 'misago';
-import escapeHtml from 'misago/utils/escape-html';
+import React from "react"
+import AttachmentComplete from "./complete"
+import AttachmentError from "./error"
+import AttachmentUpload from "./upload"
+import misago from "misago"
+import escapeHtml from "misago/utils/escape-html"
 
 export default function(props) {
   if (props.item.id) {
-    return (
-      <AttachmentComplete {...props} />
-    );
+    return <AttachmentComplete {...props} />
   }
 
   if (props.item.error) {
-    return (
-      <AttachmentError {...props} />
-    );
+    return <AttachmentError {...props} />
   }
 
-  return (
-    <AttachmentUpload {...props} />
-  );
-}
+  return <AttachmentUpload {...props} />
+}

+ 22 - 15
frontend/src/components/editor/attachments/attachment/upload.js

@@ -1,31 +1,38 @@
-// jshint ignore:start
-import React from 'react';
-import escapeHtml from 'misago/utils/escape-html';
+import React from "react"
+import escapeHtml from "misago/utils/escape-html"
 
-const STRONG = '<strong>%(name)s</strong>';
+const STRONG = "<strong>%(name)s</strong>"
 
 export default function(props) {
-  const filename = interpolate(STRONG, {
-    name: escapeHtml(props.item.filename)
-  }, true);
+  const filename = interpolate(
+    STRONG,
+    {
+      name: escapeHtml(props.item.filename)
+    },
+    true
+  )
 
-  const message = interpolate(gettext("Uploading %(filename)s... %(progress)s"), {
-    filename,
-    progress: props.item.progress + '%'
-  }, true);
+  const message = interpolate(
+    gettext("Uploading %(filename)s... %(progress)s"),
+    {
+      filename,
+      progress: props.item.progress + "%"
+    },
+    true
+  )
 
   return (
     <li className="editor-attachment-upload">
       <div className="editor-attachment-progress-bar">
         <div
           className="editor-attachment-progress"
-          style={{width: props.item.progress + '%'}}
+          style={{ width: props.item.progress + "%" }}
         />
       </div>
       <p
         className="editor-attachment-upload-message"
-        dangerouslySetInnerHTML={{__html: message}}
+        dangerouslySetInnerHTML={{ __html: message }}
       />
     </li>
-  );
-};
+  )
+}

+ 8 - 9
frontend/src/components/editor/attachments/index.js

@@ -1,12 +1,11 @@
-// jshint ignore:start
-import React from 'react';
-import List from './list';
-import Uploader from './uploader';
-import misago from 'misago';
+import React from "react"
+import List from "./list"
+import Uploader from "./uploader"
+import misago from "misago"
 
 export default function(props) {
-  if (!misago.get('user').acl.max_attachment_size) {
-    return null;
+  if (!misago.get("user").acl.max_attachment_size) {
+    return null
   }
 
   return (
@@ -14,5 +13,5 @@ export default function(props) {
       <List {...props} />
       <Uploader {...props} />
     </div>
-  );
-};
+  )
+}

+ 6 - 9
frontend/src/components/editor/attachments/list.js

@@ -1,15 +1,12 @@
-// jshint ignore:start
-import React from 'react';
-import Attachment from './attachment';
+import React from "react"
+import Attachment from "./attachment"
 
 export default function(props) {
   return (
     <ul className="list-unstyled editor-attachments-list">
-      {props.attachments.map((item) => {
-        return (
-          <Attachment item={item} key={item.id || item.key} {...props} />
-        );
+      {props.attachments.map(item => {
+        return <Attachment item={item} key={item.id || item.key} {...props} />
       })}
     </ul>
-  );
-};
+  )
+}

+ 11 - 14
frontend/src/components/editor/attachments/upload-button.js

@@ -1,29 +1,26 @@
-// jshint ignore:start
-import React from 'react';
-import misago from 'misago';
+import React from "react"
+import misago from "misago"
 
 export default class extends React.Component {
   onClick = () => {
-    document.getElementById('editor-upload-field').click();
-  };
+    document.getElementById("editor-upload-field").click()
+  }
 
   render() {
-    if (!misago.get('user').acl.max_attachment_size) {
-      return null;
+    if (!misago.get("user").acl.max_attachment_size) {
+      return null
     }
 
     return (
       <button
-        className={'btn btn-icon ' + this.props.className}
+        className={"btn btn-icon " + this.props.className}
         disabled={this.props.disabled}
         onClick={this.onClick}
-        title={gettext('Upload file')}
+        title={gettext("Upload file")}
         type="button"
       >
-        <span className="material-icon">
-          file_upload
-        </span>
+        <span className="material-icon">file_upload</span>
       </button>
-    );
+    )
   }
-};
+}

+ 38 - 38
frontend/src/components/editor/attachments/uploader.js

@@ -1,15 +1,14 @@
-// jshint ignore:start
-import React from 'react';
-import moment from 'moment';
-import misago from 'misago';
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
+import React from "react"
+import moment from "moment"
+import misago from "misago"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
 
 export default class extends React.Component {
-  onChange = (event) => {
-    const file = event.target.files[0];
+  onChange = event => {
+    const file = event.target.files[0]
     if (!file) {
-      return;
+      return
     }
 
     let upload = {
@@ -18,41 +17,42 @@ export default class extends React.Component {
       progress: 0,
       error: null,
       filename: file.name
-    };
+    }
 
-    this.props.onAttachmentsChange([upload].concat(this.props.attachments));
+    this.props.onAttachmentsChange([upload].concat(this.props.attachments))
 
-    const data = new FormData();
-    data.append('upload', file);
+    const data = new FormData()
+    data.append("upload", file)
 
-    ajax.upload(misago.get('ATTACHMENTS_API'), data, (progress) => {
-      upload.progress = progress;
-      this.props.onAttachmentsChange(this.props.attachments.concat());
-    }).then((data) => {
-      data.uploaded_on = moment(data.uploaded_on);
-      Object.assign(upload, data);
-      this.props.onAttachmentsChange(this.props.attachments.concat());
-    }, (rejection) => {
-      if (rejection.status === 400 || rejection.status === 413) {
-        upload.error = rejection.detail;
-        this.props.onAttachmentsChange(this.props.attachments.concat());
-      } else {
-        snackbar.apiError(rejection);
-      }
-    });
-  };
+    ajax
+      .upload(misago.get("ATTACHMENTS_API"), data, progress => {
+        upload.progress = progress
+        this.props.onAttachmentsChange(this.props.attachments.concat())
+      })
+      .then(
+        data => {
+          data.uploaded_on = moment(data.uploaded_on)
+          Object.assign(upload, data)
+          this.props.onAttachmentsChange(this.props.attachments.concat())
+        },
+        rejection => {
+          if (rejection.status === 400 || rejection.status === 413) {
+            upload.error = rejection.detail
+            this.props.onAttachmentsChange(this.props.attachments.concat())
+          } else {
+            snackbar.apiError(rejection)
+          }
+        }
+      )
+  }
 
   render() {
     return (
-      <input
-        id="editor-upload-field"
-        onChange={this.onChange}
-        type="file"
-      />
-    );
+      <input id="editor-upload-field" onChange={this.onChange} type="file" />
+    )
   }
-};
+}
 
 export function getRandomKey() {
-  return 'upld-' + Math.round(new Date().getTime());
-}
+  return "upld-" + Math.round(new Date().getTime())
+}

+ 63 - 65
frontend/src/components/editor/index.js

@@ -1,91 +1,91 @@
-// jshint ignore:start
-import React from 'react';
-import Code from './actions/code';
-import Emphasis from './actions/emphasis';
-import Hr from './actions/hr';
-import Image from './actions/image';
-import Link from './actions/link';
-import Striketrough from './actions/striketrough';
-import Strong from './actions/strong';
-import Quote from './actions/quote';
-import AttachmentsEditor from './attachments';
-import Upload from './attachments/upload-button';
-import MarkupPreview from './markup-preview';
-import * as textUtils from './textutils';
-import Button from 'misago/components/button';
-import misago from 'misago';
-import ajax from 'misago/services/ajax';
-import modal from 'misago/services/modal';
-import snackbar from 'misago/services/snackbar';
+import React from "react"
+import Code from "./actions/code"
+import Emphasis from "./actions/emphasis"
+import Hr from "./actions/hr"
+import Image from "./actions/image"
+import Link from "./actions/link"
+import Striketrough from "./actions/striketrough"
+import Strong from "./actions/strong"
+import Quote from "./actions/quote"
+import AttachmentsEditor from "./attachments"
+import Upload from "./attachments/upload-button"
+import MarkupPreview from "./markup-preview"
+import * as textUtils from "./textutils"
+import Button from "misago/components/button"
+import misago from "misago"
+import ajax from "misago/services/ajax"
+import modal from "misago/services/modal"
+import snackbar from "misago/services/snackbar"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isPreviewLoading: false
-    };
+    }
   }
 
   componentDidMount() {
-    $('#editor-textarea').atwho({
-      at: '@',
+    $("#editor-textarea").atwho({
+      at: "@",
       displayTpl: '<li><img src="${avatar}" alt="">${username}</li>',
-      insertTpl: '@${username}',
-      searchKey : 'username',
+      insertTpl: "@${username}",
+      searchKey: "username",
       callbacks: {
         remoteFilter: function(query, callback) {
-          $.getJSON(misago.get('MENTION_API'), {q: query}, callback);
+          $.getJSON(misago.get("MENTION_API"), { q: query }, callback)
         }
       }
-    });
+    })
 
-    $('#editor-textarea').on("inserted.atwho", (event, flag, query) => {
-      this.props.onChange(event);
-    });
+    $("#editor-textarea").on("inserted.atwho", (event, flag, query) => {
+      this.props.onChange(event)
+    })
   }
 
   onPreviewClick = () => {
     if (this.state.isPreviewLoading) {
-      return;
+      return
     }
 
     this.setState({
       isPreviewLoading: true
-    });
+    })
 
-    ajax.post(misago.get('PARSE_MARKUP_API'), {post: this.props.value}).then((data) => {
-      modal.show(
-        <MarkupPreview markup={data.parsed} />
-      );
+    ajax.post(misago.get("PARSE_MARKUP_API"), { post: this.props.value }).then(
+      data => {
+        modal.show(<MarkupPreview markup={data.parsed} />)
 
-      this.setState({
-        isPreviewLoading: false
-      });
-    }, (rejection) => {
-      if (rejection.status === 400) {
-        snackbar.error(rejection.detail);
-      } else {
-        snackbar.apiError(rejection);
-      }
+        this.setState({
+          isPreviewLoading: false
+        })
+      },
+      rejection => {
+        if (rejection.status === 400) {
+          snackbar.error(rejection.detail)
+        } else {
+          snackbar.apiError(rejection)
+        }
 
-      this.setState({
-        isPreviewLoading: false
-      });
-    });
-  };
+        this.setState({
+          isPreviewLoading: false
+        })
+      }
+    )
+  }
 
-  replaceSelection = (operation) => {
-    operation(textUtils.getSelectionText(), this._replaceSelection);
-  };
+  replaceSelection = operation => {
+    operation(textUtils.getSelectionText(), this._replaceSelection)
+  }
 
-  _replaceSelection = (newValue) => {
+  _replaceSelection = newValue => {
     this.props.onChange({
       target: {
         value: textUtils.replace(newValue)
       }
-    });
-  };
+    })
+  }
 
   render() {
     return (
@@ -97,7 +97,7 @@ export default class extends React.Component {
           id="editor-textarea"
           onChange={this.props.onChange}
           rows="9"
-        ></textarea>
+        />
         <div className="editor-footer">
           <div className="buttons-list pull-left">
             <Strong
@@ -183,14 +183,14 @@ export default class extends React.Component {
           replaceSelection={this.replaceSelection}
         />
       </div>
-    );
+    )
   }
 }
 
 export function Protect(props) {
-  if (!props.canProtect) return null;
+  if (!props.canProtect) return null
 
-  const label = props.protect ? gettext('Protected') : gettext('Protect');
+  const label = props.protect ? gettext("Protected") : gettext("Protect")
 
   return (
     <button
@@ -201,11 +201,9 @@ export function Protect(props) {
       type="button"
     >
       <span className="material-icon">
-        {props.protect ? 'lock' : 'lock_outline'}
-      </span>
-      <span className="btn-text hidden-md hidden-lg">
-        {label}
+        {props.protect ? "lock" : "lock_outline"}
       </span>
+      <span className="btn-text hidden-md hidden-lg">{label}</span>
     </button>
-  );
+  )
 }

+ 4 - 5
frontend/src/components/editor/markup-preview.js

@@ -1,6 +1,5 @@
-// jshint ignore:start
-import React from 'react';
-import MisagoMarkup from 'misago/components/misago-markup';
+import React from "react"
+import MisagoMarkup from "misago/components/misago-markup"
 
 export default function(props) {
   return (
@@ -22,5 +21,5 @@ export default function(props) {
         </div>
       </div>
     </div>
-  );
-}
+  )
+}

+ 35 - 30
frontend/src/components/editor/textutils.js

@@ -1,62 +1,67 @@
-export const textareaId = 'editor-textarea';
+export const textareaId = "editor-textarea"
 
 export function getTextarea() {
-  return document.getElementById(textareaId);
+  return document.getElementById(textareaId)
 }
 
 export function getValue() {
-  return document.getElementById(textareaId).value;
+  return document.getElementById(textareaId).value
 }
 
 export function getSelectionRange(start, end) {
   return {
     start,
     end
-  };
+  }
 }
 
 export function getSelection() {
-  const ctrl = getTextarea();
+  const ctrl = getTextarea()
   if (document.selection) {
-    ctrl.focus();
-    const range = document.selection.createRange();
-    const length = range.text.length;
-    range.moveStart('character', -ctrl.value.length);
-    return getSelectionRange(range.text.length - length, range.text.length);
-  } else if (ctrl.selectionStart || ctrl.selectionStart == '0') {
-    return getSelectionRange(ctrl.selectionStart, ctrl.selectionEnd);
+    ctrl.focus()
+    const range = document.selection.createRange()
+    const length = range.text.length
+    range.moveStart("character", -ctrl.value.length)
+    return getSelectionRange(range.text.length - length, range.text.length)
+  } else if (ctrl.selectionStart || ctrl.selectionStart == "0") {
+    return getSelectionRange(ctrl.selectionStart, ctrl.selectionEnd)
   }
 }
 
 export function getSelectionText() {
-  const range = getSelection();
-  return $.trim(getValue().substring(range.start, range.end));
+  const range = getSelection()
+  return $.trim(getValue().substring(range.start, range.end))
 }
 
-
 export function setSelection(selectionRange) {
-  const ctrl = getTextarea();
+  const ctrl = getTextarea()
   if (ctrl.setSelectionRange) {
-    ctrl.focus();
-    ctrl.setSelectionRange(selectionRange.start, selectionRange.end);
+    ctrl.focus()
+    ctrl.setSelectionRange(selectionRange.start, selectionRange.end)
   } else if (ctrl.createTextRange) {
-    const range = ctrl.createTextRange();
-    range.collapse(true);
-    range.moveStart('character', selectionRange.start);
-    range.moveEnd('character', selectionRange.end);
-    range.select();
+    const range = ctrl.createTextRange()
+    range.collapse(true)
+    range.moveStart("character", selectionRange.start)
+    range.moveEnd("character", selectionRange.end)
+    range.select()
   }
 }
 
 export function _replace(myRange, replacement) {
-  const ctrl = getTextarea();
-  const text = ctrl.value;
-  const startText = text.substring(0, myRange.start);
-  ctrl.value = text.substring(0, myRange.start) + replacement + text.substring(myRange.end);
-  setSelection(getSelectionRange(startText.length + replacement.length, startText.length + replacement.length));
-  return ctrl.value;
+  const ctrl = getTextarea()
+  const text = ctrl.value
+  const startText = text.substring(0, myRange.start)
+  ctrl.value =
+    text.substring(0, myRange.start) + replacement + text.substring(myRange.end)
+  setSelection(
+    getSelectionRange(
+      startText.length + replacement.length,
+      startText.length + replacement.length
+    )
+  )
+  return ctrl.value
 }
 
 export function replace(replacement) {
-  return _replace(getSelection(), replacement);
+  return _replace(getSelection(), replacement)
 }

+ 39 - 39
frontend/src/components/form-group.js

@@ -1,74 +1,74 @@
-import React from 'react';
+import React from "react"
 
 export default class extends React.Component {
   isValidated() {
-    return typeof this.props.validation !== "undefined";
+    return typeof this.props.validation !== "undefined"
   }
 
   getClassName() {
-    let className = 'form-group';
+    let className = "form-group"
     if (this.isValidated()) {
-      className += ' has-feedback';
+      className += " has-feedback"
       if (this.props.validation === null) {
-        className += ' has-success';
+        className += " has-success"
       } else {
-        className += ' has-error';
+        className += " has-error"
       }
     }
-    return className;
+    return className
   }
 
   getFeedback() {
     if (this.props.validation) {
-      /* jshint ignore:start */
-      return <div className="help-block errors">
-        {this.props.validation.map((error, i) => {
-          return <p key={this.props.for + 'FeedbackItem' + i}>{error}</p>;
-        })}
-      </div>;
-      /* jshint ignore:end */
+      return (
+        <div className="help-block errors">
+          {this.props.validation.map((error, i) => {
+            return <p key={this.props.for + "FeedbackItem" + i}>{error}</p>
+          })}
+        </div>
+      )
     } else {
-      return null;
+      return null
     }
   }
 
   getFeedbackDescription() {
     if (this.isValidated()) {
-      /* jshint ignore:start */
-      return <span id={this.props.for + '_status'} className="sr-only">
-        {this.props.validation ? gettext('(error)') : gettext('(success)')}
-      </span>;
-      /* jshint ignore:end */
+      return (
+        <span id={this.props.for + "_status"} className="sr-only">
+          {this.props.validation ? gettext("(error)") : gettext("(success)")}
+        </span>
+      )
     } else {
-      return null;
+      return null
     }
   }
 
   getHelpText() {
     if (this.props.helpText) {
-      /* jshint ignore:start */
-      return <p className="help-block">{this.props.helpText}</p>;
-      /* jshint ignore:end */
+      return <p className="help-block">{this.props.helpText}</p>
     } else {
-      return null;
+      return null
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className={this.getClassName()}>
-      <label className={'control-label ' + (this.props.labelClass || '')}
-             htmlFor={this.props.for || ''}>
-        {this.props.label + ':'}
-      </label>
-      <div className={this.props.controlClass || ''}>
-        {this.props.children}
-        {this.getFeedbackDescription()}
-        {this.getFeedback()}
-        {this.getHelpText()}
-        {this.props.extra || null}
+    return (
+      <div className={this.getClassName()}>
+        <label
+          className={"control-label " + (this.props.labelClass || "")}
+          htmlFor={this.props.for || ""}
+        >
+          {this.props.label + ":"}
+        </label>
+        <div className={this.props.controlClass || ""}>
+          {this.props.children}
+          {this.getFeedbackDescription()}
+          {this.getFeedback()}
+          {this.getHelpText()}
+          {this.props.extra || null}
+        </div>
       </div>
-    </div>
-    /* jshint ignore:end */
+    )
   }
 }

+ 69 - 64
frontend/src/components/form.js

@@ -1,168 +1,173 @@
-import React from 'react';
-import { required } from 'misago/utils/validators';
-import snackbar from 'misago/services/snackbar'; // jshint ignore:line
+import React from "react"
+import { required } from "misago/utils/validators"
+import snackbar from "misago/services/snackbar"
 
-let validateRequired = required();
+let validateRequired = required()
 
 export default class extends React.Component {
   validate() {
-    let errors = {};
+    let errors = {}
     if (!this.state.validators) {
-      return errors;
+      return errors
     }
 
     let validators = {
       required: this.state.validators.required || this.state.validators,
       optional: this.state.validators.optional || {}
-    };
+    }
 
-    let validatedFields = [];
+    let validatedFields = []
 
     // add required fields to validation
     for (let name in validators.required) {
-      if (validators.required.hasOwnProperty(name) &&
-          validators.required[name]) {
-        validatedFields.push(name);
+      if (
+        validators.required.hasOwnProperty(name) &&
+        validators.required[name]
+      ) {
+        validatedFields.push(name)
       }
     }
 
     // add optional fields to validation
     for (let name in validators.optional) {
-      if (validators.optional.hasOwnProperty(name) &&
-          validators.optional[name]) {
-        validatedFields.push(name);
+      if (
+        validators.optional.hasOwnProperty(name) &&
+        validators.optional[name]
+      ) {
+        validatedFields.push(name)
       }
     }
 
     // validate fields values
     for (let i in validatedFields) {
-      let name = validatedFields[i];
-      let fieldErrors = this.validateField(name, this.state[name]);
+      let name = validatedFields[i]
+      let fieldErrors = this.validateField(name, this.state[name])
 
       if (fieldErrors === null) {
-        errors[name] = null;
+        errors[name] = null
       } else if (fieldErrors) {
-        errors[name] = fieldErrors;
+        errors[name] = fieldErrors
       }
     }
 
-    return errors;
+    return errors
   }
 
   isValid() {
-    let errors = this.validate();
+    let errors = this.validate()
     for (let field in errors) {
       if (errors.hasOwnProperty(field)) {
         if (errors[field] !== null) {
-          return false;
+          return false
         }
       }
     }
 
-    return true;
+    return true
   }
 
   validateField(name, value) {
-    let errors = [];
+    let errors = []
     if (!this.state.validators) {
-      return errors;
+      return errors
     }
 
     let validators = {
       required: (this.state.validators.required || this.state.validators)[name],
       optional: (this.state.validators.optional || {})[name]
-    };
+    }
 
-    let requiredError = validateRequired(value) || false;
+    let requiredError = validateRequired(value) || false
 
     if (validators.required) {
       if (requiredError) {
-        errors = [requiredError];
+        errors = [requiredError]
       } else {
         for (let i in validators.required) {
-          let validationError = validators.required[i](value);
+          let validationError = validators.required[i](value)
           if (validationError) {
-            errors.push(validationError);
+            errors.push(validationError)
           }
         }
       }
 
-      return errors.length ? errors : null;
+      return errors.length ? errors : null
     } else if (requiredError === false && validators.optional) {
       for (let i in validators.optional) {
-        let validationError = validators.optional[i](value);
+        let validationError = validators.optional[i](value)
         if (validationError) {
-          errors.push(validationError);
+          errors.push(validationError)
         }
       }
 
-      return errors.length ? errors : null;
+      return errors.length ? errors : null
     }
 
-    return false; // false === field wasn't validated
+    return false // false === field wasn't validated
   }
 
-  /* jshint ignore:start */
-  bindInput = (name) => {
-    return (event) => {
-      this.changeValue(name, event.target.value);
+  bindInput = name => {
+    return event => {
+      this.changeValue(name, event.target.value)
     }
-  };
+  }
 
   changeValue = (name, value) => {
     let newState = {
       [name]: value
-    };
+    }
 
-    const formErrors = this.state.errors || {};
-    formErrors[name] = this.validateField(name, newState[name]);
-    newState.errors = formErrors;
+    const formErrors = this.state.errors || {}
+    formErrors[name] = this.validateField(name, newState[name])
+    newState.errors = formErrors
 
-    this.setState(newState);
-  };
+    this.setState(newState)
+  }
 
   clean() {
-    return true;
+    return true
   }
 
   send() {
-    return null;
+    return null
   }
 
   handleSuccess(success) {
-    return;
+    return
   }
 
   handleError(rejection) {
-    snackbar.apiError(rejection);
+    snackbar.apiError(rejection)
   }
 
-  handleSubmit = (event) => {
+  handleSubmit = event => {
     // we don't reload page on submissions
     if (event) {
       event.preventDefault()
     }
 
     if (this.state.isLoading) {
-      return;
+      return
     }
 
     if (this.clean()) {
-      this.setState({isLoading: true});
-      let promise = this.send();
+      this.setState({ isLoading: true })
+      let promise = this.send()
 
       if (promise) {
-        promise.then((success) => {
-          this.setState({isLoading: false});
-          this.handleSuccess(success);
-        }, (rejection) => {
-          this.setState({isLoading: false});
-          this.handleError(rejection);
-        });
+        promise.then(
+          success => {
+            this.setState({ isLoading: false })
+            this.handleSuccess(success)
+          },
+          rejection => {
+            this.setState({ isLoading: false })
+            this.handleError(rejection)
+          }
+        )
       } else {
-        this.setState({isLoading: false});
+        this.setState({ isLoading: false })
       }
     }
-  };
-  /* jshint ignore:end */
-}
+  }
+}

+ 12 - 12
frontend/src/components/li.js

@@ -1,31 +1,31 @@
-import React from 'react';
+import React from "react"
 
 export default class extends React.Component {
   isActive() {
     if (this.props.isControlled) {
-      return this.props.isActive;
+      return this.props.isActive
     } else {
       if (this.props.path) {
-        return document.location.pathname.indexOf(this.props.path) === 0;
+        return document.location.pathname.indexOf(this.props.path) === 0
       } else {
-        return false;
+        return false
       }
     }
   }
 
   getClassName() {
     if (this.isActive()) {
-      return (this.props.className || '') + ' '+ (this.props.activeClassName || 'active');
+      return (
+        (this.props.className || "") +
+        " " +
+        (this.props.activeClassName || "active")
+      )
     } else {
-      return this.props.className || '';
+      return this.props.className || ""
     }
   }
 
   render() {
-    // jshint ignore:start
-    return <li className={this.getClassName()}>
-      {this.props.children}
-    </li>;
-    // jshint ignore:end
+    return <li className={this.getClassName()}>{this.props.children}</li>
   }
-}
+}

+ 3 - 4
frontend/src/components/loader.js

@@ -1,10 +1,9 @@
-/* jshint ignore:start */
-import React from 'react';
+import React from "react"
 
 export default function(props) {
   return (
     <div className={props.className || "loader"}>
-      <div className="loader-spinning-wheel"></div>
+      <div className="loader-spinning-wheel" />
     </div>
-  );
+  )
 }

+ 47 - 42
frontend/src/components/merge-conflict.js

@@ -1,57 +1,58 @@
-// jshint ignore:start
-import React from 'react';
-import Button from './button';
-import Form from './form';
-import FormGroup from './form-group';
-import ajax from 'misago/services/ajax';
-import modal from 'misago/services/modal';
+import React from "react"
+import Button from "./button"
+import Form from "./form"
+import FormGroup from "./form-group"
+import ajax from "misago/services/ajax"
+import modal from "misago/services/modal"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoading: false,
 
-      bestAnswer: '0',
-      poll: '0',
-    };
+      bestAnswer: "0",
+      poll: "0"
+    }
   }
 
   clean() {
-    if (this.props.polls && this.state.poll === '0') {
-      const confirmation = confirm(gettext("Are you sure you want to delete all polls?"));
+    if (this.props.polls && this.state.poll === "0") {
+      const confirmation = confirm(
+        gettext("Are you sure you want to delete all polls?")
+      )
       return confirmation
     }
 
-    return true;
+    return true
   }
 
   send() {
     const data = Object.assign({}, this.props.data, {
       best_answer: this.state.bestAnswer,
       poll: this.state.poll
-    });
+    })
 
-    return ajax.post(this.props.api, data);
+    return ajax.post(this.props.api, data)
   }
 
-  handleSuccess = (success) => {
-    this.props.onSuccess(success);
-    modal.hide();
-  };
+  handleSuccess = success => {
+    this.props.onSuccess(success)
+    modal.hide()
+  }
 
-  handleError = (rejection) => {
-    this.props.onError(rejection);
-  };
+  handleError = rejection => {
+    this.props.onError(rejection)
+  }
 
-  onBestAnswerChange = (event) => {
-    this.changeValue('bestAnswer', event.target.value);
-  };
+  onBestAnswerChange = event => {
+    this.changeValue("bestAnswer", event.target.value)
+  }
 
-  onPollChange = (event) => {
-    this.changeValue('poll', event.target.value);
-  };
+  onPollChange = event => {
+    this.changeValue("poll", event.target.value)
+  }
 
   render() {
     return (
@@ -97,17 +98,19 @@ export default class extends Form {
           </form>
         </div>
       </div>
-    );
+    )
   }
 }
 
-export function BestAnswerSelect({choices, onChange, value}) {
-  if (!choices) return null;
+export function BestAnswerSelect({ choices, onChange, value }) {
+  if (!choices) return null
 
   return (
     <FormGroup
       label={gettext("Best answer")}
-      helpText={gettext("Please select the best answer for your newly merged thread. No posts will be deleted during the merge.")}
+      helpText={gettext(
+        "Please select the best answer for your newly merged thread. No posts will be deleted during the merge."
+      )}
       for="id_best_answer"
     >
       <select
@@ -116,25 +119,27 @@ export function BestAnswerSelect({choices, onChange, value}) {
         onChange={onChange}
         value={value}
       >
-        {choices.map((choice) => {
+        {choices.map(choice => {
           return (
             <option value={choice[0]} key={choice[0]}>
               {choice[1]}
             </option>
-          );
+          )
         })}
       </select>
     </FormGroup>
-  );
+  )
 }
 
 export function PollSelect({ choices, onChange, value }) {
-  if (!choices) return null;
+  if (!choices) return null
 
   return (
     <FormGroup
       label={gettext("Poll")}
-      helpText={gettext("Please select the poll for your newly merged thread. Rejected polls will be permanently deleted and cannot be recovered.")}
+      helpText={gettext(
+        "Please select the poll for your newly merged thread. Rejected polls will be permanently deleted and cannot be recovered."
+      )}
       for="id_poll"
     >
       <select
@@ -143,14 +148,14 @@ export function PollSelect({ choices, onChange, value }) {
         onChange={onChange}
         value={value}
       >
-        {choices.map((choice) => {
+        {choices.map(choice => {
           return (
             <option value={choice[0]} key={choice[0]}>
               {choice[1]}
             </option>
-          );
+          )
         })}
       </select>
     </FormGroup>
-  );
-}
+  )
+}

+ 11 - 11
frontend/src/components/misago-markup.js

@@ -1,28 +1,28 @@
-// jshint ignore:start
-import React from 'react';
-import onebox from 'misago/services/one-box';
-
+import React from "react"
+import onebox from "misago/services/one-box"
 
 export default class extends React.Component {
   componentDidMount() {
-    onebox.render(this.documentNode);
+    onebox.render(this.documentNode)
   }
 
   componentDidUpdate(prevProps, prevState) {
-    onebox.render(this.documentNode);
+    onebox.render(this.documentNode)
   }
 
   shouldComponentUpdate(nextProps, nextState) {
-    return nextProps.markup !== this.props.markup;
+    return nextProps.markup !== this.props.markup
   }
 
   render() {
     return (
       <article
         className="misago-markup"
-        dangerouslySetInnerHTML={{__html: this.props.markup}}
-        ref={(node) => { this.documentNode = node; }}
+        dangerouslySetInnerHTML={{ __html: this.props.markup }}
+        ref={node => {
+          this.documentNode = node
+        }}
       />
-    );
+    )
   }
-}
+}

+ 7 - 8
frontend/src/components/modal-loader.js

@@ -1,13 +1,12 @@
-import React from 'react';
-import Loader from 'misago/components/loader'; // jshint ignore:line
+import React from "react"
+import Loader from "misago/components/loader"
 
 export default class extends React.Component {
   render() {
-    /* jshint ignore:start */
-    return <div className="modal-body modal-loader">
-      <Loader />
-    </div>;
-    /* jshint ignore:end */
+    return (
+      <div className="modal-body modal-loader">
+        <Loader />
+      </div>
+    )
   }
 }
-

+ 24 - 30
frontend/src/components/modal-message.js

@@ -1,41 +1,35 @@
-import React from 'react'; // jshint ignore:line
-import PanelMessage from 'misago/components/panel-message';
+import React from "react"
+import PanelMessage from "misago/components/panel-message"
 
 export default class extends PanelMessage {
   getHelpText() {
     if (this.props.helpText) {
-      /* jshint ignore:start */
-      return <p className="help-block">
-        {this.props.helpText}
-      </p>;
-      /* jshint ignore:end */
+      return <p className="help-block">{this.props.helpText}</p>
     } else {
-      return null;
+      return null
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className="modal-body">
-      <div className="message-icon">
-        <span className="material-icon">
-          {this.props.icon || 'info_outline'}
-        </span>
+    return (
+      <div className="modal-body">
+        <div className="message-icon">
+          <span className="material-icon">
+            {this.props.icon || "info_outline"}
+          </span>
+        </div>
+        <div className="message-body">
+          <p className="lead">{this.props.message}</p>
+          {this.getHelpText()}
+          <button
+            className="btn btn-default"
+            data-dismiss="modal"
+            type="button"
+          >
+            {gettext("Ok")}
+          </button>
+        </div>
       </div>
-      <div className="message-body">
-        <p className="lead">
-          {this.props.message}
-        </p>
-        {this.getHelpText()}
-        <button
-          className="btn btn-default"
-          data-dismiss="modal"
-          type="button"
-        >
-          {gettext("Ok")}
-        </button>
-      </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 8 - 8
frontend/src/components/navbar-search/clean-results.js

@@ -1,14 +1,14 @@
-const MAX_RESULTS = 5;
+const MAX_RESULTS = 5
 
 export default function(data) {
-  const filtered = data.filter((section) => {
-    return section.results.count > 0;
-  });
+  const filtered = data.filter(section => {
+    return section.results.count > 0
+  })
 
-  return filtered.map((section) => {
+  return filtered.map(section => {
     return Object.assign({}, section, {
       count: section.results.count,
       results: section.results.results.slice(0, MAX_RESULTS)
-    });
-  });
-}
+    })
+  })
+}

+ 3 - 3
frontend/src/components/navbar-search/dropdown/constants.js

@@ -1,3 +1,3 @@
-export const HEADER = 'HEADER';
-export const RESULT = 'RESULT';
-export const FOOTER = 'FOOTER';
+export const HEADER = "HEADER"
+export const RESULT = "RESULT"
+export const FOOTER = "FOOTER"

+ 5 - 9
frontend/src/components/navbar-search/dropdown/dropdown-menu.js

@@ -1,17 +1,13 @@
-// jshint ignore:start
-import React from 'react';
-import Input from './input';
+import React from "react"
+import Input from "./input"
 
 export default function({ children, onChange, query }) {
   return (
     <ul className="dropdown-menu dropdown-search-results" role="menu">
       <li className="form-group">
-        <Input
-          value={query}
-          onChange={onChange}
-        />
+        <Input value={query} onChange={onChange} />
       </li>
       {children}
     </ul>
-  );
-}
+  )
+}

+ 3 - 4
frontend/src/components/navbar-search/dropdown/empty.js

@@ -1,10 +1,9 @@
-// jshint ignore:start
-import React from 'react';
+import React from "react"
 
 export default function() {
   return (
     <li className="dropdown-search-message">
       {gettext("Search returned no results.")}
     </li>
-  );
-}
+  )
+}

+ 13 - 13
frontend/src/components/navbar-search/dropdown/flatten-results.js

@@ -1,38 +1,38 @@
-import { HEADER, RESULT, FOOTER } from './constants';
+import { HEADER, RESULT, FOOTER } from "./constants"
 
 export default function(results) {
-  const flatlist = [];
-  flattenProviders(results, flatlist);
-  return flatlist;
+  const flatlist = []
+  flattenProviders(results, flatlist)
+  return flatlist
 }
 
 function flattenProviders(results, flatlist) {
-  const arrayLength = results.length;
+  const arrayLength = results.length
   for (var i = 0; i < arrayLength; i++) {
-    const provider = results[i];
+    const provider = results[i]
 
     flatlist.push({
       provider,
       type: HEADER
-    });
+    })
 
-    flattenProvider(provider, flatlist);
+    flattenProvider(provider, flatlist)
   }
 }
 
 function flattenProvider(provider, flatlist) {
-  const arrayLength = provider.results.length;
+  const arrayLength = provider.results.length
   for (var i = 0; i < arrayLength; i++) {
-    const result = provider.results[i];
+    const result = provider.results[i]
     flatlist.push({
       provider,
       result,
       type: RESULT
-    });
+    })
   }
 
   flatlist.push({
     provider,
     type: FOOTER
-  });
-}
+  })
+}

+ 19 - 22
frontend/src/components/navbar-search/dropdown/index.js

@@ -1,57 +1,54 @@
-// jshint ignore:start
-import React from 'react';
-import { RESULT } from './constants';
-import DropdownMenu from './dropdown-menu';
-import Empty from './empty';
-import Loader from './loader';
-import Result from './result';
-import flattenResults from './flatten-results';
+import React from "react"
+import { RESULT } from "./constants"
+import DropdownMenu from "./dropdown-menu"
+import Empty from "./empty"
+import Loader from "./loader"
+import Result from "./result"
+import flattenResults from "./flatten-results"
 
 export default function({ isLoading, onChange, results, query }) {
   if (!query.trim().length) {
-    return (
-      <DropdownMenu onChange={onChange} query={query} />
-    );
+    return <DropdownMenu onChange={onChange} query={query} />
   }
 
   if (results.length) {
-    const flatResults = flattenResults(results);
+    const flatResults = flattenResults(results)
 
     return (
       <DropdownMenu onChange={onChange} query={query}>
-        {flatResults.map((props) => {
-          const { type, provider, result } = props;
+        {flatResults.map(props => {
+          const { type, provider, result } = props
 
           if (type === RESULT) {
             return (
               <Result
-                key={[provider.id, type, result.id].join('_')}
+                key={[provider.id, type, result.id].join("_")}
                 {...props}
               />
-            );
+            )
           }
 
           return (
             <Result
-              key={[provider.id, type].join('_')}
+              key={[provider.id, type].join("_")}
               query={query}
               {...props}
             />
-          );
+          )
         })}
       </DropdownMenu>
-    );
+    )
   } else if (isLoading) {
     return (
       <DropdownMenu onChange={onChange} query={query}>
         <Loader />
       </DropdownMenu>
-    );
+    )
   }
 
   return (
     <DropdownMenu onChange={onChange} query={query}>
       <Empty />
     </DropdownMenu>
-  );
-}
+  )
+}

+ 3 - 4
frontend/src/components/navbar-search/dropdown/input.js

@@ -1,5 +1,4 @@
-// jshint ignore:start
-import React from 'react';
+import React from "react"
 
 export default function({ value, onChange }) {
   return (
@@ -14,5 +13,5 @@ export default function({ value, onChange }) {
       role="combobox"
       type="text"
     />
-  );
-}
+  )
+}

+ 4 - 5
frontend/src/components/navbar-search/dropdown/loader.js

@@ -1,11 +1,10 @@
-// jshint ignore:start
-import React from 'react';
-import Loader from 'misago/components/loader';
+import React from "react"
+import Loader from "misago/components/loader"
 
 export default function({ message }) {
   return (
     <li className="dropdown-search-loader">
       <Loader />
     </li>
-  );
-}
+  )
+}

+ 13 - 10
frontend/src/components/navbar-search/dropdown/result/footer.js

@@ -1,22 +1,25 @@
-// jshint ignore:start
-import React from 'react';
+import React from "react"
 
 export default function({ provider, query }) {
-  const url = provider.url + '?q=' + encodeURI(query);
+  const url = provider.url + "?q=" + encodeURI(query)
   const label = ngettext(
     'See full "%(provider)s" results page with %(count)s result.',
     'See full "%(provider)s" results page with %(count)s results.',
     provider.count
-  );
+  )
 
   return (
     <li className="dropdown-search-footer">
       <a href={url}>
-        {interpolate(label, {
-          count: provider.count,
-          provider: provider.name
-        }, true)}
+        {interpolate(
+          label,
+          {
+            count: provider.count,
+            provider: provider.name
+          },
+          true
+        )}
       </a>
     </li>
-  );
-}
+  )
+}

+ 3 - 8
frontend/src/components/navbar-search/dropdown/result/header.js

@@ -1,10 +1,5 @@
-// jshint ignore:start
-import React from 'react';
+import React from "react"
 
 export default function({ provider }) {
-  return (
-    <li className="dropdown-search-header">
-      {provider.name}
-    </li>
-  );
-}
+  return <li className="dropdown-search-header">{provider.name}</li>
+}

+ 9 - 16
frontend/src/components/navbar-search/dropdown/result/index.js

@@ -1,22 +1,15 @@
-// jshint ignore:start
-import React from 'react';
-import { HEADER, FOOTER } from '../constants';
-import Footer from './footer';
-import Header from './header';
-import Result from './result';
+import React from "react"
+import { HEADER, FOOTER } from "../constants"
+import Footer from "./footer"
+import Header from "./header"
+import Result from "./result"
 
 export default function({ provider, result, type, query }) {
   if (type === HEADER) {
-    return (
-      <Header provider={provider} />
-    );
+    return <Header provider={provider} />
   } else if (type === FOOTER) {
-    return (
-      <Footer provider={provider} query={query} />
-    );
+    return <Footer provider={provider} query={query} />
   }
 
-  return (
-    <Result provider={provider} result={result} />
-  );
-}
+  return <Result provider={provider} result={result} />
+}

+ 7 - 12
frontend/src/components/navbar-search/dropdown/result/result.js

@@ -1,16 +1,11 @@
-// jshint ignore:start
-import React from 'react';
-import Thread from './thread';
-import User from './user';
+import React from "react"
+import Thread from "./thread"
+import User from "./user"
 
 export default function({ provider, result }) {
-  if (provider.id === 'threads') {
-    return (
-      <Thread result={result} />
-    );
+  if (provider.id === "threads") {
+    return <Thread result={result} />
   }
 
-  return (
-    <User result={result} />
-  );
-}
+  return <User result={result} />
+}

+ 17 - 12
frontend/src/components/navbar-search/dropdown/result/thread.js

@@ -1,10 +1,11 @@
-// jshint ignore:start
-import moment from 'moment';
-import React from 'react';
+import moment from "moment"
+import React from "react"
 
 export default function({ result }) {
-  const { poster, thread } = result;
-  const footer = gettext("Posted by %(poster)s on %(posted_on)s in %(category)s.");
+  const { poster, thread } = result
+  const footer = gettext(
+    "Posted by %(poster)s on %(posted_on)s in %(category)s."
+  )
 
   return (
     <li>
@@ -14,13 +15,17 @@ export default function({ result }) {
           {$(result.content).text()}
         </small>
         <small className="dropdown-search-post-footer">
-          {interpolate(footer, {
-            category: result.category.name,
-            posted_on: moment(result.posted_on).format('LL'),
-            poster: result.poster_name
-          }, true)}
+          {interpolate(
+            footer,
+            {
+              category: result.category.name,
+              posted_on: moment(result.posted_on).format("LL"),
+              poster: result.poster_name
+            },
+            true
+          )}
         </small>
       </a>
     </li>
-  );
-}
+  )
+}

+ 16 - 13
frontend/src/components/navbar-search/dropdown/result/user.js

@@ -1,13 +1,12 @@
-// jshint ignore:start
-import moment from 'moment';
-import React from 'react';
-import Avatar from 'misago/components/avatar';
+import moment from "moment"
+import React from "react"
+import Avatar from "misago/components/avatar"
 
 export default function({ result }) {
-  const { rank } = result;
+  const { rank } = result
 
-  const detail = gettext("%(title)s, joined on %(joined_on)s");
-  const title = result.title || rank.title || rank.name;
+  const detail = gettext("%(title)s, joined on %(joined_on)s")
+  const title = result.title || rank.title || rank.name
 
   return (
     <li>
@@ -19,14 +18,18 @@ export default function({ result }) {
           <div className="media-body">
             <h5 className="media-heading">{result.username}</h5>
             <small>
-              {interpolate(detail, {
-                title,
-                joined_on: moment(result.joined_on).format('LL')
-              }, true)}
+              {interpolate(
+                detail,
+                {
+                  title,
+                  joined_on: moment(result.joined_on).format("LL")
+                },
+                true
+              )}
             </small>
           </div>
         </div>
       </a>
     </li>
-  );
-}
+  )
+}

+ 72 - 75
frontend/src/components/navbar-search/index.js

@@ -1,131 +1,128 @@
-// jshint ignore:start
-import React from 'react';
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
-import misago from 'misago';
-import cleanResults from './clean-results';
-import Dropdown from './dropdown';
+import React from "react"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import misago from "misago"
+import cleanResults from "./clean-results"
+import Dropdown from "./dropdown"
 
 export default class extends React.Component {
   constructor() {
-    super();
+    super()
 
     this.state = {
       isLoading: false,
       isOpen: false,
-      query: '',
+      query: "",
       results: []
-    };
+    }
 
-    this.intervalId = null;
+    this.intervalId = null
   }
 
   componentDidMount() {
-    document.addEventListener('mousedown', this.onDocumentMouseDown);
-    document.addEventListener('keydown', this.onEscape);
+    document.addEventListener("mousedown", this.onDocumentMouseDown)
+    document.addEventListener("keydown", this.onEscape)
   }
 
   componentWillUnmount() {
-    document.removeEventListener('mousedown', this.onDocumentMouseDown);
-    document.removeEventListener('keydown', this.onEscape);
+    document.removeEventListener("mousedown", this.onDocumentMouseDown)
+    document.removeEventListener("keydown", this.onEscape)
   }
 
-  onToggle = (ev) => {
+  onToggle = ev => {
     this.setState((prevState, props) => {
       if (!prevState.isOpen) {
         window.setTimeout(() => {
-          this.container.querySelector('input').focus();
-        }, 100);
+          this.container.querySelector("input").focus()
+        }, 100)
       }
 
-      return { isOpen: !prevState.isOpen };
-    });
-  };
+      return { isOpen: !prevState.isOpen }
+    })
+  }
 
-  onDocumentMouseDown = (ev) => {
-    let closeResults = true;
-    let node = ev.target;
+  onDocumentMouseDown = ev => {
+    let closeResults = true
+    let node = ev.target
 
     while (node !== null && node !== document) {
       if (node === this.container) {
-        closeResults = false;
-        return;
+        closeResults = false
+        return
       }
 
-      node = node.parentNode;
+      node = node.parentNode
     }
 
     if (closeResults) {
-      this.setState({ isOpen: false });
+      this.setState({ isOpen: false })
     }
-  };
+  }
 
-  onEscape = (ev) => {
-    if (ev.key === 'Escape') {
-      this.setState({ isOpen: false });
+  onEscape = ev => {
+    if (ev.key === "Escape") {
+      this.setState({ isOpen: false })
     }
-  };
+  }
 
-  onChange = (ev) => {
-    const query = ev.target.value;
+  onChange = ev => {
+    const query = ev.target.value
 
-    this.setState({ query });
-    this.loadResults(query.trim());
-  };
+    this.setState({ query })
+    this.loadResults(query.trim())
+  }
 
   loadResults(query) {
-    if (!query.length) return;
+    if (!query.length) return
 
-    const delay = 300 + (Math.random() * 300);
+    const delay = 300 + Math.random() * 300
 
     if (this.intervalId) {
-      window.clearTimeout(this.intervalId);
+      window.clearTimeout(this.intervalId)
     }
 
-    this.setState({ isLoading: true });
-
-    this.intervalId = window.setTimeout(
-      () => {
-        ajax.get(misago.get('SEARCH_API'), {q: query}).then(
-          (data) => {
-            this.setState({
-              intervalId: null,
-              isLoading: false,
-              results: cleanResults(data)
-            });
-          },
-          (rejection) => {
-            snackbar.apiError(rejection);
-
-            this.setState({
-              intervalId: null,
-              isLoading: false,
-              results: []
-            });
-          }
-        );
-      },
-      delay
-    );
+    this.setState({ isLoading: true })
+
+    this.intervalId = window.setTimeout(() => {
+      ajax.get(misago.get("SEARCH_API"), { q: query }).then(
+        data => {
+          this.setState({
+            intervalId: null,
+            isLoading: false,
+            results: cleanResults(data)
+          })
+        },
+        rejection => {
+          snackbar.apiError(rejection)
+
+          this.setState({
+            intervalId: null,
+            isLoading: false,
+            results: []
+          })
+        }
+      )
+    }, delay)
   }
 
   render() {
-    let className = "navbar-search dropdown";
-    if (this.state.isOpen) className += " open";
+    let className = "navbar-search dropdown"
+    if (this.state.isOpen) className += " open"
 
     return (
-      <div className={className} ref={(container) => this.container = container}>
+      <div
+        className={className}
+        ref={container => (this.container = container)}
+      >
         <a
           aria-haspopup="true"
           aria-expanded="false"
           className="navbar-icon"
           data-toggle="dropdown"
-          href={misago.get('SEARCH_URL')}
+          href={misago.get("SEARCH_URL")}
           onClick={this.onToggle}
         >
-          <i className="material-icon">
-            search
-          </i>
+          <i className="material-icon">search</i>
         </a>
         <Dropdown
           isLoading={this.state.isLoading}
@@ -134,6 +131,6 @@ export default class extends React.Component {
           query={this.state.query}
         />
       </div>
-    );
+    )
   }
-}
+}

+ 5 - 6
frontend/src/components/options/change-username/form-loading.js

@@ -1,6 +1,5 @@
-/* jshint ignore:start */
-import React from 'react';
-import PanelLoader from 'misago/components/panel-loader';
+import React from "react"
+import PanelLoader from "misago/components/panel-loader"
 
 export default function() {
   return (
@@ -9,6 +8,6 @@ export default function() {
         <h3 className="panel-title">{gettext("Change username")}</h3>
       </div>
       <PanelLoader />
-  </div>
-  );
-}
+    </div>
+  )
+}

+ 8 - 8
frontend/src/components/options/change-username/form-locked.js

@@ -1,19 +1,20 @@
-import React from 'react';
-import PanelMessage from 'misago/components/panel-message'; // jshint ignore:line
+import React from "react"
+import PanelMessage from "misago/components/panel-message"
 
 export default class extends React.Component {
   getHelpText() {
     if (this.props.options.next_on) {
       return interpolate(
-          gettext("You will be able to change your username %(next_change)s."),
-          {'next_change': this.props.options.next_on.fromNow()}, true);
+        gettext("You will be able to change your username %(next_change)s."),
+        { next_change: this.props.options.next_on.fromNow() },
+        true
+      )
     } else {
-      return gettext("You have used up available name changes.");
+      return gettext("You have used up available name changes.")
     }
   }
 
   render() {
-    /* jshint ignore:start */
     return (
       <div className="panel panel-default panel-form">
         <div className="panel-heading">
@@ -24,7 +25,6 @@ export default class extends React.Component {
           message={gettext("You can't change your username at the moment.")}
         />
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 }

+ 80 - 62
frontend/src/components/options/change-username/form.js

@@ -1,17 +1,17 @@
-import React from 'react'; // jshint ignore:line
-import Button from 'misago/components/button'; // jshint ignore:line
-import Form from 'misago/components/form';
-import FormGroup from 'misago/components/form-group'; // jshint ignore:line
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
-import * as validators from 'misago/utils/validators';
+import React from "react"
+import Button from "misago/components/button"
+import Form from "misago/components/form"
+import FormGroup from "misago/components/form-group"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import * as validators from "misago/utils/validators"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
-      username: '',
+      username: "",
 
       validators: {
         username: [
@@ -22,95 +22,113 @@ export default class extends Form {
       },
 
       isLoading: false
-    };
+    }
   }
 
   getHelpText() {
-    let phrases = [];
+    let phrases = []
 
     if (this.props.options.changes_left > 0) {
       let message = ngettext(
         "You can change your username %(changes_left)s more time.",
         "You can change your username %(changes_left)s more times.",
-        this.props.options.changes_left);
-
-      phrases.push(interpolate(message, {
-        'changes_left': this.props.options.changes_left
-      }, true));
+        this.props.options.changes_left
+      )
+
+      phrases.push(
+        interpolate(
+          message,
+          {
+            changes_left: this.props.options.changes_left
+          },
+          true
+        )
+      )
     }
 
     if (this.props.user.acl.name_changes_expire > 0) {
       let message = ngettext(
         "Used changes become available again after %(name_changes_expire)s day.",
         "Used changes become available again after %(name_changes_expire)s days.",
-        this.props.user.acl.name_changes_expire);
-
-      phrases.push(interpolate(message, {
-        'name_changes_expire': this.props.user.acl.name_changes_expire
-      }, true));
+        this.props.user.acl.name_changes_expire
+      )
+
+      phrases.push(
+        interpolate(
+          message,
+          {
+            name_changes_expire: this.props.user.acl.name_changes_expire
+          },
+          true
+        )
+      )
     }
 
-    return phrases.length ? phrases.join(' ') : null;
+    return phrases.length ? phrases.join(" ") : null
   }
 
   clean() {
-    let errors = this.validate();
+    let errors = this.validate()
     if (errors.username) {
-      snackbar.error(errors.username[0]);
-      return false;
-    } if (this.state.username.trim() === this.props.user.username) {
-      snackbar.info(gettext("Your new username is same as current one."));
-      return false;
+      snackbar.error(errors.username[0])
+      return false
+    }
+    if (this.state.username.trim() === this.props.user.username) {
+      snackbar.info(gettext("Your new username is same as current one."))
+      return false
     } else {
-      return true;
+      return true
     }
   }
 
   send() {
     return ajax.post(this.props.user.api.username, {
-      'username': this.state.username
-    });
+      username: this.state.username
+    })
   }
 
   handleSuccess(success) {
     this.setState({
-      'username': ''
-    });
+      username: ""
+    })
 
-    this.props.complete(success.username, success.slug, success.options);
+    this.props.complete(success.username, success.slug, success.options)
   }
 
   handleError(rejection) {
-    snackbar.apiError(rejection);
+    snackbar.apiError(rejection)
   }
 
   render() {
-    /* jshint ignore:start */
-    return <form onSubmit={this.handleSubmit}>
-      <div className="panel panel-default panel-form">
-        <div className="panel-heading">
-          <h3 className="panel-title">{gettext("Change username")}</h3>
-        </div>
-        <div className="panel-body">
-
-          <FormGroup label={gettext("New username")} for="id_username"
-                     helpText={this.getHelpText()}>
-            <input type="text" id="id_username" className="form-control"
-                   disabled={this.state.isLoading}
-                   onChange={this.bindInput('username')}
-                   value={this.state.username} />
-          </FormGroup>
-
-        </div>
-        <div className="panel-footer">
-
-          <Button className="btn-primary" loading={this.state.isLoading}>
-            {gettext("Change username")}
-          </Button>
-
+    return (
+      <form onSubmit={this.handleSubmit}>
+        <div className="panel panel-default panel-form">
+          <div className="panel-heading">
+            <h3 className="panel-title">{gettext("Change username")}</h3>
+          </div>
+          <div className="panel-body">
+            <FormGroup
+              label={gettext("New username")}
+              for="id_username"
+              helpText={this.getHelpText()}
+            >
+              <input
+                type="text"
+                id="id_username"
+                className="form-control"
+                disabled={this.state.isLoading}
+                onChange={this.bindInput("username")}
+                value={this.state.username}
+              />
+            </FormGroup>
+          </div>
+          <div className="panel-footer">
+            <Button className="btn-primary" loading={this.state.isLoading}>
+              {gettext("Change username")}
+            </Button>
+          </div>
         </div>
-      </div>
-    </form>;
-    /* jshint ignore:end */
+      </form>
+    )
   }
-}
+}

+ 34 - 48
frontend/src/components/options/change-username/root.js

@@ -1,38 +1,38 @@
-import moment from 'moment';
-import React from 'react';
-import FormLoading from 'misago/components/options/change-username/form-loading'; // jshint ignore:line
-import FormLocked from 'misago/components/options/change-username/form-locked'; // jshint ignore:line
-import Form from 'misago/components/options/change-username/form'; // jshint ignore:line
-import UsernameHistory from 'misago/components/username-history/root'; // jshint ignore:line
-import misago from 'misago/index';
-import { hydrate, addNameChange } from 'misago/reducers/username-history'; // jshint ignore:line
-import { updateUsername } from 'misago/reducers/users'; // jshint ignore:line
-import ajax from 'misago/services/ajax';
-import title from 'misago/services/page-title';
-import snackbar from 'misago/services/snackbar'; // jshint ignore:line
-import store from 'misago/services/store';
+import moment from "moment"
+import React from "react"
+import FormLoading from "misago/components/options/change-username/form-loading"
+import FormLocked from "misago/components/options/change-username/form-locked"
+import Form from "misago/components/options/change-username/form"
+import UsernameHistory from "misago/components/username-history/root"
+import misago from "misago/index"
+import { hydrate, addNameChange } from "misago/reducers/username-history"
+import { updateUsername } from "misago/reducers/users"
+import ajax from "misago/services/ajax"
+import title from "misago/services/page-title"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoaded: false,
       options: null
-    };
+    }
   }
 
   componentDidMount() {
     title.set({
       title: gettext("Change username"),
       parent: gettext("Change your options")
-    });
+    })
 
     Promise.all([
       ajax.get(this.props.user.api.username),
-      ajax.get(misago.get('USERNAME_CHANGES_API'), {user: this.props.user.id})
-    ]).then((data) => {
-      store.dispatch(hydrate(data[1].results));
+      ajax.get(misago.get("USERNAME_CHANGES_API"), { user: this.props.user.id })
+    ]).then(data => {
+      store.dispatch(hydrate(data[1].results))
 
       this.setState({
         isLoaded: true,
@@ -40,66 +40,52 @@ export default class extends React.Component {
           changes_left: data[0].changes_left,
           length_min: data[0].length_min,
           length_max: data[0].length_max,
-          next_on: data[0].next_on ? moment(data[0].next_on) : null,
+          next_on: data[0].next_on ? moment(data[0].next_on) : null
         }
-      });
-    });
+      })
+    })
   }
 
-  /* jshint ignore:start */
   onComplete = (username, slug, options) => {
     this.setState({
       options
-    });
+    })
 
     store.dispatch(
-      addNameChange({ username, slug }, this.props.user, this.props.user));
-    store.dispatch(
-      updateUsername(this.props.user, username, slug));
+      addNameChange({ username, slug }, this.props.user, this.props.user)
+    )
+    store.dispatch(updateUsername(this.props.user, username, slug))
 
-    snackbar.success(gettext("Your username has been changed successfully."));
-  };
-  /* jshint ignore:end */
+    snackbar.success(gettext("Your username has been changed successfully."))
+  }
 
   getChangeForm() {
     if (!this.state.isLoaded) {
-      /* jshint ignore:start */
-      return (
-        <FormLoading />
-      );
-      /* jshint ignore:end */
+      return <FormLoading />
     }
 
     if (this.state.options.changes_left === 0) {
-      /* jshint ignore:start */
-      return (
-        <FormLocked options={this.state.options} />
-      );
-      /* jshint ignore:end */
+      return <FormLocked options={this.state.options} />
     }
 
-    /* jshint ignore:start */
     return (
       <Form
         complete={this.onComplete}
         options={this.state.options}
         user={this.props.user}
       />
-    );
-    /* jshint ignore:end */
+    )
   }
 
   render() {
-    /* jshint ignore:start */
     return (
       <div>
         {this.getChangeForm()}
         <UsernameHistory
-          changes={this.props['username-history']}
+          changes={this.props["username-history"]}
           isLoaded={this.state.isLoaded}
         />
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 62 - 42
frontend/src/components/options/delete-account.js

@@ -1,60 +1,61 @@
-/* jshint ignore:start */
-import React from 'react';
-import Button from 'misago/components/button';
-import ajax from 'misago/services/ajax';
-import title from 'misago/services/page-title';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
-import misago from 'misago';
+import React from "react"
+import Button from "misago/components/button"
+import ajax from "misago/services/ajax"
+import title from "misago/services/page-title"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
+import misago from "misago"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoading: false,
-      password: '',
-    };
+      password: ""
+    }
   }
 
   componentDidMount() {
     title.set({
       title: gettext("Delete account"),
       parent: gettext("Change your options")
-    });
+    })
   }
 
-  onPasswordChange = (event) => {
-    this.setState({ password: event.target.value });
+  onPasswordChange = event => {
+    this.setState({ password: event.target.value })
   }
 
-  handleSubmit = (event) => {
-    event.preventDefault();
+  handleSubmit = event => {
+    event.preventDefault()
 
-    const { isLoading, password } = this.state;
-    const { user } = this.props;
+    const { isLoading, password } = this.state
+    const { user } = this.props
 
     if (password.length == 0) {
-      snackbar.error(gettext("Enter your password to confirm account deletion."));
-      return false;
+      snackbar.error(
+        gettext("Enter your password to confirm account deletion.")
+      )
+      return false
     }
 
-    if (isLoading) return false;
-    this.setState({ isLoading: true });
+    if (isLoading) return false
+    this.setState({ isLoading: true })
 
     ajax.post(user.api.delete, { password }).then(
-      (success) => {
-        window.location.href = misago.get('MISAGO_PATH');
+      success => {
+        window.location.href = misago.get("MISAGO_PATH")
       },
-      (rejection) => {
-        this.setState({ isLoading: false });
+      rejection => {
+        this.setState({ isLoading: false })
         if (rejection.password) {
-          snackbar.error(rejection.password[0]);
+          snackbar.error(rejection.password[0])
         } else {
           snackbar.apiError(rejection)
         }
       }
-    );
+    )
   }
 
   render() {
@@ -65,29 +66,49 @@ export default class extends React.Component {
             <h3 className="panel-title">{gettext("Delete account")}</h3>
           </div>
           <div className="panel-body">
-
             <p className="lead">
-              {gettext("You are going to delete your account. This action is nonreversible, and will result in following data being deleted:")}
+              {gettext(
+                "You are going to delete your account. This action is nonreversible, and will result in following data being deleted:"
+              )}
             </p>
 
-            <p>- {gettext("Stored IP addresses associated with content that you have posted will be deleted.")}</p>
-            <p>- {gettext("Your username will become available for other user to rename to or for new user to register their account with.")}</p>
-            <p>- {gettext("Your e-mail will become available for use in new account registration.")}</p>
-
-            <hr/>
+            <p>
+              -{" "}
+              {gettext(
+                "Stored IP addresses associated with content that you have posted will be deleted."
+              )}
+            </p>
+            <p>
+              -{" "}
+              {gettext(
+                "Your username will become available for other user to rename to or for new user to register their account with."
+              )}
+            </p>
+            <p>
+              -{" "}
+              {gettext(
+                "Your e-mail will become available for use in new account registration."
+              )}
+            </p>
 
-            <p>{gettext("All your posted content will NOT be deleted, but username associated with it will be changed to one shared by all deleted accounts.")}</p>
+            <hr />
 
+            <p>
+              {gettext(
+                "All your posted content will NOT be deleted, but username associated with it will be changed to one shared by all deleted accounts."
+              )}
+            </p>
           </div>
           <div className="panel-footer">
-
             <div className="input-group">
-              <input 
+              <input
                 className="form-control"
                 disabled={this.state.isLoading}
                 name="password-confirmation"
                 type="password"
-                placeholder={gettext("Enter your password to confirm account deletion.")}
+                placeholder={gettext(
+                  "Enter your password to confirm account deletion."
+                )}
                 value={this.state.password}
                 onChange={this.onPasswordChange}
               />
@@ -97,10 +118,9 @@ export default class extends React.Component {
                 </Button>
               </span>
             </div>
-
           </div>
         </div>
       </form>
-    );
+    )
   }
-}
+}

+ 51 - 44
frontend/src/components/options/download-data.js

@@ -1,10 +1,9 @@
-/* jshint ignore:start */
-import React from 'react';
-import moment from 'moment';
-import Button from 'misago/components/button';
-import ajax from 'misago/services/ajax';
-import title from 'misago/services/page-title';
-import snackbar from 'misago/services/snackbar';
+import React from "react"
+import moment from "moment"
+import Button from "misago/components/button"
+import ajax from "misago/services/ajax"
+import title from "misago/services/page-title"
+import snackbar from "misago/services/snackbar"
 
 export default class DownloadData extends React.Component {
   constructor(props) {
@@ -21,39 +20,41 @@ export default class DownloadData extends React.Component {
     title.set({
       title: gettext("Download your data"),
       parent: gettext("Change your options")
-    });
+    })
 
-    this.handleLoadDownloads();
+    this.handleLoadDownloads()
   }
 
   handleLoadDownloads = () => {
     ajax.get(this.props.user.api.data_downloads).then(
-      (data) => {
+      data => {
         this.setState({
           isLoading: false,
           downloads: data
-        });
+        })
       },
-      (rejection) => {
-        snackbar.apiError(rejection);
+      rejection => {
+        snackbar.apiError(rejection)
       }
-    );
-  };
+    )
+  }
 
   handleRequestDataDownload = () => {
-    this.setState({ isSubmiting: true });
+    this.setState({ isSubmiting: true })
     ajax.post(this.props.user.api.request_data_download).then(
       () => {
-        this.handleLoadDownloads();
-        snackbar.success(gettext("Your request for data download has been registered."));
-        this.setState({ isSubmiting: false });
+        this.handleLoadDownloads()
+        snackbar.success(
+          gettext("Your request for data download has been registered.")
+        )
+        this.setState({ isSubmiting: false })
       },
-      (rejection) => {
+      rejection => {
         console.log(rejection)
-        snackbar.apiError(rejection);
-        this.setState({ isSubmiting: false });
+        snackbar.apiError(rejection)
+        this.setState({ isSubmiting: false })
       }
-    );
+    )
   }
 
   render() {
@@ -64,11 +65,17 @@ export default class DownloadData extends React.Component {
             <h3 className="panel-title">{gettext("Download your data")}</h3>
           </div>
           <div className="panel-body">
+            <p>
+              {gettext(
+                'To download your data from the site, click the "Request data download" button. Depending on amount of data to be archived and number of users wanting to download their data at same time it may take up to few days for your download to be prepared. An e-mail with notification will be sent to you when your data is ready to be downloaded.'
+              )}
+            </p>
 
-            <p>{gettext("To download your data from the site, click the \"Request data download\" button. Depending on amount of data to be archived and number of users wanting to download their data at same time it may take up to few days for your download to be prepared. An e-mail with notification will be sent to you when your data is ready to be downloaded.")}</p>
-
-            <p>{gettext("The download will only be available for limited amount of time, after which it will be deleted from the site and marked as expired.")}</p>
-
+            <p>
+              {gettext(
+                "The download will only be available for limited amount of time, after which it will be deleted from the site and marked as expired."
+              )}
+            </p>
           </div>
           <table className="table">
             <thead>
@@ -78,10 +85,12 @@ export default class DownloadData extends React.Component {
               </tr>
             </thead>
             <tbody>
-              {this.state.downloads.map((item) => {
+              {this.state.downloads.map(item => {
                 return (
                   <tr key={item.id}>
-                    <td style={rowStyle}>{moment(item.requested_on).fromNow()}</td>
+                    <td style={rowStyle}>
+                      {moment(item.requested_on).fromNow()}
+                    </td>
                     <td>
                       <DownloadButton
                         exportFile={item.file}
@@ -91,10 +100,11 @@ export default class DownloadData extends React.Component {
                   </tr>
                 )
               })}
-              {this.state.downloads.length == 0 ?
+              {this.state.downloads.length == 0 ? (
                 <tr>
                   <td colSpan="2">{gettext("You have no data downloads.")}</td>
-                </tr> : null}
+                </tr>
+              ) : null}
             </tbody>
           </table>
           <div className="panel-footer text-right">
@@ -109,16 +119,16 @@ export default class DownloadData extends React.Component {
           </div>
         </div>
       </div>
-    );
+    )
   }
 }
 
 const rowStyle = {
-  verticalAlign: 'middle'
-};
+  verticalAlign: "middle"
+}
 
-const STATUS_PENDING = 0;
-const STATUS_PROCESSING = 1;
+const STATUS_PENDING = 0
+const STATUS_PROCESSING = 1
 
 const DownloadButton = ({ exportFile, status }) => {
   if (status === STATUS_PENDING || status === STATUS_PROCESSING) {
@@ -130,18 +140,15 @@ const DownloadButton = ({ exportFile, status }) => {
       >
         {gettext("Download is being prepared")}
       </Button>
-    );
+    )
   }
 
   if (exportFile) {
     return (
-      <a
-        className="btn btn-success btn-sm btn-block"
-        href={exportFile}
-      >
+      <a className="btn btn-success btn-sm btn-block" href={exportFile}>
         {gettext("Download your data")}
       </a>
-    );
+    )
   }
 
   return (
@@ -152,5 +159,5 @@ const DownloadButton = ({ exportFile, status }) => {
     >
       {gettext("Download is expired")}
     </Button>
-  );
-}
+  )
+}

+ 10 - 14
frontend/src/components/options/edit-details.js

@@ -1,27 +1,23 @@
-/* jshint ignore:start */
-import React from 'react';
-import Form from 'misago/components/edit-details';
-import title from 'misago/services/page-title';
-import snackbar from 'misago/services/snackbar';
+import React from "react"
+import Form from "misago/components/edit-details"
+import title from "misago/services/page-title"
+import snackbar from "misago/services/snackbar"
 
 export default class extends React.Component {
   componentDidMount() {
     title.set({
       title: gettext("Edit details"),
       parent: gettext("Change your options")
-    });
+    })
   }
 
   onSuccess = () => {
-    snackbar.info(gettext("Your details have been updated."));
-  };
+    snackbar.info(gettext("Your details have been updated."))
+  }
 
   render() {
     return (
-      <Form
-        api={this.props.user.api.edit_details}
-        onSuccess={this.onSuccess}
-      />
-    );
+      <Form api={this.props.user.api.edit_details} onSuccess={this.onSuccess} />
+    )
   }
-}
+}

+ 65 - 64
frontend/src/components/options/forum-options.js

@@ -1,91 +1,96 @@
-import React from 'react'; // jshint ignore:line
-import Button from 'misago/components/button'; // jshint ignore:line
-import Form from 'misago/components/form';
-import FormGroup from 'misago/components/form-group'; // jshint ignore:line
-import Select from 'misago/components/select'; // jshint ignore:line
-import YesNoSwitch from 'misago/components/yes-no-switch'; // jshint ignore:line
-import { patch } from 'misago/reducers/auth';
-import ajax from 'misago/services/ajax';
-import title from 'misago/services/page-title';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
+import React from "react"
+import Button from "misago/components/button"
+import Form from "misago/components/form"
+import FormGroup from "misago/components/form-group"
+import Select from "misago/components/select"
+import YesNoSwitch from "misago/components/yes-no-switch"
+import { patch } from "misago/reducers/auth"
+import ajax from "misago/services/ajax"
+import title from "misago/services/page-title"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
-      'isLoading': false,
+      isLoading: false,
 
-      'is_hiding_presence': props.user.is_hiding_presence,
-      'limits_private_thread_invites_to': props.user.limits_private_thread_invites_to,
-      'subscribe_to_started_threads': props.user.subscribe_to_started_threads,
-      'subscribe_to_replied_threads': props.user.subscribe_to_replied_threads,
+      is_hiding_presence: props.user.is_hiding_presence,
+      limits_private_thread_invites_to:
+        props.user.limits_private_thread_invites_to,
+      subscribe_to_started_threads: props.user.subscribe_to_started_threads,
+      subscribe_to_replied_threads: props.user.subscribe_to_replied_threads,
 
-      'errors': {}
-    };
+      errors: {}
+    }
 
     this.privateThreadInvitesChoices = [
       {
-        'value': 0,
-        'icon': 'help_outline',
-        'label': gettext("Everybody")
+        value: 0,
+        icon: "help_outline",
+        label: gettext("Everybody")
       },
       {
-        'value': 1,
-        'icon': 'done_all',
-        'label': gettext("Users I follow")
+        value: 1,
+        icon: "done_all",
+        label: gettext("Users I follow")
       },
       {
-        'value': 2,
-        'icon': 'highlight_off',
-        'label': gettext("Nobody")
+        value: 2,
+        icon: "highlight_off",
+        label: gettext("Nobody")
       }
-    ];
+    ]
 
     this.subscribeToChoices = [
       {
-        'value': 0,
-        'icon': 'star_border',
-        'label': gettext("No")
+        value: 0,
+        icon: "star_border",
+        label: gettext("No")
       },
       {
-        'value': 1,
-        'icon': 'star_half',
-        'label': gettext("Notify")
+        value: 1,
+        icon: "star_half",
+        label: gettext("Notify")
       },
       {
-        'value': 2,
-        'icon': 'star',
-        'label': gettext("Notify with e-mail")
+        value: 2,
+        icon: "star",
+        label: gettext("Notify with e-mail")
       }
-    ];
+    ]
   }
 
   send() {
     return ajax.post(this.props.user.api.options, {
       is_hiding_presence: this.state.is_hiding_presence,
-      limits_private_thread_invites_to: this.state.limits_private_thread_invites_to,
+      limits_private_thread_invites_to: this.state
+        .limits_private_thread_invites_to,
       subscribe_to_started_threads: this.state.subscribe_to_started_threads,
       subscribe_to_replied_threads: this.state.subscribe_to_replied_threads
-    });
+    })
   }
 
   handleSuccess() {
-    store.dispatch(patch({
-      is_hiding_presence: this.state.is_hiding_presence,
-      limits_private_thread_invites_to: this.state.limits_private_thread_invites_to,
-      subscribe_to_started_threads: this.state.subscribe_to_started_threads,
-      subscribe_to_replied_threads: this.state.subscribe_to_replied_threads
-    }));
-    snackbar.success(gettext("Your forum options have been changed."));
+    store.dispatch(
+      patch({
+        is_hiding_presence: this.state.is_hiding_presence,
+        limits_private_thread_invites_to: this.state
+          .limits_private_thread_invites_to,
+        subscribe_to_started_threads: this.state.subscribe_to_started_threads,
+        subscribe_to_replied_threads: this.state.subscribe_to_replied_threads
+      })
+    )
+    snackbar.success(gettext("Your forum options have been changed."))
   }
 
   handleError(rejection) {
     if (rejection.status === 400) {
-      snackbar.error(gettext("Please reload page and try again."));
+      snackbar.error(gettext("Please reload page and try again."))
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
   }
 
@@ -93,11 +98,10 @@ export default class extends Form {
     title.set({
       title: gettext("Forum options"),
       parent: gettext("Change your options")
-    });
+    })
   }
 
   render() {
-    /* jshint ignore:start */
     return (
       <form onSubmit={this.handleSubmit}>
         <div className="panel panel-default panel-form">
@@ -105,13 +109,14 @@ export default class extends Form {
             <h3 className="panel-title">{gettext("Change forum options")}</h3>
           </div>
           <div className="panel-body">
-
             <fieldset>
               <legend>{gettext("Privacy settings")}</legend>
 
               <FormGroup
                 label={gettext("Hide my presence")}
-                helpText={gettext("If you hide your presence, only members with permission to see hidden users will see when you are online.")}
+                helpText={gettext(
+                  "If you hide your presence, only members with permission to see hidden users will see when you are online."
+                )}
                 for="id_is_hiding_presence"
               >
                 <YesNoSwitch
@@ -121,7 +126,7 @@ export default class extends Form {
                   iconOff="visibility"
                   labelOn={gettext("Hide my presence from other users")}
                   labelOff={gettext("Show my presence to other users")}
-                  onChange={this.bindInput('is_hiding_presence')}
+                  onChange={this.bindInput("is_hiding_presence")}
                   value={this.state.is_hiding_presence}
                 />
               </FormGroup>
@@ -133,7 +138,7 @@ export default class extends Form {
                 <Select
                   id="id_limits_private_thread_invites_to"
                   disabled={this.state.isLoading}
-                  onChange={this.bindInput('limits_private_thread_invites_to')}
+                  onChange={this.bindInput("limits_private_thread_invites_to")}
                   value={this.state.limits_private_thread_invites_to}
                   choices={this.privateThreadInvitesChoices}
                 />
@@ -150,7 +155,7 @@ export default class extends Form {
                 <Select
                   id="id_subscribe_to_started_threads"
                   disabled={this.state.isLoading}
-                  onChange={this.bindInput('subscribe_to_started_threads')}
+                  onChange={this.bindInput("subscribe_to_started_threads")}
                   value={this.state.subscribe_to_started_threads}
                   choices={this.subscribeToChoices}
                 />
@@ -163,24 +168,20 @@ export default class extends Form {
                 <Select
                   id="id_subscribe_to_replied_threads"
                   disabled={this.state.isLoading}
-                  onChange={this.bindInput('subscribe_to_replied_threads')}
+                  onChange={this.bindInput("subscribe_to_replied_threads")}
                   value={this.state.subscribe_to_replied_threads}
                   choices={this.subscribeToChoices}
                 />
               </FormGroup>
             </fieldset>
-
           </div>
           <div className="panel-footer">
-
             <Button className="btn-primary" loading={this.state.isLoading}>
               {gettext("Save changes")}
             </Button>
-
           </div>
         </div>
       </form>
-    );
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 20 - 22
frontend/src/components/options/navs.js

@@ -1,52 +1,50 @@
-// jshint ignore:start
-import React from 'react';
-import { Link } from 'react-router';
-import Li from 'misago/components/li';
-import misago from 'misago/index';
+import React from "react"
+import { Link } from "react-router"
+import Li from "misago/components/li"
+import misago from "misago/index"
 
 export function SideNav(props) {
   return (
     <div className="list-group nav-side">
-      {props.options.map((option) => {
+      {props.options.map(option => {
         return (
           <Link
-            to={props.baseUrl + option.component + '/'}
+            to={props.baseUrl + option.component + "/"}
             className="list-group-item"
             activeClassName="active"
             key={option.component}
           >
-            <span className="material-icon">
-              {option.icon}
-            </span>
+            <span className="material-icon">{option.icon}</span>
             {option.name}
           </Link>
-        );
+        )
       })}
     </div>
-  );
+  )
 }
 
 export function CompactNav(props) {
   return (
-    <ul className={props.className || "dropdown-menu stick-to-bottom"} role="menu">
-      {props.options.map((option) => {
+    <ul
+      className={props.className || "dropdown-menu stick-to-bottom"}
+      role="menu"
+    >
+      {props.options.map(option => {
         return (
           <Li
-            path={props.baseUrl + option.component + '/'}
+            path={props.baseUrl + option.component + "/"}
             key={option.component}
           >
             <Link
-              to={props.baseUrl + option.component + '/'}
+              to={props.baseUrl + option.component + "/"}
               onClick={props.hideNav}
             >
-              <span className="material-icon hidden-sm">
-                {option.icon}
-              </span>
+              <span className="material-icon hidden-sm">{option.icon}</span>
               {option.name}
             </Link>
           </Li>
-        );
+        )
       })}
     </ul>
-  );
-}
+  )
+}

+ 34 - 46
frontend/src/components/options/root.js

@@ -1,105 +1,93 @@
-import React from 'react'; // jshint ignore:line
-import { connect } from 'react-redux';
-import DropdownToggle from 'misago/components/dropdown-toggle'; // jshint ignore:line
-import { SideNav, CompactNav } from 'misago/components/options/navs'; // jshint ignore:line
-import DeleteAccount from 'misago/components/options/delete-account';
-import EditDetails from 'misago/components/options/edit-details';
-import DownloadData from 'misago/components/options/download-data';
-import ChangeForumOptions from 'misago/components/options/forum-options';
-import ChangeUsername from 'misago/components/options/change-username/root';
-import ChangeSignInCredentials from 'misago/components/options/sign-in-credentials/root';
-import WithDropdown from 'misago/components/with-dropdown';
-import misago from 'misago/index';
+import React from "react"
+import { connect } from "react-redux"
+import DropdownToggle from "misago/components/dropdown-toggle"
+import { SideNav, CompactNav } from "misago/components/options/navs"
+import DeleteAccount from "misago/components/options/delete-account"
+import EditDetails from "misago/components/options/edit-details"
+import DownloadData from "misago/components/options/download-data"
+import ChangeForumOptions from "misago/components/options/forum-options"
+import ChangeUsername from "misago/components/options/change-username/root"
+import ChangeSignInCredentials from "misago/components/options/sign-in-credentials/root"
+import WithDropdown from "misago/components/with-dropdown"
+import misago from "misago/index"
 
 export default class extends WithDropdown {
   render() {
-    /* jshint ignore:start */
     return (
       <div className="page page-options">
         <div className="page-header-bg">
           <div className="page-header">
             <div className="container">
-
               <h1>{gettext("Change your options")}</h1>
-
             </div>
             <div className="page-tabs visible-xs-block visible-sm-block">
               <div className="container">
                 <CompactNav
                   className="nav nav-pills"
-                  baseUrl={misago.get('USERCP_URL')}
-                  options={misago.get('USER_OPTIONS')}
+                  baseUrl={misago.get("USERCP_URL")}
+                  options={misago.get("USER_OPTIONS")}
                 />
               </div>
             </div>
           </div>
         </div>
         <div className="container">
-
           <div className="row">
             <div className="col-md-3 hidden-xs hidden-sm">
-
               <SideNav
-                baseUrl={misago.get('USERCP_URL')}
-                options={misago.get('USER_OPTIONS')}
+                baseUrl={misago.get("USERCP_URL")}
+                options={misago.get("USER_OPTIONS")}
               />
-
-            </div>
-            <div className="col-md-9">
-
-              {this.props.children}
-
             </div>
+            <div className="col-md-9">{this.props.children}</div>
           </div>
-
         </div>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 }
 
 export function select(store) {
   return {
-    'tick': store.tick.tick,
-    'user': store.auth.user,
-    'username-history': store['username-history']
-  };
+    tick: store.tick.tick,
+    user: store.auth.user,
+    "username-history": store["username-history"]
+  }
 }
 
 export function paths() {
   const paths = [
     {
-      path: misago.get('USERCP_URL') + 'forum-options/',
+      path: misago.get("USERCP_URL") + "forum-options/",
       component: connect(select)(ChangeForumOptions)
     },
     {
-      path: misago.get('USERCP_URL') + 'edit-details/',
+      path: misago.get("USERCP_URL") + "edit-details/",
       component: connect(select)(EditDetails)
     },
     {
-      path: misago.get('USERCP_URL') + 'change-username/',
+      path: misago.get("USERCP_URL") + "change-username/",
       component: connect(select)(ChangeUsername)
     },
     {
-      path: misago.get('USERCP_URL') + 'sign-in-credentials/',
+      path: misago.get("USERCP_URL") + "sign-in-credentials/",
       component: connect(select)(ChangeSignInCredentials)
     }
-  ];
+  ]
 
-  if (misago.get('ENABLE_DOWNLOAD_OWN_DATA')) {
+  if (misago.get("ENABLE_DOWNLOAD_OWN_DATA")) {
     paths.push({
-      path: misago.get('USERCP_URL') + 'download-data/',
+      path: misago.get("USERCP_URL") + "download-data/",
       component: connect(select)(DownloadData)
-    });
+    })
   }
 
-  if (misago.get('ENABLE_DELETE_OWN_ACCOUNT')) {
+  if (misago.get("ENABLE_DELETE_OWN_ACCOUNT")) {
     paths.push({
-      path: misago.get('USERCP_URL') + 'delete-account/',
+      path: misago.get("USERCP_URL") + "delete-account/",
       component: connect(select)(DeleteAccount)
-    });
+    })
   }
 
-  return paths;
+  return paths
 }

+ 12 - 10
frontend/src/components/options/sign-in-credentials/UnusablePasswordMessage.js

@@ -1,6 +1,5 @@
-// jshint ignore:start
-import React from 'react';
-import misago from 'misago/index';
+import React from "react"
+import misago from "misago/index"
 
 const UnusablePasswordMessage = () => {
   return (
@@ -10,23 +9,26 @@ const UnusablePasswordMessage = () => {
       </div>
       <div className="panel-body panel-message-body">
         <div className="message-icon">
-          <span className="material-icon">
-            info_outline
-          </span>
+          <span className="material-icon">info_outline</span>
         </div>
         <div className="message-body">
           <p className="lead">
-            {gettext("You need to set a password for your account to be able to change your username or email.")}
+            {gettext(
+              "You need to set a password for your account to be able to change your username or email."
+            )}
           </p>
           <p className="help-block">
-            <a className="btn btn-primary" href={misago.get('FORGOTTEN_PASSWORD_URL')}>
+            <a
+              className="btn btn-primary"
+              href={misago.get("FORGOTTEN_PASSWORD_URL")}
+            >
               {gettext("Set password")}
             </a>
           </p>
         </div>
       </div>
     </div>
-  );
+  )
 }
 
-export default UnusablePasswordMessage;
+export default UnusablePasswordMessage

+ 72 - 67
frontend/src/components/options/sign-in-credentials/change-email.js

@@ -1,115 +1,120 @@
-import React from 'react'; // jshint ignore:line
-import Button from 'misago/components/button'; // jshint ignore:line
-import Form from 'misago/components/form';
-import FormGroup from 'misago/components/form-group'; // jshint ignore:line
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
-import * as validators from 'misago/utils/validators';
+import React from "react"
+import Button from "misago/components/button"
+import Form from "misago/components/form"
+import FormGroup from "misago/components/form-group"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import * as validators from "misago/utils/validators"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
-      new_email: '',
-      password: '',
+      new_email: "",
+      password: "",
 
       validators: {
-        new_email: [
-          validators.email()
-        ],
+        new_email: [validators.email()],
         password: []
       },
 
       isLoading: false
-    };
+    }
   }
 
   clean() {
-    let errors = this.validate();
+    let errors = this.validate()
     let lengths = [
       this.state.new_email.trim().length,
       this.state.password.trim().length
-    ];
+    ]
 
     if (lengths.indexOf(0) !== -1) {
-      snackbar.error(gettext("Fill out all fields."));
-      return false;
+      snackbar.error(gettext("Fill out all fields."))
+      return false
     }
 
     if (errors.new_email) {
-      snackbar.error(errors.new_email[0]);
-      return false;
+      snackbar.error(errors.new_email[0])
+      return false
     }
 
-    return true;
+    return true
   }
 
   send() {
     return ajax.post(this.props.user.api.change_email, {
       new_email: this.state.new_email,
-      password: this.state.password,
-    });
+      password: this.state.password
+    })
   }
 
   handleSuccess(response) {
     this.setState({
-      new_email: '',
-      password: ''
-    });
+      new_email: "",
+      password: ""
+    })
 
-    snackbar.success(response.detail);
+    snackbar.success(response.detail)
   }
 
   handleError(rejection) {
     if (rejection.status === 400) {
       if (rejection.new_email) {
-        snackbar.error(rejection.new_email);
+        snackbar.error(rejection.new_email)
       } else {
-        snackbar.error(rejection.password);
+        snackbar.error(rejection.password)
       }
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <form onSubmit={this.handleSubmit}>
-      <input type="type" style={{display: 'none'}} />
-      <input type="password" style={{display: 'none'}} />
-      <div className="panel panel-default panel-form">
-        <div className="panel-heading">
-          <h3 className="panel-title">{gettext("Change e-mail address")}</h3>
-        </div>
-        <div className="panel-body">
-
-          <FormGroup label={gettext("New e-mail")} for="id_new_email">
-            <input type="text" id="id_new_email" className="form-control"
-                   disabled={this.state.isLoading}
-                   onChange={this.bindInput('new_email')}
-                   value={this.state.new_email} />
-          </FormGroup>
-
-          <hr />
-
-          <FormGroup label={gettext("Your current password")} for="id_confirm_email">
-            <input type="password" id="id_confirm_email" className="form-control"
-                   disabled={this.state.isLoading}
-                   onChange={this.bindInput('password')}
-                   value={this.state.password} />
-          </FormGroup>
-
-        </div>
-        <div className="panel-footer">
-
-          <Button className="btn-primary" loading={this.state.isLoading}>
-            {gettext("Change e-mail")}
-          </Button>
-
+    return (
+      <form onSubmit={this.handleSubmit}>
+        <input type="type" style={{ display: "none" }} />
+        <input type="password" style={{ display: "none" }} />
+        <div className="panel panel-default panel-form">
+          <div className="panel-heading">
+            <h3 className="panel-title">{gettext("Change e-mail address")}</h3>
+          </div>
+          <div className="panel-body">
+            <FormGroup label={gettext("New e-mail")} for="id_new_email">
+              <input
+                type="text"
+                id="id_new_email"
+                className="form-control"
+                disabled={this.state.isLoading}
+                onChange={this.bindInput("new_email")}
+                value={this.state.new_email}
+              />
+            </FormGroup>
+
+            <hr />
+
+            <FormGroup
+              label={gettext("Your current password")}
+              for="id_confirm_email"
+            >
+              <input
+                type="password"
+                id="id_confirm_email"
+                className="form-control"
+                disabled={this.state.isLoading}
+                onChange={this.bindInput("password")}
+                value={this.state.password}
+              />
+            </FormGroup>
+          </div>
+          <div className="panel-footer">
+            <Button className="btn-primary" loading={this.state.isLoading}>
+              {gettext("Change e-mail")}
+            </Button>
+          </div>
         </div>
-      </div>
-    </form>;
-    /* jshint ignore:end */
+      </form>
+    )
   }
-}
+}

+ 86 - 72
frontend/src/components/options/sign-in-credentials/change-password.js

@@ -1,18 +1,18 @@
-import React from 'react'; // jshint ignore:line
-import Button from 'misago/components/button'; // jshint ignore:line
-import Form from 'misago/components/form';
-import FormGroup from 'misago/components/form-group'; // jshint ignore:line
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
+import React from "react"
+import Button from "misago/components/button"
+import Form from "misago/components/form"
+import FormGroup from "misago/components/form-group"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
-      new_password: '',
-      repeat_password: '',
-      password: '',
+      new_password: "",
+      repeat_password: "",
+      password: "",
 
       validators: {
         new_password: [],
@@ -21,108 +21,122 @@ export default class extends Form {
       },
 
       isLoading: false
-    };
+    }
   }
 
   clean() {
-    let errors = this.validate();
+    let errors = this.validate()
     let lengths = [
       this.state.new_password.trim().length,
       this.state.repeat_password.trim().length,
       this.state.password.trim().length
-    ];
+    ]
 
     if (lengths.indexOf(0) !== -1) {
-      snackbar.error(gettext("Fill out all fields."));
-      return false;
+      snackbar.error(gettext("Fill out all fields."))
+      return false
     }
 
     if (errors.new_password) {
-      snackbar.error(errors.new_password[0]);
-      return false;
+      snackbar.error(errors.new_password[0])
+      return false
     }
 
     if (this.state.new_password !== this.state.repeat_password) {
-      snackbar.error(gettext("New passwords are different."));
-      return false;
+      snackbar.error(gettext("New passwords are different."))
+      return false
     }
 
-    return true;
+    return true
   }
 
   send() {
     return ajax.post(this.props.user.api.change_password, {
       new_password: this.state.new_password,
       password: this.state.password
-    });
+    })
   }
 
   handleSuccess(response) {
     this.setState({
-      new_password: '',
-      repeat_password: '',
-      password: ''
-    });
+      new_password: "",
+      repeat_password: "",
+      password: ""
+    })
 
-    snackbar.success(response.detail);
+    snackbar.success(response.detail)
   }
 
   handleError(rejection) {
     if (rejection.status === 400) {
       if (rejection.new_password) {
-        snackbar.error(rejection.new_password);
+        snackbar.error(rejection.new_password)
       } else {
-        snackbar.error(rejection.password);
+        snackbar.error(rejection.password)
       }
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <form onSubmit={this.handleSubmit}>
-      <input type="type" style={{display: 'none'}} />
-      <input type="password" style={{display: 'none'}} />
-      <div className="panel panel-default panel-form">
-        <div className="panel-heading">
-          <h3 className="panel-title">{gettext("Change password")}</h3>
-        </div>
-        <div className="panel-body">
-
-          <FormGroup label={gettext("New password")} for="id_new_password">
-            <input type="password" id="id_new_password" className="form-control"
-                   disabled={this.state.isLoading}
-                   onChange={this.bindInput('new_password')}
-                   value={this.state.new_password} />
-          </FormGroup>
-
-          <FormGroup label={gettext("Repeat password")} for="id_repeat_password">
-            <input type="password" id="id_repeat_password" className="form-control"
-                   disabled={this.state.isLoading}
-                   onChange={this.bindInput('repeat_password')}
-                   value={this.state.repeat_password} />
-          </FormGroup>
-
-          <hr />
-
-          <FormGroup label={gettext("Your current password")} for="id_confirm_password">
-            <input type="password" id="id_confirm_password" className="form-control"
-                   disabled={this.state.isLoading}
-                   onChange={this.bindInput('password')}
-                   value={this.state.password} />
-          </FormGroup>
-
-        </div>
-        <div className="panel-footer">
-
-          <Button className="btn-primary" loading={this.state.isLoading}>
-            {gettext("Change password")}
-          </Button>
-
+    return (
+      <form onSubmit={this.handleSubmit}>
+        <input type="type" style={{ display: "none" }} />
+        <input type="password" style={{ display: "none" }} />
+        <div className="panel panel-default panel-form">
+          <div className="panel-heading">
+            <h3 className="panel-title">{gettext("Change password")}</h3>
+          </div>
+          <div className="panel-body">
+            <FormGroup label={gettext("New password")} for="id_new_password">
+              <input
+                type="password"
+                id="id_new_password"
+                className="form-control"
+                disabled={this.state.isLoading}
+                onChange={this.bindInput("new_password")}
+                value={this.state.new_password}
+              />
+            </FormGroup>
+
+            <FormGroup
+              label={gettext("Repeat password")}
+              for="id_repeat_password"
+            >
+              <input
+                type="password"
+                id="id_repeat_password"
+                className="form-control"
+                disabled={this.state.isLoading}
+                onChange={this.bindInput("repeat_password")}
+                value={this.state.repeat_password}
+              />
+            </FormGroup>
+
+            <hr />
+
+            <FormGroup
+              label={gettext("Your current password")}
+              for="id_confirm_password"
+            >
+              <input
+                type="password"
+                id="id_confirm_password"
+                className="form-control"
+                disabled={this.state.isLoading}
+                onChange={this.bindInput("password")}
+                value={this.state.password}
+              />
+            </FormGroup>
+          </div>
+          <div className="panel-footer">
+            <Button className="btn-primary" loading={this.state.isLoading}>
+              {gettext("Change password")}
+            </Button>
+          </div>
         </div>
-      </div>
-    </form>;
-    /* jshint ignore:end */
+      </form>
+    )
   }
 }

+ 11 - 14
frontend/src/components/options/sign-in-credentials/root.js

@@ -1,17 +1,16 @@
-// jshint ignore:start
-import React from 'react';
-import ChangeEmail from 'misago/components/options/sign-in-credentials/change-email';
-import ChangePassword from 'misago/components/options/sign-in-credentials/change-password';
-import misago from 'misago/index';
-import title from 'misago/services/page-title';
-import UnusablePasswordMessage from './UnusablePasswordMessage';
+import React from "react"
+import ChangeEmail from "misago/components/options/sign-in-credentials/change-email"
+import ChangePassword from "misago/components/options/sign-in-credentials/change-password"
+import misago from "misago/index"
+import title from "misago/services/page-title"
+import UnusablePasswordMessage from "./UnusablePasswordMessage"
 
 export default class extends React.Component {
   componentDidMount() {
     title.set({
       title: gettext("Change email or password"),
       parent: gettext("Change your options")
-    });
+    })
   }
 
   render() {
@@ -25,14 +24,12 @@ export default class extends React.Component {
         <ChangePassword user={this.props.user} />
 
         <p className="message-line">
-          <span className="material-icon">
-            warning
-          </span>
-          <a href={misago.get('FORGOTTEN_PASSWORD_URL')}>
+          <span className="material-icon">warning</span>
+          <a href={misago.get("FORGOTTEN_PASSWORD_URL")}>
             {gettext("Change forgotten password")}
           </a>
         </p>
       </div>
-    );
+    )
   }
-}
+}

+ 18 - 12
frontend/src/components/page-lead.js

@@ -1,26 +1,32 @@
-import React from 'react';
-import stringCount from 'misago/utils/string-count';
+import React from "react"
+import stringCount from "misago/utils/string-count"
 
 export default class extends React.Component {
   getClassName() {
     if (this.props.copy && this.props.copy.length) {
-      if (stringCount(this.props.copy, '<p') === 1 && this.props.copy.indexOf('<br') === -1) {
-        return 'page-lead lead';
+      if (
+        stringCount(this.props.copy, "<p") === 1 &&
+        this.props.copy.indexOf("<br") === -1
+      ) {
+        return "page-lead lead"
       }
     }
 
-    return 'page-lead';
+    return "page-lead"
   }
 
   render() {
     if (this.props.copy && this.props.copy.length) {
-      /* jshint ignore:start */
-      return <div className={this.getClassName()} dangerouslySetInnerHTML={{
-        __html: this.props.copy
-      }} />;
-      /* jshint ignore:end */
+      return (
+        <div
+          className={this.getClassName()}
+          dangerouslySetInnerHTML={{
+            __html: this.props.copy
+          }}
+        />
+      )
     } else {
-      return null;
+      return null
     }
   }
-}
+}

+ 7 - 8
frontend/src/components/panel-loader.js

@@ -1,13 +1,12 @@
-import React from 'react';
-import Loader from 'misago/components/loader'; // jshint ignore:line
+import React from "react"
+import Loader from "misago/components/loader"
 
 export default class extends React.Component {
   render() {
-    /* jshint ignore:start */
-    return <div className="panel-body panel-body-loading">
-      <Loader className="loader loader-spaced" />
-    </div>;
-    /* jshint ignore:end */
+    return (
+      <div className="panel-body panel-body-loading">
+        <Loader className="loader loader-spaced" />
+      </div>
+    )
   }
 }
-

+ 6 - 14
frontend/src/components/panel-message.js

@@ -1,35 +1,27 @@
-import React from 'react';
+import React from "react"
 
 export default class extends React.Component {
   getHelpText() {
     if (this.props.helpText) {
-      /* jshint ignore:start */
-      return <p className="help-block">
-        {this.props.helpText}
-      </p>;
-      /* jshint ignore:end */
+      return <p className="help-block">{this.props.helpText}</p>
     } else {
-      return null;
+      return null
     }
   }
 
   render() {
-    /* jshint ignore:start */
     return (
       <div className="panel-body panel-message-body">
         <div className="message-icon">
           <span className="material-icon">
-            {this.props.icon || 'info_outline'}
+            {this.props.icon || "info_outline"}
           </span>
         </div>
         <div className="message-body">
-          <p className="lead">
-            {this.props.message}
-          </p>
+          <p className="lead">{this.props.message}</p>
           {this.getHelpText()}
         </div>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 }

+ 7 - 12
frontend/src/components/participants/add-participant.js

@@ -1,17 +1,14 @@
-/* jshint ignore:start */
-import React from 'react';
-import AddParticipantModal from 'misago/components/add-participant';
-import modal from 'misago/services/modal';
+import React from "react"
+import AddParticipantModal from "misago/components/add-participant"
+import modal from "misago/services/modal"
 
 export default class extends React.Component {
   onClick = () => {
-    modal.show(
-      <AddParticipantModal thread={this.props.thread} />
-    );
+    modal.show(<AddParticipantModal thread={this.props.thread} />)
   }
 
   render() {
-    if (!this.props.thread.acl.can_add_participants) return null;
+    if (!this.props.thread.acl.can_add_participants) return null
 
     return (
       <div className="col-xs-12 col-sm-3">
@@ -20,12 +17,10 @@ export default class extends React.Component {
           onClick={this.onClick}
           type="button"
         >
-          <span className="material-icon">
-            person_add
-          </span>
+          <span className="material-icon">person_add</span>
           {gettext("Add participant")}
         </button>
       </div>
-    );
+    )
   }
 }

+ 70 - 43
frontend/src/components/participants/cards-list/actions.js

@@ -1,53 +1,80 @@
-import * as participants from 'misago/reducers/participants';
-import { updateAcl } from 'misago/reducers/thread';
-import misago from 'misago';
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
+import * as participants from "misago/reducers/participants"
+import { updateAcl } from "misago/reducers/thread"
+import misago from "misago"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export function leave(thread, participant) {
-  ajax.patch(thread.api.index, [
-    {op: 'remove', path: 'participants', value: participant.id}
-  ]).then(() => {
-    snackbar.success(gettext("You have left this thread."));
-    window.setTimeout(() => {
-      window.location = misago.get('PRIVATE_THREADS_URL');
-    }, 3 * 1000);
-  }, (rejection) => {
-    snackbar.apiError(rejection);
-  });
+  ajax
+    .patch(thread.api.index, [
+      { op: "remove", path: "participants", value: participant.id }
+    ])
+    .then(
+      () => {
+        snackbar.success(gettext("You have left this thread."))
+        window.setTimeout(() => {
+          window.location = misago.get("PRIVATE_THREADS_URL")
+        }, 3 * 1000)
+      },
+      rejection => {
+        snackbar.apiError(rejection)
+      }
+    )
 }
 
 export function remove(thread, participant) {
-  ajax.patch(thread.api.index, [
-    {op: 'remove', path: 'participants', value: participant.id},
-    {op: 'add', path: 'acl', value: 1}
-  ]).then((data) => {
-    store.dispatch(updateAcl(data));
-    store.dispatch(participants.replace(data.participants));
+  ajax
+    .patch(thread.api.index, [
+      { op: "remove", path: "participants", value: participant.id },
+      { op: "add", path: "acl", value: 1 }
+    ])
+    .then(
+      data => {
+        store.dispatch(updateAcl(data))
+        store.dispatch(participants.replace(data.participants))
 
-    const message = gettext("%(user)s has been removed from this thread.");
-    snackbar.success(interpolate(message, {
-      user: participant.username
-    }, true));
-  }, (rejection) => {
-    snackbar.apiError(rejection);
-  });
+        const message = gettext("%(user)s has been removed from this thread.")
+        snackbar.success(
+          interpolate(
+            message,
+            {
+              user: participant.username
+            },
+            true
+          )
+        )
+      },
+      rejection => {
+        snackbar.apiError(rejection)
+      }
+    )
 }
 
 export function changeOwner(thread, participant) {
-  ajax.patch(thread.api.index, [
-    {op: 'replace', path: 'owner', value: participant.id},
-    {op: 'add', path: 'acl', value: 1}
-  ]).then((data) => {
-    store.dispatch(updateAcl(data));
-    store.dispatch(participants.replace(data.participants));
+  ajax
+    .patch(thread.api.index, [
+      { op: "replace", path: "owner", value: participant.id },
+      { op: "add", path: "acl", value: 1 }
+    ])
+    .then(
+      data => {
+        store.dispatch(updateAcl(data))
+        store.dispatch(participants.replace(data.participants))
 
-    const message = gettext("%(user)s has been made new thread owner.");
-    snackbar.success(interpolate(message, {
-      user: participant.username
-    }, true));
-  }, (rejection) => {
-    snackbar.apiError(rejection);
-  });
-}
+        const message = gettext("%(user)s has been made new thread owner.")
+        snackbar.success(
+          interpolate(
+            message,
+            {
+              user: participant.username
+            },
+            true
+          )
+        )
+      },
+      rejection => {
+        snackbar.apiError(rejection)
+      }
+    )
+}

+ 17 - 31
frontend/src/components/participants/cards-list/card.js

@@ -1,17 +1,16 @@
-// jshint ignore:start
-import React from 'react';
-import MakeOwner from './make-owner';
-import Remove from './remove';
-import Avatar from 'misago/components/avatar';
+import React from "react"
+import MakeOwner from "./make-owner"
+import Remove from "./remove"
+import Avatar from "misago/components/avatar"
 
 export default function(props) {
-  const participant = props.participant;
+  const participant = props.participant
 
-  let className = 'btn btn-default';
+  let className = "btn btn-default"
   if (participant.is_owner) {
-    className = 'btn btn-primary';
+    className = "btn btn-primary"
   }
-  className += ' btn-user btn-block';
+  className += " btn-user btn-block"
 
   return (
     <div className="col-xs-12 col-sm-3 col-md-2 participant-card">
@@ -23,44 +22,31 @@ export default function(props) {
           data-toggle="dropdown"
           type="button"
         >
-          <Avatar
-            size="34"
-            user={participant}
-          />
-          <span className="btn-text">
-            {participant.username}
-          </span>
+          <Avatar size="34" user={participant} />
+          <span className="btn-text">{participant.username}</span>
         </button>
         <ul className="dropdown-menu stick-to-bottom">
           <UserStatus isOwner={participant.is_owner} />
           <li className="dropdown-header" />
           <li>
-            <a
-              href={participant.url}
-            >
-              {gettext("See profile")}
-            </a>
+            <a href={participant.url}>{gettext("See profile")}</a>
           </li>
-          <li role="separator" className="divider"></li>
+          <li role="separator" className="divider" />
           <MakeOwner {...props} />
           <Remove {...props} />
         </ul>
       </div>
     </div>
-  );
+  )
 }
 
 export function UserStatus({ isOwner }) {
-  if (!isOwner) return null;
+  if (!isOwner) return null
 
   return (
     <li className="dropdown-header dropdown-header-owner">
-      <span className="material-icon">
-        start
-      </span>
-      <span className="icon-text">
-        {gettext("Thread owner")}
-      </span>
+      <span className="material-icon">start</span>
+      <span className="icon-text">{gettext("Thread owner")}</span>
     </li>
-  );
+  )
 }

+ 6 - 7
frontend/src/components/participants/cards-list/index.js

@@ -1,12 +1,11 @@
-// jshint ignore:start
-import React from 'react';
-import Card from './card';
+import React from "react"
+import Card from "./card"
 
 export default function({ participants, thread, user, userIsOwner }) {
   return (
     <div className="participants-cards">
       <div className="row">
-        {participants.map((participant) => {
+        {participants.map(participant => {
           return (
             <Card
               key={participant.id}
@@ -15,9 +14,9 @@ export default function({ participants, thread, user, userIsOwner }) {
               user={user}
               userIsOwner={userIsOwner}
             />
-          );
+          )
         })}
       </div>
     </div>
-  );
-}
+  )
+}

+ 28 - 23
frontend/src/components/participants/cards-list/make-owner.js

@@ -1,44 +1,49 @@
-// jshint ignore:start
-import React from 'react';
-import { changeOwner } from './actions';
+import React from "react"
+import { changeOwner } from "./actions"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
-    this.isUser = props.participant.id === props.user.id;
+    this.isUser = props.participant.id === props.user.id
   }
 
   onClick = () => {
-    let confirmed = false;
+    let confirmed = false
     if (this.isUser) {
-      confirmed = confirm(gettext("Are you sure you want to take over this thread?"));
+      confirmed = confirm(
+        gettext("Are you sure you want to take over this thread?")
+      )
     } else {
-      const message = gettext("Are you sure you want to change thread owner to %(user)s?");
-      confirmed = confirm(interpolate(message, {
-        user: this.props.participant.username
-      }, true));
+      const message = gettext(
+        "Are you sure you want to change thread owner to %(user)s?"
+      )
+      confirmed = confirm(
+        interpolate(
+          message,
+          {
+            user: this.props.participant.username
+          },
+          true
+        )
+      )
     }
 
-    if (!confirmed) return;
+    if (!confirmed) return
 
-    changeOwner(this.props.thread, this.props.participant);
-  };
+    changeOwner(this.props.thread, this.props.participant)
+  }
 
   render() {
-    if (this.props.participant.is_owner) return null;
-    if (!this.props.thread.acl.can_change_owner) return null;
+    if (this.props.participant.is_owner) return null
+    if (!this.props.thread.acl.can_change_owner) return null
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.onClick}
-          type="button"
-        >
+        <button className="btn btn-link" onClick={this.onClick} type="button">
           {gettext("Make owner")}
         </button>
       </li>
-    );
+    )
   }
-}
+}

+ 29 - 24
frontend/src/components/participants/cards-list/remove.js

@@ -1,49 +1,54 @@
-// jshint ignore:start
-import React from 'react';
-import { remove, leave } from './actions';
+import React from "react"
+import { remove, leave } from "./actions"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
-    this.isUser = props.participant.id === props.user.id;
+    this.isUser = props.participant.id === props.user.id
   }
 
   onClick = () => {
-    let confirmed = false;
+    let confirmed = false
     if (this.isUser) {
-      confirmed = confirm(gettext("Are you sure you want to leave this thread?"));
+      confirmed = confirm(
+        gettext("Are you sure you want to leave this thread?")
+      )
     } else {
-      const message = gettext("Are you sure you want to remove %(user)s from this thread?");
-      confirmed = confirm(interpolate(message, {
-        user: this.props.participant.username
-      }, true));
+      const message = gettext(
+        "Are you sure you want to remove %(user)s from this thread?"
+      )
+      confirmed = confirm(
+        interpolate(
+          message,
+          {
+            user: this.props.participant.username
+          },
+          true
+        )
+      )
     }
 
-    if (!confirmed) return;
+    if (!confirmed) return
 
     if (this.isUser) {
-      leave(this.props.thread, this.props.participant);
+      leave(this.props.thread, this.props.participant)
     } else {
-      remove(this.props.thread, this.props.participant);
+      remove(this.props.thread, this.props.participant)
     }
-  };
+  }
 
   render() {
-    const isModerator = this.props.user.acl.can_moderate_private_threads;
+    const isModerator = this.props.user.acl.can_moderate_private_threads
 
-    if (!(this.props.userIsOwner || this.isUser || isModerator)) return null;
+    if (!(this.props.userIsOwner || this.isUser || isModerator)) return null
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.onClick}
-          type="button"
-        >
+        <button className="btn btn-link" onClick={this.onClick} type="button">
           {this.isUser ? gettext("Leave thread") : gettext("Remove")}
         </button>
       </li>
-    );
+    )
   }
-}
+}

+ 7 - 8
frontend/src/components/participants/index.js

@@ -1,11 +1,10 @@
-// jshint ignore:start
-import React from 'react';
-import AddParticipant from './add-participant';
-import CardsList from './cards-list';
-import * as utils from './utils';
+import React from "react"
+import AddParticipant from "./add-participant"
+import CardsList from "./cards-list"
+import * as utils from "./utils"
 
 export default function(props) {
-  if (!props.participants.length) return null;
+  if (!props.participants.length) return null
 
   return (
     <div className="panel panel-default panel-participants">
@@ -22,9 +21,9 @@ export default function(props) {
         </div>
       </div>
     </div>
-  );
+  )
 }
 
 export function getUserIsOwner(user, participants) {
   return participants[0].id === user.id
-}
+}

+ 11 - 6
frontend/src/components/participants/utils.js

@@ -1,11 +1,16 @@
 export function getParticipantsCopy(participants) {
-  const count = participants.length;
+  const count = participants.length
   const message = ngettext(
     "This thread has %(users)s participant.",
     "This thread has %(users)s participants.",
-    count);
+    count
+  )
 
-  return interpolate(message, {
-    users: count
-  }, true);
-}
+  return interpolate(
+    message,
+    {
+      users: count
+    },
+    true
+  )
+}

+ 44 - 46
frontend/src/components/password-strength.js

@@ -1,13 +1,13 @@
-import React from 'react';
-import zxcvbn from 'misago/services/zxcvbn';
+import React from "react"
+import zxcvbn from "misago/services/zxcvbn"
 
 export const STYLES = [
-  'progress-bar-danger',
-  'progress-bar-warning',
-  'progress-bar-warning',
-  'progress-bar-primary',
-  'progress-bar-success'
-];
+  "progress-bar-danger",
+  "progress-bar-warning",
+  "progress-bar-warning",
+  "progress-bar-primary",
+  "progress-bar-success"
+]
 
 export const LABELS = [
   gettext("Entered password is very weak."),
@@ -15,78 +15,76 @@ export const LABELS = [
   gettext("Entered password is average."),
   gettext("Entered password is strong."),
   gettext("Entered password is very strong.")
-];
+]
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
-    this._score = 0;
-    this._password = null;
-    this._inputs = [];
+    this._score = 0
+    this._password = null
+    this._inputs = []
 
     this.state = {
       loaded: false
-    };
+    }
   }
 
   componentDidMount() {
     zxcvbn.load().then(() => {
-      this.setState({ loaded: true });
-    });
+      this.setState({ loaded: true })
+    })
   }
 
   getScore(password, inputs) {
-    let cacheStale = false;
+    let cacheStale = false
 
     if (password !== this._password) {
-      cacheStale = true;
+      cacheStale = true
     }
 
     if (inputs.length !== this._inputs.length) {
-      cacheStale = true;
+      cacheStale = true
     } else {
       inputs.map((value, i) => {
         if (value.trim() !== this._inputs[i]) {
-          cacheStale = true;
+          cacheStale = true
         }
-      });
+      })
     }
 
     if (cacheStale) {
-      this._score = zxcvbn.scorePassword(password, inputs);
-      this._password = password;
+      this._score = zxcvbn.scorePassword(password, inputs)
+      this._password = password
       this._inputs = inputs.map(function(value) {
-        return value.trim();
-      });
+        return value.trim()
+      })
     }
 
-    return this._score;
+    return this._score
   }
 
   render() {
-    if (!this.state.loaded) return null;
+    if (!this.state.loaded) return null
 
-    /* jshint ignore:start */
-    let score = this.getScore(this.props.password, this.props.inputs);
+    let score = this.getScore(this.props.password, this.props.inputs)
 
-    return <div className="help-block password-strength">
-      <div className="progress">
-        <div className={"progress-bar " + STYLES[score]}
-             style={{width: (20 + (20 * score)) + '%'}}
-             role="progress-bar"
-             aria-valuenow={score}
-             aria-valuemin="0"
-             aria-valuemax="4">
-          <span className="sr-only">
-            {LABELS[score]}
-          </span>
+    return (
+      <div className="help-block password-strength">
+        <div className="progress">
+          <div
+            className={"progress-bar " + STYLES[score]}
+            style={{ width: 20 + 20 * score + "%" }}
+            role="progress-bar"
+            aria-valuenow={score}
+            aria-valuemin="0"
+            aria-valuemax="4"
+          >
+            <span className="sr-only">{LABELS[score]}</span>
+          </div>
         </div>
+        <p className="text-small">{LABELS[score]}</p>
       </div>
-      <p className="text-small">
-        {LABELS[score]}
-      </p>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 37 - 36
frontend/src/components/poll/form/choices-control.js

@@ -1,40 +1,39 @@
-// jshint ignore:start
-import React from 'react';
+import React from "react"
 
 export default class extends React.Component {
   onAdd = () => {
-    let choices = this.props.choices.slice();
+    let choices = this.props.choices.slice()
     choices.push({
       hash: generateRandomHash(),
-      label: ''
-    });
+      label: ""
+    })
 
-    this.props.setChoices(choices);
-  };
+    this.props.setChoices(choices)
+  }
 
   onChange = (hash, label) => {
-    const choices = this.props.choices.map((choice) => {
+    const choices = this.props.choices.map(choice => {
       if (choice.hash === hash) {
-        choice.label = label;
+        choice.label = label
       }
 
       return choice
-    });
-    this.props.setChoices(choices);
-  };
+    })
+    this.props.setChoices(choices)
+  }
 
-  onDelete = (hash) => {
-    const choices = this.props.choices.filter((choice) => {
-      return choice.hash !== hash;
-    });
-    this.props.setChoices(choices);
-  };
+  onDelete = hash => {
+    const choices = this.props.choices.filter(choice => {
+      return choice.hash !== hash
+    })
+    this.props.setChoices(choices)
+  }
 
   render() {
     return (
       <div className="poll-choices-control">
         <ul className="list-group">
-          {this.props.choices.map((choice) => {
+          {this.props.choices.map(choice => {
             return (
               <PollChoice
                 canDelete={this.props.choices.length > 2}
@@ -44,7 +43,7 @@ export default class extends React.Component {
                 onChange={this.onChange}
                 onDelete={this.onDelete}
               />
-            );
+            )
           })}
         </ul>
         <button
@@ -56,22 +55,23 @@ export default class extends React.Component {
           {gettext("Add choice")}
         </button>
       </div>
-
-    );
+    )
   }
 }
 
 export class PollChoice extends React.Component {
-  onChange = (event) => {
-    this.props.onChange(this.props.choice.hash, event.target.value);
-  };
+  onChange = event => {
+    this.props.onChange(this.props.choice.hash, event.target.value)
+  }
 
   onDelete = () => {
-    const deleteItem = confirm(gettext("Are you sure you want to delete this choice?"));
+    const deleteItem = confirm(
+      gettext("Are you sure you want to delete this choice?")
+    )
     if (deleteItem) {
-      this.props.onDelete(this.props.choice.hash);
+      this.props.onDelete(this.props.choice.hash)
     }
-  };
+  }
 
   render() {
     return (
@@ -83,9 +83,7 @@ export class PollChoice extends React.Component {
           title={gettext("Delete this choice")}
           type="button"
         >
-          <span className="material-icon">
-            close
-          </span>
+          <span className="material-icon">close</span>
         </button>
         <input
           disabled={this.props.disabled}
@@ -96,14 +94,17 @@ export class PollChoice extends React.Component {
           value={this.props.choice.label}
         />
       </li>
-    );
+    )
   }
 }
 
 export function generateRandomHash() {
-  let randomHash = '';
+  let randomHash = ""
   while (randomHash.length != 12) {
-    randomHash = Math.random().toString(36).replace(/[^a-zA-Z0-9]+/g, '').substr(1, 12);
+    randomHash = Math.random()
+      .toString(36)
+      .replace(/[^a-zA-Z0-9]+/g, "")
+      .substr(1, 12)
   }
-  return randomHash;
-}
+  return randomHash
+}

+ 64 - 63
frontend/src/components/poll/form/index.js

@@ -1,37 +1,36 @@
-// jshint ignore:start
-import React from 'react';
-import ChoicesControl from './choices-control';
-import Button from 'misago/components/button';
-import Form from 'misago/components/form';
-import FormGroup from 'misago/components/form-group';
-import YesNoSwitch from 'misago/components/yes-no-switch';
-import * as poll from 'misago/reducers/poll';
-import ajax from 'misago/services/ajax';
-import posting from 'misago/services/posting';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
+import React from "react"
+import ChoicesControl from "./choices-control"
+import Button from "misago/components/button"
+import Form from "misago/components/form"
+import FormGroup from "misago/components/form-group"
+import YesNoSwitch from "misago/components/yes-no-switch"
+import * as poll from "misago/reducers/poll"
+import ajax from "misago/services/ajax"
+import posting from "misago/services/posting"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     const poll = props.poll || {
-      question: '',
+      question: "",
       choices: [
         {
-          hash: 'choice-10000',
-          label: ''
+          hash: "choice-10000",
+          label: ""
         },
         {
-          hash: 'choice-20000',
-          label: ''
+          hash: "choice-20000",
+          label: ""
         }
       ],
       length: 0,
       allowed_choices: 1,
       allow_revotes: 0,
       is_public: 0
-    };
+    }
 
     this.state = {
       isLoading: false,
@@ -52,24 +51,24 @@ export default class extends Form {
       },
 
       errors: {}
-    };
+    }
   }
 
-  setChoices = (choices) => {
-    const errors = Object.assign({}, errors, {choices: null});
+  setChoices = choices => {
+    const errors = Object.assign({}, errors, { choices: null })
 
     this.setState({
       choices,
       errors
-    });
-  };
+    })
+  }
 
   onCancel = () => {
-    const cancel = confirm(gettext("Are you sure you want to discard poll?"));
+    const cancel = confirm(gettext("Are you sure you want to discard poll?"))
     if (cancel) {
-      posting.close();
+      posting.close()
     }
-  };
+  }
 
   send() {
     const data = {
@@ -79,40 +78,40 @@ export default class extends Form {
       allowed_choices: this.state.allowed_choices,
       allow_revotes: this.state.allow_revotes,
       is_public: this.state.is_public
-    };
+    }
 
     if (this.state.isEdit) {
-      return ajax.put(this.props.poll.api.index, data);
+      return ajax.put(this.props.poll.api.index, data)
     } else {
-      return ajax.post(this.props.thread.api.poll, data);
+      return ajax.post(this.props.thread.api.poll, data)
     }
   }
 
   handleSuccess(data) {
-    store.dispatch(poll.replace(data));
+    store.dispatch(poll.replace(data))
 
     if (this.state.isEdit) {
-      snackbar.success(gettext("Poll has been edited."));
+      snackbar.success(gettext("Poll has been edited."))
     } else {
-      snackbar.success(gettext("Poll has been posted."));
+      snackbar.success(gettext("Poll has been posted."))
     }
 
-    posting.close();
+    posting.close()
   }
 
   handleError(rejection) {
     if (rejection.status === 400) {
       if (rejection.non_field_errors) {
-        rejection.allowed_choices = rejection.non_field_errors;
+        rejection.allowed_choices = rejection.non_field_errors
       }
 
       this.setState({
-        'errors': Object.assign({}, rejection)
-      });
+        errors: Object.assign({}, rejection)
+      })
 
-      snackbar.error(gettext("Form contains errors."));
+      snackbar.error(gettext("Form contains errors."))
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
   }
 
@@ -123,7 +122,6 @@ export default class extends Form {
           <form onSubmit={this.handleSubmit}>
             <div className="panel panel-default panel-form">
               <div className="panel-body">
-
                 <fieldset>
                   <legend>{gettext("Question and choices")}</legend>
 
@@ -136,7 +134,7 @@ export default class extends Form {
                       className="form-control"
                       disabled={this.state.isLoading}
                       id="id_questions"
-                      onChange={this.bindInput('question')}
+                      onChange={this.bindInput("question")}
                       type="text"
                       maxLength="255"
                       value={this.state.question}
@@ -153,7 +151,6 @@ export default class extends Form {
                       setChoices={this.setChoices}
                     />
                   </FormGroup>
-
                 </fieldset>
 
                 <fieldset>
@@ -163,7 +160,9 @@ export default class extends Form {
                     <div className="col-xs-12 col-sm-6">
                       <FormGroup
                         label={gettext("Poll length")}
-                        helpText={gettext("Enter number of days for which voting in this poll should be possible or zero to run this poll indefinitely.")}
+                        helpText={gettext(
+                          "Enter number of days for which voting in this poll should be possible or zero to run this poll indefinitely."
+                        )}
                         for="id_length"
                         validation={this.state.errors.length}
                       >
@@ -171,7 +170,7 @@ export default class extends Form {
                           className="form-control"
                           disabled={this.state.isLoading}
                           id="id_length"
-                          onChange={this.bindInput('length')}
+                          onChange={this.bindInput("length")}
                           type="text"
                           value={this.state.length}
                         />
@@ -187,7 +186,7 @@ export default class extends Form {
                           className="form-control"
                           disabled={this.state.isLoading}
                           id="id_allowed_choices"
-                          onChange={this.bindInput('allowed_choices')}
+                          onChange={this.bindInput("allowed_choices")}
                           type="text"
                           maxLength="255"
                           value={this.state.allowed_choices}
@@ -213,17 +212,19 @@ export default class extends Form {
                           disabled={this.state.isLoading}
                           iconOn="check"
                           iconOff="close"
-                          labelOn={gettext("Allow participants to change their vote")}
-                          labelOff={gettext("Don't allow participants to change their vote")}
-                          onChange={this.bindInput('allow_revotes')}
+                          labelOn={gettext(
+                            "Allow participants to change their vote"
+                          )}
+                          labelOff={gettext(
+                            "Don't allow participants to change their vote"
+                          )}
+                          onChange={this.bindInput("allow_revotes")}
                           value={this.state.allow_revotes}
                         />
                       </FormGroup>
                     </div>
                   </div>
-
                 </fieldset>
-
               </div>
               <div className="panel-footer text-right">
                 <button
@@ -233,31 +234,31 @@ export default class extends Form {
                   type="button"
                 >
                   {gettext("Cancel")}
-                </button>
-                {' '}
-                <Button
-                  className="btn-primary"
-                  loading={this.state.isLoading}
-                >
-                  {this.state.isEdit ? gettext("Save changes") : gettext("Post poll")}
+                </button>{" "}
+                <Button className="btn-primary" loading={this.state.isLoading}>
+                  {this.state.isEdit
+                    ? gettext("Save changes")
+                    : gettext("Post poll")}
                 </Button>
               </div>
             </div>
           </form>
         </div>
       </div>
-    );
+    )
   }
 }
 
 export function PollPublicSwitch(props) {
-  if (props.isEdit) return null;
+  if (props.isEdit) return null
 
   return (
     <div className="col-xs-12 col-sm-6">
       <FormGroup
         label={gettext("Make voting public")}
-        helpText={gettext("Making voting public will allow everyone to access detailed list of votes, showing which users voted for which choices and at which times. This option can't be changed after poll's creation. Moderators may see voting details for all polls.")}
+        helpText={gettext(
+          "Making voting public will allow everyone to access detailed list of votes, showing which users voted for which choices and at which times. This option can't be changed after poll's creation. Moderators may see voting details for all polls."
+        )}
         for="id_is_public"
       >
         <YesNoSwitch
@@ -267,10 +268,10 @@ export function PollPublicSwitch(props) {
           iconOff="visibility_off"
           labelOn={gettext("Votes are public")}
           labelOff={gettext("Votes are hidden")}
-          onChange={props.bindInput('is_public')}
+          onChange={props.bindInput("is_public")}
           value={props.value}
         />
       </FormGroup>
     </div>
-  );
-}
+  )
+}

+ 4 - 4
frontend/src/components/poll/index.js

@@ -1,5 +1,5 @@
-import Poll from './poll';
-import PollForm from './form';
+import Poll from "./poll"
+import PollForm from "./form"
 
-export { Poll };
-export { PollForm };
+export { Poll }
+export { PollForm }

+ 69 - 46
frontend/src/components/poll/info.js

@@ -1,10 +1,9 @@
-// jshint ignore:start
-import React from 'react';
-import escapeHtml from 'misago/utils/escape-html';
+import React from "react"
+import escapeHtml from "misago/utils/escape-html"
 
-const DATE_ABBR = '<abbr title="%(absolute)s">%(relative)s</abbr>';
-const USER_SPAN = '<span class="item-title">%(user)s</span>';
-const USER_URL = '<a href="%(url)s" class="item-title">%(user)s</a>';
+const DATE_ABBR = '<abbr title="%(absolute)s">%(relative)s</abbr>'
+const USER_SPAN = '<span class="item-title">%(user)s</span>'
+const USER_URL = '<a href="%(url)s" class="item-title">%(user)s</a>'
 
 export default function(props) {
   return (
@@ -14,84 +13,108 @@ export default function(props) {
       <PollIsPublic poll={props.poll} />
       <PollCreation poll={props.poll} />
     </ul>
-  );
+  )
 }
 
 export function PollCreation(props) {
-  const message = interpolate(escapeHtml(gettext("Posted by %(poster)s %(posted_on)s.")), {
-    poster: getPoster(props.poll),
-    posted_on: getPostedOn(props.poll)
-  }, true);
+  const message = interpolate(
+    escapeHtml(gettext("Posted by %(poster)s %(posted_on)s.")),
+    {
+      poster: getPoster(props.poll),
+      posted_on: getPostedOn(props.poll)
+    },
+    true
+  )
 
   return (
     <li
       className="poll-info-creation"
-      dangerouslySetInnerHTML={{__html: message}}
+      dangerouslySetInnerHTML={{ __html: message }}
     />
-  );
+  )
 }
 
 export function getPoster(poll) {
   if (poll.url.poster) {
-    return interpolate(USER_URL, {
-      url: escapeHtml(poll.url.poster),
-      user: escapeHtml(poll.poster_name)
-    }, true);
+    return interpolate(
+      USER_URL,
+      {
+        url: escapeHtml(poll.url.poster),
+        user: escapeHtml(poll.poster_name)
+      },
+      true
+    )
   }
 
-  return interpolate(USER_SPAN, {
-    user: escapeHtml(poll.poster_name)
-  }, true);
+  return interpolate(
+    USER_SPAN,
+    {
+      user: escapeHtml(poll.poster_name)
+    },
+    true
+  )
 }
 
 export function getPostedOn(poll) {
-  return interpolate(DATE_ABBR, {
-    absolute: escapeHtml(poll.posted_on.format('LLL')),
-    relative: escapeHtml(poll.posted_on.fromNow())
-  }, true);
+  return interpolate(
+    DATE_ABBR,
+    {
+      absolute: escapeHtml(poll.posted_on.format("LLL")),
+      relative: escapeHtml(poll.posted_on.fromNow())
+    },
+    true
+  )
 }
 
 export function PollLength(props) {
   if (!props.poll.length) {
-    return null;
+    return null
   }
 
-  const message = interpolate(escapeHtml(gettext("Voting ends %(ends_on)s.")), {
-    ends_on: getEndsOn(props.poll)
-  }, true);
+  const message = interpolate(
+    escapeHtml(gettext("Voting ends %(ends_on)s.")),
+    {
+      ends_on: getEndsOn(props.poll)
+    },
+    true
+  )
 
   return (
     <li
       className="poll-info-ends-on"
-      dangerouslySetInnerHTML={{__html: message}}
+      dangerouslySetInnerHTML={{ __html: message }}
     />
-  );
+  )
 }
 
 export function getEndsOn(poll) {
-  return interpolate(DATE_ABBR, {
-    absolute: escapeHtml(poll.endsOn.format('LLL')),
-    relative: escapeHtml(poll.endsOn.fromNow())
-  }, true);
+  return interpolate(
+    DATE_ABBR,
+    {
+      absolute: escapeHtml(poll.endsOn.format("LLL")),
+      relative: escapeHtml(poll.endsOn.fromNow())
+    },
+    true
+  )
 }
 
 export function PollVotes(props) {
-  const message = ngettext("%(votes)s vote.", "%(votes)s votes.", props.votes);
-  const label = interpolate(message, {
-    'votes': props.votes
-  }, true);
+  const message = ngettext("%(votes)s vote.", "%(votes)s votes.", props.votes)
+  const label = interpolate(
+    message,
+    {
+      votes: props.votes
+    },
+    true
+  )
 
-  return (
-    <li className="poll-info-votes">{label}</li>
-  );
+  return <li className="poll-info-votes">{label}</li>
 }
 
 export function PollIsPublic(props) {
   if (!props.poll.is_public) {
-    return null;
+    return null
   }
 
-  return (
-    <li className="poll-info-public">{gettext("Votes are public.")}</li>
-  );
-}
+  return <li className="poll-info-public">{gettext("Votes are public.")}</li>
+}

+ 24 - 23
frontend/src/components/poll/poll.js

@@ -1,44 +1,45 @@
-// jshint ignore:start
-import React from 'react';
-import moment from 'moment';
-import Results from './results';
-import Voting from './voting';
+import React from "react"
+import moment from "moment"
+import Results from "./results"
+import Voting from "./voting"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
-    let showResults = true;
+    let showResults = true
     if (props.user.id && !props.poll.hasSelectedChoices) {
-      showResults = false;
+      showResults = false
     }
 
     this.state = {
       showResults
-    };
+    }
   }
 
   showResults = () => {
     this.setState({
       showResults: true
-    });
-  };
+    })
+  }
 
   showVoting = () => {
     this.setState({
       showResults: false
-    });
-  };
+    })
+  }
 
   render() {
-    if (!this.props.thread.poll) return null;
+    if (!this.props.thread.poll) return null
 
-    const isPollOver = getIsPollOver(this.props.poll);
+    const isPollOver = getIsPollOver(this.props.poll)
 
-    if (!isPollOver && this.props.poll.acl.can_vote && !this.state.showResults) {
-      return (
-        <Voting showResults={this.showResults} {...this.props} />
-      );
+    if (
+      !isPollOver &&
+      this.props.poll.acl.can_vote &&
+      !this.state.showResults
+    ) {
+      return <Voting showResults={this.showResults} {...this.props} />
     } else {
       return (
         <Results
@@ -46,14 +47,14 @@ export default class extends React.Component {
           showVoting={this.showVoting}
           {...this.props}
         />
-      );
+      )
     }
   }
 }
 
 export function getIsPollOver(poll) {
   if (poll.length) {
-    return moment().isAfter(poll.endsOn);
+    return moment().isAfter(poll.endsOn)
   }
-  return false;
-}
+  return false
+}

+ 26 - 30
frontend/src/components/poll/results/chart.js

@@ -1,26 +1,21 @@
-// jshint ignore:start
-import React from 'react';
+import React from "react"
 
 export default function(props) {
   return (
     <div className="poll-choices-bars">
-      {props.poll.choices.map((choice) => {
+      {props.poll.choices.map(choice => {
         return (
-          <PollChoice
-            choice={choice}
-            key={choice.hash}
-            poll={props.poll}
-          />
-        );
+          <PollChoice choice={choice} key={choice.hash} poll={props.poll} />
+        )
       })}
     </div>
-  );
+  )
 }
 
 export function PollChoice(props) {
-  let proc = 0;
+  let proc = 0
   if (props.choice.votes && props.poll.votes) {
-    proc = Math.ceil(props.choice.votes * 100 / props.poll.votes);
+    proc = Math.ceil((props.choice.votes * 100) / props.poll.votes)
   }
 
   return (
@@ -34,7 +29,7 @@ export function PollChoice(props) {
             aria-valuenow={proc}
             aria-valuemin="0"
             aria-valuemax="100"
-            style={{width: proc + '%'}}
+            style={{ width: proc + "%" }}
           >
             <span className="sr-only">
               {getVotesLabel(props.votes, props.proc)}
@@ -42,15 +37,12 @@ export function PollChoice(props) {
           </div>
         </div>
         <ul className="list-unstyled list-inline poll-chart">
-          <ChoiceVotes
-            proc={proc}
-            votes={props.choice.votes}
-          />
+          <ChoiceVotes proc={proc} votes={props.choice.votes} />
           <UserChoice selected={props.choice.selected} />
         </ul>
       </dd>
     </dl>
-  );
+  )
 }
 
 export function ChoiceVotes(props) {
@@ -58,29 +50,33 @@ export function ChoiceVotes(props) {
     <li className="poll-chart-votes">
       {getVotesLabel(props.votes, props.proc)}
     </li>
-  );
+  )
 }
 
 export function getVotesLabel(votes, proc) {
   const message = ngettext(
     "%(votes)s vote, %(proc)s% of total.",
-    "%(votes)s votes, %(proc)s% of total.", votes);
+    "%(votes)s votes, %(proc)s% of total.",
+    votes
+  )
 
-  return interpolate(message, {
-    'votes': votes,
-    'proc': proc
-  }, true);
+  return interpolate(
+    message,
+    {
+      votes: votes,
+      proc: proc
+    },
+    true
+  )
 }
 
 export function UserChoice(props) {
-  if (!props.selected) return null;
+  if (!props.selected) return null
 
   return (
     <li className="poll-chart-selected">
-      <span className="material-icon">
-        check_box
-      </span>
+      <span className="material-icon">check_box</span>
       {gettext("Your choice.")}
     </li>
-  );
-}
+  )
+}

+ 6 - 7
frontend/src/components/poll/results/index.js

@@ -1,8 +1,7 @@
-// jshint ignore:start
-import React from 'react';
-import Chart from './chart';
-import Options from './options';
-import PollInfo from '../info';
+import React from "react"
+import Chart from "./chart"
+import Options from "./options"
+import PollInfo from "../info"
 
 export default function(props) {
   return (
@@ -19,5 +18,5 @@ export default function(props) {
         />
       </div>
     </div>
-  );
-}
+  )
+}

+ 65 - 89
frontend/src/components/poll/results/modal.js

@@ -1,49 +1,53 @@
-// jshint ignore:start
-import React from 'react';
-import moment from 'moment';
-import Message from 'misago/components/modal-message';
-import Loader from 'misago/components/modal-loader';
-import ajax from 'misago/services/ajax';
+import React from "react"
+import moment from "moment"
+import Message from "misago/components/modal-message"
+import Loader from "misago/components/modal-loader"
+import ajax from "misago/services/ajax"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoading: true,
       error: null,
       data: []
-    };
+    }
   }
 
   componentDidMount() {
-    ajax.get(this.props.poll.api.votes).then((data) => {
-      const hydratedData = data.map((choice) => {
-        return Object.assign({}, choice, {
-          voters: choice.voters.map((voter) => {
-            return Object.assign({}, voter, {
-              voted_on: moment(voter.voted_on)
+    ajax.get(this.props.poll.api.votes).then(
+      data => {
+        const hydratedData = data.map(choice => {
+          return Object.assign({}, choice, {
+            voters: choice.voters.map(voter => {
+              return Object.assign({}, voter, {
+                voted_on: moment(voter.voted_on)
+              })
             })
           })
-        });
-      });
-
-      this.setState({
-        isLoading: false,
-        data: hydratedData
-      });
-    }, (rejection) => {
-      this.setState({
-        isLoading: false,
-        error: rejection.detail
-      });
-    });
+        })
+
+        this.setState({
+          isLoading: false,
+          data: hydratedData
+        })
+      },
+      rejection => {
+        this.setState({
+          isLoading: false,
+          error: rejection.detail
+        })
+      }
+    )
   }
 
   render() {
     return (
       <div
-        className={'modal-dialog' + (this.state.error ? ' modal-message' : ' modal-sm')}
+        className={
+          "modal-dialog" + (this.state.error ? " modal-message" : " modal-sm")
+        }
         role="document"
       >
         <div className="modal-content">
@@ -64,49 +68,32 @@ export default class extends React.Component {
             error={this.state.error}
             isLoading={this.state.isLoading}
           />
-
         </div>
       </div>
-    );
+    )
   }
 }
 
 export function ModalBody(props) {
   if (props.isLoading) {
-    return (
-      <Loader />
-    );
+    return <Loader />
   } else if (props.error) {
-    return (
-      <Message
-        icon='error_outline'
-        message={props.error}
-      />
-    );
+    return <Message icon="error_outline" message={props.error} />
   }
 
-  return (
-    <ChoicesList
-      data={props.data}
-    />
-  );
+  return <ChoicesList data={props.data} />
 }
 
 export function ChoicesList(props) {
   return (
     <div className="modal-body modal-poll-votes">
       <ul className="list-unstyled votes-details">
-        {props.data.map((choice) => {
-          return (
-            <ChoiceDetails
-              key={choice.hash}
-              {...choice}
-            />
-          );
+        {props.data.map(choice => {
+          return <ChoiceDetails key={choice.hash} {...choice} />
         })}
       </ul>
     </div>
-  );
+  )
 }
 
 export function ChoiceDetails(props) {
@@ -115,75 +102,64 @@ export function ChoiceDetails(props) {
       <h4>{props.label}</h4>
       <VotesCount votes={props.votes} />
       <VotesList voters={props.voters} />
-      <hr/>
+      <hr />
     </li>
-  );
+  )
 }
 
 export function VotesCount(props) {
   const message = ngettext(
     "%(votes)s user has voted for this choice.",
     "%(votes)s users have voted for this choice.",
-    props.votes);
-
-  const label = interpolate(message, {
-    'votes': props.votes
-  }, true);
-
-  return (
-    <p>{label}</p>
-  );
+    props.votes
+  )
+
+  const label = interpolate(
+    message,
+    {
+      votes: props.votes
+    },
+    true
+  )
+
+  return <p>{label}</p>
 }
 
 export function VotesList(props) {
-  if (!props.voters.length) return null;
+  if (!props.voters.length) return null
 
   return (
     <ul className="list-unstyled">
-      {props.voters.map((user) => {
-        return (
-          <Voter
-            key={user.username}
-            {...user}
-          />
-        );
+      {props.voters.map(user => {
+        return <Voter key={user.username} {...user} />
       })}
     </ul>
-  );
+  )
 }
 
 export function Voter(props) {
   if (props.url) {
     return (
       <li>
-        <a
-          className="item-title"
-          href={props.url}
-        >
+        <a className="item-title" href={props.url}>
           {props.username}
-        </a>
-        {' '}
+        </a>{" "}
         <VoteDate voted_on={props.voted_on} />
       </li>
-    );
+    )
   }
 
   return (
     <li>
-      <strong>{props.username}</strong>
-      {' '}
-      <VoteDate voted_on={props.voted_on} />
+      <strong>{props.username}</strong> <VoteDate voted_on={props.voted_on} />
     </li>
-  );
+  )
 }
 
 export function VoteDate(props) {
   return (
-    <abbr
-      className="text-muted"
-      title={props.voted_on.format('LLL')}
-    >
+    <abbr className="text-muted" title={props.voted_on.format("LLL")}>
       {props.voted_on.fromNow()}
     </abbr>
-  );
-}
+  )
+}

+ 71 - 75
frontend/src/components/poll/results/options.js

@@ -1,28 +1,27 @@
-// jshint ignore:start
-import React from 'react';
-import Modal from './modal';
-import * as poll from 'misago/reducers/poll';
-import * as thread from 'misago/reducers/thread';
-import ajax from 'misago/services/ajax';
-import modal from 'misago/services/modal';
-import posting from 'misago/services/posting';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
+import React from "react"
+import Modal from "./modal"
+import * as poll from "misago/reducers/poll"
+import * as thread from "misago/reducers/thread"
+import ajax from "misago/services/ajax"
+import modal from "misago/services/modal"
+import posting from "misago/services/posting"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export default function(props) {
-  const { isPollOver, poll, showVoting, thread } = props;
+  const { isPollOver, poll, showVoting, thread } = props
 
-  if (!isVisible(isPollOver, poll.acl, poll)) return null;
+  if (!isVisible(isPollOver, poll.acl, poll)) return null
 
-  const controls = [];
+  const controls = []
 
-  const canVote = poll.acl.can_vote;
-  const canChangeVote = !poll.hasSelectedChoices || poll.allow_revotes;
+  const canVote = poll.acl.can_vote
+  const canChangeVote = !poll.hasSelectedChoices || poll.allow_revotes
 
-  if (canVote && canChangeVote) controls.push(0);
-  if (poll.is_public || poll.acl.can_see_votes) controls.push(1);
-  if (poll.acl.can_edit) controls.push(2);
-  if (poll.acl.can_delete) controls.push(3);
+  if (canVote && canChangeVote) controls.push(0)
+  if (poll.is_public || poll.acl.can_see_votes) controls.push(1)
+  if (poll.acl.can_edit) controls.push(2)
+  if (poll.acl.can_delete) controls.push(3)
 
   return (
     <div className="row poll-options">
@@ -32,21 +31,11 @@ export default function(props) {
         poll={poll}
         showVoting={showVoting}
       />
-      <SeeVotes
-        controls={controls}
-        poll={poll}
-      />
-      <Edit
-        controls={controls}
-        poll={poll}
-        thread={thread}
-      />
-      <Delete
-        controls={controls}
-        poll={poll}
-      />
+      <SeeVotes controls={controls} poll={poll} />
+      <Edit controls={controls} poll={poll} thread={thread} />
+      <Delete controls={controls} poll={poll} />
     </div>
-  );
+  )
 }
 
 export function isVisible(isPollOver, acl, poll) {
@@ -55,29 +44,32 @@ export function isVisible(isPollOver, acl, poll) {
     acl.can_delete ||
     acl.can_edit ||
     acl.can_see_votes ||
-    (acl.can_vote && !isPollOver && (!poll.hasSelectedChoices || poll.allow_revotes))
-  );
+    (acl.can_vote &&
+      !isPollOver &&
+      (!poll.hasSelectedChoices || poll.allow_revotes))
+  )
 }
 
 export function getClassName(controls, control) {
-  let className = 'col-xs-6';
+  let className = "col-xs-6"
 
   if (controls.length === 1) {
-    className = 'col-xs-12';
+    className = "col-xs-12"
   }
 
   if (controls.length === 3 && controls[0] === control) {
-    className = 'col-xs-12';
+    className = "col-xs-12"
   }
 
-  return className + ' col-sm-3 col-md-2';
+  return className + " col-sm-3 col-md-2"
 }
 
 export function ChangeVote(props) {
-  const canVote = props.poll.acl.can_vote;
-  const canChangeVote = !props.poll.hasSelectedChoices || props.poll.allow_revotes;
+  const canVote = props.poll.acl.can_vote
+  const canChangeVote =
+    !props.poll.hasSelectedChoices || props.poll.allow_revotes
 
-  if (!(canVote && canChangeVote)) return null;
+  if (!(canVote && canChangeVote)) return null
 
   return (
     <div className={getClassName(props.controls, 0)}>
@@ -90,19 +82,18 @@ export function ChangeVote(props) {
         {gettext("Vote")}
       </button>
     </div>
-  );
+  )
 }
 
 export class SeeVotes extends React.Component {
   onClick = () => {
-    modal.show(
-      <Modal poll={this.props.poll} />
-    );
-  };
+    modal.show(<Modal poll={this.props.poll} />)
+  }
 
   render() {
-    const seeVotes = this.props.poll.is_public || this.props.poll.acl.can_see_votes;
-    if (!seeVotes) return null;
+    const seeVotes =
+      this.props.poll.is_public || this.props.poll.acl.can_see_votes
+    if (!seeVotes) return null
 
     return (
       <div className={getClassName(this.props.controls, 1)}>
@@ -115,7 +106,7 @@ export class SeeVotes extends React.Component {
           {gettext("See votes")}
         </button>
       </div>
-    );
+    )
   }
 }
 
@@ -127,12 +118,12 @@ export class Edit extends React.Component {
       thread: this.props.thread,
       poll: this.props.poll,
 
-      mode: 'POLL'
-    });
-  };
+      mode: "POLL"
+    })
+  }
 
   render() {
-    if (!this.props.poll.acl.can_edit) return null;
+    if (!this.props.poll.acl.can_edit) return null
 
     return (
       <div className={getClassName(this.props.controls, 2)}>
@@ -145,34 +136,39 @@ export class Edit extends React.Component {
           {gettext("Edit")}
         </button>
       </div>
-    );
+    )
   }
 }
 
 export class Delete extends React.Component {
   onClick = () => {
-    const deletePoll = confirm(gettext("Are you sure you want to delete this poll? This action is not reversible."));
-    if (!deletePoll) return false;
-
-    store.dispatch(poll.busy());
-
-    ajax.delete(this.props.poll.api.index).then(
-      this.handleSuccess, this.handleError);
-  };
+    const deletePoll = confirm(
+      gettext(
+        "Are you sure you want to delete this poll? This action is not reversible."
+      )
+    )
+    if (!deletePoll) return false
+
+    store.dispatch(poll.busy())
+
+    ajax
+      .delete(this.props.poll.api.index)
+      .then(this.handleSuccess, this.handleError)
+  }
 
-  handleSuccess = (newThreadAcl) => {
-    snackbar.success("Poll has been deleted");
-    store.dispatch(poll.remove());
-    store.dispatch(thread.updateAcl(newThreadAcl));
-  };
+  handleSuccess = newThreadAcl => {
+    snackbar.success("Poll has been deleted")
+    store.dispatch(poll.remove())
+    store.dispatch(thread.updateAcl(newThreadAcl))
+  }
 
-  handleError = (rejection) => {
-    snackbar.apiError(rejection);
-    store.dispatch(poll.release());
-  };
+  handleError = rejection => {
+    snackbar.apiError(rejection)
+    store.dispatch(poll.release())
+  }
 
   render() {
-    if (!this.props.poll.acl.can_delete) return null;
+    if (!this.props.poll.acl.can_delete) return null
 
     return (
       <div className={getClassName(this.props.controls, 3)}>
@@ -185,6 +181,6 @@ export class Delete extends React.Component {
           {gettext("Delete")}
         </button>
       </div>
-    );
+    )
   }
-}
+}

+ 25 - 23
frontend/src/components/poll/voting/help.js

@@ -1,10 +1,9 @@
-// jshint ignore:start
-import React from 'react';
-import escapeHtml from 'misago/utils/escape-html';
+import React from "react"
+import escapeHtml from "misago/utils/escape-html"
 
-const DATE_ABBR = '<abbr title="%(absolute)s">%(relative)s</abbr>';
-const USER_SPAN = '<span class="item-title">%(user)s</span>';
-const USER_URL = '<a href="%(url)s" class="item-title">%(user)s</a>';
+const DATE_ABBR = '<abbr title="%(absolute)s">%(relative)s</abbr>'
+const USER_SPAN = '<span class="item-title">%(user)s</span>'
+const USER_URL = '<a href="%(url)s" class="item-title">%(user)s</a>'
 
 export default function(props) {
   return (
@@ -12,7 +11,7 @@ export default function(props) {
       <PollChoicesLeft choicesLeft={props.choicesLeft} />
       <PollAllowRevote poll={props.poll} />
     </ul>
-  );
+  )
 }
 
 export function PollChoicesLeft({ choicesLeft }) {
@@ -21,31 +20,34 @@ export function PollChoicesLeft({ choicesLeft }) {
       <li className="poll-help-choices-left">
         {gettext("You can't select any more choices.")}
       </li>
-    );
+    )
   }
 
   const message = ngettext(
     "You can select %(choices)s more choice.",
     "You can select %(choices)s more choices.",
-    choicesLeft);
-
-  const label = interpolate(message, {
-    'choices': choicesLeft
-  }, true);
-
-  return (
-    <li className="poll-help-choices-left">{label}</li>
-  );
+    choicesLeft
+  )
+
+  const label = interpolate(
+    message,
+    {
+      choices: choicesLeft
+    },
+    true
+  )
+
+  return <li className="poll-help-choices-left">{label}</li>
 }
 
 export function PollAllowRevote(props) {
   if (props.poll.allow_revotes) {
     return (
-      <li className="poll-help-allow-revotes">{gettext("You can change your vote later.")}</li>
-    );
+      <li className="poll-help-allow-revotes">
+        {gettext("You can change your vote later.")}
+      </li>
+    )
   }
 
-  return (
-    <li className="poll-help-no-revotes">{gettext("Votes are final.")}</li>
-  );
-}
+  return <li className="poll-help-no-revotes">{gettext("Votes are final.")}</li>
+}

+ 52 - 55
frontend/src/components/poll/voting/index.js

@@ -1,116 +1,116 @@
-// jshint ignore:start
-import React from 'react';
-import ChoicesHelp from './help';
-import ChoicesSelect from './select';
-import { getChoicesLeft, getChoiceFromHash } from './utils';
-import PollInfo from '../info';
-import { Delete, Edit, getClassName } from '../results/options';
-import Button from 'misago/components/button';
-import Form from 'misago/components/form';
-import * as poll from 'misago/reducers/poll';
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
+import React from "react"
+import ChoicesHelp from "./help"
+import ChoicesSelect from "./select"
+import { getChoicesLeft, getChoiceFromHash } from "./utils"
+import PollInfo from "../info"
+import { Delete, Edit, getClassName } from "../results/options"
+import Button from "misago/components/button"
+import Form from "misago/components/form"
+import * as poll from "misago/reducers/poll"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoading: false,
 
       choices: props.poll.choices,
       choicesLeft: getChoicesLeft(props.poll, props.poll.choices)
-    };
+    }
   }
 
-  toggleChoice = (hash) => {
-    const choice = getChoiceFromHash(this.state.choices, hash);
+  toggleChoice = hash => {
+    const choice = getChoiceFromHash(this.state.choices, hash)
 
     let choices = null
     if (!choice.selected) {
-      choices = this.selectChoice(choice, hash);
+      choices = this.selectChoice(choice, hash)
     } else {
-      choices = this.deselectChoice(choice, hash);
+      choices = this.deselectChoice(choice, hash)
     }
 
     this.setState({
       choices,
       choicesLeft: getChoicesLeft(this.props.poll, choices)
-    });
-  };
+    })
+  }
 
   selectChoice = (choice, hash) => {
-    const choicesLeft = getChoicesLeft(this.props.poll, this.state.choices);
+    const choicesLeft = getChoicesLeft(this.props.poll, this.state.choices)
 
     if (!choicesLeft) {
       for (const i in this.state.choices.slice()) {
-        const item = this.state.choices[i];
+        const item = this.state.choices[i]
         if (item.selected && item.hash != hash) {
-          item.selected = false;
-          break;
+          item.selected = false
+          break
         }
       }
     }
 
-    return this.state.choices.map((choice) => {
+    return this.state.choices.map(choice => {
       return Object.assign({}, choice, {
         selected: choice.hash == hash ? true : choice.selected
-      });
-    });
-  };
+      })
+    })
+  }
 
   deselectChoice = (choice, hash) => {
-    return this.state.choices.map((choice) => {
+    return this.state.choices.map(choice => {
       return Object.assign({}, choice, {
         selected: choice.hash == hash ? false : choice.selected
-      });
-    });
-  };
+      })
+    })
+  }
 
   clean() {
     if (this.state.choicesLeft === this.props.poll.allowed_choices) {
-      snackbar.error(gettext("You need to select at least one choice"));
-      return false;
+      snackbar.error(gettext("You need to select at least one choice"))
+      return false
     }
 
-    return true;
+    return true
   }
 
   send() {
-    let data = [];
+    let data = []
     for (const i in this.state.choices.slice()) {
-      const item = this.state.choices[i];
+      const item = this.state.choices[i]
       if (item.selected) {
         data.push(item.hash)
       }
     }
 
-    return ajax.post(this.props.poll.api.votes, data);
+    return ajax.post(this.props.poll.api.votes, data)
   }
 
   handleSuccess(data) {
-    store.dispatch(poll.replace(data));
-    snackbar.success(gettext("Your vote has been saved."));
+    store.dispatch(poll.replace(data))
+    snackbar.success(gettext("Your vote has been saved."))
 
-    this.props.showResults();
+    this.props.showResults()
   }
 
   handleError(rejection) {
     if (rejection.status === 400) {
-      snackbar.error(rejection.detail);
+      snackbar.error(rejection.detail)
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
   }
 
   render() {
-    const controls = [];
+    const controls = []
 
-    if (this.props.poll.acl.can_vote) controls.push(0);
-    if (this.props.poll.is_public || this.props.poll.acl.can_see_votes) controls.push(1);
-    if (this.props.poll.acl.can_edit) controls.push(2);
-    if (this.props.poll.acl.can_delete) controls.push(3);
+    if (this.props.poll.acl.can_vote) controls.push(0)
+    if (this.props.poll.is_public || this.props.poll.acl.can_see_votes)
+      controls.push(1)
+    if (this.props.poll.acl.can_edit) controls.push(2)
+    if (this.props.poll.acl.can_delete) controls.push(3)
 
     return (
       <div className="panel panel-default panel-poll">
@@ -152,14 +152,11 @@ export default class extends Form {
                 poll={this.props.poll}
                 thread={this.props.thread}
               />
-              <Delete
-                controls={controls}
-                poll={this.props.poll}
-              />
+              <Delete controls={controls} poll={this.props.poll} />
             </div>
           </div>
         </form>
       </div>
-    );
+    )
   }
-}
+}

+ 13 - 14
frontend/src/components/poll/voting/select.js

@@ -1,43 +1,42 @@
-// jshint ignore:start
-import React from 'react';
+import React from "react"
 
 export default function(props) {
   return (
     <ul className="list-unstyled poll-select-choices">
-      {props.choices.map((choice) => {
+      {props.choices.map(choice => {
         return (
           <ChoiceSelect
             choice={choice}
             key={choice.hash}
             toggleChoice={props.toggleChoice}
           />
-        );
+        )
       })}
     </ul>
-  );
+  )
 }
 
 export class ChoiceSelect extends React.Component {
   onClick = () => {
-    this.props.toggleChoice(this.props.choice.hash);
-  };
+    this.props.toggleChoice(this.props.choice.hash)
+  }
 
   render() {
     return (
       <li className="poll-select-choice">
         <button
-          className={this.props.choice.selected ? 'btn btn-selected' : 'btn'}
+          className={this.props.choice.selected ? "btn btn-selected" : "btn"}
           onClick={this.onClick}
           type="button"
         >
           <span className="material-icon">
-            {this.props.choice.selected ? 'check_box' : 'check_box_outline_blank'}
+            {this.props.choice.selected
+              ? "check_box"
+              : "check_box_outline_blank"}
           </span>
-          <strong>
-            {this.props.choice.label}
-          </strong>
+          <strong>{this.props.choice.label}</strong>
         </button>
       </li>
-    );
+    )
   }
-}
+}

+ 8 - 8
frontend/src/components/poll/voting/utils.js

@@ -1,22 +1,22 @@
 export function getChoiceFromHash(choices, hash) {
   for (const i in choices) {
-    const choice = choices[i];
+    const choice = choices[i]
     if (choice.hash === hash) {
-      return choice;
+      return choice
     }
   }
 
-  return null;
+  return null
 }
 
 export function getChoicesLeft(poll, choices) {
-  let selection = [];
+  let selection = []
   for (const i in choices) {
-    const choice = choices[i];
+    const choice = choices[i]
     if (choice.selected) {
-      selection.push(choice);
+      selection.push(choice)
     }
   }
 
-  return poll.allowed_choices - selection.length;
-}
+  return poll.allowed_choices - selection.length
+}

+ 14 - 19
frontend/src/components/post-changelog/diff.js

@@ -1,40 +1,35 @@
-// jshint ignore:start
-import React from 'react';
+import React from "react"
 
 export default function(props) {
   return (
     <div className="modal-body post-changelog-diff">
       <ul className="list-unstyled">
         {props.diff.map((item, i) => {
-          return (
-            <DiffItem item={item} key={i} />
-          );
+          return <DiffItem item={item} key={i} />
         })}
       </ul>
     </div>
-  );
+  )
 }
 
 export function DiffItem(props) {
-  if (props.item[0] === '?') return null;
+  if (props.item[0] === "?") return null
 
   return (
-    <li className={getItemClassName(props.item)}>
-      {cleanItem(props.item)}
-    </li>
-  );
+    <li className={getItemClassName(props.item)}>{cleanItem(props.item)}</li>
+  )
 }
 
 export function getItemClassName(item) {
-  let className = 'diff-item';
-  if (item[0] === '-') {
-    className += ' diff-item-sub';
-  } else if (item[0] === '+') {
-    className += ' diff-item-add';
+  let className = "diff-item"
+  if (item[0] === "-") {
+    className += " diff-item-sub"
+  } else if (item[0] === "+") {
+    className += " diff-item-add"
   }
-  return className;
+  return className
 }
 
 export function cleanItem(item) {
-  return item.substr(2);
-}
+  return item.substr(2)
+}

+ 6 - 6
frontend/src/components/post-changelog/footer.js

@@ -1,14 +1,13 @@
-// jshint ignore:start
-import React from 'react';
-import Button from 'misago/components/button';
+import React from "react"
+import Button from "misago/components/button"
 
 export default class extends React.Component {
   onClick = () => {
-    this.props.revertEdit(this.props.edit.id);
+    this.props.revertEdit(this.props.edit.id)
   }
 
   render() {
-    if (!this.props.canRevert) return null;
+    if (!this.props.canRevert) return null
 
     return (
       <div className="modal-footer visible-xs-block">
@@ -22,4 +21,5 @@ export default class extends React.Component {
         </Button>
       </div>
     )
-  }}
+  }
+}

+ 72 - 68
frontend/src/components/post-changelog/index.js

@@ -1,20 +1,19 @@
-// jshint ignore:start
-import React from 'react';
-import Diff from './diff';
-import Footer from './footer';
-import Toolbar from './toolbar';
-import { hydrateEdit } from './utils';
-import Message from 'misago/components/modal-message';
-import Loader from 'misago/components/modal-loader';
-import * as post from 'misago/reducers/post';
-import ajax from 'misago/services/ajax';
-import modal from 'misago/services/modal';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
+import React from "react"
+import Diff from "./diff"
+import Footer from "./footer"
+import Toolbar from "./toolbar"
+import { hydrateEdit } from "./utils"
+import Message from "misago/components/modal-message"
+import Loader from "misago/components/modal-loader"
+import * as post from "misago/reducers/post"
+import ajax from "misago/services/ajax"
+import modal from "misago/services/modal"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isReady: false,
@@ -24,73 +23,81 @@ export default class extends React.Component {
 
       error: null,
       edit: null
-    };
+    }
   }
 
   componentDidMount() {
-    this.goToEdit();
+    this.goToEdit()
   }
 
-  goToEdit = (edit=null) => {
+  goToEdit = (edit = null) => {
     this.setState({
       isBusy: true
-    });
+    })
 
-    let url = this.props.post.api.edits;
+    let url = this.props.post.api.edits
     if (edit !== null) {
-      url += '?edit=' + edit;
+      url += "?edit=" + edit
     }
 
-    ajax.get(url).then((data) => {
-      this.setState({
-        isReady: true,
-        isBusy: false,
-        edit: hydrateEdit(data)
-      });
-    }, (rejection) => {
-      this.setState({
-        isReady: true,
-        isBusy: false,
-        error: rejection.detail
-      });
-    });
-  };
-
-  revertEdit = (edit) => {
-    if (this.state.isBusy) return;
-
-    const confirmation = confirm(gettext("Are you sure you with to revert this post to the state from before this edit?"));
-    if (!confirmation) return;
-
-    this.setState({
-      isBusy: true
-    });
+    ajax.get(url).then(
+      data => {
+        this.setState({
+          isReady: true,
+          isBusy: false,
+          edit: hydrateEdit(data)
+        })
+      },
+      rejection => {
+        this.setState({
+          isReady: true,
+          isBusy: false,
+          error: rejection.detail
+        })
+      }
+    )
+  }
 
-    const url = this.props.post.api.edits + '?edit=' + edit;
-    ajax.post(url).then((data) => {
-      const hydratedPost = post.hydrate(data);
-      store.dispatch(post.patch(data, hydratedPost));
+  revertEdit = edit => {
+    if (this.state.isBusy) return
 
-      snackbar.success(gettext("Post has been reverted to previous state."));
-      modal.hide();
-    }, (rejection) => {
-      snackbar.apiError(rejection);
+    const confirmation = confirm(
+      gettext(
+        "Are you sure you with to revert this post to the state from before this edit?"
+      )
+    )
+    if (!confirmation) return
 
-      this.setState({
-        isBusy: false
-      });
-    });
-  };
+    this.setState({
+      isBusy: true
+    })
+
+    const url = this.props.post.api.edits + "?edit=" + edit
+    ajax.post(url).then(
+      data => {
+        const hydratedPost = post.hydrate(data)
+        store.dispatch(post.patch(data, hydratedPost))
+
+        snackbar.success(gettext("Post has been reverted to previous state."))
+        modal.hide()
+      },
+      rejection => {
+        snackbar.apiError(rejection)
+
+        this.setState({
+          isBusy: false
+        })
+      }
+    )
+  }
 
   render() {
     if (this.state.error) {
       return (
         <ModalDialog className="modal-dialog modal-message">
-          <Message
-            message={this.state.error}
-          />
+          <Message message={this.state.error} />
         </ModalDialog>
-      );
+      )
     } else if (this.state.isReady) {
       return (
         <ModalDialog>
@@ -109,23 +116,20 @@ export default class extends React.Component {
             revertEdit={this.revertEdit}
           />
         </ModalDialog>
-      );
+      )
     }
 
     return (
       <ModalDialog>
         <Loader />
       </ModalDialog>
-    );
+    )
   }
 }
 
 export function ModalDialog(props) {
   return (
-    <div
-      className={props.className || "modal-dialog"}
-      role="document"
-    >
+    <div className={props.className || "modal-dialog"} role="document">
       <div className="modal-content">
         <div className="modal-header">
           <button
@@ -142,4 +146,4 @@ export function ModalDialog(props) {
       </div>
     </div>
   )
-}
+}

+ 52 - 45
frontend/src/components/post-changelog/toolbar.js

@@ -1,27 +1,26 @@
-// jshint ignore:start
-import React from 'react';
-import Button from 'misago/components/button';
-import escapeHtml from 'misago/utils/escape-html';
+import React from "react"
+import Button from "misago/components/button"
+import escapeHtml from "misago/utils/escape-html"
 
-const DATE_ABBR = '<abbr title="%(absolute)s">%(relative)s</abbr>';
-const USER_SPAN = '<span class="item-title">%(user)s</span>';
-const USER_URL = '<a href="%(url)s" class="item-title">%(user)s</a>';
+const DATE_ABBR = '<abbr title="%(absolute)s">%(relative)s</abbr>'
+const USER_SPAN = '<span class="item-title">%(user)s</span>'
+const USER_URL = '<a href="%(url)s" class="item-title">%(user)s</a>'
 
 export default class extends React.Component {
   goLast = () => {
-    this.props.goToEdit();
-  };
+    this.props.goToEdit()
+  }
 
   goForward = () => {
-    this.props.goToEdit(this.props.edit.next);
-  };
+    this.props.goToEdit(this.props.edit.next)
+  }
 
   goBack = () => {
-    this.props.goToEdit(this.props.edit.previous);
-  };
+    this.props.goToEdit(this.props.edit.previous)
+  }
 
   revertEdit = () => {
-    this.props.revertEdit(this.props.edit.id);
+    this.props.revertEdit(this.props.edit.id)
   }
 
   render() {
@@ -63,7 +62,7 @@ export default class extends React.Component {
           />
         </div>
       </div>
-    );
+    )
   }
 }
 
@@ -75,9 +74,7 @@ export function GoBackBtn(props) {
       onClick={props.onClick}
       title={gettext("See previous change")}
     >
-      <span className="material-icon">
-        chevron_left
-      </span>
+      <span className="material-icon">chevron_left</span>
     </Button>
   )
 }
@@ -90,9 +87,7 @@ export function GoForwardBtn(props) {
       onClick={props.onClick}
       title={gettext("See previous change")}
     >
-      <span className="material-icon">
-        chevron_right
-      </span>
+      <span className="material-icon">chevron_right</span>
     </Button>
   )
 }
@@ -105,15 +100,13 @@ export function GoLastBtn(props) {
       onClick={props.onClick}
       title={gettext("See previous change")}
     >
-      <span className="material-icon">
-        last_page
-      </span>
+      <span className="material-icon">last_page</span>
     </Button>
   )
 }
 
 export function RevertBtn(props) {
-  if (!props.canRevert) return null;
+  if (!props.canRevert) return null
 
   return (
     <div className="col-sm-3 hidden-xs">
@@ -130,29 +123,43 @@ export function RevertBtn(props) {
 }
 
 export function Label(props) {
-  let user = null;
+  let user = null
   if (props.edit.url.editor) {
-    user = interpolate(USER_URL, {
-      url: escapeHtml(props.edit.url.editor),
-      user: escapeHtml(props.edit.editor_name)
-    }, true);
+    user = interpolate(
+      USER_URL,
+      {
+        url: escapeHtml(props.edit.url.editor),
+        user: escapeHtml(props.edit.editor_name)
+      },
+      true
+    )
   } else {
-    user = interpolate(USER_SPAN, {
-      user: escapeHtml(props.edit.editor_name)
-    }, true);
+    user = interpolate(
+      USER_SPAN,
+      {
+        user: escapeHtml(props.edit.editor_name)
+      },
+      true
+    )
   }
 
-  const date = interpolate(DATE_ABBR, {
-    absolute: escapeHtml(props.edit.edited_on.format('LLL')),
-    relative: escapeHtml(props.edit.edited_on.fromNow())
-  }, true);
+  const date = interpolate(
+    DATE_ABBR,
+    {
+      absolute: escapeHtml(props.edit.edited_on.format("LLL")),
+      relative: escapeHtml(props.edit.edited_on.fromNow())
+    },
+    true
+  )
 
-  const message = interpolate(escapeHtml(gettext("By %(edited_by)s %(edited_on)s.")), {
-    edited_by: user,
-    edited_on: date
-  }, true);
+  const message = interpolate(
+    escapeHtml(gettext("By %(edited_by)s %(edited_on)s.")),
+    {
+      edited_by: user,
+      edited_on: date
+    },
+    true
+  )
 
-  return (
-    <p dangerouslySetInnerHTML={{__html: message}} />
-  );
-}
+  return <p dangerouslySetInnerHTML={{ __html: message }} />
+}

+ 3 - 3
frontend/src/components/post-changelog/utils.js

@@ -1,7 +1,7 @@
-import moment from 'moment';
+import moment from "moment"
 
 export function hydrateEdit(json) {
   return Object.assign({}, json, {
     edited_on: moment(json.edited_on)
-  });
-}
+  })
+}

+ 8 - 17
frontend/src/components/post-feed/index.js

@@ -1,26 +1,17 @@
-/* jshint ignore:start */
-import React from 'react';
-import Post from './post';
-import Preview from './preview';
+import React from "react"
+import Post from "./post"
+import Preview from "./preview"
 
 export default function({ isReady, posts, poster }) {
   if (!isReady) {
-    return (
-      <Preview />
-    );
+    return <Preview />
   }
 
   return (
     <ul className="posts-list post-feed ui-ready">
-      {posts.map((post) => {
-        return (
-          <Post
-            key={post.id}
-            post={post}
-            poster={poster}
-          />
-        );
+      {posts.map(post => {
+        return <Post key={post.id} post={post} poster={poster} />
       })}
     </ul>
-  );
-}
+  )
+}

+ 16 - 13
frontend/src/components/post-feed/post/body.js

@@ -1,29 +1,32 @@
-/* jshint ignore:start */
-import React from 'react';
-import MisagoMarkup from 'misago/components/misago-markup';
-import escapeHtml from 'misago/utils/escape-html';
+import React from "react"
+import MisagoMarkup from "misago/components/misago-markup"
+import escapeHtml from "misago/utils/escape-html"
 
 export default function(props) {
   if (props.post.content) {
-    return <Default {...props} />;
+    return <Default {...props} />
   } else {
-    return <Invalid {...props} />;
+    return <Invalid {...props} />
   }
 }
 
 export function Default(props) {
- return (
+  return (
     <div className="post-body">
       <MisagoMarkup markup={props.post.content} />
     </div>
-  );
+  )
 }
 
 export function Invalid(props) {
- return (
+  return (
     <div className="post-body post-body-invalid">
-      <p className="lead">{gettext("This post's contents cannot be displayed.")}</p>
-      <p className="text-muted">{gettext("This error is caused by invalid post content manipulation.")}</p>
+      <p className="lead">
+        {gettext("This post's contents cannot be displayed.")}
+      </p>
+      <p className="text-muted">
+        {gettext("This error is caused by invalid post content manipulation.")}
+      </p>
     </div>
-  );
-}
+  )
+}

+ 13 - 16
frontend/src/components/post-feed/post/header.js

@@ -1,25 +1,22 @@
-/* jshint ignore:start */
-import React from 'react';
+import React from "react"
 
 export default function({ post }) {
-  const { category, thread } = post;
+  const { category, thread } = post
 
-  const tooltip = interpolate(gettext("posted %(posted_on)s"), {
-    'posted_on': post.posted_on.format('LL, LT')
-  }, true);
+  const tooltip = interpolate(
+    gettext("posted %(posted_on)s"),
+    {
+      posted_on: post.posted_on.format("LL, LT")
+    },
+    true
+  )
 
   return (
     <div className="post-heading">
-      <a
-        className="btn btn-link item-title"
-        href={thread.url}
-      >
+      <a className="btn btn-link item-title" href={thread.url}>
         {thread.title}
       </a>
-      <a
-        className="btn btn-link post-category"
-        href={category.url.index}
-      >
+      <a className="btn btn-link post-category" href={category.url.index}>
         {category.name}
       </a>
       <a
@@ -30,5 +27,5 @@ export default function({ post }) {
         {post.posted_on.fromNow()}
       </a>
     </div>
-  );
-}
+  )
+}

+ 11 - 18
frontend/src/components/post-feed/post/index.js

@@ -1,32 +1,25 @@
-/* jshint ignore:start */
-import React from 'react';
-import Body from './body';
-import Header from './header';
-import PostSide from './post-side';
+import React from "react"
+import Body from "./body"
+import Header from "./header"
+import PostSide from "./post-side"
 
 export default function({ post, poster }) {
-  const user = poster || post.poster;
+  const user = poster || post.poster
 
-  let className = 'post';
+  let className = "post"
   if (user && user.rank.css_class) {
-    className += ' post-' + user.rank.css_class;
+    className += " post-" + user.rank.css_class
   }
 
   return (
-    <li
-      className={className}
-      id={'post-' + post.id}
-    >
+    <li className={className} id={"post-" + post.id}>
       <div className="panel panel-default panel-post">
         <div className="panel-body">
-          <PostSide
-            post={post}
-            poster={user}
-          />
+          <PostSide post={post} poster={user} />
           <Header post={post} />
           <Body post={post} />
         </div>
       </div>
     </li>
-  );
-}
+  )
+}

+ 7 - 13
frontend/src/components/post-feed/post/post-side/anonymous.js

@@ -1,7 +1,6 @@
-/* jshint ignore:start */
-import React from 'react';
-import Avatar from 'misago/components/avatar';
-import GoToButton from './button';
+import React from "react"
+import Avatar from "misago/components/avatar"
+import GoToButton from "./button"
 
 export default function({ post }) {
   return (
@@ -10,17 +9,12 @@ export default function({ post }) {
       <div className="media">
         <div className="media-left">
           <span>
-            <Avatar
-              className="poster-avatar"
-              size={50}
-            />
+            <Avatar className="poster-avatar" size={50} />
           </span>
         </div>
         <div className="media-body">
           <div className="media-heading">
-            <span className="item-title">
-              {post.poster_name}
-            </span>
+            <span className="item-title">{post.poster_name}</span>
           </div>
           <span className="user-title user-title-anonymous">
             {gettext("Removed user")}
@@ -28,5 +22,5 @@ export default function({ post }) {
         </div>
       </div>
     </div>
-  );
-}
+  )
+}

+ 6 - 14
frontend/src/components/post-feed/post/post-side/button.js

@@ -1,18 +1,10 @@
-/* jshint ignore:start */
-import React from 'react';
+import React from "react"
 
 export default function({ post }) {
   return (
-    <a
-      className="btn btn-default btn-icon pull-right"
-      href={post.url.index}
-    >
-      <span className="btn-text-left hidden-xs">
-        {gettext("See post")}
-      </span>
-      <span className="material-icon">
-        chevron_right
-      </span>
+    <a className="btn btn-default btn-icon pull-right" href={post.url.index}>
+      <span className="btn-text-left hidden-xs">{gettext("See post")}</span>
+      <span className="material-icon">chevron_right</span>
     </a>
-  );
-}
+  )
+}

+ 6 - 14
frontend/src/components/post-feed/post/post-side/index.js

@@ -1,19 +1,11 @@
-/* jshint ignore:start */
-import React from 'react';
-import Anonymous from './anonymous';
-import Registered from './registered';
+import React from "react"
+import Anonymous from "./anonymous"
+import Registered from "./registered"
 
 export default function({ post, poster }) {
   if (poster.id) {
-    return (
-      <Registered
-        post={post}
-        poster={poster}
-      />
-    );
+    return <Registered post={post} poster={poster} />
   }
 
-  return (
-    <Anonymous post={post} />
-  );
-}
+  return <Anonymous post={post} />
+}

+ 9 - 20
frontend/src/components/post-feed/post/post-side/registered.js

@@ -1,8 +1,7 @@
-/* jshint ignore:start */
-import React from 'react';
-import Avatar from 'misago/components/avatar';
-import GoToButton from './button';
-import UserTitle from './user-title';
+import React from "react"
+import Avatar from "misago/components/avatar"
+import GoToButton from "./button"
+import UserTitle from "./user-title"
 
 export default function({ post, poster }) {
   return (
@@ -11,28 +10,18 @@ export default function({ post, poster }) {
       <div className="media">
         <div className="media-left">
           <a href={poster.url}>
-            <Avatar
-              className="poster-avatar"
-              size={50}
-              user={poster}
-            />
+            <Avatar className="poster-avatar" size={50} user={poster} />
           </a>
         </div>
         <div className="media-body">
           <div className="media-heading">
-            <a
-              className="item-title"
-              href={poster.url}
-            >
+            <a className="item-title" href={poster.url}>
               {poster.username}
             </a>
           </div>
-          <UserTitle
-            title={poster.title}
-            rank={poster.rank}
-          />
+          <UserTitle title={poster.title} rank={poster.rank} />
         </div>
       </div>
     </div>
-  );
-}
+  )
+}

+ 7 - 12
frontend/src/components/post-feed/post/post-side/user-title.js

@@ -1,12 +1,11 @@
-/* jshint ignore:start */
-import React from 'react';
+import React from "react"
 
 export default function({ rank, title }) {
-  let userTitle = title || rank.title || rank.name;
+  let userTitle = title || rank.title || rank.name
 
-  let className = 'user-title';
+  let className = "user-title"
   if (rank.css_class) {
-    className += ' user-title-' + rank.css_class;
+    className += " user-title-" + rank.css_class
   }
 
   if (rank.is_tab) {
@@ -14,12 +13,8 @@ export default function({ rank, title }) {
       <a className={className} href={rank.url}>
         {userTitle}
       </a>
-    );
+    )
   }
 
-  return (
-    <span className={className}>
-      {userTitle}
-    </span>
-  );
-}
+  return <span className={className}>{userTitle}</span>
+}

+ 12 - 16
frontend/src/components/post-feed/preview.js

@@ -1,7 +1,6 @@
-/* jshint ignore:start */
-import React from 'react';
-import Avatar from 'misago/components/avatar';
-import * as random from 'misago/utils/random';
+import React from "react"
+import Avatar from "misago/components/avatar"
+import * as random from "misago/utils/random"
 
 export default function() {
   return (
@@ -13,10 +12,7 @@ export default function() {
               <div className="media">
                 <div className="media-left">
                   <span>
-                    <Avatar
-                      className="poster-avatar"
-                      size={50}
-                    />
+                    <Avatar className="poster-avatar" size={50} />
                   </span>
                 </div>
                 <div className="media-body">
@@ -24,7 +20,7 @@ export default function() {
                     <span className="item-title">
                       <span
                         className="ui-preview-text"
-                        style={{width: random.int(30, 200) + "px"}}
+                        style={{ width: random.int(30, 200) + "px" }}
                       >
                         &nbsp;
                       </span>
@@ -33,7 +29,7 @@ export default function() {
                   <span className="user-title user-title-anonymous">
                     <span
                       className="ui-preview-text"
-                      style={{width: random.int(30, 200) + "px"}}
+                      style={{ width: random.int(30, 200) + "px" }}
                     >
                       &nbsp;
                     </span>
@@ -44,7 +40,7 @@ export default function() {
             <div className="post-heading">
               <span
                 className="ui-preview-text"
-                style={{width: random.int(30, 200) + "px"}}
+                style={{ width: random.int(30, 200) + "px" }}
               >
                 &nbsp;
               </span>
@@ -54,21 +50,21 @@ export default function() {
                 <p>
                   <span
                     className="ui-preview-text"
-                    style={{width: random.int(30, 200) + "px"}}
+                    style={{ width: random.int(30, 200) + "px" }}
                   >
                     &nbsp;
                   </span>
                   &nbsp;
                   <span
                     className="ui-preview-text"
-                    style={{width: random.int(30, 200) + "px"}}
+                    style={{ width: random.int(30, 200) + "px" }}
                   >
                     &nbsp;
                   </span>
                   &nbsp;
                   <span
                     className="ui-preview-text"
-                    style={{width: random.int(30, 200) + "px"}}
+                    style={{ width: random.int(30, 200) + "px" }}
                   >
                     &nbsp;
                   </span>
@@ -79,5 +75,5 @@ export default function() {
         </div>
       </li>
     </ul>
-  );
-}
+  )
+}

+ 50 - 81
frontend/src/components/post-likes.js

@@ -1,101 +1,87 @@
-// jshint ignore:start
-import React from 'react';
-import moment from 'moment';
-import Avatar from 'misago/components/avatar';
-import Message from 'misago/components/modal-message';
-import Loader from 'misago/components/modal-loader';
-import ajax from 'misago/services/ajax';
-
+import React from "react"
+import moment from "moment"
+import Avatar from "misago/components/avatar"
+import Message from "misago/components/modal-message"
+import Loader from "misago/components/modal-loader"
+import ajax from "misago/services/ajax"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isReady: false,
 
       error: null,
       likes: []
-    };
+    }
   }
 
   componentDidMount() {
-    ajax.get(this.props.post.api.likes).then((data) => {
-      this.setState({
-        isReady: true,
-        likes: data.map(hydrateLike)
-      });
-    }, (rejection) => {
-      this.setState({
-        isReady: true,
-        error: rejection.detail
-      });
-    });
-  };
+    ajax.get(this.props.post.api.likes).then(
+      data => {
+        this.setState({
+          isReady: true,
+          likes: data.map(hydrateLike)
+        })
+      },
+      rejection => {
+        this.setState({
+          isReady: true,
+          error: rejection.detail
+        })
+      }
+    )
+  }
 
   render() {
     if (this.state.error) {
       return (
         <ModalDialog className="modal-message">
-          <Message
-            message={this.state.error}
-          />
+          <Message message={this.state.error} />
         </ModalDialog>
-      );
+      )
     } else if (this.state.isReady) {
       if (this.state.likes.length) {
         return (
-          <ModalDialog
-            className="modal-sm"
-            likes={this.state.likes}
-          >
-            <LikesList
-              likes={this.state.likes}
-            />
+          <ModalDialog className="modal-sm" likes={this.state.likes}>
+            <LikesList likes={this.state.likes} />
           </ModalDialog>
-        );
+        )
       }
 
       return (
         <ModalDialog className="modal-message">
-          <Message
-            message={gettext("No users have liked this post.")}
-          />
+          <Message message={gettext("No users have liked this post.")} />
         </ModalDialog>
-      );
+      )
     }
 
     return (
       <ModalDialog className="modal-sm">
         <Loader />
       </ModalDialog>
-    );
+    )
   }
 }
 
 export function hydrateLike(data) {
   return Object.assign({}, data, {
     liked_on: moment(data.liked_on)
-  });
+  })
 }
 
 export function ModalDialog({ className, children, likes }) {
-  let title = gettext("Post Likes");
+  let title = gettext("Post Likes")
   if (likes) {
-    const likesCount = likes.length;
-    const message = ngettext(
-      "%(likes)s like",
-      "%(likes)s likes",
-      likesCount);
+    const likesCount = likes.length
+    const message = ngettext("%(likes)s like", "%(likes)s likes", likesCount)
 
-    title = interpolate(message, { likes: likesCount }, true);
+    title = interpolate(message, { likes: likesCount }, true)
   }
 
   return (
-    <div
-      className={"modal-dialog " + (className || '')}
-      role="document"
-    >
+    <div className={"modal-dialog " + (className || "")} role="document">
       <div className="modal-content">
         <div className="modal-header">
           <button
@@ -118,17 +104,12 @@ export function LikesList(props) {
   return (
     <div className="modal-body modal-post-likers">
       <ul className="media-list">
-        {props.likes.map((like) => {
-          return (
-            <LikeDetails
-              key={like.id}
-              {...like}
-            />
-          );
+        {props.likes.map(like => {
+          return <LikeDetails key={like.id} {...like} />
         })}
       </ul>
     </div>
-  );
+  )
 }
 
 export function LikeDetails(props) {
@@ -136,30 +117,23 @@ export function LikeDetails(props) {
     const user = {
       id: props.liker_id,
       avatars: props.avatars
-    };
+    }
 
     return (
       <li className="media">
         <div className="media-left">
-          <a
-            className="user-avatar"
-            href={props.url}
-          >
+          <a className="user-avatar" href={props.url}>
             <Avatar size="50" user={user} />
           </a>
         </div>
         <div className="media-body">
-          <a
-            className="item-title"
-            href={props.url}
-          >
+          <a className="item-title" href={props.url}>
             {props.username}
-          </a>
-          {' '}
+          </a>{" "}
           <LikeDate likedOn={props.liked_on} />
         </div>
       </li>
-    );
+    )
   }
 
   return (
@@ -170,21 +144,16 @@ export function LikeDetails(props) {
         </span>
       </div>
       <div className="media-body">
-        <strong>{props.username}</strong>
-        {' '}
-        <LikeDate likedOn={props.liked_on} />
+        <strong>{props.username}</strong> <LikeDate likedOn={props.liked_on} />
       </div>
     </li>
-  );
+  )
 }
 
 export function LikeDate(props) {
   return (
-    <span
-      className="text-muted"
-      title={props.likedOn.format('LLL')}
-    >
+    <span className="text-muted" title={props.likedOn.format("LLL")}>
       {props.likedOn.fromNow()}
     </span>
-  );
-}
+  )
+}

+ 51 - 61
frontend/src/components/posting/edit.js

@@ -1,25 +1,25 @@
-import React from 'react'; //jshint ignore:line
-import Editor from 'misago/components/editor'; //jshint ignore:line
-import Form from 'misago/components/form';
-import Container from './utils/container'; //jshint ignore:line
-import Loader from './utils/loader'; //jshint ignore:line
-import Message from './utils/message'; //jshint ignore:line
-import * as attachments from './utils/attachments'; //jshint ignore:line
-import { getPostValidators } from './utils/validators';
-import ajax from 'misago/services/ajax';
-import posting from 'misago/services/posting'; //jshint ignore:line
-import snackbar from 'misago/services/snackbar';
+import React from "react"
+import Editor from "misago/components/editor"
+import Form from "misago/components/form"
+import Container from "./utils/container"
+import Loader from "./utils/loader"
+import Message from "./utils/message"
+import * as attachments from "./utils/attachments"
+import { getPostValidators } from "./utils/validators"
+import ajax from "misago/services/ajax"
+import posting from "misago/services/posting"
+import snackbar from "misago/services/snackbar"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isReady: false,
       isLoading: false,
       isErrored: false,
 
-      post: '',
+      post: "",
       attachments: [],
       protect: false,
 
@@ -29,15 +29,14 @@ export default class extends Form {
         post: getPostValidators()
       },
       errors: {}
-    };
+    }
   }
 
   componentDidMount() {
-    ajax.get(this.props.config).then(this.loadSuccess, this.loadError);
+    ajax.get(this.props.config).then(this.loadSuccess, this.loadError)
   }
 
-  /* jshint ignore:start */
-  loadSuccess = (data) => {
+  loadSuccess = data => {
     this.setState({
       isReady: true,
 
@@ -46,59 +45,58 @@ export default class extends Form {
       protect: data.is_protected,
 
       canProtect: data.can_protect
-    });
-  };
+    })
+  }
 
-  loadError = (rejection) => {
+  loadError = rejection => {
     this.setState({
       isErrored: rejection.detail
-    });
-  };
+    })
+  }
 
   onCancel = () => {
-    const cancel = confirm(gettext("Are you sure you want to discard changes?"));
+    const cancel = confirm(gettext("Are you sure you want to discard changes?"))
     if (cancel) {
-      posting.close();
+      posting.close()
     }
-  };
+  }
 
   onProtect = () => {
     this.setState({
       protect: true
-    });
-  };
+    })
+  }
 
   onUnprotect = () => {
     this.setState({
       protect: false
-    });
-  };
+    })
+  }
 
-  onPostChange = (event) => {
-    this.changeValue('post', event.target.value);
-  };
+  onPostChange = event => {
+    this.changeValue("post", event.target.value)
+  }
 
-  onAttachmentsChange = (attachments) => {
+  onAttachmentsChange = attachments => {
     this.setState({
       attachments
-    });
-  };
-  /* jshint ignore:end */
+    })
+  }
 
   clean() {
     if (!this.state.post.trim().length) {
-      snackbar.error(gettext("You have to enter a message."));
-      return false;
+      snackbar.error(gettext("You have to enter a message."))
+      return false
     }
 
-    const errors = this.validate();
+    const errors = this.validate()
 
     if (errors.post) {
-      snackbar.error(errors.post[0]);
-      return false;
+      snackbar.error(errors.post[0])
+      return false
     }
 
-    return true;
+    return true
   }
 
   send() {
@@ -106,17 +104,17 @@ export default class extends Form {
       post: this.state.post,
       attachments: attachments.clean(this.state.attachments),
       protect: this.state.protect
-    });
+    })
   }
 
   handleSuccess(success) {
-    snackbar.success(gettext("Reply has been edited."));
-    window.location = success.url.index;
+    snackbar.success(gettext("Reply has been edited."))
+    window.location = success.url.index
 
     // keep form loading
     this.setState({
-      'isLoading': true
-    });
+      isLoading: true
+    })
   }
 
   handleError(rejection) {
@@ -127,23 +125,21 @@ export default class extends Form {
         rejection.title || [],
         rejection.post || [],
         rejection.attachments || []
-      );
+      )
 
-      snackbar.error(errors[0]);
+      snackbar.error(errors[0])
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
   }
 
   render() {
-    /* jshint ignore:start */
     if (this.state.isReady) {
       return (
         <Container className="posting-form">
           <form onSubmit={this.handleSubmit} method="POST">
             <div className="row">
               <div className="col-md-12">
-
                 <Editor
                   attachments={this.state.attachments}
                   canProtect={this.state.canProtect}
@@ -157,21 +153,15 @@ export default class extends Form {
                   submitLabel={gettext("Edit reply")}
                   value={this.state.post}
                 />
-
               </div>
             </div>
           </form>
         </Container>
-      );
+      )
     } else if (this.state.isErrored) {
-      return (
-        <Message message={this.state.isErrored} />
-      );
+      return <Message message={this.state.isErrored} />
     } else {
-      return (
-        <Loader />
-      );
+      return <Loader />
     }
-    /* jshint ignore:end */
   }
 }

+ 15 - 24
frontend/src/components/posting/index.js

@@ -1,28 +1,19 @@
-// jshint ignore:start
-import React from 'react';
-import Start from './start';
-import StartPrivate from './start-private';
-import Reply from './reply';
-import Edit from './edit';
+import React from "react"
+import Start from "./start"
+import StartPrivate from "./start-private"
+import Reply from "./reply"
+import Edit from "./edit"
 
 export default function(props) {
-  if (props.mode === 'START') {
-    return (
-      <Start {...props} />
-    );
-  } else if (props.mode === 'START_PRIVATE') {
-    return (
-      <StartPrivate {...props} />
-    );
-  } else if (props.mode === 'REPLY') {
-    return (
-      <Reply {...props} />
-    );
-  } else if (props.mode === 'EDIT') {
-    return (
-      <Edit {...props} />
-    );
+  if (props.mode === "START") {
+    return <Start {...props} />
+  } else if (props.mode === "START_PRIVATE") {
+    return <StartPrivate {...props} />
+  } else if (props.mode === "REPLY") {
+    return <Reply {...props} />
+  } else if (props.mode === "EDIT") {
+    return <Edit {...props} />
   } else {
-    return null;
+    return null
   }
-}
+}

+ 69 - 69
frontend/src/components/posting/reply.js

@@ -1,127 +1,135 @@
-import React from 'react'; //jshint ignore:line
-import Editor from 'misago/components/editor'; //jshint ignore:line
-import Form from 'misago/components/form';
-import Container from './utils/container'; //jshint ignore:line
-import Loader from './utils/loader'; //jshint ignore:line
-import Message from './utils/message'; //jshint ignore:line
-import * as attachments from './utils/attachments'; //jshint ignore:line
-import { getPostValidators } from './utils/validators';
-import ajax from 'misago/services/ajax';
-import posting from 'misago/services/posting'; //jshint ignore:line
-import snackbar from 'misago/services/snackbar';
+import React from "react"
+import Editor from "misago/components/editor"
+import Form from "misago/components/form"
+import Container from "./utils/container"
+import Loader from "./utils/loader"
+import Message from "./utils/message"
+import * as attachments from "./utils/attachments"
+import { getPostValidators } from "./utils/validators"
+import ajax from "misago/services/ajax"
+import posting from "misago/services/posting"
+import snackbar from "misago/services/snackbar"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isReady: false,
       isLoading: false,
       isErrored: false,
 
-      post: '',
+      post: "",
       attachments: [],
 
       validators: {
         post: getPostValidators()
       },
       errors: {}
-    };
+    }
   }
 
   componentDidMount() {
-    ajax.get(this.props.config, this.props.context || null).then(this.loadSuccess, this.loadError);
+    ajax
+      .get(this.props.config, this.props.context || null)
+      .then(this.loadSuccess, this.loadError)
   }
 
   componentWillReceiveProps(nextProps) {
-    const context = this.props.context;
-    const newContext = nextProps.context;
+    const context = this.props.context
+    const newContext = nextProps.context
 
-    if (context && newContext && context.reply === newContext.reply) return;
+    if (context && newContext && context.reply === newContext.reply) return
 
-    ajax.get(nextProps.config, nextProps.context || null).then(this.appendData, snackbar.apiError);
+    ajax
+      .get(nextProps.config, nextProps.context || null)
+      .then(this.appendData, snackbar.apiError)
   }
 
-  /* jshint ignore:start */
-  loadSuccess = (data) => {
+  loadSuccess = data => {
     this.setState({
       isReady: true,
 
-      post: data.post ? ('[quote="@' +  data.poster + '"]\n' + data.post + '\n[/quote]') : ''
-    });
-  };
+      post: data.post
+        ? '[quote="@' + data.poster + '"]\n' + data.post + "\n[/quote]"
+        : ""
+    })
+  }
 
-  loadError = (rejection) => {
+  loadError = rejection => {
     this.setState({
       isErrored: rejection.detail
-    });
-  };
+    })
+  }
 
-  appendData = (data) => {
-    const newPost = data.post ? ('[quote="@' +  data.poster + '"]\n' + data.post + '\n[/quote]\n\n') : '';
+  appendData = data => {
+    const newPost = data.post
+      ? '[quote="@' + data.poster + '"]\n' + data.post + "\n[/quote]\n\n"
+      : ""
 
     this.setState((prevState, props) => {
       if (prevState.post.length > 0) {
         return {
-          post: prevState.post + '\n\n' + newPost
-        };
+          post: prevState.post + "\n\n" + newPost
+        }
       }
 
       return {
         post: newPost
-      };
-    });
-  };
+      }
+    })
+  }
 
   onCancel = () => {
-    const cancel = confirm(gettext("Are you sure you want to discard your reply?"));
+    const cancel = confirm(
+      gettext("Are you sure you want to discard your reply?")
+    )
     if (cancel) {
-      posting.close();
+      posting.close()
     }
-  };
+  }
 
-  onPostChange = (event) => {
-    this.changeValue('post', event.target.value);
-  };
+  onPostChange = event => {
+    this.changeValue("post", event.target.value)
+  }
 
-  onAttachmentsChange = (attachments) => {
+  onAttachmentsChange = attachments => {
     this.setState({
       attachments
-    });
-  };
-  /* jshint ignore:end */
+    })
+  }
 
   clean() {
     if (!this.state.post.trim().length) {
-      snackbar.error(gettext("You have to enter a message."));
-      return false;
+      snackbar.error(gettext("You have to enter a message."))
+      return false
     }
 
-    const errors = this.validate();
+    const errors = this.validate()
 
     if (errors.post) {
-      snackbar.error(errors.post[0]);
-      return false;
+      snackbar.error(errors.post[0])
+      return false
     }
 
-    return true;
+    return true
   }
 
   send() {
     return ajax.post(this.props.submit, {
       post: this.state.post,
       attachments: attachments.clean(this.state.attachments)
-    });
+    })
   }
 
   handleSuccess(success) {
-    snackbar.success(gettext("Your reply has been posted."));
-    window.location = success.url.index;
+    snackbar.success(gettext("Your reply has been posted."))
+    window.location = success.url.index
 
     // keep form loading
     this.setState({
-      'isLoading': true
-    });
+      isLoading: true
+    })
   }
 
   handleError(rejection) {
@@ -130,23 +138,21 @@ export default class extends Form {
         rejection.non_field_errors || [],
         rejection.post || [],
         rejection.attachments || []
-      );
+      )
 
-      snackbar.error(errors[0]);
+      snackbar.error(errors[0])
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
   }
 
   render() {
-    /* jshint ignore:start */
     if (this.state.isReady) {
       return (
         <Container className="posting-form">
           <form onSubmit={this.handleSubmit} method="POST">
             <div className="row">
               <div className="col-md-12">
-
                 <Editor
                   attachments={this.state.attachments}
                   loading={this.state.isLoading}
@@ -156,21 +162,15 @@ export default class extends Form {
                   submitLabel={gettext("Post reply")}
                   value={this.state.post}
                 />
-
               </div>
             </div>
           </form>
         </Container>
-      );
+      )
     } else if (this.state.isErrored) {
-      return (
-        <Message message={this.state.isErrored} />
-      );
+      return <Message message={this.state.isErrored} />
     } else {
-      return (
-        <Loader />
-      );
+      return <Loader />
     }
-    /* jshint ignore:end */
   }
 }

+ 58 - 64
frontend/src/components/posting/start-private.js

@@ -1,27 +1,27 @@
-import React from 'react'; //jshint ignore:line
-import Editor from 'misago/components/editor'; //jshint ignore:line
-import Form from 'misago/components/form';
-import Container from './utils/container'; //jshint ignore:line
-import Message from './utils/message'; //jshint ignore:line
-import * as attachments from './utils/attachments'; //jshint ignore:line
-import cleanUsernames from './utils/usernames'; //jshint ignore:line
-import { getPostValidators, getTitleValidators } from './utils/validators';
-import ajax from 'misago/services/ajax';
-import posting from 'misago/services/posting'; //jshint ignore:line
-import snackbar from 'misago/services/snackbar';
+import React from "react"
+import Editor from "misago/components/editor"
+import Form from "misago/components/form"
+import Container from "./utils/container"
+import Message from "./utils/message"
+import * as attachments from "./utils/attachments"
+import cleanUsernames from "./utils/usernames"
+import { getPostValidators, getTitleValidators } from "./utils/validators"
+import ajax from "misago/services/ajax"
+import posting from "misago/services/posting"
+import snackbar from "misago/services/snackbar"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
-    const to = (props.to || []).map((user) => user.username).join(', ');
+    const to = (props.to || []).map(user => user.username).join(", ")
 
     this.state = {
       isLoading: false,
 
       to: to,
-      title: '',
-      post: '',
+      title: "",
+      post: "",
       attachments: [],
 
       validators: {
@@ -29,65 +29,65 @@ export default class extends Form {
         post: getPostValidators()
       },
       errors: {}
-    };
+    }
   }
 
-  /* jshint ignore:start */
   onCancel = () => {
-    const cancel = confirm(gettext("Are you sure you want to discard private thread?"));
+    const cancel = confirm(
+      gettext("Are you sure you want to discard private thread?")
+    )
     if (cancel) {
-      posting.close();
+      posting.close()
     }
-  };
+  }
 
-  onToChange = (event) => {
-    this.changeValue('to', event.target.value);
-  };
+  onToChange = event => {
+    this.changeValue("to", event.target.value)
+  }
 
-  onTitleChange = (event) => {
-    this.changeValue('title', event.target.value);
-  };
+  onTitleChange = event => {
+    this.changeValue("title", event.target.value)
+  }
 
-  onPostChange = (event) => {
-    this.changeValue('post', event.target.value);
-  };
+  onPostChange = event => {
+    this.changeValue("post", event.target.value)
+  }
 
-  onAttachmentsChange = (attachments) => {
+  onAttachmentsChange = attachments => {
     this.setState({
       attachments
-    });
-  };
-  /* jshint ignore:end */
+    })
+  }
 
   clean() {
     if (!cleanUsernames(this.state.to).length) {
-      snackbar.error(gettext("You have to enter at least one recipient."));
-      return false;
+      snackbar.error(gettext("You have to enter at least one recipient."))
+      return false
     }
 
     if (!this.state.title.trim().length) {
-      snackbar.error(gettext("You have to enter thread title."));
-      return false;
+      snackbar.error(gettext("You have to enter thread title."))
+      return false
     }
 
     if (!this.state.post.trim().length) {
-      snackbar.error(gettext("You have to enter a message."));
-      return false;
+      snackbar.error(gettext("You have to enter a message."))
+      return false
     }
 
-    const errors = this.validate();
+    const errors = this.validate()
 
     if (errors.title) {
-      snackbar.error(errors.title[0]);
-      return false;
+      snackbar.error(errors.title[0])
+      return false
     }
 
     if (errors.post) {
-      snackbar.error(errors.post[0]);
-      return false;
+      snackbar.error(errors.post[0])
+      return false
     }
 
-    return true;
+    return true
   }
 
   send() {
@@ -95,18 +95,18 @@ export default class extends Form {
       to: cleanUsernames(this.state.to),
       title: this.state.title,
       post: this.state.post,
-      attachments: attachments.clean(this.state.attachments),
-    });
+      attachments: attachments.clean(this.state.attachments)
+    })
   }
 
   handleSuccess(success) {
-    snackbar.success(gettext("Your thread has been posted."));
-    window.location = success.url;
+    snackbar.success(gettext("Your thread has been posted."))
+    window.location = success.url
 
     // keep form loading
     this.setState({
-      'isLoading': true
-    });
+      isLoading: true
+    })
   }
 
   handleError(rejection) {
@@ -117,36 +117,34 @@ export default class extends Form {
         rejection.title || [],
         rejection.post || [],
         rejection.attachments || []
-      );
+      )
 
-      snackbar.error(errors[0]);
+      snackbar.error(errors[0])
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
   }
 
   render() {
-    /* jshint ignore:start */
     return (
       <Container className="posting-form" withFirstRow={true}>
         <form onSubmit={this.handleSubmit}>
           <div className="row first-row">
             <div className="col-xs-12">
-
               <input
                 className="form-control"
                 disabled={this.state.isLoading}
                 onChange={this.onToChange}
-                placeholder={gettext("Comma separated list of user names, eg.: Danny, Lisa")}
+                placeholder={gettext(
+                  "Comma separated list of user names, eg.: Danny, Lisa"
+                )}
                 type="text"
                 value={this.state.to}
               />
-
             </div>
           </div>
           <div className="row first-row">
             <div className="col-xs-12">
-
               <input
                 className="form-control"
                 disabled={this.state.isLoading}
@@ -155,12 +153,10 @@ export default class extends Form {
                 type="text"
                 value={this.state.title}
               />
-
             </div>
           </div>
           <div className="row">
             <div className="col-xs-12">
-
               <Editor
                 attachments={this.state.attachments}
                 loading={this.state.isLoading}
@@ -170,12 +166,10 @@ export default class extends Form {
                 submitLabel={gettext("Post thread")}
                 value={this.state.post}
               />
-
             </div>
           </div>
         </form>
       </Container>
-    );
-    /* jshint ignore:end */
+    )
   }
 }

+ 103 - 110
frontend/src/components/posting/start.js

@@ -1,20 +1,20 @@
-import React from 'react'; //jshint ignore:line
-import CategorySelect from 'misago/components/category-select'; //jshint ignore:line
-import Editor from 'misago/components/editor'; //jshint ignore:line
-import Form from 'misago/components/form';
-import Container from './utils/container'; //jshint ignore:line
-import Loader from './utils/loader'; //jshint ignore:line
-import Message from './utils/message'; //jshint ignore:line
-import Options from './utils/options'; //jshint ignore:line
-import * as attachments from './utils/attachments'; //jshint ignore:line
-import { getPostValidators, getTitleValidators } from './utils/validators';
-import ajax from 'misago/services/ajax';
-import posting from 'misago/services/posting'; //jshint ignore:line
-import snackbar from 'misago/services/snackbar';
+import React from "react"
+import CategorySelect from "misago/components/category-select"
+import Editor from "misago/components/editor"
+import Form from "misago/components/form"
+import Container from "./utils/container"
+import Loader from "./utils/loader"
+import Message from "./utils/message"
+import Options from "./utils/options"
+import * as attachments from "./utils/attachments"
+import { getPostValidators, getTitleValidators } from "./utils/validators"
+import ajax from "misago/services/ajax"
+import posting from "misago/services/posting"
+import snackbar from "misago/services/snackbar"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isReady: false,
@@ -24,10 +24,10 @@ export default class extends Form {
       showOptions: false,
       categoryOptions: null,
 
-      title: '',
+      title: "",
       category: props.category || null,
       categories: [],
-      post: '',
+      post: "",
       attachments: [],
       close: false,
       hide: false,
@@ -38,37 +38,39 @@ export default class extends Form {
         post: getPostValidators()
       },
       errors: {}
-    };
+    }
   }
 
   componentDidMount() {
-    ajax.get(this.props.config).then(this.loadSuccess, this.loadError);
+    ajax.get(this.props.config).then(this.loadSuccess, this.loadError)
   }
 
-  /* jshint ignore:start */
-  loadSuccess = (data) => {
-    let category = null;
-    let showOptions = false;
-    let categoryOptions = null;
+  loadSuccess = data => {
+    let category = null
+    let showOptions = false
+    let categoryOptions = null
 
     // hydrate categories, extract posting options
-    const categories = data.map((item) => {
+    const categories = data.map(item => {
       // pick first category that allows posting and if it may, override it with initial one
-      if (item.post !== false && (!category || item.id == this.state.category)) {
-        category = item.id;
-        categoryOptions = item.post;
+      if (
+        item.post !== false &&
+        (!category || item.id == this.state.category)
+      ) {
+        category = item.id
+        categoryOptions = item.post
       }
 
       if (item.post && (item.post.close || item.post.hide || item.post.pin)) {
-        showOptions = true;
+        showOptions = true
       }
 
       return Object.assign(item, {
         disabled: item.post === false,
         label: item.name,
         value: item.id
-      });
-    });
+      })
+    })
 
     this.setState({
       isReady: true,
@@ -77,35 +79,35 @@ export default class extends Form {
       categories,
       category,
       categoryOptions
-    });
-  };
+    })
+  }
 
-  loadError = (rejection) => {
+  loadError = rejection => {
     this.setState({
       isErrored: rejection.detail
-    });
-  };
+    })
+  }
 
   onCancel = () => {
-    const cancel = confirm(gettext("Are you sure you want to discard thread?"));
+    const cancel = confirm(gettext("Are you sure you want to discard thread?"))
     if (cancel) {
-      posting.close();
+      posting.close()
     }
-  };
+  }
 
-  onTitleChange = (event) => {
-    this.changeValue('title', event.target.value);
-  };
+  onTitleChange = event => {
+    this.changeValue("title", event.target.value)
+  }
 
-  onCategoryChange = (event) => {
-    const category = this.state.categories.find((item) => {
-      return event.target.value == item.value;
-    });
+  onCategoryChange = event => {
+    const category = this.state.categories.find(item => {
+      return event.target.value == item.value
+    })
 
     // if selected pin is greater than allowed, reduce it
-    let pin = this.state.pin;
+    let pin = this.state.pin
     if (category.post.pin && category.post.pin < pin) {
-      pin = category.post.pin;
+      pin = category.post.pin
     }
 
     this.setState({
@@ -113,72 +115,71 @@ export default class extends Form {
       categoryOptions: category.post,
 
       pin
-    });
-  };
+    })
+  }
 
-  onPostChange = (event) => {
-    this.changeValue('post', event.target.value);
-  };
+  onPostChange = event => {
+    this.changeValue("post", event.target.value)
+  }
 
-  onAttachmentsChange = (attachments) => {
+  onAttachmentsChange = attachments => {
     this.setState({
       attachments
-    });
-  };
+    })
+  }
 
   onClose = () => {
-    this.changeValue('close', true);
-  };
+    this.changeValue("close", true)
+  }
 
   onOpen = () => {
-    this.changeValue('close', false);
-  };
+    this.changeValue("close", false)
+  }
 
   onPinGlobally = () => {
-    this.changeValue('pin', 2);
-  };
+    this.changeValue("pin", 2)
+  }
 
   onPinLocally = () => {
-    this.changeValue('pin', 1);
-  };
+    this.changeValue("pin", 1)
+  }
 
   onUnpin = () => {
-    this.changeValue('pin', 0);
-  };
+    this.changeValue("pin", 0)
+  }
 
   onHide = () => {
-    this.changeValue('hide', true);
-  };
+    this.changeValue("hide", true)
+  }
 
   onUnhide = () => {
-    this.changeValue('hide', false);
-  };
-  /* jshint ignore:end */
+    this.changeValue("hide", false)
+  }
 
   clean() {
     if (!this.state.title.trim().length) {
-      snackbar.error(gettext("You have to enter thread title."));
-      return false;
+      snackbar.error(gettext("You have to enter thread title."))
+      return false
     }
 
     if (!this.state.post.trim().length) {
-      snackbar.error(gettext("You have to enter a message."));
-      return false;
+      snackbar.error(gettext("You have to enter a message."))
+      return false
     }
 
-    const errors = this.validate();
+    const errors = this.validate()
 
     if (errors.title) {
-      snackbar.error(errors.title[0]);
-      return false;
+      snackbar.error(errors.title[0])
+      return false
     }
 
     if (errors.post) {
-      snackbar.error(errors.post[0]);
-      return false;
+      snackbar.error(errors.post[0])
+      return false
     }
 
-    return true;
+    return true
   }
 
   send() {
@@ -190,17 +191,17 @@ export default class extends Form {
       close: this.state.close,
       hide: this.state.hide,
       pin: this.state.pin
-    });
+    })
   }
 
   handleSuccess(success) {
-    snackbar.success(gettext("Your thread has been posted."));
-    window.location = success.url;
+    snackbar.success(gettext("Your thread has been posted."))
+    window.location = success.url
 
     // keep form loading
     this.setState({
-      'isLoading': true
-    });
+      isLoading: true
+    })
   }
 
   handleError(rejection) {
@@ -211,47 +212,42 @@ export default class extends Form {
         rejection.title || [],
         rejection.post || [],
         rejection.attachments || []
-      );
+      )
 
-      snackbar.error(errors[0]);
+      snackbar.error(errors[0])
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
   }
 
   render() {
-    /* jshint ignore:start */
     if (this.state.isErrored) {
-      return (
-        <Message message={this.state.isErrored} />
-      );
+      return <Message message={this.state.isErrored} />
     }
 
     if (!this.state.isReady) {
-      return (
-        <Loader />
-      );
+      return <Loader />
     }
 
-    let columns = 0;
-    if (this.state.categoryOptions.close) columns += 1;
-    if (this.state.categoryOptions.hide) columns += 1;
-    if (this.state.categoryOptions.pin) columns += 1;
+    let columns = 0
+    if (this.state.categoryOptions.close) columns += 1
+    if (this.state.categoryOptions.hide) columns += 1
+    if (this.state.categoryOptions.pin) columns += 1
 
-    let titleStyle = null;
+    let titleStyle = null
 
     if (columns === 1) {
-      titleStyle = 'col-sm-6';
+      titleStyle = "col-sm-6"
     } else {
-      titleStyle = 'col-sm-8';
+      titleStyle = "col-sm-8"
     }
 
     if (columns === 3) {
-      titleStyle += ' col-md-6'
+      titleStyle += " col-md-6"
     } else if (columns) {
-      titleStyle += ' col-md-7'
+      titleStyle += " col-md-7"
     } else {
-      titleStyle += ' col-md-9'
+      titleStyle += " col-md-9"
     }
 
     return (
@@ -268,7 +264,7 @@ export default class extends Form {
                 value={this.state.title}
               />
             </div>
-            <div className='col-xs-12 col-sm-4 col-md-3 xs-margin-top'>
+            <div className="col-xs-12 col-sm-4 col-md-3 xs-margin-top">
               <CategorySelect
                 choices={this.state.categories}
                 disabled={this.state.isLoading}
@@ -295,7 +291,6 @@ export default class extends Form {
           </div>
           <div className="row">
             <div className="col-md-12">
-
               <Editor
                 attachments={this.state.attachments}
                 loading={this.state.isLoading}
@@ -305,12 +300,10 @@ export default class extends Form {
                 submitLabel={gettext("Post thread")}
                 value={this.state.post}
               />
-
             </div>
           </div>
         </form>
       </Container>
-    );
-    /* jshint ignore:end */
+    )
   }
 }

+ 11 - 9
frontend/src/components/posting/utils/attachments.js

@@ -1,17 +1,19 @@
-import moment from 'moment';
+import moment from "moment"
 
 export function clean(attachments) {
-  const completedAttachments = attachments.filter((attachment) => {
-    return attachment.id && !attachment.isRemoved;
-  });
+  const completedAttachments = attachments.filter(attachment => {
+    return attachment.id && !attachment.isRemoved
+  })
 
-  return completedAttachments.map((a) => { return a.id; });
+  return completedAttachments.map(a => {
+    return a.id
+  })
 }
 
 export function hydrate(attachments) {
-  return attachments.map((attachment) => {
+  return attachments.map(attachment => {
     return Object.assign({}, attachment, {
       uploaded_on: moment(attachment.uploaded_on)
-    });
-  });
-}
+    })
+  })
+}

+ 5 - 8
frontend/src/components/posting/utils/container.js

@@ -1,12 +1,9 @@
-// jshint ignore:start
-import React from 'react';
+import React from "react"
 
 export default function(props) {
   return (
-   <div className={props.className}>
-      <div className="container">
-        {props.children}
-      </div>
+    <div className={props.className}>
+      <div className="container">{props.children}</div>
     </div>
-  );
-}
+  )
+}

+ 5 - 6
frontend/src/components/posting/utils/loader.js

@@ -1,12 +1,11 @@
-// jshint ignore:start
-import React from 'react';
-import Container from './container';
-import Loader from 'misago/components/loader';
+import React from "react"
+import Container from "./container"
+import Loader from "misago/components/loader"
 
 export default function(props) {
   return (
     <Container className="posting-loader">
       <Loader />
     </Container>
-  );
-}
+  )
+}

+ 11 - 10
frontend/src/components/posting/utils/message.js

@@ -1,22 +1,23 @@
-// jshint ignore:start
-import React from 'react';
-import Container from './container';
-import posting from 'misago/services/posting';
+import React from "react"
+import Container from "./container"
+import posting from "misago/services/posting"
 
 export default function(props) {
   return (
     <Container className="posting-message">
       <div className="message-body">
         <p>
-          <span className="material-icon">
-            error_outline
-          </span>
+          <span className="material-icon">error_outline</span>
           {props.message}
         </p>
-        <button type="button" className="btn btn-default" onClick={posting.close}>
+        <button
+          type="button"
+          className="btn btn-default"
+          onClick={posting.close}
+        >
           {gettext("Dismiss")}
         </button>
       </div>
     </Container>
-  );
-}
+  )
+}

+ 47 - 56
frontend/src/components/posting/utils/options.js

@@ -1,35 +1,34 @@
-// jshint ignore:start
-import React from 'react';
+import React from "react"
 
 export default function(props) {
-  if (!props.showOptions) return null;
+  if (!props.showOptions) return null
 
-  const { columns } = props;
+  const { columns } = props
 
-  let className = 'col-xs-12 xs-margin-top';
+  let className = "col-xs-12 xs-margin-top"
 
   if (columns === 1) {
-    className += ' col-sm-2';
+    className += " col-sm-2"
   } else {
-    className += ' sm-margin-top';
+    className += " sm-margin-top"
   }
 
   if (columns === 3) {
-    className += ' col-md-3';
+    className += " col-md-3"
   } else {
-    className += ' col-md-2';
+    className += " col-md-2"
   }
-  className += ' posting-options';
+  className += " posting-options"
 
-  const columnClassName = 'col-xs-' + (12 / columns);
+  const columnClassName = "col-xs-" + 12 / columns
 
-  let textClassName = 'btn-text'
+  let textClassName = "btn-text"
   if (columns === 3) {
-      textClassName += ' visible-sm-inline-block';
+    textClassName += " visible-sm-inline-block"
   } else if (columns === 2) {
-      textClassName += ' hidden-md hidden-lg';
+    textClassName += " hidden-md hidden-lg"
   } else {
-      textClassName += ' hidden-sm';
+    textClassName += " hidden-sm"
   }
 
   return (
@@ -65,13 +64,13 @@ export default function(props) {
         />
       </div>
     </div>
-  );
+  )
 }
 
 export function CloseOptions(props) {
-  if (!props.show) return null;
+  if (!props.show) return null
 
-  const label = props.close ? gettext('Closed') : gettext('Open');
+  const label = props.close ? gettext("Closed") : gettext("Open")
 
   return (
     <div className={props.className}>
@@ -83,20 +82,18 @@ export function CloseOptions(props) {
         type="button"
       >
         <span className="material-icon">
-          {props.close ? 'lock' : 'lock_outline'}
-        </span>
-        <span className={props.textClassName}>
-          {label}
+          {props.close ? "lock" : "lock_outline"}
         </span>
+        <span className={props.textClassName}>{label}</span>
       </button>
     </div>
-  );
+  )
 }
 
 export function HideOptions(props) {
-  if (!props.show) return null;
+  if (!props.show) return null
 
-  const label = props.hide ? gettext('Hidden') : gettext('Not hidden');
+  const label = props.hide ? gettext("Hidden") : gettext("Not hidden")
 
   return (
     <div className={props.className}>
@@ -108,48 +105,46 @@ export function HideOptions(props) {
         type="button"
       >
         <span className="material-icon">
-          {props.hide ? 'visibility_off' : 'visibility'}
-        </span>
-        <span className={props.textClassName}>
-          {label}
+          {props.hide ? "visibility_off" : "visibility"}
         </span>
+        <span className={props.textClassName}>{label}</span>
       </button>
     </div>
-  );
+  )
 }
 
 export function PinOptions(props) {
-  if (!props.show) return null;
+  if (!props.show) return null
 
-  let icon = null;
-  let onClick = null;
-  let label = null;
+  let icon = null
+  let onClick = null
+  let label = null
 
   switch (props.pin) {
     case 0:
-      icon = 'radio_button_unchecked';
-      onClick = props.onPinLocally;
-      label = gettext("Unpinned");
-      break;
+      icon = "radio_button_unchecked"
+      onClick = props.onPinLocally
+      label = gettext("Unpinned")
+      break
 
     case 1:
-      icon = 'bookmark_outline';
-      onClick = props.onPinGlobally;
-      label = gettext("Pinned locally");
+      icon = "bookmark_outline"
+      onClick = props.onPinGlobally
+      label = gettext("Pinned locally")
 
       if (props.show == 2) {
-        onClick = props.onPinGlobally;
+        onClick = props.onPinGlobally
       } else {
-        onClick = props.onUnpin;
+        onClick = props.onUnpin
       }
 
-      break;
+      break
 
     case 2:
-      icon = 'bookmark';
-      onClick = props.onUnpin;
-      label = gettext("Pinned globally");
-      break;
+      icon = "bookmark"
+      onClick = props.onUnpin
+      label = gettext("Pinned globally")
+      break
   }
 
   return (
@@ -161,13 +156,9 @@ export function PinOptions(props) {
         title={label}
         type="button"
       >
-        <span className="material-icon">
-          {icon}
-        </span>
-        <span className={props.textClassName}>
-          {label}
-        </span>
+        <span className="material-icon">{icon}</span>
+        <span className={props.textClassName}>{label}</span>
       </button>
     </div>
-  );
-}
+  )
+}

+ 6 - 6
frontend/src/components/posting/utils/usernames.js

@@ -1,9 +1,9 @@
 export default function(usernames) {
-  const normalisedNames = usernames.split(',').map((i) => i.trim().toLowerCase());
-  const removedBlanks = normalisedNames.filter((i) => i.length > 0);
+  const normalisedNames = usernames.split(",").map(i => i.trim().toLowerCase())
+  const removedBlanks = normalisedNames.filter(i => i.length > 0)
   const removedDuplicates = removedBlanks.filter((name, pos) => {
-      return removedBlanks.indexOf(name) == pos;
-  });
+    return removedBlanks.indexOf(name) == pos
+  })
 
-  return removedDuplicates;
-}
+  return removedDuplicates
+}

+ 79 - 55
frontend/src/components/posting/utils/validators.js

@@ -1,78 +1,102 @@
-import { maxLength, minLength } from 'misago/utils/validators';
-import misago from 'misago';
+import { maxLength, minLength } from "misago/utils/validators"
+import misago from "misago"
 
 export function getTitleValidators() {
-  return [
-    getTitleLengthMin(),
-    getTitleLengthMax()
-  ];
+  return [getTitleLengthMin(), getTitleLengthMax()]
 }
 
 export function getPostValidators() {
-  if (misago.get('SETTINGS').post_length_max) {
-    return [
-      validatePostLengthMin(),
-      validatePostLengthMax()
-    ];
+  if (misago.get("SETTINGS").post_length_max) {
+    return [validatePostLengthMin(), validatePostLengthMax()]
   } else {
-    return [
-      validatePostLengthMin()
-    ];
+    return [validatePostLengthMin()]
   }
 }
 
 export function getTitleLengthMin() {
-  return minLength(misago.get('SETTINGS').thread_title_length_min, (limitValue, length) => {
-    const message = ngettext(
-      "Thread title should be at least %(limit_value)s character long (it has %(show_value)s).",
-      "Thread title should be at least %(limit_value)s characters long (it has %(show_value)s).",
-      limitValue);
+  return minLength(
+    misago.get("SETTINGS").thread_title_length_min,
+    (limitValue, length) => {
+      const message = ngettext(
+        "Thread title should be at least %(limit_value)s character long (it has %(show_value)s).",
+        "Thread title should be at least %(limit_value)s characters long (it has %(show_value)s).",
+        limitValue
+      )
 
-    return interpolate(message, {
-      limit_value: limitValue,
-      show_value: length
-    }, true);
-  });
+      return interpolate(
+        message,
+        {
+          limit_value: limitValue,
+          show_value: length
+        },
+        true
+      )
+    }
+  )
 }
 
 export function getTitleLengthMax() {
-  return maxLength(misago.get('SETTINGS').thread_title_length_max, (limitValue, length) => {
-    const message = ngettext(
-      "Thread title cannot be longer than %(limit_value)s character (it has %(show_value)s).",
-      "Thread title cannot be longer than %(limit_value)s characters (it has %(show_value)s).",
-      limitValue);
+  return maxLength(
+    misago.get("SETTINGS").thread_title_length_max,
+    (limitValue, length) => {
+      const message = ngettext(
+        "Thread title cannot be longer than %(limit_value)s character (it has %(show_value)s).",
+        "Thread title cannot be longer than %(limit_value)s characters (it has %(show_value)s).",
+        limitValue
+      )
 
-    return interpolate(message, {
-      limit_value: limitValue,
-      show_value: length
-    }, true);
-  });
+      return interpolate(
+        message,
+        {
+          limit_value: limitValue,
+          show_value: length
+        },
+        true
+      )
+    }
+  )
 }
 
 export function validatePostLengthMin() {
-  return minLength(misago.get('SETTINGS').post_length_min, (limitValue, length) => {
-    const message = ngettext(
-      "Posted message should be at least %(limit_value)s character long (it has %(show_value)s).",
-      "Posted message should be at least %(limit_value)s characters long (it has %(show_value)s).",
-      limitValue);
+  return minLength(
+    misago.get("SETTINGS").post_length_min,
+    (limitValue, length) => {
+      const message = ngettext(
+        "Posted message should be at least %(limit_value)s character long (it has %(show_value)s).",
+        "Posted message should be at least %(limit_value)s characters long (it has %(show_value)s).",
+        limitValue
+      )
 
-    return interpolate(message, {
-      limit_value: limitValue,
-      show_value: length
-    }, true);
-  });
+      return interpolate(
+        message,
+        {
+          limit_value: limitValue,
+          show_value: length
+        },
+        true
+      )
+    }
+  )
 }
 
 export function validatePostLengthMax() {
-  return maxLength(misago.get('SETTINGS').post_length_max || 1000000, (limitValue, length) => {
-    const message = ngettext(
-      "Posted message cannot be longer than %(limit_value)s character (it has %(show_value)s).",
-      "Posted message cannot be longer than %(limit_value)s characters (it has %(show_value)s).",
-      limitValue);
+  return maxLength(
+    misago.get("SETTINGS").post_length_max || 1000000,
+    (limitValue, length) => {
+      const message = ngettext(
+        "Posted message cannot be longer than %(limit_value)s character (it has %(show_value)s).",
+        "Posted message cannot be longer than %(limit_value)s characters (it has %(show_value)s).",
+        limitValue
+      )
 
-    return interpolate(message, {
-      limit_value: limitValue,
-      show_value: length
-    }, true);
-  });
-}
+      return interpolate(
+        message,
+        {
+          limit_value: limitValue,
+          show_value: length
+        },
+        true
+      )
+    }
+  )
+}

+ 101 - 77
frontend/src/components/posts-list/event/controls.js

@@ -1,10 +1,9 @@
-/* jshint ignore:start */
-import React from 'react';
-import moment from 'moment';
-import * as post from 'misago/reducers/post';
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
+import React from "react"
+import moment from "moment"
+import * as post from "misago/reducers/post"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export default function(props) {
   if (isVisible(props.post.acl)) {
@@ -14,43 +13,50 @@ export default function(props) {
         <Unhide {...props} />
         <Delete {...props} />
       </li>
-    );
+    )
   } else {
-    return null;
+    return null
   }
 }
 
 export function isVisible(acl) {
-  return acl.can_hide;
+  return acl.can_hide
 }
 
 export class Hide extends React.Component {
   onClick = () => {
-    store.dispatch(post.patch(this.props.post, {
-      is_hidden: true,
-      hidden_on: moment(),
-      hidden_by_name: this.props.user.username,
-      url: Object.assign(this.props.post.url, {
-        hidden_by: this.props.user.url
+    store.dispatch(
+      post.patch(this.props.post, {
+        is_hidden: true,
+        hidden_on: moment(),
+        hidden_by_name: this.props.user.username,
+        url: Object.assign(this.props.post.url, {
+          hidden_by: this.props.user.url
+        })
       })
-    }));
-
-    const op = {'op': 'replace', 'path': 'is-hidden', 'value': true};
-
-    ajax.patch(this.props.post.api.index, [op]).then((patch) => {
-      store.dispatch(post.patch(this.props.post, patch));
-    }, (rejection) => {
-      if (rejection.status === 400) {
-        snackbar.error(rejection.detail[0]);
-      } else {
-        snackbar.apiError(rejection);
+    )
+
+    const op = { op: "replace", path: "is-hidden", value: true }
+
+    ajax.patch(this.props.post.api.index, [op]).then(
+      patch => {
+        store.dispatch(post.patch(this.props.post, patch))
+      },
+      rejection => {
+        if (rejection.status === 400) {
+          snackbar.error(rejection.detail[0])
+        } else {
+          snackbar.apiError(rejection)
+        }
+
+        store.dispatch(
+          post.patch(this.props.post, {
+            is_hidden: false
+          })
+        )
       }
-
-      store.dispatch(post.patch(this.props.post, {
-        is_hidden: false
-      }));
-    });
-  };
+    )
+  }
 
   render() {
     if (!this.props.post.is_hidden) {
@@ -58,35 +64,42 @@ export class Hide extends React.Component {
         <button type="button" className="btn btn-link" onClick={this.onClick}>
           {gettext("Hide")}
         </button>
-      );
+      )
     } else {
-      return null;
+      return null
     }
   }
 }
 
 export class Unhide extends React.Component {
   onClick = () => {
-    store.dispatch(post.patch(this.props.post, {
-      is_hidden: false
-    }));
-
-    const op = {'op': 'replace', 'path': 'is-hidden', 'value': false};
-
-    ajax.patch(this.props.post.api.index, [op]).then((patch) => {
-      store.dispatch(post.patch(this.props.post, patch));
-    }, (rejection) => {
-      if (rejection.status === 400) {
-        snackbar.error(rejection.detail[0]);
-      } else {
-        snackbar.apiError(rejection);
+    store.dispatch(
+      post.patch(this.props.post, {
+        is_hidden: false
+      })
+    )
+
+    const op = { op: "replace", path: "is-hidden", value: false }
+
+    ajax.patch(this.props.post.api.index, [op]).then(
+      patch => {
+        store.dispatch(post.patch(this.props.post, patch))
+      },
+      rejection => {
+        if (rejection.status === 400) {
+          snackbar.error(rejection.detail[0])
+        } else {
+          snackbar.apiError(rejection)
+        }
+
+        store.dispatch(
+          post.patch(this.props.post, {
+            is_hidden: true
+          })
+        )
       }
-
-      store.dispatch(post.patch(this.props.post, {
-        is_hidden: true
-      }));
-    });
-  };
+    )
+  }
 
   render() {
     if (this.props.post.is_hidden) {
@@ -94,46 +107,57 @@ export class Unhide extends React.Component {
         <button type="button" className="btn btn-link" onClick={this.onClick}>
           {gettext("Unhide")}
         </button>
-      );
+      )
     } else {
-      return null;
+      return null
     }
   }
 }
 
 export class Delete extends React.Component {
   onClick = () => {
-    const decision = confirm(gettext("Are you sure you wish to delete this event? This action is not reversible!"));
+    const decision = confirm(
+      gettext(
+        "Are you sure you wish to delete this event? This action is not reversible!"
+      )
+    )
     if (decision) {
       this.delete()
     }
-  };
+  }
 
   delete = () => {
-    store.dispatch(post.patch(this.props.post, {
-      isDeleted: true
-    }));
-
-    ajax.delete(this.props.post.api.index).then(() => {
-      snackbar.success(gettext("Event has been deleted."));
-    }, (rejection) => {
-      if (rejection.status === 400) {
-        snackbar.error(rejection.detail[0]);
-      } else {
-        snackbar.apiError(rejection);
+    store.dispatch(
+      post.patch(this.props.post, {
+        isDeleted: true
+      })
+    )
+
+    ajax.delete(this.props.post.api.index).then(
+      () => {
+        snackbar.success(gettext("Event has been deleted."))
+      },
+      rejection => {
+        if (rejection.status === 400) {
+          snackbar.error(rejection.detail[0])
+        } else {
+          snackbar.apiError(rejection)
+        }
+
+        store.dispatch(
+          post.patch(this.props.post, {
+            isDeleted: false
+          })
+        )
       }
-
-      store.dispatch(post.patch(this.props.post, {
-        isDeleted: false
-      }));
-    });
-  };
+    )
+  }
 
   render() {
     return (
       <button type="button" className="btn btn-link" onClick={this.onClick}>
         {gettext("Delete")}
       </button>
-    );
+    )
   }
-}
+}

+ 21 - 24
frontend/src/components/posts-list/event/icon.js

@@ -1,40 +1,37 @@
-/* jshint ignore:start */
-import React from 'react';
+import React from "react"
 
 const ICON = {
-  changed_title: 'edit',
+  changed_title: "edit",
 
-  pinned_globally: 'bookmark',
-  pinned_locally: 'bookmark_border',
-  unpinned: 'panorama_fish_eye',
+  pinned_globally: "bookmark",
+  pinned_locally: "bookmark_border",
+  unpinned: "panorama_fish_eye",
 
-  moved: 'arrow_forward',
-  merged: 'call_merge',
+  moved: "arrow_forward",
+  merged: "call_merge",
 
-  approved: 'done',
+  approved: "done",
 
-  opened: 'lock_open',
-  closed: 'lock_outline',
+  opened: "lock_open",
+  closed: "lock_outline",
 
-  unhid: 'visibility',
-  hid: 'visibility_off',
+  unhid: "visibility",
+  hid: "visibility_off",
 
-  changed_owner: 'grade',
-  tookover: 'grade',
+  changed_owner: "grade",
+  tookover: "grade",
 
-  added_participant: 'person_add',
+  added_participant: "person_add",
 
-  owner_left: 'person_outline',
-  participant_left: 'person_outline',
-  removed_participant: 'remove_circle_outline',
+  owner_left: "person_outline",
+  participant_left: "person_outline",
+  removed_participant: "remove_circle_outline"
 }
 
 export default function(props) {
   return (
     <div className="post-avatar">
-      <span className="material-icon">
-        {ICON[props.post.event_type]}
-      </span>
+      <span className="material-icon">{ICON[props.post.event_type]}</span>
     </div>
-  );
-}
+  )
+}

+ 12 - 13
frontend/src/components/posts-list/event/index.js

@@ -1,21 +1,20 @@
-/* jshint ignore:start */
-import React from 'react';
-import Icon from './icon';
-import Info from './info';
-import Message from './message';
-import UnreadLabel from './unread-label';
-import Waypoint from '../waypoint';
+import React from "react"
+import Icon from "./icon"
+import Info from "./info"
+import Message from "./message"
+import UnreadLabel from "./unread-label"
+import Waypoint from "../waypoint"
 
 export default function(props) {
-  let className = 'event';
+  let className = "event"
   if (props.post.isDeleted) {
-    className = 'hide';
+    className = "hide"
   } else if (props.post.is_hidden) {
-    className = 'event post-hidden';
+    className = "event post-hidden"
   }
 
   return (
-    <li id={'post-' + props.post.id} className={className}>
+    <li id={"post-" + props.post.id} className={className}>
       <UnreadLabel post={props.post} />
       <div className="row">
         <div className="col-xs-2 col-sm-3 text-right">
@@ -29,5 +28,5 @@ export default function(props) {
         </div>
       </div>
     </li>
-  );
-}
+  )
+}

+ 82 - 48
frontend/src/components/posts-list/event/info.js

@@ -1,12 +1,11 @@
-/* jshint ignore:start */
-import React from 'react';
-import escapeHtml from 'misago/utils/escape-html';
-import Controls from './controls';
+import React from "react"
+import escapeHtml from "misago/utils/escape-html"
+import Controls from "./controls"
 
-const DATE_ABBR = '<abbr title="%(absolute)s">%(relative)s</abbr>';
-const DATE_URL = '<a href="%(url)s" title="%(absolute)s">%(relative)s</a>';
-const USER_SPAN = '<span class="item-title">%(user)s</span>';
-const USER_URL = '<a href="%(url)s" class="item-title">%(user)s</a>';
+const DATE_ABBR = '<abbr title="%(absolute)s">%(relative)s</abbr>'
+const DATE_URL = '<a href="%(url)s" title="%(absolute)s">%(relative)s</a>'
+const USER_SPAN = '<span class="item-title">%(user)s</span>'
+const USER_URL = '<a href="%(url)s" class="item-title">%(user)s</a>'
 
 export default function(props) {
   return (
@@ -15,69 +14,104 @@ export default function(props) {
       <Poster {...props} />
       <Controls {...props} />
     </ul>
-  );
+  )
 }
 
 export function Hidden(props) {
   if (props.post.is_hidden) {
-    let user = null;
+    let user = null
     if (props.post.url.hidden_by) {
-      user = interpolate(USER_URL, {
-        url: escapeHtml(props.post.url.hidden_by),
-        user: escapeHtml(props.post.hidden_by_name)
-      }, true);
+      user = interpolate(
+        USER_URL,
+        {
+          url: escapeHtml(props.post.url.hidden_by),
+          user: escapeHtml(props.post.hidden_by_name)
+        },
+        true
+      )
     } else {
-      user = interpolate(USER_SPAN, {
-        user: escapeHtml(props.post.hidden_by_name)
-      }, true);
+      user = interpolate(
+        USER_SPAN,
+        {
+          user: escapeHtml(props.post.hidden_by_name)
+        },
+        true
+      )
     }
 
-    const date = interpolate(DATE_ABBR, {
-      absolute: escapeHtml(props.post.hidden_on.format('LLL')),
-      relative: escapeHtml(props.post.hidden_on.fromNow())
-    }, true);
+    const date = interpolate(
+      DATE_ABBR,
+      {
+        absolute: escapeHtml(props.post.hidden_on.format("LLL")),
+        relative: escapeHtml(props.post.hidden_on.fromNow())
+      },
+      true
+    )
 
-    const message = interpolate(escapeHtml(gettext("Hidden by %(event_by)s %(event_on)s.")), {
-      event_by: user,
-      event_on: date
-    }, true);
+    const message = interpolate(
+      escapeHtml(gettext("Hidden by %(event_by)s %(event_on)s.")),
+      {
+        event_by: user,
+        event_on: date
+      },
+      true
+    )
 
     return (
       <li
         className="event-hidden-message"
-        dangerouslySetInnerHTML={{__html: message}}
+        dangerouslySetInnerHTML={{ __html: message }}
       />
-    );
+    )
   } else {
-    return null;
+    return null
   }
 }
 
 export function Poster(props) {
-  let user = null;
+  let user = null
   if (props.post.poster) {
-    user = interpolate(USER_URL, {
-      url: escapeHtml(props.post.poster.url),
-      user: escapeHtml(props.post.poster_name)
-    }, true);
+    user = interpolate(
+      USER_URL,
+      {
+        url: escapeHtml(props.post.poster.url),
+        user: escapeHtml(props.post.poster_name)
+      },
+      true
+    )
   } else {
-    user = interpolate(USER_SPAN, {
-      user: escapeHtml(props.post.poster_name)
-    }, true);
+    user = interpolate(
+      USER_SPAN,
+      {
+        user: escapeHtml(props.post.poster_name)
+      },
+      true
+    )
   }
 
-  const date = interpolate(DATE_URL, {
-    url: escapeHtml(props.post.url.index),
-    absolute: escapeHtml(props.post.posted_on.format('LLL')),
-    relative: escapeHtml(props.post.posted_on.fromNow())
-  }, true);
+  const date = interpolate(
+    DATE_URL,
+    {
+      url: escapeHtml(props.post.url.index),
+      absolute: escapeHtml(props.post.posted_on.format("LLL")),
+      relative: escapeHtml(props.post.posted_on.fromNow())
+    },
+    true
+  )
 
-  const message = interpolate(escapeHtml(gettext("By %(event_by)s %(event_on)s.")), {
-    event_by: user,
-    event_on: date
-  }, true);
+  const message = interpolate(
+    escapeHtml(gettext("By %(event_by)s %(event_on)s.")),
+    {
+      event_by: user,
+      event_on: date
+    },
+    true
+  )
 
   return (
-    <li className="event-posters" dangerouslySetInnerHTML={{__html: message}} />
-  );
-}
+    <li
+      className="event-posters"
+      dangerouslySetInnerHTML={{ __html: message }}
+    />
+  )
+}

+ 154 - 99
frontend/src/components/posts-list/event/message.js

@@ -1,6 +1,5 @@
-/* jshint ignore:start */
-import React from 'react';
-import escapeHtml from 'misago/utils/escape-html';
+import React from "react"
+import escapeHtml from "misago/utils/escape-html"
 
 const MESSAGE = {
   pinned_globally: gettext("Thread has been pinned globally."),
@@ -18,137 +17,193 @@ const MESSAGE = {
   tookover: gettext("Took thread over."),
 
   owner_left: gettext("Owner has left thread. This thread is now closed."),
-  participant_left: gettext("Participant has left thread."),
+  participant_left: gettext("Participant has left thread.")
 }
 
 const ITEM_LINK = '<a href="%(url)s" class="item-title">%(name)s</a>'
-const ITEM_SPAN = '<span class="item-title">%(name)s</span>';
+const ITEM_SPAN = '<span class="item-title">%(name)s</span>'
 
 export default function(props) {
   if (MESSAGE[props.post.event_type]) {
-    return (
-      <p className="event-message">
-        {MESSAGE[props.post.event_type]}
-      </p>
-    );
-  } else if (props.post.event_type === 'changed_title') {
-    return (
-      <ChangedTitle {...props} />
-    );
-  } else if (props.post.event_type === 'moved') {
-    return (
-      <Moved {...props} />
-    );
-  } else if (props.post.event_type === 'merged') {
-    return (
-      <Merged {...props} />
-    );
-  } else if (props.post.event_type === 'changed_owner') {
-    return (
-      <ChangedOwner {...props} />
-    );
-  } else if (props.post.event_type === 'added_participant') {
-    return (
-      <AddedParticipant {...props} />
-    );
-  } else if (props.post.event_type === 'removed_participant') {
-    return (
-      <RemovedParticipant {...props} />
-    );
+    return <p className="event-message">{MESSAGE[props.post.event_type]}</p>
+  } else if (props.post.event_type === "changed_title") {
+    return <ChangedTitle {...props} />
+  } else if (props.post.event_type === "moved") {
+    return <Moved {...props} />
+  } else if (props.post.event_type === "merged") {
+    return <Merged {...props} />
+  } else if (props.post.event_type === "changed_owner") {
+    return <ChangedOwner {...props} />
+  } else if (props.post.event_type === "added_participant") {
+    return <AddedParticipant {...props} />
+  } else if (props.post.event_type === "removed_participant") {
+    return <RemovedParticipant {...props} />
   } else {
-    return null;
+    return null
   }
 }
 
 export function ChangedTitle(props) {
-  const msgstring = escapeHtml(gettext("Thread title has been changed from %(old_title)s."));
-  const oldTitle = interpolate(ITEM_SPAN, {
-    name: escapeHtml(props.post.event_context.old_title)
-  }, true);
-  const message = interpolate(msgstring, {
-    old_title: oldTitle
-  }, true);
+  const msgstring = escapeHtml(
+    gettext("Thread title has been changed from %(old_title)s.")
+  )
+  const oldTitle = interpolate(
+    ITEM_SPAN,
+    {
+      name: escapeHtml(props.post.event_context.old_title)
+    },
+    true
+  )
+  const message = interpolate(
+    msgstring,
+    {
+      old_title: oldTitle
+    },
+    true
+  )
 
   return (
-    <p className="event-message" dangerouslySetInnerHTML={{__html: message}} />
-  );
+    <p
+      className="event-message"
+      dangerouslySetInnerHTML={{ __html: message }}
+    />
+  )
 }
 
 export function Moved(props) {
-  const msgstring = escapeHtml(gettext("Thread has been moved from %(from_category)s."));
-  const fromCategory = interpolate(ITEM_LINK, {
-    url: escapeHtml(props.post.event_context.from_category.url),
-    name: escapeHtml(props.post.event_context.from_category.name)
-  }, true);
-
-  const message = interpolate(msgstring, {
-    from_category: fromCategory
-  }, true);
+  const msgstring = escapeHtml(
+    gettext("Thread has been moved from %(from_category)s.")
+  )
+  const fromCategory = interpolate(
+    ITEM_LINK,
+    {
+      url: escapeHtml(props.post.event_context.from_category.url),
+      name: escapeHtml(props.post.event_context.from_category.name)
+    },
+    true
+  )
+
+  const message = interpolate(
+    msgstring,
+    {
+      from_category: fromCategory
+    },
+    true
+  )
 
   return (
-    <p className="event-message" dangerouslySetInnerHTML={{__html: message}} />
-  );
+    <p
+      className="event-message"
+      dangerouslySetInnerHTML={{ __html: message }}
+    />
+  )
 }
 
 export function Merged(props) {
-  const msgstring = escapeHtml(gettext("The %(merged_thread)s thread has been merged into this thread."));
-  const mergedThread = interpolate(ITEM_SPAN, {
-    name: escapeHtml(props.post.event_context.merged_thread)
-  }, true);
-
-  const message = interpolate(msgstring, {
-    merged_thread: mergedThread
-  }, true);
+  const msgstring = escapeHtml(
+    gettext("The %(merged_thread)s thread has been merged into this thread.")
+  )
+  const mergedThread = interpolate(
+    ITEM_SPAN,
+    {
+      name: escapeHtml(props.post.event_context.merged_thread)
+    },
+    true
+  )
+
+  const message = interpolate(
+    msgstring,
+    {
+      merged_thread: mergedThread
+    },
+    true
+  )
 
   return (
-    <p className="event-message" dangerouslySetInnerHTML={{__html: message}} />
-  );
+    <p
+      className="event-message"
+      dangerouslySetInnerHTML={{ __html: message }}
+    />
+  )
 }
 
 export function ChangedOwner(props) {
-  const msgstring = escapeHtml(gettext("Changed thread owner to %(user)s."));
-  const newOwner = interpolate(ITEM_LINK, {
-    url: escapeHtml(props.post.event_context.user.url),
-    name: escapeHtml(props.post.event_context.user.username)
-  }, true);
-
-  const message = interpolate(msgstring, {
-    user: newOwner
-  }, true);
+  const msgstring = escapeHtml(gettext("Changed thread owner to %(user)s."))
+  const newOwner = interpolate(
+    ITEM_LINK,
+    {
+      url: escapeHtml(props.post.event_context.user.url),
+      name: escapeHtml(props.post.event_context.user.username)
+    },
+    true
+  )
+
+  const message = interpolate(
+    msgstring,
+    {
+      user: newOwner
+    },
+    true
+  )
 
   return (
-    <p className="event-message" dangerouslySetInnerHTML={{__html: message}} />
-  );
+    <p
+      className="event-message"
+      dangerouslySetInnerHTML={{ __html: message }}
+    />
+  )
 }
 
 export function AddedParticipant(props) {
-  const msgstring = escapeHtml(gettext("Added %(user)s to thread."));
-  const newOwner = interpolate(ITEM_LINK, {
-    url: escapeHtml(props.post.event_context.user.url),
-    name: escapeHtml(props.post.event_context.user.username)
-  }, true);
-
-  const message = interpolate(msgstring, {
-    user: newOwner
-  }, true);
+  const msgstring = escapeHtml(gettext("Added %(user)s to thread."))
+  const newOwner = interpolate(
+    ITEM_LINK,
+    {
+      url: escapeHtml(props.post.event_context.user.url),
+      name: escapeHtml(props.post.event_context.user.username)
+    },
+    true
+  )
+
+  const message = interpolate(
+    msgstring,
+    {
+      user: newOwner
+    },
+    true
+  )
 
   return (
-    <p className="event-message" dangerouslySetInnerHTML={{__html: message}} />
-  );
+    <p
+      className="event-message"
+      dangerouslySetInnerHTML={{ __html: message }}
+    />
+  )
 }
 
 export function RemovedParticipant(props) {
-  const msgstring = escapeHtml(gettext("Removed %(user)s from thread."));
-  const newOwner = interpolate(ITEM_LINK, {
-    url: escapeHtml(props.post.event_context.user.url),
-    name: escapeHtml(props.post.event_context.user.username)
-  }, true);
-
-  const message = interpolate(msgstring, {
-    user: newOwner
-  }, true);
+  const msgstring = escapeHtml(gettext("Removed %(user)s from thread."))
+  const newOwner = interpolate(
+    ITEM_LINK,
+    {
+      url: escapeHtml(props.post.event_context.user.url),
+      name: escapeHtml(props.post.event_context.user.username)
+    },
+    true
+  )
+
+  const message = interpolate(
+    msgstring,
+    {
+      user: newOwner
+    },
+    true
+  )
 
   return (
-    <p className="event-message" dangerouslySetInnerHTML={{__html: message}} />
-  );
-}
+    <p
+      className="event-message"
+      dangerouslySetInnerHTML={{ __html: message }}
+    />
+  )
+}

+ 5 - 10
frontend/src/components/posts-list/event/unread-label.js

@@ -1,20 +1,15 @@
-/* jshint ignore:start */
-import React from 'react';
+import React from "react"
 
 export default function({ post }) {
-  if (post.is_read) return null;
+  if (post.is_read) return null
 
   return (
     <div className="row">
       <div className="col-xs-10 col-xs-offset-2 col-sm-9 col-sm-offset-3 text-left">
-
         <div className="event-label">
-          <span className="label label-unread">
-            {gettext("New event")}
-          </span>
+          <span className="label label-unread">{gettext("New event")}</span>
         </div>
-
       </div>
     </div>
-  );
-}
+  )
+}

+ 10 - 21
frontend/src/components/posts-list/index.js

@@ -1,8 +1,7 @@
-/* jshint ignore:start */
-import React from 'react';
-import Event from './event';
-import Post from './post';
-import PostPreview from './post/preview';
+import React from "react"
+import Event from "./event"
+import Post from "./post"
+import PostPreview from "./post/preview"
 
 export default function(props) {
   if (!props.posts.isLoaded) {
@@ -10,32 +9,22 @@ export default function(props) {
       <ul className="posts-list ui-preview">
         <PostPreview />
       </ul>
-    );
+    )
   }
 
   return (
     <ul className="posts-list ui-ready">
-      {props.posts.results.map((post) => {
-        return (
-          <ListItem
-            key={post.id}
-            post={post}
-            {...props}
-          />
-        );
+      {props.posts.results.map(post => {
+        return <ListItem key={post.id} post={post} {...props} />
       })}
     </ul>
-  );
+  )
 }
 
 export function ListItem(props) {
   if (props.post.is_event) {
-    return (
-      <Event {...props} />
-    );
+    return <Event {...props} />
   }
 
-  return (
-    <Post {...props} />
-  );
+  return <Post {...props} />
 }

+ 59 - 37
frontend/src/components/posts-list/post/attachments/attachment.js

@@ -1,25 +1,27 @@
-/* jshint ignore:start */
-import React from 'react';
-import misago from 'misago';
-import escapeHtml from 'misago/utils/escape-html';
-import formatFilesize from 'misago/utils/file-size';
+import React from "react"
+import misago from "misago"
+import escapeHtml from "misago/utils/escape-html"
+import formatFilesize from "misago/utils/file-size"
 
-const DATE_ABBR = '<abbr title="%(absolute)s">%(relative)s</abbr>';
-const USER_SPAN = '<span class="item-title">%(user)s</span>';
-const USER_URL = '<a href="%(url)s" class="item-title">%(user)s</a>';
+const DATE_ABBR = '<abbr title="%(absolute)s">%(relative)s</abbr>'
+const USER_SPAN = '<span class="item-title">%(user)s</span>'
+const USER_URL = '<a href="%(url)s" class="item-title">%(user)s</a>'
 
 export default function(props) {
   return (
     <div className="col-xs-12 col-md-6">
       <AttachmentPreview {...props} />
       <div className="post-attachment">
-        <a href={props.attachment.url.index} className="attachment-name item-title">
+        <a
+          href={props.attachment.url.index}
+          className="attachment-name item-title"
+        >
           {props.attachment.filename}
         </a>
         <AttachmentDetails {...props} />
       </div>
     </div>
-  );
+  )
 }
 
 export function AttachmentPreview(props) {
@@ -28,13 +30,13 @@ export function AttachmentPreview(props) {
       <div className="post-attachment-preview">
         <AttachmentThumbnail {...props} />
       </div>
-    );
+    )
   } else {
     return (
       <div className="post-attachment-preview">
         <AttachmentIcon {...props} />
       </div>
-    );
+    )
   }
 }
 
@@ -43,49 +45,69 @@ export function AttachmentIcon(props) {
     <a href={props.attachment.url.index} className="material-icon">
       insert_drive_file
     </a>
-  );
+  )
 }
 
 export function AttachmentThumbnail(props) {
-  const url = props.attachment.url.thumb || props.attachment.url.index;
+  const url = props.attachment.url.thumb || props.attachment.url.index
   return (
     <a
       className="post-thumbnail"
       href={props.attachment.url.index}
-      style={{backgroundImage: 'url("' + escapeHtml(url) + '")'}}
+      style={{ backgroundImage: 'url("' + escapeHtml(url) + '")' }}
     />
-  );
+  )
 }
 
 export function AttachmentDetails(props) {
-  let user = null;
+  let user = null
   if (props.attachment.url.uploader) {
-    user = interpolate(USER_URL, {
-      url: escapeHtml(props.attachment.url.uploader),
-      user: escapeHtml(props.attachment.uploader_name)
-    }, true);
+    user = interpolate(
+      USER_URL,
+      {
+        url: escapeHtml(props.attachment.url.uploader),
+        user: escapeHtml(props.attachment.uploader_name)
+      },
+      true
+    )
   } else {
-    user = interpolate(USER_SPAN, {
-      user: escapeHtml(props.attachment.uploader_name)
-    }, true);
+    user = interpolate(
+      USER_SPAN,
+      {
+        user: escapeHtml(props.attachment.uploader_name)
+      },
+      true
+    )
   }
 
-  const date = interpolate(DATE_ABBR, {
-    absolute: escapeHtml(props.attachment.uploaded_on.format('LLL')),
-    relative: escapeHtml(props.attachment.uploaded_on.fromNow())
-  }, true);
+  const date = interpolate(
+    DATE_ABBR,
+    {
+      absolute: escapeHtml(props.attachment.uploaded_on.format("LLL")),
+      relative: escapeHtml(props.attachment.uploaded_on.fromNow())
+    },
+    true
+  )
 
-  const message = interpolate(escapeHtml(gettext("%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s.")), {
-    filetype: props.attachment.filetype,
-    size: formatFilesize(props.attachment.size),
-    uploader: user,
-    uploaded_on: date
-  }, true);
+  const message = interpolate(
+    escapeHtml(
+      gettext(
+        "%(filetype)s, %(size)s, uploaded by %(uploader)s %(uploaded_on)s."
+      )
+    ),
+    {
+      filetype: props.attachment.filetype,
+      size: formatFilesize(props.attachment.size),
+      uploader: user,
+      uploaded_on: date
+    },
+    true
+  )
 
   return (
     <p
       className="post-attachment-description"
-      dangerouslySetInnerHTML={{__html: message}}
+      dangerouslySetInnerHTML={{ __html: message }}
     />
-  );
-}
+  )
+}

+ 17 - 14
frontend/src/components/posts-list/post/attachments/index.js

@@ -1,38 +1,41 @@
-/* jshint ignore:start */
-import React from 'react';
-import batch from 'misago/utils/batch';
-import Attachment from './attachment';
+import React from "react"
+import batch from "misago/utils/batch"
+import Attachment from "./attachment"
 
 export default function(props) {
   if (!isVisible(props.post)) {
-    return null;
+    return null
   }
 
   return (
     <div className="post-attachments">
-      {batch(props.post.attachments, 2).map((row) => {
-        const key = row.map((a) => {return a ? a.id : 0;}).join('_');
-        return <Row key={key} row={row} />;
+      {batch(props.post.attachments, 2).map(row => {
+        const key = row
+          .map(a => {
+            return a ? a.id : 0
+          })
+          .join("_")
+        return <Row key={key} row={row} />
       })}
     </div>
-  );
+  )
 }
 
 export function isVisible(post) {
-  return (!post.is_hidden || post.acl.can_see_hidden) && post.attachments;
+  return (!post.is_hidden || post.acl.can_see_hidden) && post.attachments
 }
 
 export function Row(props) {
   return (
     <div className="row">
-      {props.row.map((attachment) => {
+      {props.row.map(attachment => {
         return (
           <Attachment
             attachment={attachment}
             key={attachment ? attachment.id : 0}
           />
-        );
+        )
       })}
     </div>
-  );
-}
+  )
+}

+ 59 - 37
frontend/src/components/posts-list/post/body.js

@@ -1,67 +1,89 @@
-/* jshint ignore:start */
-import React from 'react';
-import Waypoint from '../waypoint';
-import MisagoMarkup from 'misago/components/misago-markup';
-import escapeHtml from 'misago/utils/escape-html';
+import React from "react"
+import Waypoint from "../waypoint"
+import MisagoMarkup from "misago/components/misago-markup"
+import escapeHtml from "misago/utils/escape-html"
 
-const HIDDEN_BY_URL = '<a href="%(url)s" class="item-title">%(user)s</a>';
-const HIDDEN_BY_SPAN = '<span class="item-title">%(user)s</span>';
-const HIDDEN_ON = '<abbr class="last-title" title="%(absolute)s">%(relative)s</abbr>';
+const HIDDEN_BY_URL = '<a href="%(url)s" class="item-title">%(user)s</a>'
+const HIDDEN_BY_SPAN = '<span class="item-title">%(user)s</span>'
+const HIDDEN_ON =
+  '<abbr class="last-title" title="%(absolute)s">%(relative)s</abbr>'
 
 export default function(props) {
   if (props.post.is_hidden && !props.post.acl.can_see_hidden) {
-    return <Hidden {...props} />;
+    return <Hidden {...props} />
   } else if (props.post.content) {
-    return <Default {...props} />;
+    return <Default {...props} />
   } else {
-    return <Invalid {...props} />;
+    return <Invalid {...props} />
   }
 }
 
 export function Default(props) {
- return (
+  return (
     <Waypoint className="post-body" post={props.post}>
       <MisagoMarkup markup={props.post.content} />
     </Waypoint>
-  );
+  )
 }
 
 export function Hidden(props) {
-  let user = null;
+  let user = null
   if (props.post.hidden_by) {
-    user = interpolate(HIDDEN_BY_URL, {
-      url: escapeHtml(props.post.url.hidden_by),
-      user: escapeHtml(props.post.hidden_by_name)
-    }, true);
+    user = interpolate(
+      HIDDEN_BY_URL,
+      {
+        url: escapeHtml(props.post.url.hidden_by),
+        user: escapeHtml(props.post.hidden_by_name)
+      },
+      true
+    )
   } else {
-    user = interpolate(HIDDEN_BY_SPAN, {
-      user: escapeHtml(props.post.hidden_by_name)
-    }, true);
+    user = interpolate(
+      HIDDEN_BY_SPAN,
+      {
+        user: escapeHtml(props.post.hidden_by_name)
+      },
+      true
+    )
   }
 
-  const date = interpolate(HIDDEN_ON, {
-    absolute: escapeHtml(props.post.hidden_on.format('LLL')),
-    relative: escapeHtml(props.post.hidden_on.fromNow())
-  }, true);
+  const date = interpolate(
+    HIDDEN_ON,
+    {
+      absolute: escapeHtml(props.post.hidden_on.format("LLL")),
+      relative: escapeHtml(props.post.hidden_on.fromNow())
+    },
+    true
+  )
 
-  const message = interpolate(escapeHtml(gettext("Hidden by %(hidden_by)s %(hidden_on)s.")), {
-    hidden_by: user,
-    hidden_on: date
-  }, true);
+  const message = interpolate(
+    escapeHtml(gettext("Hidden by %(hidden_by)s %(hidden_on)s.")),
+    {
+      hidden_by: user,
+      hidden_on: date
+    },
+    true
+  )
 
   return (
     <Waypoint className="post-body post-body-hidden" post={props.post}>
-      <p className="lead">{gettext("This post is hidden. You cannot see its contents.")}</p>
-      <p className="text-muted" dangerouslySetInnerHTML={{__html: message}} />
+      <p className="lead">
+        {gettext("This post is hidden. You cannot see its contents.")}
+      </p>
+      <p className="text-muted" dangerouslySetInnerHTML={{ __html: message }} />
     </Waypoint>
-  );
+  )
 }
 
 export function Invalid(props) {
- return (
+  return (
     <Waypoint className="post-body post-body-invalid" post={props.post}>
-      <p className="lead">{gettext("This post's contents cannot be displayed.")}</p>
-      <p className="text-muted">{gettext("This error is caused by invalid post content manipulation.")}</p>
+      <p className="lead">
+        {gettext("This post's contents cannot be displayed.")}
+      </p>
+      <p className="text-muted">
+        {gettext("This error is caused by invalid post content manipulation.")}
+      </p>
     </Waypoint>
-  );
-}
+  )
+}

+ 170 - 148
frontend/src/components/posts-list/post/controls/actions.js

@@ -1,199 +1,216 @@
-import moment from 'moment';
-import * as thread from 'misago/reducers/thread';
-import * as post from 'misago/reducers/post';
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
+import moment from "moment"
+import * as thread from "misago/reducers/thread"
+import * as post from "misago/reducers/post"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export function approve(props) {
-  store.dispatch(post.patch(props.post, {
-    is_unapproved: false
-  }));
+  store.dispatch(
+    post.patch(props.post, {
+      is_unapproved: false
+    })
+  )
 
-  const ops = [
-    {'op': 'replace', 'path': 'is-unapproved', 'value': false}
-  ];
+  const ops = [{ op: "replace", path: "is-unapproved", value: false }]
 
   const previousState = {
     is_unapproved: props.post.is_unapproved
-  };
+  }
 
-  patch(props, ops, previousState);
+  patch(props, ops, previousState)
 }
 
 export function protect(props) {
-  store.dispatch(post.patch(props.post, {
-    is_protected: true
-  }));
+  store.dispatch(
+    post.patch(props.post, {
+      is_protected: true
+    })
+  )
 
-  const ops = [
-    {'op': 'replace', 'path': 'is-protected', 'value': true}
-  ];
+  const ops = [{ op: "replace", path: "is-protected", value: true }]
 
   const previousState = {
     is_protected: props.post.is_protected
-  };
+  }
 
-  patch(props, ops, previousState);
+  patch(props, ops, previousState)
 }
 
 export function unprotect(props) {
-  store.dispatch(post.patch(props.post, {
-    is_protected: false
-  }));
+  store.dispatch(
+    post.patch(props.post, {
+      is_protected: false
+    })
+  )
 
-  const ops = [
-    {'op': 'replace', 'path': 'is-protected', 'value': false}
-  ];
+  const ops = [{ op: "replace", path: "is-protected", value: false }]
 
   const previousState = {
     is_protected: props.post.is_protected
-  };
+  }
 
-  patch(props, ops, previousState);
+  patch(props, ops, previousState)
 }
 
 export function hide(props) {
-  store.dispatch(post.patch(props.post, {
-    is_hidden: true,
-    hidden_on: moment(),
-    hidden_by_name: props.user.username,
-    url: Object.assign(props.post.url, {
-      hidden_by: props.user.url
+  store.dispatch(
+    post.patch(props.post, {
+      is_hidden: true,
+      hidden_on: moment(),
+      hidden_by_name: props.user.username,
+      url: Object.assign(props.post.url, {
+        hidden_by: props.user.url
+      })
     })
-  }));
+  )
 
-  const ops = [
-    {'op': 'replace', 'path': 'is-hidden', 'value': true}
-  ];
+  const ops = [{ op: "replace", path: "is-hidden", value: true }]
 
   const previousState = {
     is_hidden: props.post.is_hidden,
     hidden_on: props.post.hidden_on,
     hidden_by_name: props.post.hidden_by_name,
     url: props.post.url
-  };
+  }
 
-  patch(props, ops, previousState);
+  patch(props, ops, previousState)
 }
 
 export function unhide(props) {
-  store.dispatch(post.patch(props.post, {
-    is_hidden: false
-  }));
+  store.dispatch(
+    post.patch(props.post, {
+      is_hidden: false
+    })
+  )
 
-  const ops = [
-    {'op': 'replace', 'path': 'is-hidden', 'value': false}
-  ];
+  const ops = [{ op: "replace", path: "is-hidden", value: false }]
 
   const previousState = {
     is_hidden: props.post.is_hidden
-  };
+  }
 
-  patch(props, ops, previousState);
+  patch(props, ops, previousState)
 }
 
 export function like(props) {
-  const lastLikes = props.post.last_likes || [];
-  const concatedLikes = [props.user].concat(lastLikes);
-  const finalLikes = concatedLikes.length > 3 ? concatedLikes.slice(0, -1) : concatedLikes;
+  const lastLikes = props.post.last_likes || []
+  const concatedLikes = [props.user].concat(lastLikes)
+  const finalLikes =
+    concatedLikes.length > 3 ? concatedLikes.slice(0, -1) : concatedLikes
+
+  store.dispatch(
+    post.patch(props.post, {
+      is_liked: true,
+      likes: props.post.likes + 1,
+      last_likes: finalLikes
+    })
+  )
 
-  store.dispatch(post.patch(props.post, {
-    is_liked: true,
-    likes: props.post.likes + 1,
-    last_likes: finalLikes
-  }));
-
-  const ops = [
-    {'op': 'replace', 'path': 'is-liked', 'value': true}
-  ];
+  const ops = [{ op: "replace", path: "is-liked", value: true }]
 
   const previousState = {
     is_liked: props.post.is_liked,
     likes: props.post.likes,
     last_likes: props.post.last_likes
-  };
+  }
 
-  patch(props, ops, previousState);
+  patch(props, ops, previousState)
 }
 
 export function unlike(props) {
-  store.dispatch(post.patch(props.post, {
-    is_liked: false,
-    likes: props.post.likes - 1,
-    last_likes: props.post.last_likes.filter((user) => {
-      return !user.id || user.id !== props.user.id;
+  store.dispatch(
+    post.patch(props.post, {
+      is_liked: false,
+      likes: props.post.likes - 1,
+      last_likes: props.post.last_likes.filter(user => {
+        return !user.id || user.id !== props.user.id
+      })
     })
-  }));
+  )
 
-  const ops = [
-    {'op': 'replace', 'path': 'is-liked', 'value': false}
-  ];
+  const ops = [{ op: "replace", path: "is-liked", value: false }]
 
   const previousState = {
     is_liked: props.post.is_liked,
     likes: props.post.likes,
     last_likes: props.post.last_likes
-  };
+  }
 
-  patch(props, ops, previousState);
+  patch(props, ops, previousState)
 }
 
 export function patch(props, ops, previousState) {
-  ajax.patch(props.post.api.index, ops).then((newState) => {
-    store.dispatch(post.patch(props.post, newState));
-  }, (rejection) => {
-    if (rejection.status === 400) {
-      snackbar.error(rejection.detail[0]);
-    } else {
-      snackbar.apiError(rejection);
+  ajax.patch(props.post.api.index, ops).then(
+    newState => {
+      store.dispatch(post.patch(props.post, newState))
+    },
+    rejection => {
+      if (rejection.status === 400) {
+        snackbar.error(rejection.detail[0])
+      } else {
+        snackbar.apiError(rejection)
+      }
+
+      store.dispatch(post.patch(props.post, previousState))
     }
-
-    store.dispatch(post.patch(props.post, previousState));
-  });
+  )
 }
 
 export function remove(props) {
-  let confirmed = confirm(gettext("Are you sure you want to delete this post? This action is not reversible!"));
+  let confirmed = confirm(
+    gettext(
+      "Are you sure you want to delete this post? This action is not reversible!"
+    )
+  )
   if (!confirmed) {
-    return;
+    return
   }
 
-  store.dispatch(post.patch(props.post, {
-    isDeleted: true
-  }));
-
-  ajax.delete(props.post.api.index).then(() => {
-    snackbar.success(gettext("Post has been deleted."));
-  }, (rejection) => {
-    if (rejection.status === 400) {
-      snackbar.error(rejection.detail);
-    } else {
-      snackbar.apiError(rejection);
+  store.dispatch(
+    post.patch(props.post, {
+      isDeleted: true
+    })
+  )
+
+  ajax.delete(props.post.api.index).then(
+    () => {
+      snackbar.success(gettext("Post has been deleted."))
+    },
+    rejection => {
+      if (rejection.status === 400) {
+        snackbar.error(rejection.detail)
+      } else {
+        snackbar.apiError(rejection)
+      }
+
+      store.dispatch(
+        post.patch(props.post, {
+          isDeleted: false
+        })
+      )
     }
-
-    store.dispatch(post.patch(props.post, {
-      isDeleted: false
-    }));
-  });
+  )
 }
 
 export function markAsBestAnswer(props) {
-  const { post, user } = props;
-
-  store.dispatch(thread.update({
-    best_answer: post.id,
-    best_answer_is_protected: post.is_protected,
-    best_answer_marked_on: moment(),
-    best_answer_marked_by: user.id,
-    best_answer_marked_by_name: user.username,
-    best_answer_marked_by_slug: user.slug,
-  }));
+  const { post, user } = props
+
+  store.dispatch(
+    thread.update({
+      best_answer: post.id,
+      best_answer_is_protected: post.is_protected,
+      best_answer_marked_on: moment(),
+      best_answer_marked_by: user.id,
+      best_answer_marked_by_name: user.username,
+      best_answer_marked_by_slug: user.slug
+    })
+  )
 
   const ops = [
-    { 'op': 'replace', 'path': 'best-answer', 'value': post.id },
-    { 'op': 'add', 'path': 'acl', 'value': true }
-  ];
+    { op: "replace", path: "best-answer", value: post.id },
+    { op: "add", path: "acl", value: true }
+  ]
 
   const previousState = {
     best_answer: props.thread.best_answer,
@@ -201,28 +218,30 @@ export function markAsBestAnswer(props) {
     best_answer_marked_on: props.thread.best_answer_marked_on,
     best_answer_marked_by: props.thread.best_answer_marked_by,
     best_answer_marked_by_name: props.thread.best_answer_marked_by_name,
-    best_answer_marked_by_slug: props.thread.best_answer_marked_by_slug,
-  };
+    best_answer_marked_by_slug: props.thread.best_answer_marked_by_slug
+  }
 
-  patchThread(props, ops, previousState);
+  patchThread(props, ops, previousState)
 }
 
 export function unmarkBestAnswer(props) {
-  const { post } = props;
-
-  store.dispatch(thread.update({
-    best_answer: null,
-    best_answer_is_protected: false,
-    best_answer_marked_on: null,
-    best_answer_marked_by: null,
-    best_answer_marked_by_name: null,
-    best_answer_marked_by_slug: null,
-  }));
+  const { post } = props
+
+  store.dispatch(
+    thread.update({
+      best_answer: null,
+      best_answer_is_protected: false,
+      best_answer_marked_on: null,
+      best_answer_marked_by: null,
+      best_answer_marked_by_name: null,
+      best_answer_marked_by_slug: null
+    })
+  )
 
   const ops = [
-    { 'op': 'remove', 'path': 'best-answer', 'value': post.id },
-    { 'op': 'add', 'path': 'acl', 'value': true }
-  ];
+    { op: "remove", path: "best-answer", value: post.id },
+    { op: "add", path: "acl", value: true }
+  ]
 
   const previousState = {
     best_answer: props.thread.best_answer,
@@ -230,25 +249,28 @@ export function unmarkBestAnswer(props) {
     best_answer_marked_on: props.thread.best_answer_marked_on,
     best_answer_marked_by: props.thread.best_answer_marked_by,
     best_answer_marked_by_name: props.thread.best_answer_marked_by_name,
-    best_answer_marked_by_slug: props.thread.best_answer_marked_by_slug,
-  };
+    best_answer_marked_by_slug: props.thread.best_answer_marked_by_slug
+  }
 
-  patchThread(props, ops, previousState);
+  patchThread(props, ops, previousState)
 }
 
 export function patchThread(props, ops, previousState) {
-  ajax.patch(props.thread.api.index, ops).then((newState) => {
-    if (newState.best_answer_marked_on) {
-      newState.best_answer_marked_on = moment(newState.best_answer_marked_on);
-    }
-    store.dispatch(thread.update(newState));
-  }, (rejection) => {
-    if (rejection.status === 400) {
-      snackbar.error(rejection.detail[0]);
-    } else {
-      snackbar.apiError(rejection);
+  ajax.patch(props.thread.api.index, ops).then(
+    newState => {
+      if (newState.best_answer_marked_on) {
+        newState.best_answer_marked_on = moment(newState.best_answer_marked_on)
+      }
+      store.dispatch(thread.update(newState))
+    },
+    rejection => {
+      if (rejection.status === 400) {
+        snackbar.error(rejection.detail[0])
+      } else {
+        snackbar.apiError(rejection)
+      }
+
+      store.dispatch(thread.update(previousState))
     }
-
-    store.dispatch(thread.update(previousState));
-  });
+  )
 }

+ 114 - 194
frontend/src/components/posts-list/post/controls/dropdown.js

@@ -1,11 +1,10 @@
-/* jshint ignore:start */
-import React from 'react';
-import modal from 'misago/services/modal';
-import posting from 'misago/services/posting';
-import * as moderation from './actions';
-import MoveModal from './move';
-import PostChangelog from 'misago/components/post-changelog';
-import SplitModal from './split';
+import React from "react"
+import modal from "misago/services/modal"
+import posting from "misago/services/posting"
+import * as moderation from "./actions"
+import MoveModal from "./move"
+import PostChangelog from "misago/components/post-changelog"
+import SplitModal from "./split"
 
 export default function(props) {
   return (
@@ -24,156 +23,129 @@ export default function(props) {
       <Unhide {...props} />
       <Delete {...props} />
     </ul>
-  );
+  )
 }
 
 export class Permalink extends React.Component {
   onClick = () => {
-    let permaUrl = window.location.protocol + '//';
-    permaUrl += window.location.host;
-    permaUrl += this.props.post.url.index;
+    let permaUrl = window.location.protocol + "//"
+    permaUrl += window.location.host
+    permaUrl += this.props.post.url.index
 
-    prompt(gettext("Permament link to this post:"), permaUrl);
-  };
+    prompt(gettext("Permament link to this post:"), permaUrl)
+  }
 
   render() {
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.onClick}
-          type="button"
-        >
-          <span className="material-icon">
-            link
-          </span>
+        <button className="btn btn-link" onClick={this.onClick} type="button">
+          <span className="material-icon">link</span>
           {gettext("Permament link")}
         </button>
       </li>
-    );
+    )
   }
 }
 
 export class Edit extends React.Component {
   onClick = () => {
     posting.open({
-      mode: 'EDIT',
+      mode: "EDIT",
 
       config: this.props.post.api.editor,
       submit: this.props.post.api.index
-    });
-  };
+    })
+  }
 
   render() {
     if (!this.props.post.acl.can_edit) return null
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.onClick}
-          type="button"
-        >
-          <span className="material-icon">
-            edit
-          </span>
+        <button className="btn btn-link" onClick={this.onClick} type="button">
+          <span className="material-icon">edit</span>
           {gettext("Edit")}
         </button>
       </li>
-    );
+    )
   }
 }
 
 export class MarkAsBestAnswer extends React.Component {
   onClick = () => {
-    moderation.markAsBestAnswer(this.props);
-  };
+    moderation.markAsBestAnswer(this.props)
+  }
 
   render() {
-    const { post, thread } = this.props;
+    const { post, thread } = this.props
 
-    if (!thread.acl.can_mark_best_answer) return null;
-    if (!post.acl.can_mark_as_best_answer) return null;
-    if (post.id === thread.best_answer) return null;
-    if (thread.best_answer && !thread.acl.can_change_best_answer) return null;
+    if (!thread.acl.can_mark_best_answer) return null
+    if (!post.acl.can_mark_as_best_answer) return null
+    if (post.id === thread.best_answer) return null
+    if (thread.best_answer && !thread.acl.can_change_best_answer) return null
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.onClick}
-          type="button"
-        >
-          <span className="material-icon">
-            check_box
-          </span>
+        <button className="btn btn-link" onClick={this.onClick} type="button">
+          <span className="material-icon">check_box</span>
           {gettext("Mark as best answer")}
         </button>
       </li>
-    );
+    )
   }
 }
 
 export class UnmarkMarkBestAnswer extends React.Component {
   onClick = () => {
-    moderation.unmarkBestAnswer(this.props);
-  };
+    moderation.unmarkBestAnswer(this.props)
+  }
 
   render() {
-    const { post, thread } = this.props;
+    const { post, thread } = this.props
 
-    if (post.id !== thread.best_answer) return null;
-    if (!thread.acl.can_unmark_best_answer) return null;
+    if (post.id !== thread.best_answer) return null
+    if (!thread.acl.can_unmark_best_answer) return null
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.onClick}
-          type="button"
-        >
-          <span className="material-icon">
-            check_box_outline_blank
-          </span>
+        <button className="btn btn-link" onClick={this.onClick} type="button">
+          <span className="material-icon">check_box_outline_blank</span>
           {gettext("Unmark best answer")}
         </button>
       </li>
-    );
+    )
   }
 }
 
 export class PostEdits extends React.Component {
   onClick = () => {
-    modal.show(
-      <PostChangelog post={this.props.post} />
-    )
-  };
+    modal.show(<PostChangelog post={this.props.post} />)
+  }
 
   render() {
-    const isHidden = this.props.post.is_hidden && !this.props.post.acl.can_see_hidden;
-    const isUnedited = this.props.post.edits === 0;
-    if (isHidden || isUnedited) return null;
+    const isHidden =
+      this.props.post.is_hidden && !this.props.post.acl.can_see_hidden
+    const isUnedited = this.props.post.edits === 0
+    if (isHidden || isUnedited) return null
 
     const message = ngettext(
       "This post was edited %(edits)s time.",
       "This post was edited %(edits)s times.",
       this.props.post.edits
-    );
+    )
 
-    const title = interpolate(message, {
-      'edits': this.props.post.edits
-    }, true);
+    const title = interpolate(
+      message,
+      {
+        edits: this.props.post.edits
+      },
+      true
+    )
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.onClick}
-          type="button"
-        >
-          <span className="material-icon">
-            edit
-          </span>
+        <button className="btn btn-link" onClick={this.onClick} type="button">
+          <span className="material-icon">edit</span>
           {gettext("Changes history")}
         </button>
       </li>
@@ -183,215 +155,163 @@ export class PostEdits extends React.Component {
 
 export class Approve extends React.Component {
   onClick = () => {
-    moderation.approve(this.props);
-  };
+    moderation.approve(this.props)
+  }
 
   render() {
-    if (!this.props.post.acl.can_approve) return null;
-    if (!this.props.post.is_unapproved) return null;
+    if (!this.props.post.acl.can_approve) return null
+    if (!this.props.post.is_unapproved) return null
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.onClick}
-          type="button"
-        >
-          <span className="material-icon">
-            done
-          </span>
+        <button className="btn btn-link" onClick={this.onClick} type="button">
+          <span className="material-icon">done</span>
           {gettext("Approve")}
         </button>
       </li>
-    );
+    )
   }
 }
 
 export class Move extends React.Component {
   onClick = () => {
-    modal.show(
-      <MoveModal {...this.props} />
-    );
-  };
+    modal.show(<MoveModal {...this.props} />)
+  }
 
   render() {
-    if (!this.props.post.acl.can_move) return null;
+    if (!this.props.post.acl.can_move) return null
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.onClick}
-          type="button"
-        >
-          <span className="material-icon">
-            arrow_forward
-          </span>
+        <button className="btn btn-link" onClick={this.onClick} type="button">
+          <span className="material-icon">arrow_forward</span>
           {gettext("Move")}
         </button>
       </li>
-    );
+    )
   }
 }
 
 export class Split extends React.Component {
   onClick = () => {
-    modal.show(
-      <SplitModal {...this.props} />
-    );
-  };
+    modal.show(<SplitModal {...this.props} />)
+  }
 
   render() {
-    if (!this.props.post.acl.can_move) return null;
+    if (!this.props.post.acl.can_move) return null
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.onClick}
-          type="button"
-        >
-          <span className="material-icon">
-            call_split
-          </span>
+        <button className="btn btn-link" onClick={this.onClick} type="button">
+          <span className="material-icon">call_split</span>
           {gettext("Split")}
         </button>
       </li>
-    );
+    )
   }
 }
 
 export class Protect extends React.Component {
   onClick = () => {
-    moderation.protect(this.props);
-  };
+    moderation.protect(this.props)
+  }
 
   render() {
-    if (!this.props.post.acl.can_protect) return null;
-    if (this.props.post.is_protected) return null;
+    if (!this.props.post.acl.can_protect) return null
+    if (this.props.post.is_protected) return null
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.onClick}
-          type="button"
-        >
-          <span className="material-icon">
-            lock_outline
-          </span>
+        <button className="btn btn-link" onClick={this.onClick} type="button">
+          <span className="material-icon">lock_outline</span>
           {gettext("Protect")}
         </button>
       </li>
-    );
+    )
   }
 }
 
 export class Unprotect extends React.Component {
   onClick = () => {
-    moderation.unprotect(this.props);
-  };
+    moderation.unprotect(this.props)
+  }
 
   render() {
-    if (!this.props.post.acl.can_protect) return null;
-    if (!this.props.post.is_protected) return null;
+    if (!this.props.post.acl.can_protect) return null
+    if (!this.props.post.is_protected) return null
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.onClick}
-          type="button"
-        >
-          <span className="material-icon">
-            lock_open
-          </span>
+        <button className="btn btn-link" onClick={this.onClick} type="button">
+          <span className="material-icon">lock_open</span>
           {gettext("Remove protection")}
         </button>
       </li>
-    );
+    )
   }
 }
 
 export class Hide extends React.Component {
   onClick = () => {
-    moderation.hide(this.props);
-  };
+    moderation.hide(this.props)
+  }
 
   render() {
-    const { post, thread } = this.props;
+    const { post, thread } = this.props
 
-    if (post.id === thread.best_answer) return null;
-    if (!post.acl.can_hide) return null;
-    if (post.is_hidden) return null;
+    if (post.id === thread.best_answer) return null
+    if (!post.acl.can_hide) return null
+    if (post.is_hidden) return null
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.onClick}
-          type="button"
-        >
-          <span className="material-icon">
-            visibility_off
-          </span>
+        <button className="btn btn-link" onClick={this.onClick} type="button">
+          <span className="material-icon">visibility_off</span>
           {gettext("Hide")}
         </button>
       </li>
-    );
+    )
   }
 }
 
 export class Unhide extends React.Component {
   onClick = () => {
-    moderation.unhide(this.props);
-  };
+    moderation.unhide(this.props)
+  }
 
   render() {
-    if (!this.props.post.acl.can_unhide) return null;
-    if (!this.props.post.is_hidden) return null;
+    if (!this.props.post.acl.can_unhide) return null
+    if (!this.props.post.is_hidden) return null
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.onClick}
-          type="button"
-        >
-          <span className="material-icon">
-            visibility
-          </span>
+        <button className="btn btn-link" onClick={this.onClick} type="button">
+          <span className="material-icon">visibility</span>
           {gettext("Unhide")}
         </button>
       </li>
-    );
+    )
   }
 }
 
 export class Delete extends React.Component {
   onClick = () => {
-    moderation.remove(this.props);
-  };
+    moderation.remove(this.props)
+  }
 
   render() {
-    const { post, thread } = this.props;
+    const { post, thread } = this.props
 
-    if (post.id === thread.best_answer) return null;
-    if (!post.acl.can_delete) return null;
+    if (post.id === thread.best_answer) return null
+    if (!post.acl.can_delete) return null
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.onClick}
-          type="button"
-        >
-          <span className="material-icon">
-            clear
-          </span>
+        <button className="btn btn-link" onClick={this.onClick} type="button">
+          <span className="material-icon">clear</span>
           {gettext("Delete")}
         </button>
       </li>
-    );
+    )
   }
-}
+}

+ 5 - 8
frontend/src/components/posts-list/post/controls/index.js

@@ -1,6 +1,5 @@
-/* jshint ignore:start */
-import React from 'react';
-import Dropdown from './dropdown';
+import React from "react"
+import Dropdown from "./dropdown"
 
 export default function(props) {
   return (
@@ -12,11 +11,9 @@ export default function(props) {
         data-toggle="dropdown"
         type="button"
       >
-        <span className="material-icon">
-          expand_more
-        </span>
+        <span className="material-icon">expand_more</span>
       </button>
       <Dropdown {...props} />
     </div>
-  );
-}
+  )
+}

+ 34 - 30
frontend/src/components/posts-list/post/controls/move.js

@@ -1,67 +1,68 @@
-// jshint ignore:start
-import React from 'react';
-import Button from 'misago/components/button';
-import Form from 'misago/components/form';
-import FormGroup from 'misago/components/form-group';
-import * as post from 'misago/reducers/post';
-import ajax from 'misago/services/ajax';
-import modal from 'misago/services/modal';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
+import React from "react"
+import Button from "misago/components/button"
+import Form from "misago/components/form"
+import FormGroup from "misago/components/form-group"
+import * as post from "misago/reducers/post"
+import ajax from "misago/services/ajax"
+import modal from "misago/services/modal"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoading: false,
 
-      url: '',
+      url: "",
 
       validators: {
         url: []
       },
       errors: {}
-    };
+    }
   }
 
   clean() {
     if (!this.state.url.trim().length) {
-      snackbar.error(gettext("You have to enter link to the other thread."));
-      return false;
+      snackbar.error(gettext("You have to enter link to the other thread."))
+      return false
     }
 
-    return true;
+    return true
   }
 
   send() {
     return ajax.post(this.props.thread.api.posts.move, {
       new_thread: this.state.url,
       posts: [this.props.post.id]
-    });
+    })
   }
 
   handleSuccess(success) {
-    store.dispatch(post.patch(this.props.post, {
-      isDeleted: true
-    }));
+    store.dispatch(
+      post.patch(this.props.post, {
+        isDeleted: true
+      })
+    )
 
-    modal.hide();
+    modal.hide()
 
-    snackbar.success(gettext("Selected post was moved to the other thread."));
+    snackbar.success(gettext("Selected post was moved to the other thread."))
   }
 
   handleError(rejection) {
     if (rejection.status === 400) {
-      snackbar.error(rejection.detail);
+      snackbar.error(rejection.detail)
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
   }
 
-  onUrlChange = (event) => {
-    this.changeValue('url', event.target.value);
-  };
+  onUrlChange = event => {
+    this.changeValue("url", event.target.value)
+  }
 
   render() {
     return (
@@ -84,14 +85,17 @@ export default class extends Form {
               </FormGroup>
             </div>
             <div className="modal-footer">
-              <button className="btn btn-primary" loading={this.state.isLoading}>
+              <button
+                className="btn btn-primary"
+                loading={this.state.isLoading}
+              >
                 {gettext("Move post")}
               </button>
             </div>
           </div>
         </form>
       </div>
-    );
+    )
   }
 }
 
@@ -108,5 +112,5 @@ export function ModalHeader(props) {
       </button>
       <h4 className="modal-title">{gettext("Move post")}</h4>
     </div>
-  );
+  )
 }

+ 193 - 170
frontend/src/components/posts-list/post/controls/split.js

@@ -1,84 +1,80 @@
-/* jshint ignore:start */
-import React from 'react';
-import Button from 'misago/components/button'; // jshint ignore:line
-import Form from 'misago/components/form';
-import FormGroup from 'misago/components/form-group'; // jshint ignore:line
-import CategorySelect from 'misago/components/category-select'; // jshint ignore:line
-import ModalLoader from 'misago/components/modal-loader'; // jshint ignore:line
-import Select from 'misago/components/select'; // jshint ignore:line
-import * as post from 'misago/reducers/post';
-import ajax from 'misago/services/ajax'; // jshint ignore:line
-import modal from 'misago/services/modal'; // jshint ignore:line
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store'; // jshint ignore:line
-import * as validators from 'misago/utils/validators';
+import React from "react"
+import Button from "misago/components/button"
+import Form from "misago/components/form"
+import FormGroup from "misago/components/form-group"
+import CategorySelect from "misago/components/category-select"
+import ModalLoader from "misago/components/modal-loader"
+import Select from "misago/components/select"
+import * as post from "misago/reducers/post"
+import ajax from "misago/services/ajax"
+import modal from "misago/services/modal"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
+import * as validators from "misago/utils/validators"
 
 export default function(props) {
-  return (
-    <PostingConfig {...props} Form={ModerationForm} />
-  );
+  return <PostingConfig {...props} Form={ModerationForm} />
 }
 
 export class PostingConfig extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoaded: false,
       isError: false,
 
       categories: []
-    };
+    }
   }
 
   componentDidMount() {
-    ajax.get(misago.get('THREAD_EDITOR_API')).then((data) => {
-      // hydrate categories, extract posting options
-      const categories = data.map((item) => {
-        return Object.assign(item, {
-          disabled: item.post === false,
-          label: item.name,
-          value: item.id,
-          post: item.post
-        });
-      });
-
-      this.setState({
-        isLoaded: true,
-        categories
-      })
-    }, (rejection) => {
-      this.setState({
-        isError: rejection.detail
-      });
-    });
+    ajax.get(misago.get("THREAD_EDITOR_API")).then(
+      data => {
+        // hydrate categories, extract posting options
+        const categories = data.map(item => {
+          return Object.assign(item, {
+            disabled: item.post === false,
+            label: item.name,
+            value: item.id,
+            post: item.post
+          })
+        })
+
+        this.setState({
+          isLoaded: true,
+          categories
+        })
+      },
+      rejection => {
+        this.setState({
+          isError: rejection.detail
+        })
+      }
+    )
   }
 
   render() {
     if (this.state.isError) {
-      return (
-        <Error message={this.state.isError} />
-      );
+      return <Error message={this.state.isError} />
     } else if (this.state.isLoaded) {
       return (
         <ModerationForm {...this.props} categories={this.state.categories} />
-      );
+      )
     } else {
-      return (
-        <Loader />
-      );
+      return <Loader />
     }
   }
 }
 
 export class ModerationForm extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoading: false,
 
-      title: '',
+      title: "",
       category: null,
       categories: props.categories,
       weight: 0,
@@ -86,65 +82,63 @@ export class ModerationForm extends Form {
       is_closed: false,
 
       validators: {
-        title: [
-          validators.required()
-        ]
+        title: [validators.required()]
       },
 
       errors: {}
-    };
+    }
 
     this.isHiddenChoices = [
       {
-        'value': 0,
-        'icon': 'visibility',
-        'label': gettext("No")
+        value: 0,
+        icon: "visibility",
+        label: gettext("No")
       },
       {
-        'value': 1,
-        'icon': 'visibility_off',
-        'label': gettext("Yes")
-      },
-    ];
+        value: 1,
+        icon: "visibility_off",
+        label: gettext("Yes")
+      }
+    ]
 
     this.isClosedChoices = [
       {
-        'value': false,
-        'icon': 'lock_outline',
-        'label': gettext("No")
+        value: false,
+        icon: "lock_outline",
+        label: gettext("No")
       },
       {
-        'value': true,
-        'icon': 'lock',
-        'label': gettext("Yes")
-      },
-    ];
+        value: true,
+        icon: "lock",
+        label: gettext("Yes")
+      }
+    ]
 
-    this.acl = {};
-    this.props.categories.forEach((category) => {
+    this.acl = {}
+    this.props.categories.forEach(category => {
       if (category.post) {
         if (!this.state.category) {
-          this.state.category = category.id;
+          this.state.category = category.id
         }
 
         this.acl[category.id] = {
           can_pin_threads: category.post.pin,
           can_close_threads: category.post.close,
-          can_hide_threads: category.post.hide,
-        };
+          can_hide_threads: category.post.hide
+        }
       }
-    });
+    })
   }
 
   clean() {
     if (this.isValid()) {
-      return true;
+      return true
     } else {
-      snackbar.error(gettext("Form contains errors."));
+      snackbar.error(gettext("Form contains errors."))
       this.setState({
         errors: this.validate()
-      });
-      return false;
+      })
+      return false
     }
   }
 
@@ -156,120 +150,143 @@ export class ModerationForm extends Form {
       is_hidden: this.state.is_hidden,
       is_closed: this.state.is_closed,
       posts: [this.props.post.id]
-    });
+    })
   }
 
   handleSuccess(apiResponse) {
-    store.dispatch(post.patch(this.props.post, {
-      isDeleted: true
-    }));
+    store.dispatch(
+      post.patch(this.props.post, {
+        isDeleted: true
+      })
+    )
 
-    modal.hide();
+    modal.hide()
 
-    snackbar.success(gettext("Selected post was split into new thread."));
+    snackbar.success(gettext("Selected post was split into new thread."))
   }
 
   handleError(rejection) {
     if (rejection.status === 400) {
       this.setState({
-        'errors': Object.assign({}, this.state.errors, rejection)
-      });
-      snackbar.error(gettext("Form contains errors."));
+        errors: Object.assign({}, this.state.errors, rejection)
+      })
+      snackbar.error(gettext("Form contains errors."))
     } else if (rejection.status === 403 && Array.isArray(rejection)) {
-      modal.show(<ErrorsModal errors={rejection} />);
+      modal.show(<ErrorsModal errors={rejection} />)
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
   }
 
-  onCategoryChange = (ev) => {
-    const categoryId = ev.target.value;
+  onCategoryChange = ev => {
+    const categoryId = ev.target.value
     const newState = {
       category: categoryId
-    };
+    }
 
     if (this.acl[categoryId].can_pin_threads < newState.weight) {
-      newState.weight = 0;
+      newState.weight = 0
     }
 
     if (!this.acl[categoryId].can_hide_threads) {
-      newState.is_hidden = 0;
+      newState.is_hidden = 0
     }
 
     if (!this.acl[categoryId].can_close_threads) {
-      newState.is_closed = false;
+      newState.is_closed = false
     }
 
-    this.setState(newState);
-  };
+    this.setState(newState)
+  }
 
   getWeightChoices() {
     const choices = [
       {
-        'value': 0,
-        'icon': 'remove',
-        'label': gettext("Not pinned"),
+        value: 0,
+        icon: "remove",
+        label: gettext("Not pinned")
       },
       {
-        'value': 1,
-        'icon': 'bookmark_border',
-        'label': gettext("Pinned locally"),
+        value: 1,
+        icon: "bookmark_border",
+        label: gettext("Pinned locally")
       }
-    ];
+    ]
 
     if (this.acl[this.state.category].can_pin_threads == 2) {
       choices.push({
-        'value': 2,
-        'icon': 'bookmark',
-        'label': gettext("Pinned globally"),
-      });
+        value: 2,
+        icon: "bookmark",
+        label: gettext("Pinned globally")
+      })
     }
 
-    return choices;
+    return choices
   }
 
   renderWeightField() {
     if (this.acl[this.state.category].can_pin_threads) {
-      return <FormGroup label={gettext("Thread weight")}
-                        for="id_weight"
-                        labelClass="col-sm-4" controlClass="col-sm-8">
-        <Select id="id_weight"
-                onChange={this.bindInput('weight')}
-                value={this.state.weight}
-                choices={this.getWeightChoices()} />
-      </FormGroup>;
+      return (
+        <FormGroup
+          label={gettext("Thread weight")}
+          for="id_weight"
+          labelClass="col-sm-4"
+          controlClass="col-sm-8"
+        >
+          <Select
+            id="id_weight"
+            onChange={this.bindInput("weight")}
+            value={this.state.weight}
+            choices={this.getWeightChoices()}
+          />
+        </FormGroup>
+      )
     } else {
-      return null;
+      return null
     }
   }
 
   renderHiddenField() {
     if (this.acl[this.state.category].can_hide_threads) {
-      return <FormGroup label={gettext("Hide thread")}
-                        for="id_is_hidden"
-                        labelClass="col-sm-4" controlClass="col-sm-8">
-        <Select id="id_is_closed"
-                onChange={this.bindInput('is_hidden')}
-                value={this.state.is_hidden}
-                choices={this.isHiddenChoices} />
-      </FormGroup>;
+      return (
+        <FormGroup
+          label={gettext("Hide thread")}
+          for="id_is_hidden"
+          labelClass="col-sm-4"
+          controlClass="col-sm-8"
+        >
+          <Select
+            id="id_is_closed"
+            onChange={this.bindInput("is_hidden")}
+            value={this.state.is_hidden}
+            choices={this.isHiddenChoices}
+          />
+        </FormGroup>
+      )
     } else {
-      return null;
+      return null
     }
   }
 
   renderClosedField() {
     if (this.acl[this.state.category].can_close_threads) {
-      return <FormGroup label={gettext("Close thread")}
-                        for="id_is_closed"
-                        labelClass="col-sm-4" controlClass="col-sm-8">
-        <Select id="id_is_closed"
-                onChange={this.bindInput('is_closed')}
-                value={this.state.is_closed}
-                choices={this.isClosedChoices} />
-      </FormGroup>;
+      return (
+        <FormGroup
+          label={gettext("Close thread")}
+          for="id_is_closed"
+          labelClass="col-sm-4"
+          controlClass="col-sm-8"
+        >
+          <Select
+            id="id_is_closed"
+            onChange={this.bindInput("is_closed")}
+            value={this.state.is_closed}
+            choices={this.isClosedChoices}
+          />
+        </FormGroup>
+      )
     } else {
-      return null;
+      return null
     }
   }
 
@@ -278,34 +295,42 @@ export class ModerationForm extends Form {
       <Modal className="modal-dialog">
         <form onSubmit={this.handleSubmit}>
           <div className="modal-body">
-
-            <FormGroup label={gettext("Thread title")}
-                       for="id_title"
-                       labelClass="col-sm-4" controlClass="col-sm-8"
-                       validation={this.state.errors.title}>
-              <input id="id_title"
-                     className="form-control"
-                     type="text"
-                     onChange={this.bindInput('title')}
-                     value={this.state.title} />
+            <FormGroup
+              label={gettext("Thread title")}
+              for="id_title"
+              labelClass="col-sm-4"
+              controlClass="col-sm-8"
+              validation={this.state.errors.title}
+            >
+              <input
+                id="id_title"
+                className="form-control"
+                type="text"
+                onChange={this.bindInput("title")}
+                value={this.state.title}
+              />
             </FormGroup>
-            <div className="clearfix"></div>
-
-            <FormGroup label={gettext("Category")}
-                       for="id_category"
-                       labelClass="col-sm-4" controlClass="col-sm-8"
-                       validation={this.state.errors.category}>
-              <CategorySelect id="id_category"
-                              onChange={this.onCategoryChange}
-                              value={this.state.category}
-                              choices={this.state.categories} />
+            <div className="clearfix" />
+
+            <FormGroup
+              label={gettext("Category")}
+              for="id_category"
+              labelClass="col-sm-4"
+              controlClass="col-sm-8"
+              validation={this.state.errors.category}
+            >
+              <CategorySelect
+                id="id_category"
+                onChange={this.onCategoryChange}
+                value={this.state.category}
+                choices={this.state.categories}
+              />
             </FormGroup>
-            <div className="clearfix"></div>
+            <div className="clearfix" />
 
             {this.renderWeightField()}
             {this.renderHiddenField()}
             {this.renderClosedField()}
-
           </div>
           <div className="modal-footer">
             <Button className="btn-primary" loading={this.state.isLoading}>
@@ -314,7 +339,7 @@ export class ModerationForm extends Form {
           </div>
         </form>
       </Modal>
-    );
+    )
   }
 }
 
@@ -323,27 +348,23 @@ export function Loader() {
     <Modal className="modal-dialog">
       <ModalLoader />
     </Modal>
-  );
+  )
 }
 
 export function Error(props) {
   return (
     <Modal className="modal-dialog modal-message">
       <div className="message-icon">
-        <span className="material-icon">
-          info_outline
-        </span>
+        <span className="material-icon">info_outline</span>
       </div>
       <div className="message-body">
         <p className="lead">
           {gettext("You can't move this post at the moment.")}
         </p>
-        <p>
-          {props.message}
-        </p>
+        <p>{props.message}</p>
       </div>
     </Modal>
-  );
+  )
 }
 
 export function Modal(props) {
@@ -359,10 +380,12 @@ export function Modal(props) {
           >
             <span aria-hidden="true">&times;</span>
           </button>
-          <h4 className="modal-title">{gettext("Split post into new thread")}</h4>
+          <h4 className="modal-title">
+            {gettext("Split post into new thread")}
+          </h4>
         </div>
         {props.children}
       </div>
     </div>
-  );
-}
+  )
+}

+ 39 - 24
frontend/src/components/posts-list/post/flags.js

@@ -1,60 +1,75 @@
-/* jshint ignore:start */
-import React from 'react';
+import React from "react"
 
 export function FlagBestAnswer({ post, thread, user }) {
   if (!(isVisible(post) && post.id === thread.best_answer)) {
-    return null;
+    return null
   }
-  
-  let message = null;
+
+  let message = null
   if (user.id && thread.best_answer_marked_by === user.id) {
-    message = interpolate(gettext("Marked as best answer by you %(marked_on)s."), {
-      marked_on: thread.best_answer_marked_on.fromNow()
-    }, true);
+    message = interpolate(
+      gettext("Marked as best answer by you %(marked_on)s."),
+      {
+        marked_on: thread.best_answer_marked_on.fromNow()
+      },
+      true
+    )
   } else {
-    message = interpolate(gettext("Marked as best answer by %(marked_by)s %(marked_on)s."), {
-      marked_by: thread.best_answer_marked_by_name,
-      marked_on: thread.best_answer_marked_on.fromNow()
-    }, true);
+    message = interpolate(
+      gettext("Marked as best answer by %(marked_by)s %(marked_on)s."),
+      {
+        marked_by: thread.best_answer_marked_by_name,
+        marked_on: thread.best_answer_marked_on.fromNow()
+      },
+      true
+    )
   }
-  
+
   return (
     <div className="post-status-message post-status-best-answer">
       <span className="material-icon">check_box</span>
       <p>{message}</p>
     </div>
-  );
+  )
 }
 
 export function FlagHidden(props) {
   if (!(isVisible(props.post) && props.post.is_hidden)) {
-    return null;
+    return null
   }
 
   return (
     <div className="post-status-message post-status-hidden">
       <span className="material-icon">visibility_off</span>
-      <p>{gettext("This post is hidden. Only users with permission may see its contents.")}</p>
+      <p>
+        {gettext(
+          "This post is hidden. Only users with permission may see its contents."
+        )}
+      </p>
     </div>
-  );
+  )
 }
 
 export function FlagUnapproved(props) {
   if (!(isVisible(props.post) && props.post.is_unapproved)) {
-    return null;
+    return null
   }
 
   return (
     <div className="post-status-message post-status-unapproved">
       <span className="material-icon">remove_circle_outline</span>
-      <p>{gettext("This post is unapproved. Only users with permission to approve posts and its author may see its contents.")}</p>
+      <p>
+        {gettext(
+          "This post is unapproved. Only users with permission to approve posts and its author may see its contents."
+        )}
+      </p>
     </div>
-  );
+  )
 }
 
 export function FlagProtected(props) {
   if (!(isVisible(props.post) && props.post.is_protected)) {
-    return null;
+    return null
   }
 
   return (
@@ -62,9 +77,9 @@ export function FlagProtected(props) {
       <span className="material-icon">lock_outline</span>
       <p>{gettext("This post is protected. Only moderators may change it.")}</p>
     </div>
-  );
+  )
 }
 
 export function isVisible(post) {
-  return !post.is_hidden || post.acl.can_see_hidden;
-}
+  return !post.is_hidden || post.acl.can_see_hidden
+}

+ 98 - 97
frontend/src/components/posts-list/post/footer.js

@@ -1,12 +1,11 @@
-/* jshint ignore:start */
-import React from 'react';
-import * as actions from './controls/actions';
-import LikesModal from 'misago/components/post-likes';
-import modal from 'misago/services/modal';
-import posting from 'misago/services/posting';
+import React from "react"
+import * as actions from "./controls/actions"
+import LikesModal from "misago/components/post-likes"
+import modal from "misago/services/modal"
+import posting from "misago/services/posting"
 
 export default function(props) {
-  if (!isVisible(props.post)) return null;
+  if (!isVisible(props.post)) return null
 
   return (
     <div className="post-footer">
@@ -18,36 +17,34 @@ export default function(props) {
         likes={props.post.likes}
         {...props}
       />
-      <LikesCompact
-        likes={props.post.likes}
-        {...props}
-      />
+      <LikesCompact likes={props.post.likes} {...props} />
       <Reply {...props} />
       <Edit {...props} />
     </div>
-  );
+  )
 }
 
 export function isVisible(post) {
-  return (!post.is_hidden || post.acl.can_see_hidden) && (
-    post.acl.can_reply ||
-    post.acl.can_edit ||
-    (post.acl.can_see_likes && (post.last_likes || []).length) ||
-    post.acl.can_like
-  );
+  return (
+    (!post.is_hidden || post.acl.can_see_hidden) &&
+    (post.acl.can_reply ||
+      post.acl.can_edit ||
+      (post.acl.can_see_likes && (post.last_likes || []).length) ||
+      post.acl.can_like)
+  )
 }
 
 export class MarkAsBestAnswer extends React.Component {
   onClick = () => {
-    actions.markAsBestAnswer(this.props);
-  };
+    actions.markAsBestAnswer(this.props)
+  }
 
   render() {
-    const { post, thread } = this.props;
+    const { post, thread } = this.props
 
-    if (!thread.acl.can_mark_best_answer) return null;
-    if (!post.acl.can_mark_as_best_answer) return null;
-    if (thread.best_answer && !thread.acl.can_change_best_answer) return null;
+    if (!thread.acl.can_mark_best_answer) return null
+    if (!post.acl.can_mark_as_best_answer) return null
+    if (thread.best_answer && !thread.acl.can_change_best_answer) return null
 
     return (
       <button
@@ -56,27 +53,24 @@ export class MarkAsBestAnswer extends React.Component {
         onClick={this.onClick}
         type="button"
       >
-        <span className="material-icon">
-          check_box
-        </span>
+        <span className="material-icon">check_box</span>
         {gettext("Best answer")}
       </button>
-    );
+    )
   }
 }
 
-
 export class MarkAsBestAnswerCompact extends React.Component {
   onClick = () => {
-    actions.markAsBestAnswer(this.props);
-  };
+    actions.markAsBestAnswer(this.props)
+  }
 
   render() {
-    const { post, thread } = this.props;
+    const { post, thread } = this.props
 
-    if (!thread.acl.can_mark_best_answer) return null;
-    if (!post.acl.can_mark_as_best_answer) return null;
-    if (thread.best_answer && !thread.acl.can_change_best_answer) return null;
+    if (!thread.acl.can_mark_best_answer) return null
+    if (!post.acl.can_mark_as_best_answer) return null
+    if (thread.best_answer && !thread.acl.can_change_best_answer) return null
 
     return (
       <button
@@ -85,29 +79,27 @@ export class MarkAsBestAnswerCompact extends React.Component {
         onClick={this.onClick}
         type="button"
       >
-        <span className="material-icon">
-          check_box
-        </span>
+        <span className="material-icon">check_box</span>
       </button>
-    );
+    )
   }
 }
 
 export class Like extends React.Component {
   onClick = () => {
     if (this.props.post.is_liked) {
-      actions.unlike(this.props);
+      actions.unlike(this.props)
     } else {
-      actions.like(this.props);
+      actions.like(this.props)
     }
-  };
+  }
 
   render() {
-    if (!this.props.post.acl.can_like) return null;
+    if (!this.props.post.acl.can_like) return null
 
-    let className = 'btn btn-default btn-sm pull-left';
+    let className = "btn btn-default btn-sm pull-left"
     if (this.props.post.is_liked) {
-      className = 'btn btn-success btn-sm pull-left';
+      className = "btn btn-success btn-sm pull-left"
     }
 
     return (
@@ -119,22 +111,18 @@ export class Like extends React.Component {
       >
         {this.props.post.is_liked ? gettext("Liked") : gettext("Like")}
       </button>
-    );
+    )
   }
 }
 
 export class Likes extends React.Component {
   onClick = () => {
-    modal.show(
-      <LikesModal
-        post={this.props.post}
-      />
-    );
-  };
+    modal.show(<LikesModal post={this.props.post} />)
+  }
 
   render() {
-    const hasLikes = (this.props.post.last_likes || []).length > 0;
-    if (!this.props.post.acl.can_see_likes || !hasLikes) return null;
+    const hasLikes = (this.props.post.last_likes || []).length > 0
+    if (!this.props.post.acl.can_see_likes || !hasLikes) return null
 
     if (this.props.post.acl.can_see_likes === 2) {
       return (
@@ -145,21 +133,21 @@ export class Likes extends React.Component {
         >
           {getLikesMessage(this.props.likes, this.props.lastLikes)}
         </button>
-      );
+      )
     }
 
     return (
       <p className="pull-left hidden-xs">
         {getLikesMessage(this.props.likes, this.props.lastLikes)}
       </p>
-    );
+    )
   }
 }
 
 export class LikesCompact extends Likes {
   render() {
-    const hasLikes = (this.props.post.last_likes || []).length > 0;
-    if (!this.props.post.acl.can_see_likes || !hasLikes) return null;
+    const hasLikes = (this.props.post.last_likes || []).length > 0
+    if (!this.props.post.acl.can_see_likes || !hasLikes) return null
 
     if (this.props.post.acl.can_see_likes === 2) {
       return (
@@ -168,65 +156,78 @@ export class LikesCompact extends Likes {
           onClick={this.onClick}
           type="button"
         >
-          <span className="material-icon">
-            favorite
-          </span>
+          <span className="material-icon">favorite</span>
           {this.props.likes}
         </button>
-      );
+      )
     }
 
     return (
       <p className="likes-compact pull-left visible-xs-block">
-        <span className="material-icon">
-          favorite
-        </span>
+        <span className="material-icon">favorite</span>
         {this.props.likes}
       </p>
-    );
+    )
   }
 }
 
 export function getLikesMessage(likes, users) {
-  const usernames = users.slice(0, 3).map((u) => u.username);
+  const usernames = users.slice(0, 3).map(u => u.username)
 
   if (usernames.length == 1) {
-    return interpolate(gettext("%(user)s likes this."), {
-      user: usernames[0]
-    }, true);
+    return interpolate(
+      gettext("%(user)s likes this."),
+      {
+        user: usernames[0]
+      },
+      true
+    )
   }
 
-  const hiddenLikes = likes - usernames.length;
+  const hiddenLikes = likes - usernames.length
 
-  const otherUsers = usernames.slice(0, -1).join(', ');
-  const lastUser = usernames.slice(-1)[0];
+  const otherUsers = usernames.slice(0, -1).join(", ")
+  const lastUser = usernames.slice(-1)[0]
 
-  const usernamesList = interpolate(gettext("%(users)s and %(last_user)s"), {
-    users: otherUsers,
-    last_user: lastUser
-  }, true);
+  const usernamesList = interpolate(
+    gettext("%(users)s and %(last_user)s"),
+    {
+      users: otherUsers,
+      last_user: lastUser
+    },
+    true
+  )
 
   if (hiddenLikes === 0) {
-    return interpolate(gettext("%(users)s like this."), {
-      users: usernamesList
-    }, true);
+    return interpolate(
+      gettext("%(users)s like this."),
+      {
+        users: usernamesList
+      },
+      true
+    )
   }
 
   const message = ngettext(
     "%(users)s and %(likes)s other user like this.",
     "%(users)s and %(likes)s other users like this.",
-    hiddenLikes);
-
-  return interpolate(message, {
-    users: usernames.join(', '),
-    likes: hiddenLikes
-  }, true);
+    hiddenLikes
+  )
+
+  return interpolate(
+    message,
+    {
+      users: usernames.join(", "),
+      likes: hiddenLikes
+    },
+    true
+  )
 }
 
 export class Reply extends React.Component {
   onClick = () => {
     posting.open({
-      mode: 'REPLY',
+      mode: "REPLY",
 
       config: this.props.thread.api.editor,
       submit: this.props.thread.api.posts.index,
@@ -234,8 +235,8 @@ export class Reply extends React.Component {
       context: {
         reply: this.props.post.id
       }
-    });
-  };
+    })
+  }
 
   render() {
     if (this.props.post.acl.can_reply) {
@@ -247,9 +248,9 @@ export class Reply extends React.Component {
         >
           {gettext("Reply")}
         </button>
-      );
+      )
     } else {
-      return null;
+      return null
     }
   }
 }
@@ -257,12 +258,12 @@ export class Reply extends React.Component {
 export class Edit extends React.Component {
   onClick = () => {
     posting.open({
-      mode: 'EDIT',
+      mode: "EDIT",
 
       config: this.props.post.api.editor,
       submit: this.props.post.api.index
-    });
-  };
+    })
+  }
 
   render() {
     if (this.props.post.acl.can_edit) {
@@ -274,9 +275,9 @@ export class Edit extends React.Component {
         >
           {gettext("Edit")}
         </button>
-      );
+      )
     } else {
-      return null;
+      return null
     }
   }
-}
+}

+ 67 - 51
frontend/src/components/posts-list/post/header.js

@@ -1,10 +1,13 @@
-/* jshint ignore:start */
-import React from 'react';
-import Controls from './controls';
-import Select from './select';
-import {StatusIcon, getStatusClassName, getStatusDescription} from 'misago/components/user-status';
-import PostChangelog from 'misago/components/post-changelog';
-import modal from 'misago/services/modal';
+import React from "react"
+import Controls from "./controls"
+import Select from "./select"
+import {
+  StatusIcon,
+  getStatusClassName,
+  getStatusDescription
+} from "misago/components/user-status"
+import PostChangelog from "misago/components/post-changelog"
+import modal from "misago/services/modal"
 
 export default function(props) {
   return (
@@ -19,33 +22,35 @@ export default function(props) {
       <Select {...props} />
       <Controls {...props} />
     </div>
-  );
+  )
 }
 
 export function UnreadLabel(props) {
-  if (props.post.is_read) return null;
+  if (props.post.is_read) return null
 
   return (
-    <span className="label label-unread hidden-xs">
-      {gettext("New post")}
-    </span>
-  );
+    <span className="label label-unread hidden-xs">{gettext("New post")}</span>
+  )
 }
 
 export function UnreadCompact(props) {
-  if (props.post.is_read) return null;
+  if (props.post.is_read) return null
 
   return (
     <span className="label label-unread visible-xs-inline-block">
       {gettext("New")}
     </span>
-  );
+  )
 }
 
 export function PostedOn(props) {
-  const tooltip = interpolate(gettext("posted %(posted_on)s"), {
-    'posted_on': props.post.posted_on.format('LL, LT')
-  }, true);
+  const tooltip = interpolate(
+    gettext("posted %(posted_on)s"),
+    {
+      posted_on: props.post.posted_on.format("LL, LT")
+    },
+    true
+  )
 
   return (
     <a
@@ -55,7 +60,7 @@ export function PostedOn(props) {
     >
       {props.post.posted_on.fromNow()}
     </a>
-  );
+  )
 }
 
 export function PostedOnCompact(props) {
@@ -66,36 +71,39 @@ export function PostedOnCompact(props) {
     >
       {props.post.posted_on.fromNow(true)}
     </a>
-  );
+  )
 }
 
 export class PostEdits extends React.Component {
   onClick = () => {
-    modal.show(
-      <PostChangelog post={this.props.post} />
-    )
-  };
+    modal.show(<PostChangelog post={this.props.post} />)
+  }
 
   render() {
-    const isHidden = this.props.post.is_hidden && !this.props.post.acl.can_see_hidden;
-    const isUnedited = this.props.post.edits === 0;
-    if (isHidden || isUnedited) return null;
+    const isHidden =
+      this.props.post.is_hidden && !this.props.post.acl.can_see_hidden
+    const isUnedited = this.props.post.edits === 0
+    if (isHidden || isUnedited) return null
 
     const tooltip = ngettext(
       "This post was edited %(edits)s time.",
       "This post was edited %(edits)s times.",
       this.props.post.edits
-    );
+    )
 
-    const title = interpolate(tooltip, {
-      'edits': this.props.post.edits
-    }, true);
+    const title = interpolate(
+      tooltip,
+      {
+        edits: this.props.post.edits
+      },
+      true
+    )
 
     const label = ngettext(
       "edited %(edits)s time",
       "edited %(edits)s times",
       this.props.post.edits
-    );
+    )
 
     return (
       <button
@@ -104,9 +112,13 @@ export class PostEdits extends React.Component {
         title={title}
         type="button"
       >
-        {interpolate(label, {
-          'edits': this.props.post.edits
-        }, true)}
+        {interpolate(
+          label,
+          {
+            edits: this.props.post.edits
+          },
+          true
+        )}
       </button>
     )
   }
@@ -114,15 +126,16 @@ export class PostEdits extends React.Component {
 
 export class PostEditsCompacts extends PostEdits {
   render() {
-    const isHidden = this.props.post.is_hidden && !this.props.post.acl.can_see_hidden;
-    const isUnedited = this.props.post.edits === 0;
-    if (isHidden || isUnedited) return null;
+    const isHidden =
+      this.props.post.is_hidden && !this.props.post.acl.can_see_hidden
+    const isUnedited = this.props.post.edits === 0
+    if (isHidden || isUnedited) return null
 
     const label = ngettext(
       "%(edits)s edit",
       "%(edits)s edits",
       this.props.post.edits
-    );
+    )
 
     return (
       <button
@@ -130,21 +143,26 @@ export class PostEditsCompacts extends PostEdits {
         onClick={this.onClick}
         type="button"
       >
-        {interpolate(label, {
-          'edits': this.props.post.edits
-        }, true)}
+        {interpolate(
+          label,
+          {
+            edits: this.props.post.edits
+          },
+          true
+        )}
       </button>
     )
   }
 }
 
 export function ProtectedLabel(props) {
-  const postAuthor = props.post.poster && props.post.poster.id === props.user.id;
-  const hasAcl = props.post.acl.can_protect;
-  const isVisible = props.user.id && props.post.is_protected && (postAuthor || hasAcl);
+  const postAuthor = props.post.poster && props.post.poster.id === props.user.id
+  const hasAcl = props.post.acl.can_protect
+  const isVisible =
+    props.user.id && props.post.is_protected && (postAuthor || hasAcl)
 
   if (!isVisible) {
-    return null;
+    return null
   }
 
   return (
@@ -152,10 +170,8 @@ export function ProtectedLabel(props) {
       className="label label-protected hidden-xs"
       title={gettext("This post is protected and may not be edited.")}
     >
-      <span className="material-icon">
-        lock_outline
-      </span>
+      <span className="material-icon">lock_outline</span>
       {gettext("protected")}
     </span>
-  );
-}
+  )
+}

+ 20 - 18
frontend/src/components/posts-list/post/index.js

@@ -1,33 +1,36 @@
-/* jshint ignore:start */
-import React from 'react';
-import Attachments from './attachments';
-import Body from './body';
-import { FlagBestAnswer, FlagHidden, FlagUnapproved, FlagProtected } from './flags';
-import Footer from './footer';
-import Header from './header';
-import PostSide from './post-side';
+import React from "react"
+import Attachments from "./attachments"
+import Body from "./body"
+import {
+  FlagBestAnswer,
+  FlagHidden,
+  FlagUnapproved,
+  FlagProtected
+} from "./flags"
+import Footer from "./footer"
+import Header from "./header"
+import PostSide from "./post-side"
 
 export default function(props) {
-  let className = 'post';
+  let className = "post"
   if (props.post.isDeleted) {
-    className = 'hide';
+    className = "hide"
   } else if (props.post.is_hidden && !props.post.acl.can_see_hidden) {
-    className = 'post post-hidden';
+    className = "post post-hidden"
   }
 
   if (props.post.poster && props.post.poster.rank.css_class) {
-    className += ' post-' + props.post.poster.rank.css_class;
+    className += " post-" + props.post.poster.rank.css_class
   }
 
   if (!props.post.is_read) {
-    className += ' post-new';
+    className += " post-new"
   }
 
   return (
-    <li id={'post-' + props.post.id} className={className}>
+    <li id={"post-" + props.post.id} className={className}>
       <div className="panel panel-default panel-post">
         <div className="panel-body">
-
           <div className="row">
             <PostSide {...props} />
             <div className="col-xs-12 col-md-9">
@@ -41,9 +44,8 @@ export default function(props) {
               <Footer {...props} />
             </div>
           </div>
-
         </div>
       </div>
     </li>
-  );
-}
+  )
+}

+ 16 - 27
frontend/src/components/posts-list/post/post-side/anonymous.js

@@ -1,44 +1,33 @@
-/* jshint ignore:start */
-import React from 'react';
-import Avatar from 'misago/components/avatar';
-import Controls from 'misago/components/posts-list/post/controls';
-import Select from 'misago/components/posts-list/post/select';
-import UserStatus, { StatusIcon, StatusLabel } from 'misago/components/user-status';
-import UserPostcount from './user-postcount';
-import UserTitle from './user-title';
+import React from "react"
+import Avatar from "misago/components/avatar"
+import Controls from "misago/components/posts-list/post/controls"
+import Select from "misago/components/posts-list/post/select"
+import UserStatus, {
+  StatusIcon,
+  StatusLabel
+} from "misago/components/user-status"
+import UserPostcount from "./user-postcount"
+import UserTitle from "./user-title"
 
 export default function({ post, thread }) {
   return (
     <div className="col-xs-12 col-md-3 post-side post-side-anonymous">
-      <Select
-        post={post}
-        thread={thread}
-      />
-      <Controls
-        post={post}
-        thread={thread}
-      />
+      <Select post={post} thread={thread} />
+      <Controls post={post} thread={thread} />
       <div className="media">
         <div className="media-left">
           <span>
-            <Avatar
-              className="poster-avatar"
-              size={100}
-            />
+            <Avatar className="poster-avatar" size={100} />
           </span>
         </div>
         <div className="media-body">
-
-          <span className="media-heading item-title">
-            {post.poster_name}
-          </span>
+          <span className="media-heading item-title">{post.poster_name}</span>
 
           <span className="user-title user-title-anonymous">
             {gettext("Removed user")}
           </span>
-
         </div>
       </div>
     </div>
-  );
-}
+  )
+}

+ 2 - 2
frontend/src/components/posts-list/post/post-side/has-visible-title.js

@@ -1,3 +1,3 @@
 export default function({ title, rank }) {
-  return rank.is_tab || !!title || !!rank.title;
-}
+  return rank.is_tab || !!title || !!rank.title
+}

+ 6 - 11
frontend/src/components/posts-list/post/post-side/index.js

@@ -1,16 +1,11 @@
-/* jshint ignore:start */
-import React from 'react';
-import Anonymous from './anonymous';
-import Registered from './registered';
+import React from "react"
+import Anonymous from "./anonymous"
+import Registered from "./registered"
 
 export default function(props) {
   if (props.post.poster) {
-    return (
-      <Registered {...props} />
-    );
+    return <Registered {...props} />
   }
 
-  return (
-    <Anonymous {...props} />
-  );
-}
+  return <Anonymous {...props} />
+}

+ 16 - 35
frontend/src/components/posts-list/post/post-side/registered.js

@@ -1,43 +1,28 @@
-/* jshint ignore:start */
-import React from 'react';
-import Avatar from 'misago/components/avatar';
-import Controls from 'misago/components/posts-list/post/controls';
-import Select from 'misago/components/posts-list/post/select';
-import UserStatus, { StatusIcon } from 'misago/components/user-status';
-import UserPostcount from './user-postcount';
-import UserStatusLabel from './user-status';
-import UserTitle from './user-title';
+import React from "react"
+import Avatar from "misago/components/avatar"
+import Controls from "misago/components/posts-list/post/controls"
+import Select from "misago/components/posts-list/post/select"
+import UserStatus, { StatusIcon } from "misago/components/user-status"
+import UserPostcount from "./user-postcount"
+import UserStatusLabel from "./user-status"
+import UserTitle from "./user-title"
 
 export default function({ post, thread }) {
-  const { poster } = post;
+  const { poster } = post
 
   return (
     <div className="col-xs-12 col-md-3 post-side post-side-registered">
-      <Select
-        post={post}
-        thread={thread}
-      />
-      <Controls
-        post={post}
-        thread={thread}
-      />
+      <Select post={post} thread={thread} />
+      <Controls post={post} thread={thread} />
       <div className="media">
         <div className="media-left">
           <a href={poster.url}>
-            <Avatar
-              className="poster-avatar"
-              size={100}
-              user={poster}
-            />
+            <Avatar className="poster-avatar" size={100} user={poster} />
           </a>
         </div>
         <div className="media-body">
-
           <div className="media-heading">
-            <a
-              className="item-title"
-              href={poster.url}
-            >
+            <a className="item-title" href={poster.url}>
               {poster.username}
             </a>
             <UserStatus status={poster.status}>
@@ -45,16 +30,12 @@ export default function({ post, thread }) {
             </UserStatus>
           </div>
 
-          <UserTitle
-            rank={poster.rank}
-            title={poster.title}
-          />
+          <UserTitle rank={poster.rank} title={poster.title} />
 
           <UserStatusLabel poster={poster} />
           <UserPostcount poster={poster} />
-
         </div>
       </div>
     </div>
-  );
-}
+  )
+}

+ 14 - 15
frontend/src/components/posts-list/post/post-side/user-postcount.js

@@ -1,24 +1,23 @@
-/* jshint ignore:start */
-import React from 'react';
-import hasVisibleTitle from './has-visible-title';
+import React from "react"
+import hasVisibleTitle from "./has-visible-title"
 
 export default function({ poster }) {
-  const message = ngettext(
-    "%(posts)s post",
-    "%(posts)s posts",
-    poster.posts
-  );
+  const message = ngettext("%(posts)s post", "%(posts)s posts", poster.posts)
 
-  let className = 'user-postcount';
+  let className = "user-postcount"
   if (hasVisibleTitle(poster)) {
-    className += ' hidden-xs hidden-sm';
+    className += " hidden-xs hidden-sm"
   }
 
   return (
     <span className={className}>
-      {interpolate(message, {
-        'posts': poster.posts
-      }, true)}
+      {interpolate(
+        message,
+        {
+          posts: poster.posts
+        },
+        true
+      )}
     </span>
-  );
-}
+  )
+}

+ 8 - 13
frontend/src/components/posts-list/post/post-side/user-status.js

@@ -1,23 +1,18 @@
-/* jshint ignore:start */
-import React from 'react';
-import UserStatus, { StatusLabel } from 'misago/components/user-status';
-import hasVisibleTitle from './has-visible-title';
+import React from "react"
+import UserStatus, { StatusLabel } from "misago/components/user-status"
+import hasVisibleTitle from "./has-visible-title"
 
 export default function({ poster }) {
-  let className = 'hidden-xs';
+  let className = "hidden-xs"
   if (hasVisibleTitle(poster)) {
-    className += ' hidden-sm';
+    className += " hidden-sm"
   }
 
   return (
     <span className={className}>
       <UserStatus status={poster.status}>
-        <StatusLabel
-          status={poster.status}
-          user={poster}
-        />
+        <StatusLabel status={poster.status} user={poster} />
       </UserStatus>
     </span>
-  );
-
-}
+  )
+}

+ 10 - 17
frontend/src/components/posts-list/post/post-side/user-title.js

@@ -1,32 +1,25 @@
-/* jshint ignore:start */
-import React from 'react';
+import React from "react"
 
 export default function({ rank, title }) {
-  let userTitle = title || rank.title;
+  let userTitle = title || rank.title
   if (!userTitle && rank.is_tab) {
-    userTitle = rank.name;
+    userTitle = rank.name
   }
 
-  if (!userTitle) return null;
+  if (!userTitle) return null
 
-  let className = 'user-title';
+  let className = "user-title"
   if (rank.css_class) {
-    className += ' user-title-' + rank.css_class;
+    className += " user-title-" + rank.css_class
   }
 
   if (rank.is_tab) {
     return (
       <div className={className}>
-        <a href={rank.url}>
-          {userTitle}
-        </a>
+        <a href={rank.url}>{userTitle}</a>
       </div>
-    );
+    )
   }
 
-  return (
-    <div className={className}>
-      {userTitle}
-    </div>
-  );
-}
+  return <div className={className}>{userTitle}</div>
+}

+ 35 - 11
frontend/src/components/posts-list/post/preview.js

@@ -1,7 +1,6 @@
-/* jshint ignore:start */
-import React from 'react';
-import Avatar from 'misago/components/avatar';
-import * as random from 'misago/utils/random';
+import React from "react"
+import Avatar from "misago/components/avatar"
+import * as random from "misago/utils/random"
 
 export default function(props) {
   return (
@@ -13,19 +12,44 @@ export default function(props) {
         <div className="post-body">
           <div className="panel panel-default panel-post">
             <div className="panel-heading post-heading">
-              <span className="ui-preview-text" style={{width: random.int(30, 100) + "px"}}>&nbsp;</span>
-              <span className="ui-preview-text" style={{width: random.int(30, 100) + "px"}}>&nbsp;</span>
+              <span
+                className="ui-preview-text"
+                style={{ width: random.int(30, 100) + "px" }}
+              >
+                &nbsp;
+              </span>
+              <span
+                className="ui-preview-text"
+                style={{ width: random.int(30, 100) + "px" }}
+              >
+                &nbsp;
+              </span>
             </div>
             <div className="panel-body">
               <article className="misago-markup">
-                <p className="ui-preview-text" style={{width: random.int(50, 100) + "%"}}>&nbsp;</p>
-                <p className="ui-preview-text" style={{width: random.int(50, 100) + "%"}}>&nbsp;</p>
-                <p className="ui-preview-text" style={{width: random.int(50, 100) + "%"}}>&nbsp;</p>
+                <p
+                  className="ui-preview-text"
+                  style={{ width: random.int(50, 100) + "%" }}
+                >
+                  &nbsp;
+                </p>
+                <p
+                  className="ui-preview-text"
+                  style={{ width: random.int(50, 100) + "%" }}
+                >
+                  &nbsp;
+                </p>
+                <p
+                  className="ui-preview-text"
+                  style={{ width: random.int(50, 100) + "%" }}
+                >
+                  &nbsp;
+                </p>
               </article>
             </div>
           </div>
         </div>
       </div>
     </li>
-  );
-}
+  )
+}

+ 17 - 14
frontend/src/components/posts-list/post/select.js

@@ -1,20 +1,21 @@
-/* jshint ignore:start */
-import React from 'react';
-import * as posts from 'misago/reducers/posts';
-import store from 'misago/services/store';
+import React from "react"
+import * as posts from "misago/reducers/posts"
+import store from "misago/services/store"
 
-export default class extends React.Component{
+export default class extends React.Component {
   onClick = () => {
     if (this.props.post.isSelected) {
-      store.dispatch(posts.deselect(this.props.post));
+      store.dispatch(posts.deselect(this.props.post))
     } else {
-      store.dispatch(posts.select(this.props.post));
+      store.dispatch(posts.select(this.props.post))
     }
-  };
+  }
 
   render() {
-    if (!(this.props.thread.acl.can_merge_posts || isVisible(this.props.post.acl))) {
-      return null;
+    if (
+      !(this.props.thread.acl.can_merge_posts || isVisible(this.props.post.acl))
+    ) {
+      return null
     }
 
     return (
@@ -25,11 +26,13 @@ export default class extends React.Component{
           type="button"
         >
           <span className="material-icon">
-            {this.props.post.isSelected ? 'check_box' : 'check_box_outline_blank'}
+            {this.props.post.isSelected
+              ? "check_box"
+              : "check_box_outline_blank"}
           </span>
         </button>
       </div>
-    );
+    )
   }
 }
 
@@ -41,5 +44,5 @@ export function isVisible(acl) {
     acl.can_unhide ||
     acl.can_delete ||
     acl.can_move
-  );
-}
+  )
+}

+ 40 - 31
frontend/src/components/posts-list/waypoint.js

@@ -1,59 +1,68 @@
-/* jshint ignore:start */
-import React from 'react';
-import * as post from 'misago/reducers/post';
-import * as thread from 'misago/reducers/thread';
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
+import React from "react"
+import * as post from "misago/reducers/post"
+import * as thread from "misago/reducers/thread"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export default class extends React.Component {
   /*
   Super naive and de-facto placeholder implementation for reading posts on scroll
   */
   componentDidMount() {
-    if(this.props.post.is_read) return; // don't register read tracker
+    if (this.props.post.is_read) return // don't register read tracker
 
     $(this.documentNode).waypoint({
-      handler: (direction) => {
-        if (direction !== 'down' || this.props.post.is_read) return;
+      handler: direction => {
+        if (direction !== "down" || this.props.post.is_read) return
 
         // after 1500ms run flag post as read logic
         window.setTimeout(() => {
           // check if post's bottom edge is still in viewport
-          const boundingClientRect = this.documentNode.getBoundingClientRect();
-          const offsetBottom = boundingClientRect.height + boundingClientRect.top;
-          const clientHeight = document.documentElement.clientHeight;
+          const boundingClientRect = this.documentNode.getBoundingClientRect()
+          const offsetBottom =
+            boundingClientRect.height + boundingClientRect.top
+          const clientHeight = document.documentElement.clientHeight
 
-          if (offsetBottom < 5) return; // scrolled past the post
-          if (offsetBottom > clientHeight) return; // scrolled back up
+          if (offsetBottom < 5) return // scrolled past the post
+          if (offsetBottom > clientHeight) return // scrolled back up
 
           // mark post as read
-          store.dispatch(post.patch(this.props.post, {
-            is_read: true
-          }));
+          store.dispatch(
+            post.patch(this.props.post, {
+              is_read: true
+            })
+          )
 
           // call API to let it know we have unread post
           ajax.post(this.props.post.api.read).then(
-            (data) => {
-              store.dispatch(thread.update(this.props.thread, {
-                is_read: data.thread_is_read
-              }));
+            data => {
+              store.dispatch(
+                thread.update(this.props.thread, {
+                  is_read: data.thread_is_read
+                })
+              )
             },
-            (rejection) => {
-              snackbar.apiError(rejection);
+            rejection => {
+              snackbar.apiError(rejection)
             }
-          );
-        }, 1000);
+          )
+        }, 1000)
       },
-      offset: 'bottom-in-view'
-    });
+      offset: "bottom-in-view"
+    })
   }
 
   render() {
     return (
-      <div className={this.props.className} ref={(node) => { this.documentNode = node; }}>
+      <div
+        className={this.props.className}
+        ref={node => {
+          this.documentNode = node
+        }}
+      >
         {this.props.children}
       </div>
-    );
+    )
   }
-}
+}

+ 110 - 102
frontend/src/components/profile/ban-details.js

@@ -1,55 +1,54 @@
-import moment from 'moment';
-import React from 'react';
-import PanelLoader from 'misago/components/panel-loader'; // jshint ignore:line
-import PanelMessage from 'misago/components/panel-message'; // jshint ignore:line
-import misago from 'misago/index';
-import polls from 'misago/services/polls';
-import title from 'misago/services/page-title';
+import moment from "moment"
+import React from "react"
+import PanelLoader from "misago/components/panel-loader"
+import PanelMessage from "misago/components/panel-message"
+import misago from "misago/index"
+import polls from "misago/services/polls"
+import title from "misago/services/page-title"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
-    if (misago.has('PROFILE_BAN')) {
-      this.initWithPreloadedData(misago.pop('PROFILE_BAN'));
+    if (misago.has("PROFILE_BAN")) {
+      this.initWithPreloadedData(misago.pop("PROFILE_BAN"))
     } else {
-      this.initWithoutPreloadedData();
+      this.initWithoutPreloadedData()
     }
 
-    this.startPolling(props.profile.api.ban);
+    this.startPolling(props.profile.api.ban)
   }
 
   initWithPreloadedData(ban) {
     if (ban.expires_on) {
-      ban.expires_on = moment(ban.expires_on);
+      ban.expires_on = moment(ban.expires_on)
     }
 
     this.state = {
       isLoaded: true,
       ban
-    };
+    }
   }
 
   initWithoutPreloadedData() {
     this.state = {
       isLoaded: false
-    };
+    }
   }
 
   startPolling(api) {
     polls.start({
-      poll: 'ban-details',
+      poll: "ban-details",
       url: api,
       frequency: 90 * 1000,
       update: this.update,
       error: this.error
-    });
+    })
   }
 
-  /* jshint ignore:start */
-  update = (ban) => {
+  update = ban => {
     if (ban.expires_on) {
-      ban.expires_on = moment(ban.expires_on);
+      ban.expires_on = moment(ban.expires_on)
     }
 
     this.setState({
@@ -57,138 +56,147 @@ export default class extends React.Component {
       error: null,
 
       ban
-    });
-  };
+    })
+  }
 
-  error = (error) => {
+  error = error => {
     this.setState({
       isLoaded: true,
       error: error.detail,
       ban: null
-    });
-  };
-  /* jshint ignore:end */
+    })
+  }
 
   componentDidMount() {
     title.set({
       title: gettext("Ban details"),
       parent: this.props.profile.username
-    });
+    })
   }
 
   componentWillUnmount() {
-    polls.stop('ban-details');
+    polls.stop("ban-details")
   }
 
   getUserMessage() {
     if (this.state.ban.user_message) {
-      /* jshint ignore:start */
-      return <div className="panel-body ban-message ban-user-message">
-        <h4>{gettext("User-shown ban message")}</h4>
-        <div className="lead" dangerouslySetInnerHTML={{
-            __html: this.state.ban.user_message.html
-          }} />
-      </div>;
-      /* jshint ignore:end */
+      return (
+        <div className="panel-body ban-message ban-user-message">
+          <h4>{gettext("User-shown ban message")}</h4>
+          <div
+            className="lead"
+            dangerouslySetInnerHTML={{
+              __html: this.state.ban.user_message.html
+            }}
+          />
+        </div>
+      )
     } else {
-      return null;
+      return null
     }
   }
 
   getStaffMessage() {
     if (this.state.ban.staff_message) {
-      /* jshint ignore:start */
-      return <div className="panel-body ban-message ban-staff-message">
-        <h4>{gettext("Team-shown ban message")}</h4>
-        <div className="lead" dangerouslySetInnerHTML={{
-            __html: this.state.ban.staff_message.html
-          }} />
-      </div>;
-      /* jshint ignore:end */
-      } else {
-      return null;
+      return (
+        <div className="panel-body ban-message ban-staff-message">
+          <h4>{gettext("Team-shown ban message")}</h4>
+          <div
+            className="lead"
+            dangerouslySetInnerHTML={{
+              __html: this.state.ban.staff_message.html
+            }}
+          />
+        </div>
+      )
+    } else {
+      return null
     }
   }
 
   getExpirationMessage() {
     if (this.state.ban.expires_on) {
       if (this.state.ban.expires_on.isAfter(moment())) {
-        /* jshint ignore:start */
         let title = interpolate(
-          gettext("This ban expires on %(expires_on)s."), {
-            'expires_on': this.state.ban.expires_on.format('LL, LT')
-          }, true);
+          gettext("This ban expires on %(expires_on)s."),
+          {
+            expires_on: this.state.ban.expires_on.format("LL, LT")
+          },
+          true
+        )
 
         let message = interpolate(
-          gettext("This ban expires %(expires_on)s."), {
-            'expires_on': this.state.ban.expires_on.fromNow()
-          }, true);
-
-        return <abbr title={title}>
-          {message}
-        </abbr>;
-        /* jshint ignore:end */
+          gettext("This ban expires %(expires_on)s."),
+          {
+            expires_on: this.state.ban.expires_on.fromNow()
+          },
+          true
+        )
+
+        return <abbr title={title}>{message}</abbr>
       } else {
-        return gettext("This ban has expired.");
+        return gettext("This ban has expired.")
       }
     } else {
-      return interpolate(gettext("%(username)s's ban is permanent."), {
-        'username': this.props.profile.username
-      }, true);
+      return interpolate(
+        gettext("%(username)s's ban is permanent."),
+        {
+          username: this.props.profile.username
+        },
+        true
+      )
     }
   }
 
   getPanelBody() {
     if (this.state.ban) {
       if (Object.keys(this.state.ban).length) {
-        /* jshint ignore:start */
-        return <div>
-          {this.getUserMessage()}
-          {this.getStaffMessage()}
-
-          <div className="panel-body ban-expires">
-            <h4>{gettext("Ban expiration")}</h4>
-            <p className="lead">
-              {this.getExpirationMessage()}
-            </p>
+        return (
+          <div>
+            {this.getUserMessage()}
+            {this.getStaffMessage()}
+
+            <div className="panel-body ban-expires">
+              <h4>{gettext("Ban expiration")}</h4>
+              <p className="lead">{this.getExpirationMessage()}</p>
+            </div>
           </div>
-        </div>;
-        /* jshint ignore:end */
+        )
       } else {
-        /* jshint ignore:start */
-        return <div>
-          <PanelMessage message={gettext("No ban is active at the moment.")} />
-        </div>;
-        /* jshint ignore:end */
+        return (
+          <div>
+            <PanelMessage
+              message={gettext("No ban is active at the moment.")}
+            />
+          </div>
+        )
       }
     } else if (this.state.error) {
-      /* jshint ignore:start */
-      return <div>
-        <PanelMessage icon="error_outline"
-                      message={this.state.error} />
-      </div>;
-      /* jshint ignore:end */
+      return (
+        <div>
+          <PanelMessage icon="error_outline" message={this.state.error} />
+        </div>
+      )
     } else {
-      /* jshint ignore:start */
-      return <div>
-        <PanelLoader />
-      </div>;
-      /* jshint ignore:end */
+      return (
+        <div>
+          <PanelLoader />
+        </div>
+      )
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className="profile-ban-details">
-      <div className="panel panel-default">
-        <div className="panel-heading">
-          <h3 className="panel-title">{gettext("Ban details")}</h3>
-        </div>
-
-        {this.getPanelBody()}
+    return (
+      <div className="profile-ban-details">
+        <div className="panel panel-default">
+          <div className="panel-heading">
+            <h3 className="panel-title">{gettext("Ban details")}</h3>
+          </div>
 
+          {this.getPanelBody()}
+        </div>
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 8 - 11
frontend/src/components/profile/details/empty-message.js

@@ -1,25 +1,22 @@
-/* jshint ignore:start */
-import React from 'react';
+import React from "react"
 
 export default function({ isAuthenticated, profile }) {
-  let message = null;
+  let message = null
   if (isAuthenticated) {
-    message = gettext("You are not sharing any details with others.");
+    message = gettext("You are not sharing any details with others.")
   } else {
     message = interpolate(
       gettext("%(username)s is not sharing any details with others."),
       {
-        'username': profile.username,
+        username: profile.username
       },
       true
-    );
+    )
   }
 
   return (
     <div className="panel panel-default">
-      <div className="panel-body text-center lead">
-        {message}
-      </div>
+      <div className="panel-body text-center lead">{message}</div>
     </div>
-  );
-}
+  )
+}

+ 9 - 15
frontend/src/components/profile/details/field-value.js

@@ -1,24 +1,20 @@
-/* jshint ignore:start */
-import React from 'react';
+import React from "react"
 
 export default function({ html, text, url }) {
   if (html) {
     return (
       <div
         className="form-control-static col-md-9"
-        dangerouslySetInnerHTML={{__html: html}}
+        dangerouslySetInnerHTML={{ __html: html }}
       />
-    );
+    )
   }
 
   return (
     <div className="form-control-static col-md-9">
-      <SafeValue
-        text={text}
-        url={url}
-      />
+      <SafeValue text={text} url={url} />
     </div>
-  );
+  )
 }
 
 export function SafeValue({ text, url }) {
@@ -29,14 +25,12 @@ export function SafeValue({ text, url }) {
           {text || url}
         </a>
       </p>
-    );
+    )
   }
 
   if (text) {
-    return (
-      <p>{text}</p>
-    );
+    return <p>{text}</p>
   }
 
-  return null;
-}
+  return null
+}

+ 5 - 8
frontend/src/components/profile/details/field.js

@@ -1,14 +1,11 @@
-/* jshint ignore:start */
-import React from 'react';
-import FieldValue from './field-value';
+import React from "react"
+import FieldValue from "./field-value"
 
 export default function(props) {
   return (
     <div className="form-group">
-      <strong className="control-label col-md-3">
-        {props.name}:
-      </strong>
+      <strong className="control-label col-md-3">{props.name}:</strong>
       <FieldValue {...props} />
     </div>
-  );
-}
+  )
+}

+ 5 - 12
frontend/src/components/profile/details/form.js

@@ -1,15 +1,8 @@
-/* jshint ignore:start */
-import React from 'react';
-import Form from 'misago/components/edit-details';
+import React from "react"
+import Form from "misago/components/edit-details"
 
 export default function({ api, display, onCancel, onSuccess }) {
-  if (!display) return null;
+  if (!display) return null
 
-  return (
-    <Form
-      api={api}
-      onCancel={onCancel}
-      onSuccess={onSuccess}
-    />
-  );
-}
+  return <Form api={api} onCancel={onCancel} onSuccess={onSuccess} />
+}

+ 5 - 6
frontend/src/components/profile/details/group.js

@@ -1,6 +1,5 @@
-/* jshint ignore:start */
-import React from 'react';
-import Field from './field';
+import React from "react"
+import Field from "./field"
 
 export default function({ fields, name }) {
   return (
@@ -19,10 +18,10 @@ export default function({ fields, name }) {
                 text={text}
                 url={url}
               />
-            );
+            )
           })}
         </div>
       </div>
     </div>
-  );
-}
+  )
+}

+ 17 - 25
frontend/src/components/profile/details/groups-list.js

@@ -1,38 +1,30 @@
-/* jshint ignore:start */
-import React from 'react';
-import EmptyMessage from './empty-message';
-import Group from './group';
-import Loader from 'misago/components/loader';
+import React from "react"
+import EmptyMessage from "./empty-message"
+import Group from "./group"
+import Loader from "misago/components/loader"
 
-export default function({ display, groups, isAuthenticated, loading, profile }) {
-  if (!display) return null;
+export default function({
+  display,
+  groups,
+  isAuthenticated,
+  loading,
+  profile
+}) {
+  if (!display) return null
 
   if (loading) {
-    return (
-      <Loader />
-    );
+    return <Loader />
   }
 
   if (!groups.length) {
-    return (
-      <EmptyMessage
-        isAuthenticated={isAuthenticated}
-        profile={profile}
-      />
-    );
+    return <EmptyMessage isAuthenticated={isAuthenticated} profile={profile} />
   }
 
   return (
     <div>
       {groups.map((group, i) => {
-        return (
-          <Group
-            fields={group.fields}
-            key={i}
-            name={group.name}
-          />
-        );
+        return <Group fields={group.fields} key={i} name={group.name} />
       })}
     </div>
-  );
-}
+  )
+}

+ 7 - 13
frontend/src/components/profile/details/header.js

@@ -1,5 +1,4 @@
-/* jshint ignore:start */
-import React from 'react';
+import React from "react"
 
 export default function({ onEdit, showEditButton }) {
   return (
@@ -7,22 +6,17 @@ export default function({ onEdit, showEditButton }) {
       <nav className="toolbar">
         <div className="row">
           <div className="col-sm-8 col-md-10">
-            <h3 className="md-margin-top-no">
-              {gettext("Details")}
-            </h3>
+            <h3 className="md-margin-top-no">{gettext("Details")}</h3>
           </div>
-          <EditButton
-            onEdit={onEdit}
-            showEditButton={showEditButton}
-          />
+          <EditButton onEdit={onEdit} showEditButton={showEditButton} />
         </div>
       </nav>
     </div>
-  );
+  )
 }
 
 export function EditButton({ onEdit, showEditButton }) {
-  if (!showEditButton) return null;
+  if (!showEditButton) return null
 
   return (
     <div className="col-sm-4 col-md-2">
@@ -34,5 +28,5 @@ export function EditButton({ onEdit, showEditButton }) {
         {gettext("Edit")}
       </button>
     </div>
-  );
-}
+  )
+}

+ 29 - 30
frontend/src/components/profile/details/index.js

@@ -1,61 +1,60 @@
-/* jshint ignore:start */
-import React from 'react';
-import Form from './form';
-import GroupsList from './groups-list';
-import Header from './header';
-import ProfileDetailsData from 'misago/data/profile-details';
-import { load as loadDetails } from 'misago/reducers/profile-details';
-import title from 'misago/services/page-title';
-import snackbar from 'misago/services/snackbar';
+import React from "react"
+import Form from "./form"
+import GroupsList from "./groups-list"
+import Header from "./header"
+import ProfileDetailsData from "misago/data/profile-details"
+import { load as loadDetails } from "misago/reducers/profile-details"
+import title from "misago/services/page-title"
+import snackbar from "misago/services/snackbar"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       editing: false
-    };
+    }
   }
 
   componentDidMount() {
     title.set({
       title: gettext("Details"),
       parent: this.props.profile.username
-    });
+    })
   }
 
   onCancel = () => {
-    this.setState({ editing: false });
-  };
+    this.setState({ editing: false })
+  }
 
   onEdit = () => {
-    this.setState({ editing: true });
-  };
+    this.setState({ editing: true })
+  }
 
-  onSuccess = (newDetails) => {
-    const { dispatch, isAuthenticated, profile } = this.props;
+  onSuccess = newDetails => {
+    const { dispatch, isAuthenticated, profile } = this.props
 
-    let message = null;
+    let message = null
     if (isAuthenticated) {
-      message = gettext("Your details have been updated.");
+      message = gettext("Your details have been updated.")
     } else {
       message = interpolate(
         gettext("%(username)s's details have been updated."),
         {
-          'username': profile.username,
+          username: profile.username
         },
         true
-      );
+      )
     }
 
-    snackbar.info(message);
-    dispatch(loadDetails(newDetails));
-    this.setState({ editing: false });
-  };
+    snackbar.info(message)
+    dispatch(loadDetails(newDetails))
+    this.setState({ editing: false })
+  }
 
   render() {
-    const { dispatch, isAuthenticated, profile, profileDetails } = this.props;
-    const loading = profileDetails.id !== profile.id;
+    const { dispatch, isAuthenticated, profile, profileDetails } = this.props
+    const loading = profileDetails.id !== profile.id
 
     return (
       <ProfileDetailsData
@@ -84,6 +83,6 @@ export default class extends React.Component {
           />
         </div>
       </ProfileDetailsData>
-    );
+    )
   }
-}
+}

+ 65 - 38
frontend/src/components/profile/feed/index.js

@@ -1,39 +1,52 @@
-// jshint ignore:start
-import React from 'react';
-import Route from './route';
+import React from "react"
+import Route from "./route"
 
 export function Threads(props) {
-  let emptyMessage = null;
+  let emptyMessage = null
   if (props.user.id === props.profile.id) {
-    emptyMessage = gettext("You have no started threads.");
+    emptyMessage = gettext("You have no started threads.")
   } else {
-    emptyMessage = interpolate(gettext("%(username)s started no threads."), {
-      'username': props.profile.username
-    }, true);
+    emptyMessage = interpolate(
+      gettext("%(username)s started no threads."),
+      {
+        username: props.profile.username
+      },
+      true
+    )
   }
 
-  let header = null;
+  let header = null
   if (!props.posts.isLoaded) {
-    header = gettext('Loading...');
+    header = gettext("Loading...")
   } else if (props.profile.id === props.user.id) {
     const message = ngettext(
       "You have started %(threads)s thread.",
       "You have started %(threads)s threads.",
-      props.posts.count);
+      props.posts.count
+    )
 
-    header = interpolate(message, {
-      'threads': props.posts.count
-    }, true);
+    header = interpolate(
+      message,
+      {
+        threads: props.posts.count
+      },
+      true
+    )
   } else {
     const message = ngettext(
       "%(username)s has started %(threads)s thread.",
       "%(username)s has started %(threads)s threads.",
-      props.posts.count);
+      props.posts.count
+    )
 
-    header = interpolate(message, {
-      'username': props.profile.username,
-      'threads': props.posts.count
-    }, true);
+    header = interpolate(
+      message,
+      {
+        username: props.profile.username,
+        threads: props.posts.count
+      },
+      true
+    )
   }
 
   return (
@@ -44,41 +57,55 @@ export function Threads(props) {
       title={gettext("Threads")}
       {...props}
     />
-  );
+  )
 }
 
 export function Posts(props) {
-  let emptyMessage = null;
+  let emptyMessage = null
   if (props.user.id === props.profile.id) {
-    emptyMessage = gettext("You have posted no messages.");
+    emptyMessage = gettext("You have posted no messages.")
   } else {
-    emptyMessage = interpolate(gettext("%(username)s posted no messages."), {
-      'username': props.profile.username
-    }, true);
+    emptyMessage = interpolate(
+      gettext("%(username)s posted no messages."),
+      {
+        username: props.profile.username
+      },
+      true
+    )
   }
 
-  let header = null;
+  let header = null
   if (!props.posts.isLoaded) {
-    header = gettext('Loading...');
+    header = gettext("Loading...")
   } else if (props.profile.id === props.user.id) {
     const message = ngettext(
       "You have posted %(posts)s message.",
       "You have posted %(posts)s messages.",
-      props.posts.count);
+      props.posts.count
+    )
 
-    header = interpolate(message, {
-      'posts': props.posts.count
-    }, true);
+    header = interpolate(
+      message,
+      {
+        posts: props.posts.count
+      },
+      true
+    )
   } else {
     const message = ngettext(
       "%(username)s has posted %(posts)s message.",
       "%(username)s has posted %(posts)s messages.",
-      props.posts.count);
+      props.posts.count
+    )
 
-    header = interpolate(message, {
-      'username': props.profile.username,
-      'posts': props.posts.count
-    }, true);
+    header = interpolate(
+      message,
+      {
+        username: props.profile.username,
+        posts: props.posts.count
+      },
+      true
+    )
   }
 
   return (
@@ -89,5 +116,5 @@ export function Posts(props) {
       title={gettext("Posts")}
       {...props}
     />
-  );
-}
+  )
+}

+ 51 - 47
frontend/src/components/profile/feed/route.js

@@ -1,68 +1,70 @@
-// jshint ignore:start
-import React from 'react';
-import PostFeed from 'misago/components/post-feed';
-import Button from 'misago/components/button';
-import * as posts from 'misago/reducers/posts';
-import title from 'misago/services/page-title';
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
+import React from "react"
+import PostFeed from "misago/components/post-feed"
+import Button from "misago/components/button"
+import * as posts from "misago/reducers/posts"
+import title from "misago/services/page-title"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoading: false
     }
   }
 
-  loadItems(page=1) {
-    ajax.get(this.props.api, {
-      page: page || 1
-    }).then((data) => {
-      if (page === 1) {
-        store.dispatch(posts.load(data));
-      } else {
-        store.dispatch(posts.append(data));
-      }
+  loadItems(page = 1) {
+    ajax
+      .get(this.props.api, {
+        page: page || 1
+      })
+      .then(
+        data => {
+          if (page === 1) {
+            store.dispatch(posts.load(data))
+          } else {
+            store.dispatch(posts.append(data))
+          }
 
-      this.setState({
-        isLoading: false
-      });
-    }, (rejection) => {
-      this.setState({
-        isLoading: false
-      });
+          this.setState({
+            isLoading: false
+          })
+        },
+        rejection => {
+          this.setState({
+            isLoading: false
+          })
 
-      snackbar.apiError(rejection);
-    });
+          snackbar.apiError(rejection)
+        }
+      )
   }
 
   loadMore = () => {
     this.setState({
       isLoading: true
-    });
+    })
 
-    this.loadItems(this.props.posts.page + 1);
-  };
+    this.loadItems(this.props.posts.page + 1)
+  }
 
   componentDidMount() {
     title.set({
       title: this.props.title,
       parent: this.props.profile.username
-    });
+    })
 
-    this.loadItems();
+    this.loadItems()
   }
 
   render() {
     return (
       <div className="profile-feed">
         <nav className="toolbar">
-          <h3 className="toolbar-left">
-            {this.props.header}
-          </h3>
+          <h3 className="toolbar-left">{this.props.header}</h3>
         </nav>
         <Feed
           isLoading={this.state.isLoading}
@@ -70,15 +72,13 @@ export default class extends React.Component {
           {...this.props}
         />
       </div>
-    );
+    )
   }
 }
 
 export function Feed(props) {
   if (!props.posts.count) {
-    return (
-      <p className="lead">{props.emptyMessage}</p>
-    );
+    return <p className="lead">{props.emptyMessage}</p>
   }
 
   return (
@@ -94,11 +94,11 @@ export function Feed(props) {
         more={props.posts.more}
       />
     </div>
-  );
+  )
 }
 
 export function LoadMoreButton(props) {
-  if (!props.more) return null;
+  if (!props.more) return null
 
   return (
     <div className="pager-more">
@@ -107,10 +107,14 @@ export function LoadMoreButton(props) {
         loading={props.isLoading}
         onClick={props.loadMore}
       >
-        {interpolate(gettext("Show more (%(more)s)"), {
-          'more': props.more
-        }, true)}
+        {interpolate(
+          gettext("Show more (%(more)s)"),
+          {
+            more: props.more
+          },
+          true
+        )}
       </Button>
     </div>
-  );
-}
+  )
+}

+ 53 - 48
frontend/src/components/profile/follow-button.js

@@ -1,86 +1,91 @@
-import React from 'react';
-import Button from 'misago/components/button'; // jshint ignore:line
-import { patch } from 'misago/reducers/profile'; // jshint ignore:line
-import ajax from 'misago/services/ajax'; // jshint ignore:line
-import snackbar from 'misago/services/snackbar'; // jshint ignore:line
-import store from 'misago/services/store'; // jshint ignore:line
+import React from "react"
+import Button from "misago/components/button"
+import { patch } from "misago/reducers/profile"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoading: false
-    };
+    }
   }
 
   getClassName() {
     if (this.props.profile.is_followed) {
-      return this.props.className + ' btn-default btn-following';
+      return this.props.className + " btn-default btn-following"
     } else {
-      return this.props.className + ' btn-default btn-follow';
+      return this.props.className + " btn-default btn-follow"
     }
   }
 
   getIcon() {
     if (this.props.profile.is_followed) {
-      return 'favorite';
+      return "favorite"
     } else {
-      return 'favorite_border';
+      return "favorite_border"
     }
   }
 
   getLabel() {
     if (this.props.profile.is_followed) {
-      return gettext("Following");
+      return gettext("Following")
     } else {
-      return gettext("Follow");
+      return gettext("Follow")
     }
   }
 
-  /* jshint ignore:start */
   action = () => {
     this.setState({
       isLoading: true
-    });
+    })
 
     if (this.props.profile.is_followed) {
-      store.dispatch(patch({
-        is_followed: false,
-        followers: this.props.profile.followers - 1
-      }));
+      store.dispatch(
+        patch({
+          is_followed: false,
+          followers: this.props.profile.followers - 1
+        })
+      )
     } else {
-      store.dispatch(patch({
-        is_followed: true,
-        followers: this.props.profile.followers + 1
-      }));
+      store.dispatch(
+        patch({
+          is_followed: true,
+          followers: this.props.profile.followers + 1
+        })
+      )
     }
 
-    ajax.post(this.props.profile.api.follow).then((data) => {
-      this.setState({
-        isLoading: false
-      });
+    ajax.post(this.props.profile.api.follow).then(
+      data => {
+        this.setState({
+          isLoading: false
+        })
 
-      store.dispatch(patch(data));
-    }, (rejection) => {
-      this.setState({
-        isLoading: false
-      });
-      snackbar.apiError(rejection);
-    });
-  };
-  /* jshint ignore:end */
+        store.dispatch(patch(data))
+      },
+      rejection => {
+        this.setState({
+          isLoading: false
+        })
+        snackbar.apiError(rejection)
+      }
+    )
+  }
 
   render() {
-    /* jshint ignore:start */
-    return <Button className={this.getClassName()}
-                   disabled={this.state.isLoading}
-                   onClick={this.action}>
-      <span className="material-icon">
-        {this.getIcon()}
-      </span>
-      {this.getLabel()}
-    </Button>;
-    /* jshint ignore:end */
+    return (
+      <Button
+        className={this.getClassName()}
+        disabled={this.state.isLoading}
+        onClick={this.action}
+      >
+        <span className="material-icon">{this.getIcon()}</span>
+        {this.getLabel()}
+      </Button>
+    )
   }
-}
+}

+ 136 - 116
frontend/src/components/profile/followers.js

@@ -1,31 +1,31 @@
-import React from 'react';
-import Button from 'misago/components/button'; // jshint ignore:line
-import Search from 'misago/components/quick-search'; // jshint ignore:line
-import UsersList from 'misago/components/users-list'; // jshint ignore:line
-import misago from 'misago/index';
-import { hydrate, append } from 'misago/reducers/users'; // jshint ignore:line
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
-import title from 'misago/services/page-title';
+import React from "react"
+import Button from "misago/components/button"
+import Search from "misago/components/quick-search"
+import UsersList from "misago/components/users-list"
+import misago from "misago/index"
+import { hydrate, append } from "misago/reducers/users"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
+import title from "misago/services/page-title"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
-    this.setSpecialProps();
+    this.setSpecialProps()
 
     if (misago.has(this.PRELOADED_DATA_KEY)) {
-      this.initWithPreloadedData(misago.pop(this.PRELOADED_DATA_KEY));
+      this.initWithPreloadedData(misago.pop(this.PRELOADED_DATA_KEY))
     } else {
-      this.initWithoutPreloadedData();
+      this.initWithoutPreloadedData()
     }
   }
 
   setSpecialProps() {
-    this.PRELOADED_DATA_KEY = 'PROFILE_FOLLOWERS';
-    this.TITLE = gettext('Followers');
-    this.API_FILTER = 'followers';
+    this.PRELOADED_DATA_KEY = "PROFILE_FOLLOWERS"
+    this.TITLE = gettext("Followers")
+    this.API_FILTER = "followers"
   }
 
   initWithPreloadedData(data) {
@@ -33,16 +33,16 @@ export default class extends React.Component {
       isLoaded: true,
       isBusy: false,
 
-      search: '',
+      search: "",
 
       count: data.count,
       more: data.more,
 
       page: data.page,
       pages: data.pages
-    };
+    }
 
-    store.dispatch(hydrate(data.results));
+    store.dispatch(hydrate(data.results))
   }
 
   initWithoutPreloadedData() {
@@ -50,63 +50,71 @@ export default class extends React.Component {
       isLoaded: false,
       isBusy: false,
 
-      search: '',
+      search: "",
 
       count: 0,
       more: 0,
 
       page: 1,
       pages: 1
-    };
+    }
 
-    this.loadUsers();
+    this.loadUsers()
   }
 
-  loadUsers(page=1, search=null) {
-    const apiUrl = this.props.profile.api[this.API_FILTER];
-
-    ajax.get(apiUrl, {
-      search: search,
-      page: page || 1
-    }, 'user-' + this.API_FILTER).then((data) => {
-      if (page === 1) {
-        store.dispatch(hydrate(data.results));
-      } else {
-        store.dispatch(append(data.results));
-      }
-
-      this.setState({
-        isLoaded: true,
-        isBusy: false,
-
-        count: data.count,
-        more: data.more,
-
-        page: data.page,
-        pages: data.pages
-      });
-    }, (rejection) => {
-      snackbar.apiError(rejection);
-    });
+  loadUsers(page = 1, search = null) {
+    const apiUrl = this.props.profile.api[this.API_FILTER]
+
+    ajax
+      .get(
+        apiUrl,
+        {
+          search: search,
+          page: page || 1
+        },
+        "user-" + this.API_FILTER
+      )
+      .then(
+        data => {
+          if (page === 1) {
+            store.dispatch(hydrate(data.results))
+          } else {
+            store.dispatch(append(data.results))
+          }
+
+          this.setState({
+            isLoaded: true,
+            isBusy: false,
+
+            count: data.count,
+            more: data.more,
+
+            page: data.page,
+            pages: data.pages
+          })
+        },
+        rejection => {
+          snackbar.apiError(rejection)
+        }
+      )
   }
 
   componentDidMount() {
     title.set({
       title: this.TITLE,
       parent: this.props.profile.username
-    });
+    })
   }
 
-  /* jshint ignore:start */
   loadMore = () => {
     this.setState({
       isBusy: true
-    });
+    })
 
-    this.loadUsers(this.state.page + 1, this.state.search);
-  };
+    this.loadUsers(this.state.page + 1, this.state.search)
+  }
 
-  search = (ev) => {
+  search = ev => {
     this.setState({
       isLoaded: false,
       isBusy: true,
@@ -118,62 +126,79 @@ export default class extends React.Component {
 
       page: 1,
       pages: 1
-    });
+    })
 
-    this.loadUsers(1, ev.target.value);
-  };
-  /* jshint ignore:end */
+    this.loadUsers(1, ev.target.value)
+  }
 
   getLabel() {
     if (!this.state.isLoaded) {
-      return gettext('Loading...');
+      return gettext("Loading...")
     } else if (this.state.search) {
       let message = ngettext(
         "Found %(users)s user.",
         "Found %(users)s users.",
-        this.state.count);
-
-      return interpolate(message, {
-        'users': this.state.count
-      }, true);
+        this.state.count
+      )
+
+      return interpolate(
+        message,
+        {
+          users: this.state.count
+        },
+        true
+      )
     } else if (this.props.profile.id === this.props.user.id) {
       let message = ngettext(
         "You have %(users)s follower.",
         "You have %(users)s followers.",
-        this.state.count);
-
-      return interpolate(message, {
-        'users': this.state.count
-      }, true);
+        this.state.count
+      )
+
+      return interpolate(
+        message,
+        {
+          users: this.state.count
+        },
+        true
+      )
     } else {
       let message = ngettext(
         "%(username)s has %(users)s follower.",
         "%(username)s has %(users)s followers.",
-        this.state.count);
-
-      return interpolate(message, {
-        'username': this.props.profile.username,
-        'users': this.state.count
-      }, true);
+        this.state.count
+      )
+
+      return interpolate(
+        message,
+        {
+          username: this.props.profile.username,
+          users: this.state.count
+        },
+        true
+      )
     }
   }
 
   getEmptyMessage() {
     if (this.state.search) {
-      return gettext("Search returned no users matching specified criteria.");
+      return gettext("Search returned no users matching specified criteria.")
     } else if (this.props.user.id === this.props.profile.id) {
-      return gettext("You have no followers.");
+      return gettext("You have no followers.")
     } else {
-      return interpolate(gettext("%(username)s has no followers."), {
-        'username': this.props.profile.username
-      }, true);
+      return interpolate(
+        gettext("%(username)s has no followers."),
+        {
+          username: this.props.profile.username
+        },
+        true
+      )
     }
   }
 
   getMoreButton() {
-    if (!this.state.more) return null;
+    if (!this.state.more) return null
 
-    /* jshint ignore:start */
     return (
       <div className="pager-more">
         <Button
@@ -181,25 +206,23 @@ export default class extends React.Component {
           loading={this.state.isBusy}
           onClick={this.loadMore}
         >
-          {interpolate(gettext("Show more (%(more)s)"), {
-            'more': this.state.more
-          }, true)}
+          {interpolate(
+            gettext("Show more (%(more)s)"),
+            {
+              more: this.state.more
+            },
+            true
+          )}
         </Button>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   getListBody() {
     if (this.state.isLoaded && this.state.count === 0) {
-      /* jshint ignore:start */
-      return <p className="lead">
-        {this.getEmptyMessage()}
-      </p>;
-      /* jshint ignore:end */
+      return <p className="lead">{this.getEmptyMessage()}</p>
     }
 
-    /* jshint ignore:start */
     return (
       <div>
         <UsersList
@@ -210,32 +233,29 @@ export default class extends React.Component {
 
         {this.getMoreButton()}
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   getClassName() {
-    return 'profile-' + this.API_FILTER;
+    return "profile-" + this.API_FILTER
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className={this.getClassName()}>
-
-      <nav className="toolbar">
-        <h3 className="toolbar-left">
-          {this.getLabel()}
-        </h3>
-
-        <Search className="toolbar-right"
-                value={this.state.search}
-                onChange={this.search}
-                placeholder={gettext("Search users...")} />
-      </nav>
-
-      {this.getListBody()}
-
-    </div>;
-    /* jshint ignore:end */
+    return (
+      <div className={this.getClassName()}>
+        <nav className="toolbar">
+          <h3 className="toolbar-left">{this.getLabel()}</h3>
+
+          <Search
+            className="toolbar-right"
+            value={this.state.search}
+            onChange={this.search}
+            placeholder={gettext("Search users...")}
+          />
+        </nav>
+
+        {this.getListBody()}
+      </div>
+    )
   }
-}
+}

+ 44 - 25
frontend/src/components/profile/follows.js

@@ -1,56 +1,75 @@
-import React from 'react'; // jshint ignore:line
-import Followers from 'misago/components/profile/followers';
+import React from "react"
+import Followers from "misago/components/profile/followers"
 
 export default class extends Followers {
   setSpecialProps() {
-    this.PRELOADED_DATA_KEY = 'PROFILE_FOLLOWS';
-    this.TITLE = gettext('Follows');
-    this.API_FILTER = 'follows';
+    this.PRELOADED_DATA_KEY = "PROFILE_FOLLOWS"
+    this.TITLE = gettext("Follows")
+    this.API_FILTER = "follows"
   }
 
   getLabel() {
     if (!this.state.isLoaded) {
-      return gettext('Loading...');
+      return gettext("Loading...")
     } else if (this.state.search) {
       let message = ngettext(
         "Found %(users)s user.",
         "Found %(users)s users.",
-        this.state.count);
+        this.state.count
+      )
 
-      return interpolate(message, {
-        'users': this.state.count
-      }, true);
+      return interpolate(
+        message,
+        {
+          users: this.state.count
+        },
+        true
+      )
     } else if (this.props.profile.id === this.props.user.id) {
       let message = ngettext(
         "You are following %(users)s user.",
         "You are following %(users)s users.",
-        this.state.count);
+        this.state.count
+      )
 
-      return interpolate(message, {
-        'users': this.state.count
-      }, true);
+      return interpolate(
+        message,
+        {
+          users: this.state.count
+        },
+        true
+      )
     } else {
       let message = ngettext(
         "%(username)s is following %(users)s user.",
         "%(username)s is following %(users)s users.",
-        this.state.count);
+        this.state.count
+      )
 
-      return interpolate(message, {
-        'username': this.props.profile.username,
-        'users': this.state.count
-      }, true);
+      return interpolate(
+        message,
+        {
+          username: this.props.profile.username,
+          users: this.state.count
+        },
+        true
+      )
     }
   }
 
   getEmptyMessage() {
     if (this.state.search) {
-      return gettext("Search returned no users matching specified criteria.");
+      return gettext("Search returned no users matching specified criteria.")
     } else if (this.props.user.id === this.props.profile.id) {
-      return gettext("You are not following any users.");
+      return gettext("You are not following any users.")
     } else {
-      return interpolate(gettext("%(username)s is not following any users."), {
-        'username': this.props.profile.username
-      }, true);
+      return interpolate(
+        gettext("%(username)s is not following any users."),
+        {
+          username: this.props.profile.username
+        },
+        true
+      )
     }
   }
-}
+}

+ 72 - 109
frontend/src/components/profile/header.js

@@ -1,21 +1,17 @@
-import React from 'react';
-import Avatar from 'misago/components/avatar'; // jshint ignore:line
-import DropdownToggle from 'misago/components/dropdown-toggle'; // jshint ignore:line
-import FollowButton from './follow-button'; // jshint ignore:line
-import MessageButton from './message-button'; // jshint ignore:line
-import ModerationNav from './moderation/nav'; // jshint ignore:line
-import { CompactNav } from './navs'; // jshint ignore:line
-import Status, { StatusIcon, StatusLabel } from 'misago/components/user-status'; // jshint ignore:line
+import React from "react"
+import Avatar from "misago/components/avatar"
+import DropdownToggle from "misago/components/dropdown-toggle"
+import FollowButton from "./follow-button"
+import MessageButton from "./message-button"
+import ModerationNav from "./moderation/nav"
+import { CompactNav } from "./navs"
+import Status, { StatusIcon, StatusLabel } from "misago/components/user-status"
 
 export default class extends React.Component {
   getUserStatus() {
-    /* jshint ignore:start */
     return (
       <li className="user-status-display">
-        <Status
-          user={this.props.profile}
-          status={this.props.profile.status}
-        >
+        <Status user={this.props.profile} status={this.props.profile.status}>
           <StatusIcon
             user={this.props.profile}
             status={this.props.profile.status}
@@ -27,108 +23,90 @@ export default class extends React.Component {
           />
         </Status>
       </li>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   getUserRank() {
     if (this.props.profile.rank.is_tab) {
-      /* jshint ignore:start */
       return (
         <li className="user-rank">
           <a href={this.props.profile.rank.url} className="item-title">
             {this.props.profile.rank.name}
           </a>
         </li>
-      );
-      /* jshint ignore:end */
+      )
     } else {
-      /* jshint ignore:start */
       return (
         <li className="user-rank">
           <span className="item-title">{this.props.profile.rank.name}</span>
         </li>
-      );
-      /* jshint ignore:end */
+      )
     }
   }
 
   getUserTitle() {
     if (this.props.profile.title) {
-      /* jshint ignore:start */
-      return (
-        <li className="user-title">
-          {this.props.profile.title}
-        </li>
-      );
-      /* jshint ignore:end */
+      return <li className="user-title">{this.props.profile.title}</li>
     } else if (this.props.profile.rank.title) {
-      /* jshint ignore:start */
-      return (
-        <li className="user-title">
-          {this.props.profile.rank.title}
-        </li>
-      );
-      /* jshint ignore:end */
+      return <li className="user-title">{this.props.profile.rank.title}</li>
     } else {
-      return null;
+      return null
     }
   }
 
   getJoinedOn() {
-    /* jshint ignore:start */
-    let title = interpolate(gettext("Joined on %(joined_on)s"), {
-      'joined_on': this.props.profile.joined_on.format('LL, LT')
-    }, true);
-
-    let age = interpolate(gettext("Joined %(joined_on)s"), {
-      'joined_on': this.props.profile.joined_on.fromNow()
-    }, true);
+    let title = interpolate(
+      gettext("Joined on %(joined_on)s"),
+      {
+        joined_on: this.props.profile.joined_on.format("LL, LT")
+      },
+      true
+    )
+
+    let age = interpolate(
+      gettext("Joined %(joined_on)s"),
+      {
+        joined_on: this.props.profile.joined_on.fromNow()
+      },
+      true
+    )
 
     return (
       <li className="user-joined-on">
-        <abbr title={title}>
-          {age}
-        </abbr>
+        <abbr title={title}>{age}</abbr>
       </li>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   getEmail() {
     if (this.props.profile.email) {
-      /* jshint ignore:start */
       return (
         <li className="user-email">
-          <a href={'mailto:' + this.props.profile.email} className="item-title">
+          <a href={"mailto:" + this.props.profile.email} className="item-title">
             {this.props.profile.email}
           </a>
         </li>
-      );
-      /* jshint ignore:end */
+      )
     } else {
-      return null;
+      return null
     }
   }
 
   getFollowButton() {
     if (this.props.profile.acl.can_follow) {
-      /* jshint ignore:start */
       return (
         <FollowButton
           className="btn btn-block btn-outline"
           profile={this.props.profile}
         />
-      );
-      /* jshint ignore:end */
+      )
     } else {
-      return null;
+      return null
     }
   }
 
   getModerationButton() {
     if (this.props.profile.acl.can_moderate) {
-      /* jshint ignore:start */
       return (
         <div className="btn-group btn-group-justified">
           <div className="btn-group">
@@ -139,46 +117,43 @@ export default class extends React.Component {
               aria-haspopup="true"
               aria-expanded="false"
             >
-              <span className="material-icon">
-                tonality
-              </span>
+              <span className="material-icon">tonality</span>
               {gettext("Moderation")}
             </button>
             <ModerationNav profile={this.props.profile} />
           </div>
         </div>
-      );
-      /* jshint ignore:end */
+      )
     } else {
-      return null;
+      return null
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    const canFollow = this.props.profile.acl.can_follow;
-    const canModerate = this.props.profile.acl.can_moderate;
+    const canFollow = this.props.profile.acl.can_follow
+    const canModerate = this.props.profile.acl.can_moderate
 
-    const isProfileOwner = this.props.user.id === this.props.profile.id;
-    const canMessage = !isProfileOwner && this.props.user.acl.can_start_private_threads;
+    const isProfileOwner = this.props.user.id === this.props.profile.id
+    const canMessage =
+      !isProfileOwner && this.props.user.acl.can_start_private_threads
 
-    let cols = 0;
-    if (canFollow) cols += 1;
-    if (canModerate) cols += 1;
-    if (canMessage) cols += 1;
+    let cols = 0
+    if (canFollow) cols += 1
+    if (canModerate) cols += 1
+    if (canMessage) cols += 1
 
-    const colsWidth = cols ? 2 * cols + 1 : 0;
+    const colsWidth = cols ? 2 * cols + 1 : 0
 
-    let headerClassName = 'page-header';
+    let headerClassName = "page-header"
     if (this.props.profile.rank.css_class) {
-      headerClassName += ' page-header-rank-' + this.props.profile.rank.css_class;
+      headerClassName +=
+        " page-header-rank-" + this.props.profile.rank.css_class
     }
 
     return (
       <div className="page-header-bg">
         <div className={headerClassName}>
           <div className="container">
-
             <IsDisabledMessage
               isActive={this.props.profile.is_active}
               isDeletingAccount={this.props.profile.is_deleting_account}
@@ -186,10 +161,8 @@ export default class extends React.Component {
 
             <div className="row">
               <div className="col-md-9 col-md-offset-3">
-
                 <div className="row">
                   <div className={"col-sm-" + (12 - colsWidth)}>
-
                     <Avatar
                       className="user-avatar user-avatar-sm"
                       user={this.props.profile}
@@ -197,20 +170,18 @@ export default class extends React.Component {
                       size2x="200"
                     />
                     <h1>{this.props.profile.username}</h1>
-
                   </div>
                   {!!cols && (
                     <div className={"col-sm-" + colsWidth}>
-
                       <div className="row xs-margin-top sm-margin-top">
                         {!!canMessage && (
-                        <div className={getColStyle(cols, 0)}>
-                          <MessageButton
-                            className="btn btn-default btn-block btn-outline"
-                            profile={this.props.profile}
-                            user={this.props.user}
-                          />
-                        </div>
+                          <div className={getColStyle(cols, 0)}>
+                            <MessageButton
+                              className="btn btn-default btn-block btn-outline"
+                              profile={this.props.profile}
+                              user={this.props.user}
+                            />
+                          </div>
                         )}
                         {!!canFollow && (
                           <div className={getColStyle(cols, 1)}>
@@ -223,19 +194,16 @@ export default class extends React.Component {
                           </div>
                         )}
                       </div>
-
                     </div>
                   )}
                 </div>
               </div>
             </div>
-
           </div>
           <div className="header-stats">
             <div className="container">
               <div className="row">
                 <div className="col-md-9 col-md-offset-3">
-
                   <ul className="list-inline">
                     {this.getUserStatus()}
                     {this.getUserRank()}
@@ -243,7 +211,6 @@ export default class extends React.Component {
                     {this.getJoinedOn()}
                     {this.getEmail()}
                   </ul>
-
                 </div>
               </div>
             </div>
@@ -254,51 +221,47 @@ export default class extends React.Component {
             pages={this.props.pages}
             profile={this.props.profile}
           />
-
         </div>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 }
 
-/* jshint ignore:start */
 export function IsDisabledMessage({ isActive, isDeletingAccount }) {
-  if (isActive !== false && isDeletingAccount !== true) return null;
+  if (isActive !== false && isDeletingAccount !== true) return null
 
-  let message = null;
+  let message = null
   if (isDeletingAccount) {
-    message = gettext("This user is deleting their account.");
+    message = gettext("This user is deleting their account.")
   } else {
-    message = gettext("This user's account has been disabled by administrator.");
+    message = gettext("This user's account has been disabled by administrator.")
   }
 
   return (
     <div className="alert alert-danger">
       <p>{message}</p>
     </div>
-  );
+  )
 }
 
 export function getColStyle(cols, col) {
-  let colStyle = "";
+  let colStyle = ""
 
   if (cols == 1) {
-    colStyle = "col-xs-12";
+    colStyle = "col-xs-12"
   }
 
   if (cols == 2) {
-    colStyle = "col-xs-6 col-sm-6";
+    colStyle = "col-xs-6 col-sm-6"
   }
 
   if (cols == 3) {
     if (col == 2) {
-      colStyle = "col-xs-12 col-sm-4 xs-margin-top";
+      colStyle = "col-xs-12 col-sm-4 xs-margin-top"
     } else {
-      colStyle += "col-xs-6 col-sm-4";
+      colStyle += "col-xs-6 col-sm-4"
     }
   }
 
-  return colStyle;
+  return colStyle
 }
-/* jshint ignore:end */

+ 12 - 15
frontend/src/components/profile/message-button.js

@@ -1,23 +1,22 @@
-// jshint ignore:start
-import React from 'react';
-import posting from 'misago/services/posting';
-import misago from 'misago';
+import React from "react"
+import posting from "misago/services/posting"
+import misago from "misago"
 
 export default class extends React.Component {
   onClick = () => {
     posting.open({
-      mode: 'START_PRIVATE',
-      submit: misago.get('PRIVATE_THREADS_API'),
+      mode: "START_PRIVATE",
+      submit: misago.get("PRIVATE_THREADS_API"),
 
       to: [this.props.profile]
-    });
-  };
+    })
+  }
 
   render() {
-    const canMessage = this.props.user.acl.can_start_private_threads;
-    const isProfileOwner = this.props.user.id === this.props.profile.id;
+    const canMessage = this.props.user.acl.can_start_private_threads
+    const isProfileOwner = this.props.user.id === this.props.profile.id
 
-    if (!canMessage || isProfileOwner) return null;
+    if (!canMessage || isProfileOwner) return null
 
     return (
       <button
@@ -25,11 +24,9 @@ export default class extends React.Component {
         onClick={this.onClick}
         type="button"
       >
-        <span className="material-icon">
-          comment
-        </span>
+        <span className="material-icon">comment</span>
         {gettext("Message")}
       </button>
     )
   }
-}
+}

+ 133 - 110
frontend/src/components/profile/moderation/avatar-controls.js

@@ -1,53 +1,56 @@
-import React from 'react'; // jshint ignore:line
-import Button from 'misago/components/button'; // jshint ignore:line
-import Form from 'misago/components/form';
-import FormGroup from 'misago/components/form-group'; // jshint ignore:line
-import Loader from 'misago/components/modal-loader'; // jshint ignore:line
-import YesNoSwitch from 'misago/components/yes-no-switch'; // jshint ignore:line
-import ModalMessage from 'misago/components/modal-message'; // jshint ignore:line
-import { updateAvatar } from 'misago/reducers/users'; // jshint ignore:line
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
+import React from "react"
+import Button from "misago/components/button"
+import Form from "misago/components/form"
+import FormGroup from "misago/components/form-group"
+import Loader from "misago/components/modal-loader"
+import YesNoSwitch from "misago/components/yes-no-switch"
+import ModalMessage from "misago/components/modal-message"
+import { updateAvatar } from "misago/reducers/users"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoaded: false,
       isLoading: false,
       error: null,
 
-      is_avatar_locked: '',
-      avatar_lock_user_message: '',
-      avatar_lock_staff_message: ''
-    };
+      is_avatar_locked: "",
+      avatar_lock_user_message: "",
+      avatar_lock_staff_message: ""
+    }
   }
 
   componentDidMount() {
-    ajax.get(this.props.profile.api.moderate_avatar).then((options) => {
-      this.setState({
-        isLoaded: true,
-
-        is_avatar_locked: options.is_avatar_locked,
-        avatar_lock_user_message: options.avatar_lock_user_message || '',
-        avatar_lock_staff_message: options.avatar_lock_staff_message || ''
-      });
-    }, (rejection) => {
-      this.setState({
-        isLoaded: true,
-        error: rejection.detail
-      });
-    });
+    ajax.get(this.props.profile.api.moderate_avatar).then(
+      options => {
+        this.setState({
+          isLoaded: true,
+
+          is_avatar_locked: options.is_avatar_locked,
+          avatar_lock_user_message: options.avatar_lock_user_message || "",
+          avatar_lock_staff_message: options.avatar_lock_staff_message || ""
+        })
+      },
+      rejection => {
+        this.setState({
+          isLoaded: true,
+          error: rejection.detail
+        })
+      }
+    )
   }
 
   clean() {
     if (this.isValid()) {
-      return true;
+      return true
     } else {
-      snackbar.error(this.validate().username[0]);
-      return false;
+      snackbar.error(this.validate().username[0])
+      return false
     }
   }
 
@@ -56,105 +59,125 @@ export default class extends Form {
       is_avatar_locked: this.state.is_avatar_locked,
       avatar_lock_user_message: this.state.avatar_lock_user_message,
       avatar_lock_staff_message: this.state.avatar_lock_staff_message
-    });
+    })
   }
 
   handleSuccess(apiResponse) {
-    store.dispatch(updateAvatar(this.props.profile, apiResponse.avatar_hash));
-    snackbar.success(gettext("Avatar controls have been changed."));
+    store.dispatch(updateAvatar(this.props.profile, apiResponse.avatar_hash))
+    snackbar.success(gettext("Avatar controls have been changed."))
   }
 
   getFormBody() {
-    /* jshint ignore:start */
-    return <form onSubmit={this.handleSubmit}>
-      <div className="modal-body">
-
-        <FormGroup label={gettext("Lock avatar")}
-                   helpText={gettext("Locking user avatar will prohibit user from changing his avatar and will reset his/her avatar to default one.")}
-                   for="id_is_avatar_locked">
-          <YesNoSwitch id="id_is_avatar_locked"
-                       disabled={this.state.isLoading}
-                       iconOn="lock_outline"
-                       iconOff="lock_open"
-                       labelOn={gettext("Disallow user from changing avatar")}
-                       labelOff={gettext("Allow user to change avatar")}
-                       onChange={this.bindInput('is_avatar_locked')}
-                       value={this.state.is_avatar_locked} />
-        </FormGroup>
-
-        <FormGroup label={gettext("User message")}
-                   helpText={gettext("Optional message for user explaining why he/she is prohibited form changing avatar.")}
-                   for="id_avatar_lock_user_message">
-          <textarea id="id_avatar_lock_user_message"
-                    className="form-control"
-                    rows="4"
-                    disabled={this.state.isLoading}
-                    onChange={this.bindInput('avatar_lock_user_message')}
-                    value={this.state.avatar_lock_user_message} />
-        </FormGroup>
-
-        <FormGroup label={gettext("Staff message")}
-                   helpText={gettext("Optional message for forum team members explaining why user is prohibited form changing avatar.")}
-                   for="id_avatar_lock_staff_message">
-          <textarea id="id_avatar_lock_staff_message"
-                    className="form-control"
-                    rows="4"
-                    disabled={this.state.isLoading}
-                    onChange={this.bindInput('avatar_lock_staff_message')}
-                    value={this.state.avatar_lock_staff_message} />
-        </FormGroup>
-
-      </div>
-      <div className="modal-footer">
-        <button type="button" className="btn btn-default" data-dismiss="modal">
-          {gettext("Close")}
-        </button>
-        <Button className="btn-primary" loading={this.state.isLoading}>
-          {gettext("Save changes")}
-        </Button>
-      </div>
-    </form>;
-    /* jshint ignore:end */
+    return (
+      <form onSubmit={this.handleSubmit}>
+        <div className="modal-body">
+          <FormGroup
+            label={gettext("Lock avatar")}
+            helpText={gettext(
+              "Locking user avatar will prohibit user from changing his avatar and will reset his/her avatar to default one."
+            )}
+            for="id_is_avatar_locked"
+          >
+            <YesNoSwitch
+              id="id_is_avatar_locked"
+              disabled={this.state.isLoading}
+              iconOn="lock_outline"
+              iconOff="lock_open"
+              labelOn={gettext("Disallow user from changing avatar")}
+              labelOff={gettext("Allow user to change avatar")}
+              onChange={this.bindInput("is_avatar_locked")}
+              value={this.state.is_avatar_locked}
+            />
+          </FormGroup>
+
+          <FormGroup
+            label={gettext("User message")}
+            helpText={gettext(
+              "Optional message for user explaining why he/she is prohibited form changing avatar."
+            )}
+            for="id_avatar_lock_user_message"
+          >
+            <textarea
+              id="id_avatar_lock_user_message"
+              className="form-control"
+              rows="4"
+              disabled={this.state.isLoading}
+              onChange={this.bindInput("avatar_lock_user_message")}
+              value={this.state.avatar_lock_user_message}
+            />
+          </FormGroup>
+
+          <FormGroup
+            label={gettext("Staff message")}
+            helpText={gettext(
+              "Optional message for forum team members explaining why user is prohibited form changing avatar."
+            )}
+            for="id_avatar_lock_staff_message"
+          >
+            <textarea
+              id="id_avatar_lock_staff_message"
+              className="form-control"
+              rows="4"
+              disabled={this.state.isLoading}
+              onChange={this.bindInput("avatar_lock_staff_message")}
+              value={this.state.avatar_lock_staff_message}
+            />
+          </FormGroup>
+        </div>
+        <div className="modal-footer">
+          <button
+            type="button"
+            className="btn btn-default"
+            data-dismiss="modal"
+          >
+            {gettext("Close")}
+          </button>
+          <Button className="btn-primary" loading={this.state.isLoading}>
+            {gettext("Save changes")}
+          </Button>
+        </div>
+      </form>
+    )
   }
 
   getModalBody() {
     if (this.state.error) {
-      /* jshint ignore:start */
-      return <ModalMessage icon="remove_circle_outline"
-                           message={this.state.error} />;
-      /* jshint ignore:end */
+      return (
+        <ModalMessage icon="remove_circle_outline" message={this.state.error} />
+      )
     } else if (this.state.isLoaded) {
-      return this.getFormBody();
+      return this.getFormBody()
     } else {
-      /* jshint ignore:start */
-      return <Loader />;
-      /* jshint ignore:end */
+      return <Loader />
     }
   }
 
   getClassName() {
     if (this.state.error) {
-      return "modal-dialog modal-message modal-avatar-controls";
+      return "modal-dialog modal-message modal-avatar-controls"
     } else {
-      return "modal-dialog modal-avatar-controls";
+      return "modal-dialog modal-avatar-controls"
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className={this.getClassName()}
-                role="document">
-      <div className="modal-content">
-        <div className="modal-header">
-          <button type="button" className="close" data-dismiss="modal"
-                  aria-label={gettext("Close")}>
-            <span aria-hidden="true">&times;</span>
-          </button>
-          <h4 className="modal-title">{gettext("Avatar controls")}</h4>
+    return (
+      <div className={this.getClassName()} role="document">
+        <div className="modal-content">
+          <div className="modal-header">
+            <button
+              type="button"
+              className="close"
+              data-dismiss="modal"
+              aria-label={gettext("Close")}
+            >
+              <span aria-hidden="true">&times;</span>
+            </button>
+            <h4 className="modal-title">{gettext("Avatar controls")}</h4>
+          </div>
+          {this.getModalBody()}
         </div>
-        {this.getModalBody()}
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 95 - 90
frontend/src/components/profile/moderation/change-username.js

@@ -1,143 +1,148 @@
-import React from 'react'; // jshint ignore:line
-import Button from 'misago/components/button'; // jshint ignore:line
-import Form from 'misago/components/form';
-import FormGroup from 'misago/components/form-group'; // jshint ignore:line
-import Loader from 'misago/components/modal-loader'; // jshint ignore:line
-import ModalMessage from 'misago/components/modal-message'; // jshint ignore:line
-import { addNameChange } from 'misago/reducers/username-history'; // jshint ignore:line
-import { updateUsername } from 'misago/reducers/users'; // jshint ignore:line
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
-import * as validators from 'misago/utils/validators';
+import React from "react"
+import Button from "misago/components/button"
+import Form from "misago/components/form"
+import FormGroup from "misago/components/form-group"
+import Loader from "misago/components/modal-loader"
+import ModalMessage from "misago/components/modal-message"
+import { addNameChange } from "misago/reducers/username-history"
+import { updateUsername } from "misago/reducers/users"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
+import * as validators from "misago/utils/validators"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoaded: false,
       isLoading: false,
       error: null,
 
-      username: '',
+      username: "",
       validators: {
-        username: [
-          validators.usernameContent()
-        ]
+        username: [validators.usernameContent()]
       }
-    };
+    }
   }
 
   componentDidMount() {
-    ajax.get(this.props.profile.api.moderate_username).then(() => {
-      this.setState({
-        isLoaded: true
-      });
-    }, (rejection) => {
-      this.setState({
-        isLoaded: true,
-        error: rejection.detail
-      });
-    });
+    ajax.get(this.props.profile.api.moderate_username).then(
+      () => {
+        this.setState({
+          isLoaded: true
+        })
+      },
+      rejection => {
+        this.setState({
+          isLoaded: true,
+          error: rejection.detail
+        })
+      }
+    )
   }
 
   clean() {
     if (this.isValid()) {
-      return true;
+      return true
     } else {
-      snackbar.error(this.validate().username[0]);
-      return false;
+      snackbar.error(this.validate().username[0])
+      return false
     }
   }
 
   send() {
     return ajax.post(this.props.profile.api.moderate_username, {
       username: this.state.username
-    });
+    })
   }
 
   handleSuccess(apiResponse) {
     this.setState({
-      username: ''
-    });
+      username: ""
+    })
 
-    store.dispatch(addNameChange(
-      apiResponse, this.props.profile, this.props.user));
-    store.dispatch(updateUsername(
-      this.props.profile, apiResponse.username, apiResponse.slug));
+    store.dispatch(
+      addNameChange(apiResponse, this.props.profile, this.props.user)
+    )
+    store.dispatch(
+      updateUsername(this.props.profile, apiResponse.username, apiResponse.slug)
+    )
 
-    snackbar.success(gettext("Username has been changed."));
+    snackbar.success(gettext("Username has been changed."))
   }
 
   getFormBody() {
-    /* jshint ignore:start */
-    return <form onSubmit={this.handleSubmit}>
-      <div className="modal-body">
-
-        <FormGroup label={gettext("New username")} for="id_username">
-          <input type="text" id="id_username" className="form-control"
-                 disabled={this.state.isLoading}
-                 onChange={this.bindInput('username')}
-                 value={this.state.username} />
-        </FormGroup>
-
-      </div>
-      <div className="modal-footer">
-        <button
-          className="btn btn-default"
-          data-dismiss="modal"
-          disabled={this.state.isLoading}
-          type="button"
-        >
-          {gettext("Cancel")}
-        </button>
-        <Button className="btn-primary" loading={this.state.isLoading}>
-          {gettext("Change username")}
-        </Button>
-      </div>
-    </form>;
-    /* jshint ignore:end */
+    return (
+      <form onSubmit={this.handleSubmit}>
+        <div className="modal-body">
+          <FormGroup label={gettext("New username")} for="id_username">
+            <input
+              type="text"
+              id="id_username"
+              className="form-control"
+              disabled={this.state.isLoading}
+              onChange={this.bindInput("username")}
+              value={this.state.username}
+            />
+          </FormGroup>
+        </div>
+        <div className="modal-footer">
+          <button
+            className="btn btn-default"
+            data-dismiss="modal"
+            disabled={this.state.isLoading}
+            type="button"
+          >
+            {gettext("Cancel")}
+          </button>
+          <Button className="btn-primary" loading={this.state.isLoading}>
+            {gettext("Change username")}
+          </Button>
+        </div>
+      </form>
+    )
   }
 
   getModalBody() {
     if (this.state.error) {
-      /* jshint ignore:start */
-      return <ModalMessage icon="remove_circle_outline"
-                           message={this.state.error} />;
-      /* jshint ignore:end */
+      return (
+        <ModalMessage icon="remove_circle_outline" message={this.state.error} />
+      )
     } else if (this.state.isLoaded) {
-      return this.getFormBody();
+      return this.getFormBody()
     } else {
-      /* jshint ignore:start */
-      return <Loader />;
-      /* jshint ignore:end */
+      return <Loader />
     }
   }
 
   getClassName() {
     if (this.state.error) {
-      return "modal-dialog modal-message modal-rename-user";
+      return "modal-dialog modal-message modal-rename-user"
     } else {
-      return "modal-dialog modal-rename-user";
+      return "modal-dialog modal-rename-user"
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className={this.getClassName()}
-                role="document">
-      <div className="modal-content">
-        <div className="modal-header">
-          <button type="button" className="close" data-dismiss="modal"
-                  aria-label={gettext("Close")}>
-            <span aria-hidden="true">&times;</span>
-          </button>
-          <h4 className="modal-title">{gettext("Change username")}</h4>
+    return (
+      <div className={this.getClassName()} role="document">
+        <div className="modal-content">
+          <div className="modal-header">
+            <button
+              type="button"
+              className="close"
+              data-dismiss="modal"
+              aria-label={gettext("Close")}
+            >
+              <span aria-hidden="true">&times;</span>
+            </button>
+            <h4 className="modal-title">{gettext("Change username")}</h4>
+          </div>
+          {this.getModalBody()}
         </div>
-        {this.getModalBody()}
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 139 - 121
frontend/src/components/profile/moderation/delete-account.js

@@ -1,17 +1,17 @@
-import React from 'react'; // jshint ignore:line
-import Button from 'misago/components/button'; // jshint ignore:line
-import Form from 'misago/components/form';
-import FormGroup from 'misago/components/form-group'; // jshint ignore:line
-import Loader from 'misago/components/modal-loader'; // jshint ignore:line
-import ModalMessage from 'misago/components/modal-message'; // jshint ignore:line
-import YesNoSwitch from 'misago/components/yes-no-switch'; // jshint ignore:line
-import misago from 'misago/index'; // jshint ignore:line
-import ajax from 'misago/services/ajax';
-import polls from 'misago/services/polls';
+import React from "react"
+import Button from "misago/components/button"
+import Form from "misago/components/form"
+import FormGroup from "misago/components/form-group"
+import Loader from "misago/components/modal-loader"
+import ModalMessage from "misago/components/modal-message"
+import YesNoSwitch from "misago/components/yes-no-switch"
+import misago from "misago/index"
+import ajax from "misago/services/ajax"
+import polls from "misago/services/polls"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoaded: false,
@@ -23,176 +23,194 @@ export default class extends Form {
       confirm: false,
 
       with_content: false
-    };
+    }
   }
 
   componentDidMount() {
-    ajax.get(this.props.profile.api.delete).then(() => {
-      this.setState({
-        isLoaded: true
-      });
+    ajax.get(this.props.profile.api.delete).then(
+      () => {
+        this.setState({
+          isLoaded: true
+        })
 
-      this.countdown();
-    }, (rejection) => {
-      this.setState({
-        isLoaded: true,
-        error: rejection.detail
-      });
-    });
+        this.countdown()
+      },
+      rejection => {
+        this.setState({
+          isLoaded: true,
+          error: rejection.detail
+        })
+      }
+    )
   }
 
-  /* jshint ignore:start */
   countdown = () => {
     window.setTimeout(() => {
       if (this.state.countdown > 1) {
         this.setState({
-          countdown: this.state.countdown - 1,
-        });
-        this.countdown();
+          countdown: this.state.countdown - 1
+        })
+        this.countdown()
       } else if (!this.state.confirm) {
         this.setState({
           confirm: true
-        });
+        })
       }
-    }, 1000);
-  };
-  /* jshint ignore:end */
+    }, 1000)
+  }
 
   send() {
     return ajax.post(this.props.profile.api.delete, {
       with_content: this.state.with_content
-    });
+    })
   }
 
   handleSuccess() {
-    polls.stop('user-profile');
+    polls.stop("user-profile")
 
     if (this.state.with_content) {
       this.setState({
-        isDeleted: interpolate(gettext("%(username)s's account, threads, posts and other content has been deleted."), {
-          'username': this.props.profile.username
-        }, true)
-      });
+        isDeleted: interpolate(
+          gettext(
+            "%(username)s's account, threads, posts and other content has been deleted."
+          ),
+          {
+            username: this.props.profile.username
+          },
+          true
+        )
+      })
     } else {
       this.setState({
-        isDeleted: interpolate(gettext("%(username)s's account has been deleted and other content has been hidden."), {
-          'username': this.props.profile.username
-        }, true)
-      });
+        isDeleted: interpolate(
+          gettext(
+            "%(username)s's account has been deleted and other content has been hidden."
+          ),
+          {
+            username: this.props.profile.username
+          },
+          true
+        )
+      })
     }
   }
 
   getButtonLabel() {
     if (this.state.confirm) {
-      return interpolate(gettext("Delete %(username)s"), {
-        'username': this.props.profile.username
-      }, true);
+      return interpolate(
+        gettext("Delete %(username)s"),
+        {
+          username: this.props.profile.username
+        },
+        true
+      )
     } else {
-      return interpolate(gettext("Please wait... (%(countdown)ss)"), {
-        'countdown': this.state.countdown
-      }, true);
+      return interpolate(
+        gettext("Please wait... (%(countdown)ss)"),
+        {
+          countdown: this.state.countdown
+        },
+        true
+      )
     }
   }
 
   getForm() {
-    /* jshint ignore:start */
-    return <form onSubmit={this.handleSubmit}>
-      <div className="modal-body">
-
-        <FormGroup label={gettext("User content")}
-                   for="id_with_content">
-          <YesNoSwitch id="id_with_content"
-                       disabled={this.state.isLoading}
-                       labelOn={gettext("Delete together with user's account")}
-                       labelOff={gettext("Hide after deleting user's account")}
-                       onChange={this.bindInput('with_content')}
-                       value={this.state.with_content} />
-        </FormGroup>
-
-      </div>
-      <div className="modal-footer">
-
-        <button type="button"
-                className="btn btn-default"
-                data-dismiss="modal">
-          {gettext("Cancel")}
-        </button>
-
-        <Button className="btn-danger"
-                loading={this.state.isLoading}
-                disabled={!this.state.confirm}>
-          {this.getButtonLabel()}
-        </Button>
+    return (
+      <form onSubmit={this.handleSubmit}>
+        <div className="modal-body">
+          <FormGroup label={gettext("User content")} for="id_with_content">
+            <YesNoSwitch
+              id="id_with_content"
+              disabled={this.state.isLoading}
+              labelOn={gettext("Delete together with user's account")}
+              labelOff={gettext("Hide after deleting user's account")}
+              onChange={this.bindInput("with_content")}
+              value={this.state.with_content}
+            />
+          </FormGroup>
+        </div>
+        <div className="modal-footer">
+          <button
+            type="button"
+            className="btn btn-default"
+            data-dismiss="modal"
+          >
+            {gettext("Cancel")}
+          </button>
 
-      </div>
-    </form>;
-    /* jshint ignore:end */
+          <Button
+            className="btn-danger"
+            loading={this.state.isLoading}
+            disabled={!this.state.confirm}
+          >
+            {this.getButtonLabel()}
+          </Button>
+        </div>
+      </form>
+    )
   }
 
   getDeletedBody() {
-    /* jshint ignore:start */
-    return <div className="modal-body">
-      <div className="message-icon">
-        <span className="material-icon">
-          info_outline
-        </span>
-      </div>
-      <div className="message-body">
-        <p className="lead">
-          {this.state.isDeleted}
-        </p>
-        <p>
-          <a href={misago.get('USERS_LIST_URL')}>
-            {gettext("Return to users list")}
-          </a>
-        </p>
+    return (
+      <div className="modal-body">
+        <div className="message-icon">
+          <span className="material-icon">info_outline</span>
+        </div>
+        <div className="message-body">
+          <p className="lead">{this.state.isDeleted}</p>
+          <p>
+            <a href={misago.get("USERS_LIST_URL")}>
+              {gettext("Return to users list")}
+            </a>
+          </p>
+        </div>
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
 
   getModalBody() {
     if (this.state.error) {
-      /* jshint ignore:start */
-      return <ModalMessage icon="remove_circle_outline"
-                           message={this.state.error} />;
-      /* jshint ignore:end */
+      return (
+        <ModalMessage icon="remove_circle_outline" message={this.state.error} />
+      )
     } else if (this.state.isLoaded) {
       if (this.state.isDeleted) {
-        return this.getDeletedBody();
+        return this.getDeletedBody()
       } else {
-        return this.getForm();
+        return this.getForm()
       }
     } else {
-      /* jshint ignore:start */
-      return <Loader />;
-      /* jshint ignore:end */
+      return <Loader />
     }
   }
 
   getClassName() {
     if (this.state.error || this.state.isDeleted) {
-      return "modal-dialog modal-message modal-delete-account";
+      return "modal-dialog modal-message modal-delete-account"
     } else {
-      return "modal-dialog modal-delete-account";
+      return "modal-dialog modal-delete-account"
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className={this.getClassName()}
-                role="document">
-      <div className="modal-content">
-        <div className="modal-header">
-          <button type="button" className="close" data-dismiss="modal"
-                  aria-label={gettext("Close")}>
-            <span aria-hidden="true">&times;</span>
-          </button>
-          <h4 className="modal-title">{gettext("Delete user account")}</h4>
+    return (
+      <div className={this.getClassName()} role="document">
+        <div className="modal-content">
+          <div className="modal-header">
+            <button
+              type="button"
+              className="close"
+              data-dismiss="modal"
+              aria-label={gettext("Close")}
+            >
+              <span aria-hidden="true">&times;</span>
+            </button>
+            <h4 className="modal-title">{gettext("Delete user account")}</h4>
+          </div>
+          {this.getModalBody()}
         </div>
-        {this.getModalBody()}
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 26 - 48
frontend/src/components/profile/moderation/nav.js

@@ -1,30 +1,25 @@
-import React from 'react';
-import { connect } from 'react-redux'; // jshint ignore:line
-import AvatarControls from 'misago/components/profile/moderation/avatar-controls'; // jshint ignore:line
-import ChangeUsername from 'misago/components/profile/moderation/change-username'; // jshint ignore:line
-import DeleteAccount from 'misago/components/profile/moderation/delete-account'; // jshint ignore:line
-import modal from 'misago/services/modal'; // jshint ignore:line
+import React from "react"
+import { connect } from "react-redux"
+import AvatarControls from "misago/components/profile/moderation/avatar-controls"
+import ChangeUsername from "misago/components/profile/moderation/change-username"
+import DeleteAccount from "misago/components/profile/moderation/delete-account"
+import modal from "misago/services/modal"
 
-/* jshint ignore:start */
 let select = function(store) {
   return {
     tick: store.tick,
     user: store.auth,
-    profile: store.profile,
-  };
-};
-/* jshint ignore:end */
+    profile: store.profile
+  }
+}
 
 export default class extends React.Component {
-  /* jshint ignore:start */
   showAvatarDialog = () => {
-    modal.show(connect(select)(AvatarControls));
-  };
-  /* jshint ignore:end */
+    modal.show(connect(select)(AvatarControls))
+  }
 
   getAvatarButton() {
     if (this.props.profile.acl.can_moderate_avatar) {
-      /* jshint ignore:start */
       return (
         <li>
           <button
@@ -32,28 +27,22 @@ export default class extends React.Component {
             className="btn btn-link"
             onClick={this.showAvatarDialog}
           >
-            <span className="material-icon">
-              portrait
-            </span>
+            <span className="material-icon">portrait</span>
             {gettext("Avatar controls")}
           </button>
         </li>
-      );
-      /* jshint ignore:end */
+      )
     } else {
-      return null;
+      return null
     }
   }
 
-  /* jshint ignore:start */
   showRenameDialog = () => {
-    modal.show(connect(select)(ChangeUsername));
-  };
-  /* jshint ignore:end */
+    modal.show(connect(select)(ChangeUsername))
+  }
 
   getRenameButton() {
     if (this.props.profile.acl.can_rename) {
-      /* jshint ignore:start */
       return (
         <li>
           <button
@@ -61,28 +50,22 @@ export default class extends React.Component {
             className="btn btn-link"
             onClick={this.showRenameDialog}
           >
-            <span className="material-icon">
-              credit_card
-            </span>
+            <span className="material-icon">credit_card</span>
             {gettext("Change username")}
           </button>
         </li>
-      );
-      /* jshint ignore:end */
+      )
     } else {
-      return null;
+      return null
     }
   }
 
-  /* jshint ignore:start */
   showDeleteDialog = () => {
-    modal.show(connect(select)(DeleteAccount));
-  };
-  /* jshint ignore:end */
+    modal.show(connect(select)(DeleteAccount))
+  }
 
   getDeleteButton() {
     if (this.props.profile.acl.can_delete) {
-      /* jshint ignore:start */
       return (
         <li>
           <button
@@ -90,21 +73,17 @@ export default class extends React.Component {
             className="btn btn-link"
             onClick={this.showDeleteDialog}
           >
-            <span className="material-icon">
-              clear
-            </span>
+            <span className="material-icon">clear</span>
             {gettext("Delete account")}
           </button>
         </li>
-      );
-      /* jshint ignore:end */
+      )
     } else {
-      return null;
+      return null
     }
   }
 
   render() {
-    /* jshint ignore:start */
     return (
       <ul
         className="dropdown-menu dropdown-menu-right stick-to-bottom"
@@ -114,7 +93,6 @@ export default class extends React.Component {
         {this.getRenameButton()}
         {this.getDeleteButton()}
       </ul>
-    );
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 16 - 24
frontend/src/components/profile/navs.js

@@ -1,62 +1,54 @@
-import React from 'react';
-import { Link } from 'react-router'; // jshint ignore:line
-import Li from 'misago/components/li'; //jshint ignore:line
-import FollowButton from 'misago/components/profile/follow-button'; // jshint ignore:line
-import misago from 'misago/index'; //jshint ignore:line
+import React from "react"
+import { Link } from "react-router"
+import Li from "misago/components/li"
+import FollowButton from "misago/components/profile/follow-button"
+import misago from "misago/index"
 
 export class SideNav extends React.Component {
   render() {
-    // jshint ignore:start
     return (
       <div className="list-group nav-side">
-        {this.props.pages.map((page) => {
+        {this.props.pages.map(page => {
           return (
             <Link
-              to={this.props.baseUrl + page.component + '/'}
+              to={this.props.baseUrl + page.component + "/"}
               className="list-group-item"
               activeClassName="active"
               key={page.component}
             >
-              <span className="material-icon">
-                {page.icon}
-              </span>
+              <span className="material-icon">{page.icon}</span>
               {page.name}
             </Link>
-          );
+          )
         })}
       </div>
-    );
-    // jshint ignore:end
+    )
   }
 }
 
-// jshint ignore:start
 export function CompactNav(props) {
   return (
     <div className="page-tabs hidden-md hidden-lg">
       <div className="container">
         <ul className="nav nav-pills" role="menu">
-          {props.pages.map((page) => {
+          {props.pages.map(page => {
             return (
               <Li
-                path={props.baseUrl + page.component + '/'}
+                path={props.baseUrl + page.component + "/"}
                 key={page.component}
               >
                 <Link
-                  to={props.baseUrl + page.component + '/'}
+                  to={props.baseUrl + page.component + "/"}
                   onClick={props.hideNav}
                 >
-                  <span className="material-icon">
-                    {page.icon}
-                  </span>
+                  <span className="material-icon">{page.icon}</span>
                   {page.name}
                 </Link>
               </Li>
-            );
+            )
           })}
         </ul>
       </div>
     </div>
-  );
+  )
 }
-// jshint ignore:end

+ 50 - 59
frontend/src/components/profile/root.js

@@ -1,51 +1,47 @@
-import React from 'react'; // jshint ignore:line
-import { connect } from 'react-redux';
-import BanDetails from './ban-details'; // jshint ignore:line
-import Details from './details'; // jshint ignore:line
-import { Posts, Threads } from './feed'; // jshint ignore:line
-import Followers from './followers'; // jshint ignore:line
-import Follows from './follows'; // jshint ignore:line
-import UsernameHistory from './username-history'; // jshint ignore:line
-import Header from './header'; // jshint ignore:line
-import ModerationNav from './moderation/nav'; // jshint ignore:line
-import { SideNav, CompactNav } from './navs'; // jshint ignore:line
-import Avatar from 'misago/components/avatar'; // jshint ignore:line
-import WithDropdown from 'misago/components/with-dropdown';
-import misago from 'misago';
-import { hydrate } from 'misago/reducers/profile'; // jshint ignore:line
-import polls from 'misago/services/polls';
-import store from 'misago/services/store'; // jshint ignore:line
+import React from "react"
+import { connect } from "react-redux"
+import BanDetails from "./ban-details"
+import Details from "./details"
+import { Posts, Threads } from "./feed"
+import Followers from "./followers"
+import Follows from "./follows"
+import UsernameHistory from "./username-history"
+import Header from "./header"
+import ModerationNav from "./moderation/nav"
+import { SideNav, CompactNav } from "./navs"
+import Avatar from "misago/components/avatar"
+import WithDropdown from "misago/components/with-dropdown"
+import misago from "misago"
+import { hydrate } from "misago/reducers/profile"
+import polls from "misago/services/polls"
+import store from "misago/services/store"
 
 export default class extends WithDropdown {
   constructor(props) {
-    super(props);
+    super(props)
 
-    this.startPolling(props.profile.api.index);
+    this.startPolling(props.profile.api.index)
   }
 
   startPolling(api) {
     polls.start({
-      poll: 'user-profile',
+      poll: "user-profile",
       url: api,
       frequency: 90 * 1000,
       update: this.update
-    });
+    })
   }
 
-  /* jshint ignore:start */
-  update = (data) => {
-    store.dispatch(hydrate(data));
-  };
-  /* jshint ignore:end */
+  update = data => {
+    store.dispatch(hydrate(data))
+  }
 
   render() {
-    /* jshint ignore:start */
-    const baseUrl = misago.get('PROFILE').url;
-    const pages = misago.get('PROFILE_PAGES');
+    const baseUrl = misago.get("PROFILE").url
+    const pages = misago.get("PROFILE_PAGES")
 
     return (
       <div className="page page-user-profile">
-
         <Header
           baseUrl={baseUrl}
           pages={pages}
@@ -55,10 +51,8 @@ export default class extends WithDropdown {
           user={this.props.user}
         />
         <div className="container">
-
           <div className="row">
             <div className="col-md-3 hidden-xs hidden-sm">
-
               <div className="profile-side-avatar">
                 <Avatar user={this.props.profile} size="400" />
               </div>
@@ -68,17 +62,12 @@ export default class extends WithDropdown {
                 pages={pages}
                 profile={this.props.profile}
               />
-
-            </div>
-            <div className="col-md-9">
-              {this.props.children}
             </div>
+            <div className="col-md-9">{this.props.children}</div>
           </div>
-
         </div>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 }
 
@@ -91,29 +80,31 @@ export function select(store) {
     users: store.users,
     posts: store.posts,
     profile: store.profile,
-    profileDetails: store['profile-details'],
-    'username-history': store['username-history']
-  };
+    profileDetails: store["profile-details"],
+    "username-history": store["username-history"]
+  }
 }
 
 const COMPONENTS = {
-  'posts': Posts,
-  'threads': Threads,
-  'followers': Followers,
-  'follows': Follows,
-  'details': Details,
-  'username-history': UsernameHistory,
-  'ban-details': BanDetails
-};
+  posts: Posts,
+  threads: Threads,
+  followers: Followers,
+  follows: Follows,
+  details: Details,
+  "username-history": UsernameHistory,
+  "ban-details": BanDetails
+}
 
 export function paths() {
-  let paths = [];
-  misago.get('PROFILE_PAGES').forEach(function(item) {
-    paths.push(Object.assign({}, item, {
-      path: misago.get('PROFILE').url + item.component + '/',
-      component: connect(select)(COMPONENTS[item.component]),
-    }));
-  });
-
-  return paths;
+  let paths = []
+  misago.get("PROFILE_PAGES").forEach(function(item) {
+    paths.push(
+      Object.assign({}, item, {
+        path: misago.get("PROFILE").url + item.component + "/",
+        component: connect(select)(COMPONENTS[item.component])
+      })
+    )
+  })
+
+  return paths
 }

+ 137 - 107
frontend/src/components/profile/username-history.js

@@ -1,22 +1,22 @@
-import React from 'react';
-import Button from 'misago/components/button'; // jshint ignore:line
-import Search from 'misago/components/quick-search'; // jshint ignore:line
-import UsernameHistory from 'misago/components/username-history/root'; // jshint ignore:line
-import misago from 'misago/index';
-import { hydrate, append } from 'misago/reducers/username-history'; // jshint ignore:line
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
-import title from 'misago/services/page-title';
+import React from "react"
+import Button from "misago/components/button"
+import Search from "misago/components/quick-search"
+import UsernameHistory from "misago/components/username-history/root"
+import misago from "misago/index"
+import { hydrate, append } from "misago/reducers/username-history"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
+import title from "misago/services/page-title"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
-    if (misago.has('PROFILE_NAME_HISTORY')) {
-      this.initWithPreloadedData(misago.pop('PROFILE_NAME_HISTORY'));
+    if (misago.has("PROFILE_NAME_HISTORY")) {
+      this.initWithPreloadedData(misago.pop("PROFILE_NAME_HISTORY"))
     } else {
-      this.initWithoutPreloadedData();
+      this.initWithoutPreloadedData()
     }
   }
 
@@ -25,16 +25,16 @@ export default class extends React.Component {
       isLoaded: true,
       isBusy: false,
 
-      search: '',
+      search: "",
 
       count: data.count,
       more: data.more,
 
       page: data.page,
       pages: data.pages
-    };
+    }
 
-    store.dispatch(hydrate(data.results));
+    store.dispatch(hydrate(data.results))
   }
 
   initWithoutPreloadedData() {
@@ -42,62 +42,70 @@ export default class extends React.Component {
       isLoaded: false,
       isBusy: false,
 
-      search: '',
+      search: "",
 
       count: 0,
       more: 0,
 
       page: 1,
       pages: 1
-    };
+    }
 
-    this.loadChanges();
+    this.loadChanges()
   }
 
-  loadChanges(page=1, search=null) {
-    ajax.get(misago.get('USERNAME_CHANGES_API'), {
-      user: this.props.profile.id,
-      search: search,
-      page: page || 1
-    }, 'search-username-history').then((data) => {
-      if (page === 1) {
-        store.dispatch(hydrate(data.results));
-      } else {
-        store.dispatch(append(data.results));
-      }
-
-      this.setState({
-        isLoaded: true,
-        isBusy: false,
-
-        count: data.count,
-        more: data.more,
-
-        page: data.page,
-        pages: data.pages
-      });
-    }, (rejection) => {
-      snackbar.apiError(rejection);
-    });
+  loadChanges(page = 1, search = null) {
+    ajax
+      .get(
+        misago.get("USERNAME_CHANGES_API"),
+        {
+          user: this.props.profile.id,
+          search: search,
+          page: page || 1
+        },
+        "search-username-history"
+      )
+      .then(
+        data => {
+          if (page === 1) {
+            store.dispatch(hydrate(data.results))
+          } else {
+            store.dispatch(append(data.results))
+          }
+
+          this.setState({
+            isLoaded: true,
+            isBusy: false,
+
+            count: data.count,
+            more: data.more,
+
+            page: data.page,
+            pages: data.pages
+          })
+        },
+        rejection => {
+          snackbar.apiError(rejection)
+        }
+      )
   }
 
   componentDidMount() {
     title.set({
       title: gettext("Username history"),
       parent: this.props.profile.username
-    });
+    })
   }
 
-  /* jshint ignore:start */
   loadMore = () => {
     this.setState({
       isBusy: true
-    });
+    })
 
-    this.loadChanges(this.state.page + 1, this.state.search);
-  };
+    this.loadChanges(this.state.page + 1, this.state.search)
+  }
 
-  search = (ev) => {
+  search = ev => {
     this.setState({
       isLoaded: false,
       isBusy: true,
@@ -109,62 +117,81 @@ export default class extends React.Component {
 
       page: 1,
       pages: 1
-    });
+    })
 
-    this.loadChanges(1, ev.target.value);
-  };
-  /* jshint ignore:end */
+    this.loadChanges(1, ev.target.value)
+  }
 
   getLabel() {
     if (!this.state.isLoaded) {
-      return gettext('Loading...');
+      return gettext("Loading...")
     } else if (this.state.search) {
       let message = ngettext(
         "Found %(changes)s username change.",
         "Found %(changes)s username changes.",
-        this.state.count);
-
-      return interpolate(message, {
-        'changes': this.state.count
-      }, true);
+        this.state.count
+      )
+
+      return interpolate(
+        message,
+        {
+          changes: this.state.count
+        },
+        true
+      )
     } else if (this.props.profile.id === this.props.user.id) {
       let message = ngettext(
         "Your username was changed %(changes)s time.",
         "Your username was changed %(changes)s times.",
-        this.state.count);
-
-      return interpolate(message, {
-        'changes': this.state.count
-      }, true);
+        this.state.count
+      )
+
+      return interpolate(
+        message,
+        {
+          changes: this.state.count
+        },
+        true
+      )
     } else {
       let message = ngettext(
         "%(username)s's username was changed %(changes)s time.",
         "%(username)s's username was changed %(changes)s times.",
-        this.state.count);
-
-      return interpolate(message, {
-        'username': this.props.profile.username,
-        'changes': this.state.count
-      }, true);
+        this.state.count
+      )
+
+      return interpolate(
+        message,
+        {
+          username: this.props.profile.username,
+          changes: this.state.count
+        },
+        true
+      )
     }
   }
 
   getEmptyMessage() {
     if (this.state.search) {
-      return gettext("Search returned no username changes matching specified criteria.");
+      return gettext(
+        "Search returned no username changes matching specified criteria."
+      )
     } else if (this.props.user.id === this.props.profile.id) {
-      return gettext("No name changes have been recorded for your account.");
+      return gettext("No name changes have been recorded for your account.")
     } else {
-      return interpolate(gettext("%(username)s's username was never changed."), {
-        'username': this.props.profile.username
-      }, true);
+      return interpolate(
+        gettext("%(username)s's username was never changed."),
+        {
+          username: this.props.profile.username
+        },
+        true
+      )
     }
   }
 
   getMoreButton() {
-    if (!this.state.more) return null;
+    if (!this.state.more) return null
 
-    /* jshint ignore:start */
     return (
       <div className="pager-more">
         <Button
@@ -172,37 +199,40 @@ export default class extends React.Component {
           loading={this.state.isBusy}
           onClick={this.loadMore}
         >
-          {interpolate(gettext("Show older (%(more)s)"), {
-            'more': this.state.more
-          }, true)}
+          {interpolate(
+            gettext("Show older (%(more)s)"),
+            {
+              more: this.state.more
+            },
+            true
+          )}
         </Button>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className="profile-username-history">
-
-      <nav className="toolbar">
-        <h3 className="toolbar-left">
-          {this.getLabel()}
-        </h3>
-
-        <Search className="toolbar-right"
-                value={this.state.search}
-                onChange={this.search}
-                placeholder={gettext("Search history...")} />
-      </nav>
-
-      <UsernameHistory isLoaded={this.state.isLoaded}
-                       emptyMessage={this.getEmptyMessage()}
-                       changes={this.props['username-history']} />
-
-      {this.getMoreButton()}
-
-    </div>;
-    /* jshint ignore:end */
+    return (
+      <div className="profile-username-history">
+        <nav className="toolbar">
+          <h3 className="toolbar-left">{this.getLabel()}</h3>
+
+          <Search
+            className="toolbar-right"
+            value={this.state.search}
+            onChange={this.search}
+            placeholder={gettext("Search history...")}
+          />
+        </nav>
+
+        <UsernameHistory
+          isLoaded={this.state.isLoaded}
+          emptyMessage={this.getEmptyMessage()}
+          changes={this.props["username-history"]}
+        />
+
+        {this.getMoreButton()}
+      </div>
+    )
   }
-}
+}

+ 16 - 16
frontend/src/components/quick-search.js

@@ -1,26 +1,26 @@
-import React from 'react';
+import React from "react"
 
 export default class extends React.Component {
   getClassName() {
     if (this.props.className) {
-      return "form-search " + this.props.className;
+      return "form-search " + this.props.className
     } else {
-      return "form-search";
+      return "form-search"
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className={this.getClassName()}>
-      <input type="text"
-             className="form-control"
-             value={this.props.value}
-             onChange={this.props.onChange}
-             placeholder={this.props.placeholder || gettext("Search...")} />
-      <span className="material-icon">
-        search
-      </span>
-    </div>;
-    /* jshint ignore:end */
+    return (
+      <div className={this.getClassName()}>
+        <input
+          type="text"
+          className="form-control"
+          value={this.props.value}
+          onChange={this.props.onChange}
+          placeholder={this.props.placeholder || gettext("Search...")}
+        />
+        <span className="material-icon">search</span>
+      </div>
+    )
   }
-}
+}

+ 36 - 43
frontend/src/components/register-button.js

@@ -1,77 +1,70 @@
-import React from 'react';
-import Loader from 'misago/components/loader'; // jshint ignore:line
-import RegisterForm from 'misago/components/register.js'; // jshint ignore:line
-import ajax from 'misago/services/ajax'; // jshint ignore:line
-import captcha from 'misago/services/captcha'; // jshint ignore:line
-import modal from 'misago/services/modal'; // jshint ignore:line
-import snackbar from 'misago/services/snackbar'; // jshint ignore:line
+import React from "react"
+import Loader from "misago/components/loader"
+import RegisterForm from "misago/components/register.js"
+import ajax from "misago/services/ajax"
+import captcha from "misago/services/captcha"
+import modal from "misago/services/modal"
+import snackbar from "misago/services/snackbar"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoading: false,
       isLoaded: false,
 
-      criteria: null,
-    };
+      criteria: null
+    }
   }
 
-  /* jshint ignore:start */
   showRegisterForm = () => {
-    if (misago.get('SETTINGS').account_activation === 'closed') {
-      snackbar.info(gettext("New registrations are currently disabled."));
+    if (misago.get("SETTINGS").account_activation === "closed") {
+      snackbar.info(gettext("New registrations are currently disabled."))
     } else if (this.state.isLoaded) {
-      modal.show(
-        <RegisterForm
-          criteria={this.state.criteria}
-        />
-      );
+      modal.show(<RegisterForm criteria={this.state.criteria} />)
     } else {
-      this.setState({ isLoading: true });
+      this.setState({ isLoading: true })
 
       Promise.all([
         captcha.load(),
-        ajax.get(misago.get('AUTH_CRITERIA_API'))
-      ]).then((result) => {
-        this.setState({
-          isLoading: false,
-          isLoaded: true,
-          criteria: result[1]
-        });
+        ajax.get(misago.get("AUTH_CRITERIA_API"))
+      ]).then(
+        result => {
+          this.setState({
+            isLoading: false,
+            isLoaded: true,
+            criteria: result[1]
+          })
 
-        modal.show(
-          <RegisterForm
-            criteria={result[1]}
-          />
-        );
-      }, () => {
-        this.setState({ isLoading: false });
+          modal.show(<RegisterForm criteria={result[1]} />)
+        },
+        () => {
+          this.setState({ isLoading: false })
 
-        snackbar.error(gettext("Registration is currently unavailable due to an error."));
-      });
+          snackbar.error(
+            gettext("Registration is currently unavailable due to an error.")
+          )
+        }
+      )
     }
-  };
-  /* jshint ignore:end */
+  }
 
   getClassName() {
-    return this.props.className + (this.state.isLoading ? ' btn-loading' : '');
+    return this.props.className + (this.state.isLoading ? " btn-loading" : "")
   }
 
   render() {
-    /* jshint ignore:start */
     return (
       <button
-        className={'btn ' + this.getClassName()}
+        className={"btn " + this.getClassName()}
         disabled={this.state.isLoading}
         onClick={this.showRegisterForm}
         type="button"
       >
         {gettext("Register")}
-        {this.state.isLoading ? <Loader /> : null }
+        {this.state.isLoading ? <Loader /> : null}
       </button>
-    );
-    /* jshint ignore:end */
+    )
   }
 }

+ 232 - 211
frontend/src/components/register.js

@@ -1,318 +1,345 @@
-import React from 'react';
-import Button from 'misago/components/button'; // jshint ignore:line
-import Form from 'misago/components/form';
-import FormGroup from 'misago/components/form-group'; // jshint ignore:line
-import PasswordStrength from 'misago/components/password-strength'; // jshint ignore:line
-import RegisterLegalFootnote from 'misago/components/RegisterLegalFootnote'; // jshint ignore:line
-import StartSocialAuth from 'misago/components/StartSocialAuth'; // jshint ignore:line
-import misago from 'misago';
-import ajax from 'misago/services/ajax';
-import auth from 'misago/services/auth'; // jshint ignore:line
-import captcha from 'misago/services/captcha';
-import modal from 'misago/services/modal';
-import snackbar from 'misago/services/snackbar';
-import showBannedPage from 'misago/utils/banned-page';
-import * as validators from 'misago/utils/validators';
+import React from "react"
+import Button from "misago/components/button"
+import Form from "misago/components/form"
+import FormGroup from "misago/components/form-group"
+import PasswordStrength from "misago/components/password-strength"
+import RegisterLegalFootnote from "misago/components/RegisterLegalFootnote"
+import StartSocialAuth from "misago/components/StartSocialAuth"
+import misago from "misago"
+import ajax from "misago/services/ajax"
+import auth from "misago/services/auth"
+import captcha from "misago/services/captcha"
+import modal from "misago/services/modal"
+import snackbar from "misago/services/snackbar"
+import showBannedPage from "misago/utils/banned-page"
+import * as validators from "misago/utils/validators"
 
 export class RegisterForm extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
-    const { username, password } = this.props.criteria;
+    const { username, password } = this.props.criteria
 
-    let passwordMinLength = 0;
-    password.forEach((item) => {
-      if (item.name === 'MinimumLengthValidator') {
-        passwordMinLength = item.min_length;
+    let passwordMinLength = 0
+    password.forEach(item => {
+      if (item.name === "MinimumLengthValidator") {
+        passwordMinLength = item.min_length
       }
-    });
+    })
 
     const formValidators = {
       username: [
         validators.usernameContent(),
-          validators.usernameMinLength(username.min_length),
-          validators.usernameMaxLength(username.max_length)
-        ],
-      email: [
-        validators.email()
-      ],
-      password: [
-        validators.passwordMinLength(passwordMinLength)
+        validators.usernameMinLength(username.min_length),
+        validators.usernameMaxLength(username.max_length)
       ],
+      email: [validators.email()],
+      password: [validators.passwordMinLength(passwordMinLength)],
       captcha: captcha.validator()
-    };
+    }
 
-    if (!!misago.get('TERMS_OF_SERVICE_ID')) {
-      formValidators.termsOfService = [validators.requiredTermsOfService()];
+    if (!!misago.get("TERMS_OF_SERVICE_ID")) {
+      formValidators.termsOfService = [validators.requiredTermsOfService()]
     }
 
-    if (!!misago.get('PRIVACY_POLICY_ID')) {
-      formValidators.privacyPolicy = [validators.requiredPrivacyPolicy()];
+    if (!!misago.get("PRIVACY_POLICY_ID")) {
+      formValidators.privacyPolicy = [validators.requiredPrivacyPolicy()]
     }
 
     this.state = {
       isLoading: false,
 
-      username: '',
-      email: '',
-      password: '',
-      captcha: '',
+      username: "",
+      email: "",
+      password: "",
+      captcha: "",
 
       termsOfService: null,
       privacyPolicy: null,
 
       validators: formValidators,
       errors: {}
-    };
+    }
   }
 
   clean() {
     if (this.isValid()) {
-      return true;
+      return true
     } else {
-      snackbar.error(gettext("Form contains errors."));
+      snackbar.error(gettext("Form contains errors."))
       this.setState({
         errors: this.validate()
-      });
-      return false;
+      })
+      return false
     }
   }
 
   send() {
-    return ajax.post(misago.get('USERS_API'), {
+    return ajax.post(misago.get("USERS_API"), {
       username: this.state.username,
       email: this.state.email,
       password: this.state.password,
       captcha: this.state.captcha,
       terms_of_service: this.state.termsOfService,
       privacy_policy: this.state.privacyPolicy
-    });
+    })
   }
 
   handleSuccess(apiResponse) {
-    this.props.callback(apiResponse);
+    this.props.callback(apiResponse)
   }
 
   handleError(rejection) {
     if (rejection.status === 400) {
       this.setState({
-        'errors': Object.assign({}, this.state.errors, rejection)
-      });
+        errors: Object.assign({}, this.state.errors, rejection)
+      })
 
       if (rejection.__all__ && rejection.__all__.length > 0) {
-        snackbar.error(rejection.__all__[0]);
+        snackbar.error(rejection.__all__[0])
       } else {
-        snackbar.error(gettext("Form contains errors."));
+        snackbar.error(gettext("Form contains errors."))
       }
     } else if (rejection.status === 403 && rejection.ban) {
-      showBannedPage(rejection.ban);
-      modal.hide();
+      showBannedPage(rejection.ban)
+      modal.hide()
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
   }
 
-  /* jshint ignore:start */
-  handlePrivacyPolicyChange = (event) => {
-    const value = event.target.value;
-    this.handleToggleAgreement('privacyPolicy', value);
-  };
+  handlePrivacyPolicyChange = event => {
+    const value = event.target.value
+    this.handleToggleAgreement("privacyPolicy", value)
+  }
 
-  handleTermsOfServiceChange = (event) => {
-    const value = event.target.value;
-    this.handleToggleAgreement('termsOfService', value);
-  };
+  handleTermsOfServiceChange = event => {
+    const value = event.target.value
+    this.handleToggleAgreement("termsOfService", value)
+  }
 
   handleToggleAgreement = (agreement, value) => {
     this.setState((prevState, props) => {
       if (prevState[agreement] === null) {
-        const errors = { ...prevState.errors, [agreement]: null };
-        return { errors, [agreement]: value };
+        const errors = { ...prevState.errors, [agreement]: null }
+        return { errors, [agreement]: value }
       }
 
-      const validator = this.state.validators[agreement][0];
-      const errors = { ...prevState.errors, [agreement]: [validator(null)] };
-      return { errors, [agreement]: null };
+      const validator = this.state.validators[agreement][0]
+      const errors = { ...prevState.errors, [agreement]: [validator(null)] }
+      return { errors, [agreement]: null }
     })
-  };
-  /* jshint ignore:end */
+  }
 
   render() {
-    /* jshint ignore:start */
-    return <div className="modal-dialog modal-register" role="document">
-      <div className="modal-content">
-        <div className="modal-header">
-          <button type="button" className="close" data-dismiss="modal"
-                  aria-label={gettext("Close")}>
-            <span aria-hidden="true">&times;</span>
-          </button>
-          <h4 className="modal-title">{gettext("Register")}</h4>
-        </div>
-        <form onSubmit={this.handleSubmit}>
-          <input type="type" style={{display: 'none'}} />
-          <input type="password" style={{display: 'none'}} />
-          <div className="modal-body">
-
-            <StartSocialAuth
-              buttonClassName="col-xs-12 col-sm-6"
-              buttonLabel={gettext("Join with %(site)s")}
-              formLabel={gettext("Or create forum account:")}
-            />
-            
-            <FormGroup label={gettext("Username")} for="id_username"
-                       validation={this.state.errors.username}>
-              <input type="text" id="id_username" className="form-control"
-                     aria-describedby="id_username_status"
-                     disabled={this.state.isLoading}
-                     onChange={this.bindInput('username')}
-                     value={this.state.username} />
-            </FormGroup>
-
-            <FormGroup label={gettext("E-mail")} for="id_email"
-                       validation={this.state.errors.email}>
-              <input type="text" id="id_email" className="form-control"
-                     aria-describedby="id_email_status"
-                     disabled={this.state.isLoading}
-                     onChange={this.bindInput('email')}
-                     value={this.state.email} />
-            </FormGroup>
-
-            <FormGroup label={gettext("Password")} for="id_password"
-                       validation={this.state.errors.password}
-                       extra={
-                          <PasswordStrength
-                            password={this.state.password}
-                            inputs={[
-                              this.state.username,
-                              this.state.email
-                            ]}
-                          />
-                        } >
-              <input type="password" id="id_password" className="form-control"
-                     aria-describedby="id_password_status"
-                     disabled={this.state.isLoading}
-                     onChange={this.bindInput('password')}
-                     value={this.state.password} />
-            </FormGroup>
-
-            {captcha.component({
-              form: this,
-            })}
-
-            <RegisterLegalFootnote
-              errors={this.state.errors}
-              privacyPolicy={this.state.privacyPolicy}
-              termsOfService={this.state.termsOfService}
-              onPrivacyPolicyChange={this.handlePrivacyPolicyChange}
-              onTermsOfServiceChange={this.handleTermsOfServiceChange}
-            />
-
-          </div>
-          <div className="modal-footer">
+    return (
+      <div className="modal-dialog modal-register" role="document">
+        <div className="modal-content">
+          <div className="modal-header">
             <button
-              className="btn btn-default"
-              data-dismiss="modal"
-              disabled={this.state.isLoading}
               type="button"
+              className="close"
+              data-dismiss="modal"
+              aria-label={gettext("Close")}
             >
-              {gettext("Cancel")}
+              <span aria-hidden="true">&times;</span>
             </button>
-            <Button className="btn-primary" loading={this.state.isLoading}>
-              {gettext("Register account")}
-            </Button>
+            <h4 className="modal-title">{gettext("Register")}</h4>
           </div>
-        </form>
+          <form onSubmit={this.handleSubmit}>
+            <input type="type" style={{ display: "none" }} />
+            <input type="password" style={{ display: "none" }} />
+            <div className="modal-body">
+              <StartSocialAuth
+                buttonClassName="col-xs-12 col-sm-6"
+                buttonLabel={gettext("Join with %(site)s")}
+                formLabel={gettext("Or create forum account:")}
+              />
+
+              <FormGroup
+                label={gettext("Username")}
+                for="id_username"
+                validation={this.state.errors.username}
+              >
+                <input
+                  type="text"
+                  id="id_username"
+                  className="form-control"
+                  aria-describedby="id_username_status"
+                  disabled={this.state.isLoading}
+                  onChange={this.bindInput("username")}
+                  value={this.state.username}
+                />
+              </FormGroup>
+
+              <FormGroup
+                label={gettext("E-mail")}
+                for="id_email"
+                validation={this.state.errors.email}
+              >
+                <input
+                  type="text"
+                  id="id_email"
+                  className="form-control"
+                  aria-describedby="id_email_status"
+                  disabled={this.state.isLoading}
+                  onChange={this.bindInput("email")}
+                  value={this.state.email}
+                />
+              </FormGroup>
+
+              <FormGroup
+                label={gettext("Password")}
+                for="id_password"
+                validation={this.state.errors.password}
+                extra={
+                  <PasswordStrength
+                    password={this.state.password}
+                    inputs={[this.state.username, this.state.email]}
+                  />
+                }
+              >
+                <input
+                  type="password"
+                  id="id_password"
+                  className="form-control"
+                  aria-describedby="id_password_status"
+                  disabled={this.state.isLoading}
+                  onChange={this.bindInput("password")}
+                  value={this.state.password}
+                />
+              </FormGroup>
+
+              {captcha.component({
+                form: this
+              })}
+
+              <RegisterLegalFootnote
+                errors={this.state.errors}
+                privacyPolicy={this.state.privacyPolicy}
+                termsOfService={this.state.termsOfService}
+                onPrivacyPolicyChange={this.handlePrivacyPolicyChange}
+                onTermsOfServiceChange={this.handleTermsOfServiceChange}
+              />
+            </div>
+            <div className="modal-footer">
+              <button
+                className="btn btn-default"
+                data-dismiss="modal"
+                disabled={this.state.isLoading}
+                type="button"
+              >
+                {gettext("Cancel")}
+              </button>
+              <Button className="btn-primary" loading={this.state.isLoading}>
+                {gettext("Register account")}
+              </Button>
+            </div>
+          </form>
+        </div>
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
 }
 
 export class RegisterComplete extends React.Component {
   getLead() {
-    if (this.props.activation === 'user') {
-      return gettext("%(username)s, your account has been created but you need to activate it before you will be able to sign in.");
-    } else if (this.props.activation === 'admin') {
-      return gettext("%(username)s, your account has been created but board administrator will have to activate it before you will be able to sign in.");
+    if (this.props.activation === "user") {
+      return gettext(
+        "%(username)s, your account has been created but you need to activate it before you will be able to sign in."
+      )
+    } else if (this.props.activation === "admin") {
+      return gettext(
+        "%(username)s, your account has been created but board administrator will have to activate it before you will be able to sign in."
+      )
     }
   }
 
   getSubscript() {
-    if (this.props.activation === 'user') {
-      return gettext("We have sent an e-mail to %(email)s with link that you have to click to activate your account.");
-    } else if (this.props.activation === 'admin') {
-      return gettext("We will send an e-mail to %(email)s when this takes place.");
+    if (this.props.activation === "user") {
+      return gettext(
+        "We have sent an e-mail to %(email)s with link that you have to click to activate your account."
+      )
+    } else if (this.props.activation === "admin") {
+      return gettext(
+        "We will send an e-mail to %(email)s when this takes place."
+      )
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className="modal-dialog modal-message modal-register"
-                role="document">
-      <div className="modal-content">
-        <div className="modal-header">
-          <button type="button" className="close" data-dismiss="modal"
-                  aria-label={gettext("Close")}>
-            <span aria-hidden="true">&times;</span>
-          </button>
-          <h4 className="modal-title">{gettext("Registration complete")}</h4>
-        </div>
-        <div className="modal-body">
-          <div className="message-icon">
-            <span className="material-icon">
-              info_outline
-            </span>
-          </div>
-          <div className="message-body">
-            <p className="lead">
-              {interpolate(
-                this.getLead(),
-                {'username': this.props.username}, true)}
-            </p>
-            <p>
-              {interpolate(
-                this.getSubscript(),
-                {'email': this.props.email}, true)}
-            </p>
+    return (
+      <div
+        className="modal-dialog modal-message modal-register"
+        role="document"
+      >
+        <div className="modal-content">
+          <div className="modal-header">
             <button
-              className="btn btn-default"
-              data-dismiss="modal"
               type="button"
+              className="close"
+              data-dismiss="modal"
+              aria-label={gettext("Close")}
             >
-              {gettext("Ok")}
+              <span aria-hidden="true">&times;</span>
             </button>
+            <h4 className="modal-title">{gettext("Registration complete")}</h4>
+          </div>
+          <div className="modal-body">
+            <div className="message-icon">
+              <span className="material-icon">info_outline</span>
+            </div>
+            <div className="message-body">
+              <p className="lead">
+                {interpolate(
+                  this.getLead(),
+                  { username: this.props.username },
+                  true
+                )}
+              </p>
+              <p>
+                {interpolate(
+                  this.getSubscript(),
+                  { email: this.props.email },
+                  true
+                )}
+              </p>
+              <button
+                className="btn btn-default"
+                data-dismiss="modal"
+                type="button"
+              >
+                {gettext("Ok")}
+              </button>
+            </div>
           </div>
         </div>
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
 }
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       complete: false
-    };
+    }
   }
 
-  /* jshint ignore:start */
-  completeRegistration = (apiResponse) => {
-    if (apiResponse.activation === 'active') {
-      modal.hide();
-      auth.signIn(apiResponse);
+  completeRegistration = apiResponse => {
+    if (apiResponse.activation === "active") {
+      modal.hide()
+      auth.signIn(apiResponse)
     } else {
       this.setState({
         complete: apiResponse
-      });
+      })
     }
-  };
-  /* jshint ignore:end */
+  }
 
   render() {
-    /* jshint ignore:start */
     if (this.state.complete) {
       return (
         <RegisterComplete
@@ -320,15 +347,9 @@ export default class extends React.Component {
           email={this.state.complete.email}
           username={this.state.complete.username}
         />
-      );
+      )
     }
 
-    return (
-      <RegisterForm
-        callback={this.completeRegistration}
-        {...this.props}
-      />
-    );
-    /* jshint ignore:end */
+    return <RegisterForm callback={this.completeRegistration} {...this.props} />
   }
 }

+ 59 - 67
frontend/src/components/request-activation-link.js

@@ -1,108 +1,105 @@
-import React from 'react'; // jshint ignore:line
-import misago from 'misago/index';
-import Button from 'misago/components/button'; // jshint ignore:line
-import Form from 'misago/components/form';
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
-import * as validators from 'misago/utils/validators';
-import showBannedPage from 'misago/utils/banned-page';
+import React from "react"
+import misago from "misago/index"
+import Button from "misago/components/button"
+import Form from "misago/components/form"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import * as validators from "misago/utils/validators"
+import showBannedPage from "misago/utils/banned-page"
 
 export class RequestLinkForm extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
-      'isLoading': false,
+      isLoading: false,
 
-      'email': '',
+      email: "",
 
-      'validators': {
-        'email': [
-          validators.email()
-        ]
+      validators: {
+        email: [validators.email()]
       }
-    };
+    }
   }
 
   clean() {
     if (this.isValid()) {
-      return true;
+      return true
     } else {
-      snackbar.error(gettext("Enter a valid email address."));
-      return false;
+      snackbar.error(gettext("Enter a valid email address."))
+      return false
     }
   }
 
   send() {
-    return ajax.post(misago.get('SEND_ACTIVATION_API'), {
-      'email': this.state.email
-    });
+    return ajax.post(misago.get("SEND_ACTIVATION_API"), {
+      email: this.state.email
+    })
   }
 
   handleSuccess(apiResponse) {
-    this.props.callback(apiResponse);
+    this.props.callback(apiResponse)
   }
 
   handleError(rejection) {
-    if (['already_active', 'inactive_admin'].indexOf(rejection.code) > -1) {
-      snackbar.info(rejection.detail);
+    if (["already_active", "inactive_admin"].indexOf(rejection.code) > -1) {
+      snackbar.info(rejection.detail)
     } else if (rejection.status === 403 && rejection.ban) {
-      showBannedPage(rejection.ban);
+      showBannedPage(rejection.ban)
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
   }
 
   render() {
-    /* jshint ignore:start */
     return (
       <div className="well well-form well-form-request-activation-link">
         <form onSubmit={this.handleSubmit}>
           <div className="form-group">
             <div className="control-input">
-
-              <input type="text" className="form-control"
-                     placeholder={gettext("Your e-mail address")}
-                     disabled={this.state.isLoading}
-                     onChange={this.bindInput('email')}
-                     value={this.state.email} />
-
+              <input
+                type="text"
+                className="form-control"
+                placeholder={gettext("Your e-mail address")}
+                disabled={this.state.isLoading}
+                onChange={this.bindInput("email")}
+                value={this.state.email}
+              />
             </div>
           </div>
 
-          <Button className="btn-primary btn-block"
-                  loading={this.state.isLoading}>
+          <Button
+            className="btn-primary btn-block"
+            loading={this.state.isLoading}
+          >
             {gettext("Send link")}
           </Button>
-
         </form>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 }
 
 export class LinkSent extends React.Component {
   getMessage() {
-    return interpolate(gettext("Activation link was sent to %(email)s"), {
-      email: this.props.user.email
-    }, true);
+    return interpolate(
+      gettext("Activation link was sent to %(email)s"),
+      {
+        email: this.props.user.email
+      },
+      true
+    )
   }
 
   render() {
-    /* jshint ignore:start */
     return (
       <div className="well well-form well-form-request-activation-link well-done">
         <div className="done-message">
           <div className="message-icon">
-            <span className="material-icon">
-              check
-            </span>
+            <span className="material-icon">check</span>
           </div>
           <div className="message-body">
-            <p>
-              {this.getMessage()}
-            </p>
+            <p>{this.getMessage()}</p>
           </div>
           <button
             className="btn btn-primary btn-block"
@@ -113,41 +110,36 @@ export class LinkSent extends React.Component {
           </button>
         </div>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 }
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       complete: false
-    };
+    }
   }
 
-  /* jshint ignore:start */
-  complete = (apiResponse) => {
+  complete = apiResponse => {
     this.setState({
       complete: apiResponse
-    });
-  };
+    })
+  }
 
   reset = () => {
     this.setState({
       complete: false
-    });
-  };
-  /* jshint ignore:end */
+    })
+  }
 
   render() {
-    /* jshint ignore:start */
     if (this.state.complete) {
-      return <LinkSent user={this.state.complete} callback={this.reset} />;
+      return <LinkSent user={this.state.complete} callback={this.reset} />
     } else {
-      return <RequestLinkForm callback={this.complete} />;
-    };
-    /* jshint ignore:end */
+      return <RequestLinkForm callback={this.complete} />
+    }
   }
-}
+}

+ 108 - 122
frontend/src/components/request-password-reset.js

@@ -1,182 +1,176 @@
-import React from 'react'; // jshint ignore:line
-import ReactDOM from 'react-dom'; // jshint ignore:line
-import misago from 'misago/index';
-import Button from 'misago/components/button'; // jshint ignore:line
-import Form from 'misago/components/form';
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
-import * as validators from 'misago/utils/validators';
-import showBannedPage from 'misago/utils/banned-page';
+import React from "react"
+import ReactDOM from "react-dom"
+import misago from "misago/index"
+import Button from "misago/components/button"
+import Form from "misago/components/form"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import * as validators from "misago/utils/validators"
+import showBannedPage from "misago/utils/banned-page"
 
 export class RequestResetForm extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
-      'isLoading': false,
+      isLoading: false,
 
-      'email': '',
+      email: "",
 
-      'validators': {
-        'email': [
-          validators.email()
-        ]
+      validators: {
+        email: [validators.email()]
       }
-    };
+    }
   }
 
   clean() {
     if (this.isValid()) {
-      return true;
+      return true
     } else {
-      snackbar.error(gettext("Enter a valid email address."));
-      return false;
+      snackbar.error(gettext("Enter a valid email address."))
+      return false
     }
   }
 
   send() {
-    return ajax.post(misago.get('SEND_PASSWORD_RESET_API'), {
-      'email': this.state.email
-    });
+    return ajax.post(misago.get("SEND_PASSWORD_RESET_API"), {
+      email: this.state.email
+    })
   }
 
   handleSuccess(apiResponse) {
-    this.props.callback(apiResponse);
+    this.props.callback(apiResponse)
   }
 
   handleError(rejection) {
-    if (['inactive_user', 'inactive_admin'].indexOf(rejection.code) > -1) {
-      this.props.showInactivePage(rejection);
+    if (["inactive_user", "inactive_admin"].indexOf(rejection.code) > -1) {
+      this.props.showInactivePage(rejection)
     } else if (rejection.status === 403 && rejection.ban) {
-      showBannedPage(rejection.ban);
+      showBannedPage(rejection.ban)
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className="well well-form well-form-request-password-reset">
-      <form onSubmit={this.handleSubmit}>
-        <div className="form-group">
-          <div className="control-input">
-
-            <input type="text" className="form-control"
-                   placeholder={gettext("Your e-mail address")}
-                   disabled={this.state.isLoading}
-                   onChange={this.bindInput('email')}
-                   value={this.state.email} />
-
+    return (
+      <div className="well well-form well-form-request-password-reset">
+        <form onSubmit={this.handleSubmit}>
+          <div className="form-group">
+            <div className="control-input">
+              <input
+                type="text"
+                className="form-control"
+                placeholder={gettext("Your e-mail address")}
+                disabled={this.state.isLoading}
+                onChange={this.bindInput("email")}
+                value={this.state.email}
+              />
+            </div>
           </div>
-        </div>
 
-        <Button className="btn-primary btn-block"
-                loading={this.state.isLoading}>
-          {gettext("Send link")}
-        </Button>
-
-      </form>
-    </div>;
-    /* jshint ignore:end */
+          <Button
+            className="btn-primary btn-block"
+            loading={this.state.isLoading}
+          >
+            {gettext("Send link")}
+          </Button>
+        </form>
+      </div>
+    )
   }
 }
 
 export class LinkSent extends React.Component {
   getMessage() {
-    return interpolate(gettext("Reset password link was sent to %(email)s"), {
-      email: this.props.user.email
-    }, true);
+    return interpolate(
+      gettext("Reset password link was sent to %(email)s"),
+      {
+        email: this.props.user.email
+      },
+      true
+    )
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className="well well-form well-form-request-password-reset well-done">
-      <div className="done-message">
-        <div className="message-icon">
-          <span className="material-icon">
-            check
-          </span>
-        </div>
-        <div className="message-body">
-          <p>
-            {this.getMessage()}
-          </p>
+    return (
+      <div className="well well-form well-form-request-password-reset well-done">
+        <div className="done-message">
+          <div className="message-icon">
+            <span className="material-icon">check</span>
+          </div>
+          <div className="message-body">
+            <p>{this.getMessage()}</p>
+          </div>
+          <button
+            type="button"
+            className="btn btn-primary btn-block"
+            onClick={this.props.callback}
+          >
+            {gettext("Request another link")}
+          </button>
         </div>
-        <button type="button" className="btn btn-primary btn-block"
-                onClick={this.props.callback}>
-          {gettext("Request another link")}
-        </button>
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
 }
 
 export class AccountInactivePage extends React.Component {
   getActivateButton() {
-    if (this.props.activation === 'inactive_user') {
-      /* jshint ignore:start */
-      return <p>
-        <a href={misago.get('REQUEST_ACTIVATION_URL')}>
-          {gettext("Activate your account.")}
-        </a>
-      </p>;
-      /* jshint ignore:end */
+    if (this.props.activation === "inactive_user") {
+      return (
+        <p>
+          <a href={misago.get("REQUEST_ACTIVATION_URL")}>
+            {gettext("Activate your account.")}
+          </a>
+        </p>
+      )
     } else {
-      return null;
+      return null
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className="page page-message page-message-info page-forgotten-password-inactive">
-      <div className="container">
-        <div className="message-panel">
-
-          <div className="message-icon">
-            <span className="material-icon">
-              info_outline
-            </span>
-          </div>
-
-          <div className="message-body">
-            <p className="lead">
-              {gettext("Your account is inactive.")}
-            </p>
-            <p>
-              {this.props.message}
-            </p>
-            {this.getActivateButton()}
+    return (
+      <div className="page page-message page-message-info page-forgotten-password-inactive">
+        <div className="container">
+          <div className="message-panel">
+            <div className="message-icon">
+              <span className="material-icon">info_outline</span>
+            </div>
+
+            <div className="message-body">
+              <p className="lead">{gettext("Your account is inactive.")}</p>
+              <p>{this.props.message}</p>
+              {this.getActivateButton()}
+            </div>
           </div>
-
         </div>
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
 }
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       complete: false
-    };
+    }
   }
 
-  /* jshint ignore:start */
-  complete = (apiResponse) => {
+  complete = apiResponse => {
     this.setState({
       complete: apiResponse
-    });
-  };
+    })
+  }
 
   reset = () => {
     this.setState({
       complete: false
-    });
-  };
+    })
+  }
 
   showInactivePage(apiResponse) {
     ReactDOM.render(
@@ -184,20 +178,13 @@ export default class extends React.Component {
         activation={apiResponse.code}
         message={apiResponse.detail}
       />,
-      document.getElementById('page-mount')
-    );
+      document.getElementById("page-mount")
+    )
   }
-  /* jshint ignore:end */
 
   render() {
-    /* jshint ignore:start */
     if (this.state.complete) {
-      return (
-        <LinkSent
-          callback={this.reset}
-          user={this.state.complete}
-        />
-      );
+      return <LinkSent callback={this.reset} user={this.state.complete} />
     }
 
     return (
@@ -205,7 +192,6 @@ export default class extends React.Component {
         callback={this.complete}
         showInactivePage={this.showInactivePage}
       />
-    );
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 89 - 87
frontend/src/components/reset-password-form.js

@@ -1,143 +1,145 @@
-import React from 'react'; // jshint ignore:line
-import ReactDOM from 'react-dom'; // jshint ignore:line
-import misago from 'misago/index';
-import Button from 'misago/components/button'; // jshint ignore:line
-import Form from 'misago/components/form';
-import SignInModal from 'misago/components/sign-in.js';
-import ajax from 'misago/services/ajax';
-import auth from 'misago/services/auth'; // jshint ignore:line
-import modal from 'misago/services/modal';
-import snackbar from 'misago/services/snackbar';
-import showBannedPage from 'misago/utils/banned-page';
+import React from "react"
+import ReactDOM from "react-dom"
+import misago from "misago/index"
+import Button from "misago/components/button"
+import Form from "misago/components/form"
+import SignInModal from "misago/components/sign-in.js"
+import ajax from "misago/services/ajax"
+import auth from "misago/services/auth"
+import modal from "misago/services/modal"
+import snackbar from "misago/services/snackbar"
+import showBannedPage from "misago/utils/banned-page"
 
 export class ResetPasswordForm extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
-      'isLoading': false,
+      isLoading: false,
 
-      'password': ''
-    };
+      password: ""
+    }
   }
 
   clean() {
     if (this.state.password.trim().length) {
-      return true;
+      return true
     } else {
-      snackbar.error(gettext("Enter new password."));
-      return false;
+      snackbar.error(gettext("Enter new password."))
+      return false
     }
   }
 
   send() {
-    return ajax.post(misago.get('CHANGE_PASSWORD_API'), {
-      'password': this.state.password
-    });
+    return ajax.post(misago.get("CHANGE_PASSWORD_API"), {
+      password: this.state.password
+    })
   }
 
   handleSuccess(apiResponse) {
-    this.props.callback(apiResponse);
+    this.props.callback(apiResponse)
   }
 
   handleError(rejection) {
     if (rejection.status === 403 && rejection.ban) {
-      showBannedPage(rejection.ban);
+      showBannedPage(rejection.ban)
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className="well well-form well-form-reset-password">
-      <form onSubmit={this.handleSubmit}>
-        <div className="form-group">
-          <div className="control-input">
-
-            <input type="password" className="form-control"
-                   placeholder={gettext("Enter new password")}
-                   disabled={this.state.isLoading}
-                   onChange={this.bindInput('password')}
-                   value={this.state.password} />
-
+    return (
+      <div className="well well-form well-form-reset-password">
+        <form onSubmit={this.handleSubmit}>
+          <div className="form-group">
+            <div className="control-input">
+              <input
+                type="password"
+                className="form-control"
+                placeholder={gettext("Enter new password")}
+                disabled={this.state.isLoading}
+                onChange={this.bindInput("password")}
+                value={this.state.password}
+              />
+            </div>
           </div>
-        </div>
 
-        <Button className="btn-primary btn-block"
-                loading={this.state.isLoading}>
-          {gettext("Change password")}
-        </Button>
-
-      </form>
-    </div>;
-    /* jshint ignore:end */
+          <Button
+            className="btn-primary btn-block"
+            loading={this.state.isLoading}
+          >
+            {gettext("Change password")}
+          </Button>
+        </form>
+      </div>
+    )
   }
 }
 
 export class PasswordChangedPage extends React.Component {
   getMessage() {
-    return interpolate(gettext("%(username)s, your password has been changed successfully."), {
-      username: this.props.user.username
-    }, true);
+    return interpolate(
+      gettext("%(username)s, your password has been changed successfully."),
+      {
+        username: this.props.user.username
+      },
+      true
+    )
   }
 
   showSignIn() {
-    modal.show(SignInModal);
+    modal.show(SignInModal)
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className="page page-message page-message-success page-forgotten-password-changed">
-      <div className="container">
-        <div className="message-panel">
-
-          <div className="message-icon">
-            <span className="material-icon">
-              check
-            </span>
+    return (
+      <div className="page page-message page-message-success page-forgotten-password-changed">
+        <div className="container">
+          <div className="message-panel">
+            <div className="message-icon">
+              <span className="material-icon">check</span>
+            </div>
+
+            <div className="message-body">
+              <p className="lead">{this.getMessage()}</p>
+              <p>
+                {gettext(
+                  "You will have to sign in using new password before continuing."
+                )}
+              </p>
+              <p>
+                <button
+                  type="button"
+                  className="btn btn-primary"
+                  onClick={this.showSignIn}
+                >
+                  {gettext("Sign in")}
+                </button>
+              </p>
+            </div>
           </div>
-
-          <div className="message-body">
-            <p className="lead">
-              {this.getMessage()}
-            </p>
-            <p>
-              {gettext("You will have to sign in using new password before continuing.")}
-            </p>
-            <p>
-              <button type="button" className="btn btn-primary" onClick={this.showSignIn}>
-                {gettext("Sign in")}
-              </button>
-            </p>
-          </div>
-
         </div>
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
 }
 
 export default class extends React.Component {
-  /* jshint ignore:start */
-  complete = (apiResponse) => {
-    auth.softSignOut();
+  complete = apiResponse => {
+    auth.softSignOut()
 
     // nuke "redirect_to" field so we don't end
     // coming back to error page after sign in
-    $('#hidden-login-form input[name="redirect_to"]').remove();
+    $('#hidden-login-form input[name="redirect_to"]').remove()
 
     ReactDOM.render(
       <PasswordChangedPage user={apiResponse} />,
-      document.getElementById('page-mount')
-    );
-  };
-  /* jshint ignore:end */
+      document.getElementById("page-mount")
+    )
+  }
 
   render() {
-    /* jshint ignore:start */
-    return <ResetPasswordForm callback={this.complete} />;
-    /* jshint ignore:end */
+    return <ResetPasswordForm callback={this.complete} />
   }
-}
+}

+ 51 - 42
frontend/src/components/search/form.js

@@ -1,76 +1,81 @@
-// jshint ignore:start
-import React from 'react';
-import misago from 'misago';
-import Form from 'misago/components/form';
-import { load as updatePosts } from 'misago/reducers/posts';
-import { update as updateSearch } from 'misago/reducers/search';
-import { hydrate as updateUsers } from 'misago/reducers/users';
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
+import React from "react"
+import misago from "misago"
+import Form from "misago/components/form"
+import { load as updatePosts } from "misago/reducers/posts"
+import { update as updateSearch } from "misago/reducers/search"
+import { hydrate as updateUsers } from "misago/reducers/users"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoading: false,
 
       query: props.search.query
-    };
+    }
   }
 
   componentDidMount() {
     if (this.state.query.length) {
-      this.handleSubmit();
+      this.handleSubmit()
     }
   }
 
-  onQueryChange = (event) => {
-    this.changeValue('query', event.target.value);
-  };
+  onQueryChange = event => {
+    this.changeValue("query", event.target.value)
+  }
 
   clean() {
     if (!this.state.query.trim().length) {
-      snackbar.error(gettext("You have to enter search query."));
-      return false;
+      snackbar.error(gettext("You have to enter search query."))
+      return false
     }
 
-    return true;
+    return true
   }
 
   send() {
-    store.dispatch(updateSearch({
-      isLoading: true
-    }));
+    store.dispatch(
+      updateSearch({
+        isLoading: true
+      })
+    )
 
-    return ajax.get(misago.get('SEARCH_API'), {
+    return ajax.get(misago.get("SEARCH_API"), {
       q: this.state.query.trim()
-    });
+    })
   }
 
   handleSuccess(providers) {
-    store.dispatch(updateSearch({
-      query: this.state.query.trim(),
-      isLoading: false,
-      providers
-    }));
+    store.dispatch(
+      updateSearch({
+        query: this.state.query.trim(),
+        isLoading: false,
+        providers
+      })
+    )
 
-    providers.forEach((provider) => {
-      if (provider.id === 'users') {
-        store.dispatch(updateUsers(provider.results.results));
-      } else if (provider.id === 'threads') {
-        store.dispatch(updatePosts(provider.results));
+    providers.forEach(provider => {
+      if (provider.id === "users") {
+        store.dispatch(updateUsers(provider.results.results))
+      } else if (provider.id === "threads") {
+        store.dispatch(updatePosts(provider.results))
       }
-    });
+    })
   }
 
   handleError(rejection) {
-    snackbar.apiError(rejection);
+    snackbar.apiError(rejection)
 
-    store.dispatch(updateSearch({
-      isLoading: false
-    }));
+    store.dispatch(
+      updateSearch({
+        isLoading: false
+      })
+    )
   }
 
   render() {
@@ -89,7 +94,9 @@ export default class extends Form {
                       <div className="form-group">
                         <input
                           className="form-control"
-                          disabled={this.props.search.isLoading || this.state.isLoading}
+                          disabled={
+                            this.props.search.isLoading || this.state.isLoading
+                          }
                           onChange={this.onQueryChange}
                           type="text"
                           value={this.state.query}
@@ -99,7 +106,9 @@ export default class extends Form {
                     <div className="col-xs-12 col-sm-4 col-md-3">
                       <button
                         className="btn btn-primary btn-block btn-outline"
-                        disabled={this.props.search.isLoading || this.state.isLoading}
+                        disabled={
+                          this.props.search.isLoading || this.state.isLoading
+                        }
                       >
                         {gettext("Search")}
                       </button>
@@ -113,4 +122,4 @@ export default class extends Form {
       </div>
     )
   }
-}
+}

+ 14 - 14
frontend/src/components/search/index.js

@@ -1,28 +1,28 @@
-import { connect } from 'react-redux';
-import SearchThreads from './threads';
-import SearchUsers from './users';
+import { connect } from "react-redux"
+import SearchThreads from "./threads"
+import SearchUsers from "./users"
 
 const components = {
   threads: SearchThreads,
   users: SearchUsers
-};
+}
 
 export function select(store) {
   return {
-    'posts': store.posts,
-    'search': store.search,
-    'tick': store.tick.tick,
-    'user': store.auth.user,
-    'users': store.users,
-  };
+    posts: store.posts,
+    search: store.search,
+    tick: store.tick.tick,
+    user: store.auth.user,
+    users: store.users
+  }
 }
 
 export default function(providers) {
-  return providers.map((provider) => {
+  return providers.map(provider => {
     return {
       path: provider.url,
       component: connect(select)(components[provider.id]),
       provider: provider
-    };
-  });
-}
+    }
+  })
+}

+ 14 - 23
frontend/src/components/search/page.js

@@ -1,15 +1,11 @@
-// jshint ignore:start
-import React from 'react';
-import SearchForm from './form';
-import SideNav from './sidenav';
+import React from "react"
+import SearchForm from "./form"
+import SideNav from "./sidenav"
 
 export default function(props) {
   return (
     <div className="page page-search">
-      <SearchForm
-        provider={props.provider}
-        search={props.search}
-      />
+      <SearchForm provider={props.provider} search={props.search} />
       <div className="container">
         <div className="row">
           <div className="col-md-3">
@@ -17,10 +13,7 @@ export default function(props) {
           </div>
           <div className="col-md-9">
             {props.children}
-            <SearchTime
-              provider={props.provider}
-              search={props.search}
-            />
+            <SearchTime provider={props.provider} search={props.search} />
           </div>
         </div>
       </div>
@@ -29,22 +22,20 @@ export default function(props) {
 }
 
 export function SearchTime(props) {
-  let time = null;
-  props.search.providers.forEach((p) => {
+  let time = null
+  props.search.providers.forEach(p => {
     if (p.id === props.provider.id) {
-      time = p.time;
+      time = p.time
     }
-  });
+  })
 
-  if (time === null) return null;
+  if (time === null) return null
 
-  const copy = gettext("Search took %(time)s s to complete");
+  const copy = gettext("Search took %(time)s s to complete")
 
   return (
     <footer className="search-footer">
-      <p>
-        {interpolate(copy, {time}, true)}
-      </p>
+      <p>{interpolate(copy, { time }, true)}</p>
     </footer>
-  );
-}
+  )
+}

+ 12 - 19
frontend/src/components/search/sidenav.js

@@ -1,11 +1,10 @@
-// jshint ignore:start
-import React from 'react';
-import { Link } from 'react-router';
+import React from "react"
+import { Link } from "react-router"
 
 export default function(props) {
   return (
     <div className="list-group nav-side">
-      {props.providers.map((provider) => {
+      {props.providers.map(provider => {
         return (
           <Link
             activeClassName="active"
@@ -13,31 +12,25 @@ export default function(props) {
             key={provider.id}
             to={provider.url}
           >
-            <span className="material-icon">
-              {provider.icon}
-            </span>
+            <span className="material-icon">{provider.icon}</span>
             {provider.name}
             <Badge results={provider.results} />
           </Link>
-        );
+        )
       })}
     </div>
-  );
+  )
 }
 
 export function Badge(props) {
-  if (!props.results) return null;
+  if (!props.results) return null
 
-  let count = props.results.count;
+  let count = props.results.count
   if (count > 1000000) {
-    count = Math.ceil(count / 1000000) + 'KK'
+    count = Math.ceil(count / 1000000) + "KK"
   } else if (count > 1000) {
-    count = Math.ceil(count / 1000) + 'K'
+    count = Math.ceil(count / 1000) + "K"
   }
 
-  return (
-    <span className="badge">
-      {count}
-    </span>
-  );
-}
+  return <span className="badge">{count}</span>
+}

+ 42 - 25
frontend/src/components/search/threads/footer.js

@@ -1,40 +1,57 @@
-// jshint ignore:start
-import React from 'react';
-import MisagoMarkup from 'misago/components/misago-markup';
-import escapeHtml from 'misago/utils/escape-html';
+import React from "react"
+import MisagoMarkup from "misago/components/misago-markup"
+import escapeHtml from "misago/utils/escape-html"
 
-const CATEGORY_SPAN = '<span class="category-name">%(name)s</span>';
-const ITEM_SPAN = '<span class="item-title">%(name)s</span>';
+const CATEGORY_SPAN = '<span class="category-name">%(name)s</span>'
+const ITEM_SPAN = '<span class="item-title">%(name)s</span>'
 
 export default function(props) {
-  const template = gettext('%(user)s, %(posted_on)s in "%(thread)s", %(category)s');
+  const template = gettext(
+    '%(user)s, %(posted_on)s in "%(thread)s", %(category)s'
+  )
 
-  let username = null;
+  let username = null
   if (props.post.poster) {
-    username = props.post.poster.username;
+    username = props.post.poster.username
   } else {
-    username = props.post.poster_name;
+    username = props.post.poster_name
   }
 
-  const message = interpolate(escapeHtml(template), {
-    category: interpolate(CATEGORY_SPAN, {
-      name: escapeHtml(props.category.name)
-    }, true),
-    thread: interpolate(ITEM_SPAN, {
-      name: escapeHtml(props.thread.title)
-    }, true),
-    user: interpolate(ITEM_SPAN, {
-      name: escapeHtml(username)
-    }, true),
-    posted_on: escapeHtml(props.post.hidden_on.fromNow()),
-  }, true);
+  const message = interpolate(
+    escapeHtml(template),
+    {
+      category: interpolate(
+        CATEGORY_SPAN,
+        {
+          name: escapeHtml(props.category.name)
+        },
+        true
+      ),
+      thread: interpolate(
+        ITEM_SPAN,
+        {
+          name: escapeHtml(props.thread.title)
+        },
+        true
+      ),
+      user: interpolate(
+        ITEM_SPAN,
+        {
+          name: escapeHtml(username)
+        },
+        true
+      ),
+      posted_on: escapeHtml(props.post.hidden_on.fromNow())
+    },
+    true
+  )
 
   return (
     <div className="panel-footer post-infeed-footer">
       <a
-        dangerouslySetInnerHTML={{__html: message}}
+        dangerouslySetInnerHTML={{ __html: message }}
         href={props.post.url.index}
       />
     </div>
-  );
-}
+  )
+}

+ 9 - 16
frontend/src/components/search/threads/index.js

@@ -1,18 +1,11 @@
-// jshint ignore:start
-import React from 'react';
-import SearchPage from '../page';
-import Results from './results';
+import React from "react"
+import SearchPage from "../page"
+import Results from "./results"
 
 export default function(props) {
   return (
-    <SearchPage
-      provider={props.route.provider}
-      search={props.search}
-    >
-      <Blankslate
-        query={props.search.query}
-        posts={props.posts}
-      >
+    <SearchPage provider={props.route.provider} search={props.search}>
+      <Blankslate query={props.search.query} posts={props.posts}>
         <Results
           provider={props.route.provider}
           query={props.search.query}
@@ -24,19 +17,19 @@ export default function(props) {
 }
 
 export function Blankslate(props) {
-  if (props.posts && props.posts.count) return props.children;
+  if (props.posts && props.posts.count) return props.children
 
   if (props.query.length) {
     return (
       <p className="lead">
         {gettext("No threads matching search query have been found.")}
       </p>
-    );
+    )
   }
 
   return (
     <p className="lead">
       {gettext("Enter at least two characters to search threads.")}
     </p>
-  );
-}
+  )
+}

+ 14 - 11
frontend/src/components/search/threads/post.js

@@ -1,11 +1,10 @@
-// jshint ignore:start
-import React from 'react';
-import PostFooter from './footer';
-import MisagoMarkup from 'misago/components/misago-markup';
+import React from "react"
+import PostFooter from "./footer"
+import MisagoMarkup from "misago/components/misago-markup"
 
 export default function(props) {
   return (
-    <li id={'post-' + props.post.id} className="post post-infeed">
+    <li id={"post-" + props.post.id} className="post post-infeed">
       <div className="post-border">
         <div className="post-body">
           <div className="panel panel-default panel-post">
@@ -19,7 +18,7 @@ export default function(props) {
         </div>
       </div>
     </li>
-  );
+  )
 }
 
 export function PostBody(props) {
@@ -28,13 +27,17 @@ export function PostBody(props) {
       <div className="panel-body">
         <MisagoMarkup markup={props.content} />
       </div>
-    );
+    )
   }
 
   return (
     <div className="panel-body panel-body-invalid">
-      <p className="lead">{gettext("This post's contents cannot be displayed.")}</p>
-      <p className="text-muted">{gettext("This error is caused by invalid post content manipulation.")}</p>
+      <p className="lead">
+        {gettext("This post's contents cannot be displayed.")}
+      </p>
+      <p className="text-muted">
+        {gettext("This error is caused by invalid post content manipulation.")}
+      </p>
     </div>
-  );
-}
+  )
+}

+ 50 - 40
frontend/src/components/search/threads/results.js

@@ -1,56 +1,66 @@
-// jshint ignore:start
-import React from 'react';
-import PostFeed from 'misago/components/post-feed';
-import Button from 'misago/components/button';
-import MisagoMarkup from 'misago/components/misago-markup';
-import { update as updatePosts, append as appendPosts } from 'misago/reducers/posts';
-import { updateProvider } from 'misago/reducers/search';
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
+import React from "react"
+import PostFeed from "misago/components/post-feed"
+import Button from "misago/components/button"
+import MisagoMarkup from "misago/components/misago-markup"
+import {
+  update as updatePosts,
+  append as appendPosts
+} from "misago/reducers/posts"
+import { updateProvider } from "misago/reducers/search"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export default function(props) {
   return (
     <div>
-      <PostFeed
-        isReady={true}
-        posts={props.results}
-      />
+      <PostFeed isReady={true} posts={props.results} />
       <LoadMore {...props} />
     </div>
-  );
+  )
 }
 
 export class LoadMore extends React.Component {
   onClick = () => {
-    store.dispatch(updatePosts({
-      isBusy: true
-    }));
+    store.dispatch(
+      updatePosts({
+        isBusy: true
+      })
+    )
 
-    ajax.get(this.props.provider.api, {
-      q: this.props.query,
-      page: this.props.next
-    }).then((providers) => {
-      providers.forEach((provider) => {
-        if (provider.id !== 'threads') return;
-        store.dispatch(appendPosts(provider.results));
-        store.dispatch(updateProvider(provider));
-      });
+    ajax
+      .get(this.props.provider.api, {
+        q: this.props.query,
+        page: this.props.next
+      })
+      .then(
+        providers => {
+          providers.forEach(provider => {
+            if (provider.id !== "threads") return
+            store.dispatch(appendPosts(provider.results))
+            store.dispatch(updateProvider(provider))
+          })
 
-      store.dispatch(updatePosts({
-        isBusy: false
-      }));
-    }, (rejection) => {
-      snackbar.apiError(rejection);
+          store.dispatch(
+            updatePosts({
+              isBusy: false
+            })
+          )
+        },
+        rejection => {
+          snackbar.apiError(rejection)
 
-      store.dispatch(updatePosts({
-        isBusy: false
-      }));
-    });
-  };
+          store.dispatch(
+            updatePosts({
+              isBusy: false
+            })
+          )
+        }
+      )
+  }
 
   render() {
-    if (!this.props.more) return null;
+    if (!this.props.more) return null
 
     return (
       <div className="pager-more">
@@ -62,6 +72,6 @@ export class LoadMore extends React.Component {
           {gettext("Show more")}
         </Button>
       </div>
-    );
+    )
   }
-}
+}

+ 9 - 21
frontend/src/components/search/users/index.js

@@ -1,43 +1,31 @@
-// jshint ignore:start
-import React from 'react';
-import SearchPage from '../page';
-import UsersList from 'misago/components/users-list';
+import React from "react"
+import SearchPage from "../page"
+import UsersList from "misago/components/users-list"
 
 export default function(props) {
   return (
-    <SearchPage
-      provider={props.route.provider}
-      search={props.search}
-    >
-      <Blankslate
-        query={props.search.query}
-        users={props.users}
-      >
-        <UsersList
-          cols={3}
-          isReady={true}
-          users={props.users}
-        />
+    <SearchPage provider={props.route.provider} search={props.search}>
+      <Blankslate query={props.search.query} users={props.users}>
+        <UsersList cols={3} isReady={true} users={props.users} />
       </Blankslate>
     </SearchPage>
   )
 }
 
 export function Blankslate(props) {
-  if (props.users.length) return props.children;
+  if (props.users.length) return props.children
 
   if (props.query.length) {
     return (
       <p className="lead">
         {gettext("No users matching search query have been found.")}
       </p>
-    );
+    )
   }
 
   return (
     <p className="lead">
       {gettext("Enter at least two characters to search users.")}
     </p>
-  );
+  )
 }
-

+ 47 - 48
frontend/src/components/select.js

@@ -1,74 +1,73 @@
-import React from 'react';
+import React from "react"
 
 export default class extends React.Component {
   getChoice() {
-    let choice = null;
-    this.props.choices.map((item) => {
+    let choice = null
+    this.props.choices.map(item => {
       if (item.value === this.props.value) {
-        choice = item;
+        choice = item
       }
-    });
-    return choice;
+    })
+    return choice
   }
 
   getIcon() {
-    return this.getChoice().icon;
+    return this.getChoice().icon
   }
 
   getLabel() {
-    return this.getChoice().label;
+    return this.getChoice().label
   }
 
-  /* jshint ignore:start */
-  change = (value) => {
+  change = value => {
     return () => {
       this.props.onChange({
         target: {
           value: value
         }
-      });
-    };
-  };
-  /* jshint ignore:end */
+      })
+    }
+  }
 
   render() {
-    /* jshint ignore:start */
-    return <div className="btn-group btn-select-group">
-      <button type="button"
-              className="btn btn-select dropdown-toggle"
-              id={this.props.id || null}
-              data-toggle="dropdown"
-              aria-haspopup="true"
-              aria-expanded="false"
-              aria-describedby={this.props['aria-describedby'] || null}
-              disabled={this.props.disabled || false}>
-        <Icon icon={this.getIcon()} />
-        {this.getLabel()}
-      </button>
-      <ul className="dropdown-menu">
-        {this.props.choices.map((item, i) => {
-          return <li key={i}>
-            <button type="button" className="btn-link"
-                    onClick={this.change(item.value)}>
-              <Icon icon={item.icon} />
-              {item.label}
-            </button>
-          </li>;
-        })}
-      </ul>
-    </div>;
-    /* jshint ignore:end */
+    return (
+      <div className="btn-group btn-select-group">
+        <button
+          type="button"
+          className="btn btn-select dropdown-toggle"
+          id={this.props.id || null}
+          data-toggle="dropdown"
+          aria-haspopup="true"
+          aria-expanded="false"
+          aria-describedby={this.props["aria-describedby"] || null}
+          disabled={this.props.disabled || false}
+        >
+          <Icon icon={this.getIcon()} />
+          {this.getLabel()}
+        </button>
+        <ul className="dropdown-menu">
+          {this.props.choices.map((item, i) => {
+            return (
+              <li key={i}>
+                <button
+                  type="button"
+                  className="btn-link"
+                  onClick={this.change(item.value)}
+                >
+                  <Icon icon={item.icon} />
+                  {item.label}
+                </button>
+              </li>
+            )
+          })}
+        </ul>
+      </div>
+    )
   }
 }
 
-/* jshint ignore:start */
 export function Icon({ icon }) {
-  if (!icon) return null;
+  if (!icon) return null
 
-  return (
-    <span className="material-icon">
-      {icon}
-    </span>
-  )
+  return <span className="material-icon">{icon}</span>
 }
-/* jshint ignore:end */

+ 57 - 66
frontend/src/components/sign-in.js

@@ -1,113 +1,107 @@
-import React from 'react'; // jshint ignore:line
-import misago from 'misago/index';
-import Button from 'misago/components/button'; // jshint ignore:line
-import Form from 'misago/components/form';
-import StartSocialAuth from 'misago/components/StartSocialAuth'; // jshint ignore:line
-import ajax from 'misago/services/ajax';
-import modal from 'misago/services/modal';
-import snackbar from 'misago/services/snackbar';
-import showBannedPage from 'misago/utils/banned-page';
+import React from "react"
+import misago from "misago/index"
+import Button from "misago/components/button"
+import Form from "misago/components/form"
+import StartSocialAuth from "misago/components/StartSocialAuth"
+import ajax from "misago/services/ajax"
+import modal from "misago/services/modal"
+import snackbar from "misago/services/snackbar"
+import showBannedPage from "misago/utils/banned-page"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
-      'isLoading': false,
-      'showActivation': false,
+      isLoading: false,
+      showActivation: false,
 
-      'username': '',
-      'password': '',
+      username: "",
+      password: "",
 
-      'validators': {
-        'username': [],
-        'password': []
+      validators: {
+        username: [],
+        password: []
       }
-    };
+    }
   }
 
   clean() {
     if (!this.isValid()) {
-      snackbar.error(gettext("Fill out both fields."));
-      return false;
+      snackbar.error(gettext("Fill out both fields."))
+      return false
     } else {
-      return true;
+      return true
     }
   }
 
   send() {
-    return ajax.post(misago.get('AUTH_API'), {
-      'username': this.state.username,
-      'password': this.state.password
-    });
+    return ajax.post(misago.get("AUTH_API"), {
+      username: this.state.username,
+      password: this.state.password
+    })
   }
 
   handleSuccess() {
-    let form = $('#hidden-login-form');
+    let form = $("#hidden-login-form")
 
-    form.append('<input type="text" name="username" />');
-    form.append('<input type="password" name="password" />');
+    form.append('<input type="text" name="username" />')
+    form.append('<input type="password" name="password" />')
 
     // fill out form with user credentials and submit it, this will tell
     // Misago to redirect user back to right page, and will trigger browser's
     // key ring feature
-    form.find('input[type="hidden"]').val(ajax.getCsrfToken());
-    form.find('input[name="redirect_to"]').val(window.location.pathname);
-    form.find('input[name="username"]').val(this.state.username);
-    form.find('input[name="password"]').val(this.state.password);
-    form.submit();
+    form.find('input[type="hidden"]').val(ajax.getCsrfToken())
+    form.find('input[name="redirect_to"]').val(window.location.pathname)
+    form.find('input[name="username"]').val(this.state.username)
+    form.find('input[name="password"]').val(this.state.password)
+    form.submit()
 
     // keep form loading
     this.setState({
-      'isLoading': true
-    });
+      isLoading: true
+    })
   }
 
   handleError(rejection) {
     if (rejection.status === 400) {
-      if (rejection.code === 'inactive_admin') {
-        snackbar.info(rejection.detail);
-      } else if (rejection.code === 'inactive_user') {
-        snackbar.info(rejection.detail);
+      if (rejection.code === "inactive_admin") {
+        snackbar.info(rejection.detail)
+      } else if (rejection.code === "inactive_user") {
+        snackbar.info(rejection.detail)
         this.setState({
-          'showActivation': true
-        });
-      } else if (rejection.code === 'banned') {
-        showBannedPage(rejection.detail);
-        modal.hide();
+          showActivation: true
+        })
+      } else if (rejection.code === "banned") {
+        showBannedPage(rejection.detail)
+        modal.hide()
       } else {
-        snackbar.error(rejection.detail);
+        snackbar.error(rejection.detail)
       }
     } else if (rejection.status === 403 && rejection.ban) {
-      showBannedPage(rejection.ban);
-      modal.hide();
+      showBannedPage(rejection.ban)
+      modal.hide()
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
   }
 
   getActivationButton() {
-    if (!this.state.showActivation) return null;
+    if (!this.state.showActivation) return null
 
-    /* jshint ignore:start */
     return (
       <a
         className="btn btn-success btn-block"
-        href={misago.get('REQUEST_ACTIVATION_URL')}
+        href={misago.get("REQUEST_ACTIVATION_URL")}
       >
         {gettext("Activate account")}
       </a>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   render() {
-    /* jshint ignore:start */
     return (
-      <div
-        className="modal-dialog modal-sm modal-sign-in"
-        role="document"
-      >
+      <div className="modal-dialog modal-sm modal-sign-in" role="document">
         <div className="modal-content">
           <div className="modal-header">
             <button
@@ -122,7 +116,6 @@ export default class extends Form {
           </div>
           <form onSubmit={this.handleSubmit}>
             <div className="modal-body">
-
               <StartSocialAuth
                 buttonLabel={gettext("Sign in with %(site)s")}
                 formLabel={gettext("Or use your forum account:")}
@@ -135,7 +128,7 @@ export default class extends Form {
                     className="form-control input-lg"
                     disabled={this.state.isLoading}
                     id="id_username"
-                    onChange={this.bindInput('username')}
+                    onChange={this.bindInput("username")}
                     placeholder={gettext("Username or e-mail")}
                     type="text"
                     value={this.state.username}
@@ -149,14 +142,13 @@ export default class extends Form {
                     className="form-control input-lg"
                     disabled={this.state.isLoading}
                     id="id_password"
-                    onChange={this.bindInput('password')}
+                    onChange={this.bindInput("password")}
                     placeholder={gettext("Password")}
                     type="password"
                     value={this.state.password}
                   />
                 </div>
               </div>
-
             </div>
             <div className="modal-footer">
               {this.getActivationButton()}
@@ -168,15 +160,14 @@ export default class extends Form {
               </Button>
               <a
                 className="btn btn-default btn-block"
-                href={misago.get('FORGOTTEN_PASSWORD_URL')}
+                href={misago.get("FORGOTTEN_PASSWORD_URL")}
               >
-                 {gettext("Forgot password?")}
+                {gettext("Forgot password?")}
               </a>
             </div>
           </form>
         </div>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 }

+ 18 - 20
frontend/src/components/snackbar.js

@@ -1,36 +1,34 @@
-import React from 'react';
+import React from "react"
 
-/* jshint ignore:start */
 const TYPES_CLASSES = {
-  'info': 'alert-info',
-  'success': 'alert-success',
-  'warning': 'alert-warning',
-  'error': 'alert-danger'
-};
-/* jshint ignore:end */
+  info: "alert-info",
+  success: "alert-success",
+  warning: "alert-warning",
+  error: "alert-danger"
+}
 
 export class Snackbar extends React.Component {
   getSnackbarClass() {
-    let snackbarClass = 'alerts-snackbar';
+    let snackbarClass = "alerts-snackbar"
     if (this.props.isVisible) {
-      snackbarClass += ' in';
+      snackbarClass += " in"
     } else {
-      snackbarClass += ' out';
+      snackbarClass += " out"
     }
-    return snackbarClass;
+    return snackbarClass
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className={this.getSnackbarClass()}>
-      <p className={'alert ' + TYPES_CLASSES[this.props.type]}>
-        {this.props.message}
-      </p>
-    </div>;
-    /* jshint ignore:end */
+    return (
+      <div className={this.getSnackbarClass()}>
+        <p className={"alert " + TYPES_CLASSES[this.props.type]}>
+          {this.props.message}
+        </p>
+      </div>
+    )
   }
 }
 
 export function select(state) {
-  return state.snackbar;
+  return state.snackbar
 }

+ 26 - 22
frontend/src/components/social-auth/complete.js

@@ -1,23 +1,28 @@
-/* jshint ignore:start */
-import React from 'react';
-import Header from './header';
-import misago from 'misago';
+import React from "react"
+import Header from "./header"
+import misago from "misago"
 
 const Complete = ({ activation, backend_name, username }) => {
-  let icon = '';
-  let message = '';
-  if (activation === 'user') {
-    message = gettext("%(username)s, your account has been created but you need to activate it before you will be able to sign in.");
-  } else if (activation === 'admin') {
-    message = gettext("%(username)s, your account has been created but board administrator will have to activate it before you will be able to sign in.");
+  let icon = ""
+  let message = ""
+  if (activation === "user") {
+    message = gettext(
+      "%(username)s, your account has been created but you need to activate it before you will be able to sign in."
+    )
+  } else if (activation === "admin") {
+    message = gettext(
+      "%(username)s, your account has been created but board administrator will have to activate it before you will be able to sign in."
+    )
   } else {
-    message = gettext("%(username)s, your account has been created and you have been signed in to it.")
+    message = gettext(
+      "%(username)s, your account has been created and you have been signed in to it."
+    )
   }
 
-  if (activation === 'active') {
-    icon = 'check';
+  if (activation === "active") {
+    icon = "check"
   } else {
-    icon = 'info_outline';
+    icon = "info_outline"
   }
 
   return (
@@ -26,16 +31,15 @@ const Complete = ({ activation, backend_name, username }) => {
       <div className="container">
         <div className="row">
           <div className="col-md-6 col-md-offset-3">
-
             <div className="panel panel-default panel-form">
               <div className="panel-heading">
-                <h3 className="panel-title">{gettext("Registration completed!")}</h3>
+                <h3 className="panel-title">
+                  {gettext("Registration completed!")}
+                </h3>
               </div>
               <div className="panel-body panel-message-body">
                 <div className="message-icon">
-                  <span className="material-icon">
-                    {icon}
-                  </span>
+                  <span className="material-icon">{icon}</span>
                 </div>
                 <div className="message-body">
                   <p className="lead">
@@ -44,7 +48,7 @@ const Complete = ({ activation, backend_name, username }) => {
                   <p className="help-block">
                     <a
                       className="btn btn-default"
-                      href={misago.get('MISAGO_PATH')}
+                      href={misago.get("MISAGO_PATH")}
                     >
                       {gettext("Return to forum index")}
                     </a>
@@ -56,7 +60,7 @@ const Complete = ({ activation, backend_name, username }) => {
         </div>
       </div>
     </div>
-  );
+  )
 }
 
-export default Complete;
+export default Complete

+ 6 - 9
frontend/src/components/social-auth/header.js

@@ -1,21 +1,18 @@
-/* jshint ignore:start */
-import React from 'react';
+import React from "react"
 
 const Header = ({ backendName }) => {
-  const pageTitleTpl = gettext("Sign in with %(backend)s");
-  const pageTitle = interpolate(pageTitleTpl, { backend: backendName }, true);
+  const pageTitleTpl = gettext("Sign in with %(backend)s")
+  const pageTitle = interpolate(pageTitleTpl, { backend: backendName }, true)
 
   return (
     <div className="page-header-bg">
       <div className="page-header">
         <div className="container">
-          <h1>
-            {pageTitle}
-          </h1>
+          <h1>{pageTitle}</h1>
         </div>
       </div>
     </div>
-  );
+  )
 }
 
-export default Header;
+export default Header

+ 14 - 15
frontend/src/components/social-auth/index.js

@@ -1,30 +1,29 @@
-/* jshint ignore:start */
-import React from 'react';
-import Register from './register';
-import Complete from './complete';
+import React from "react"
+import Register from "./register"
+import Complete from "./complete"
 
 export default class SocialAuth extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       step: props.step,
 
-      activation: props.activation || '',
-      email: props.email || '',
-      username: props.username || ''
-    };
+      activation: props.activation || "",
+      email: props.email || "",
+      username: props.username || ""
+    }
   }
 
   handleRegistrationComplete = ({ activation, email, step, username }) => {
-    this.setState({ activation, email, step, username });
-  };
+    this.setState({ activation, email, step, username })
+  }
 
   render() {
     const { backend_name, url } = this.props
     const { activation, email, step, username } = this.state
 
-    if (step === 'register') {
+    if (step === "register") {
       return (
         <Register
           backend_name={backend_name}
@@ -33,7 +32,7 @@ export default class SocialAuth extends React.Component {
           username={username}
           onRegistrationComplete={this.handleRegistrationComplete}
         />
-      );
+      )
     }
 
     return (
@@ -43,7 +42,7 @@ export default class SocialAuth extends React.Component {
         email={email}
         url={url}
         username={username}
-      /> 
+      />
     )
   }
-}
+}

+ 86 - 82
frontend/src/components/social-auth/register.js

@@ -1,40 +1,35 @@
-/* jshint ignore:start */
-import React from 'react';
-import misago from 'misago';
-import RegisterLegalFootnote from 'misago/components/RegisterLegalFootnote';
-import Button from 'misago/components/button';
-import Form from 'misago/components/form';
-import FormGroup from 'misago/components/form-group';
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
-import * as validators from 'misago/utils/validators';
-import Header from './header';
+import React from "react"
+import misago from "misago"
+import RegisterLegalFootnote from "misago/components/RegisterLegalFootnote"
+import Button from "misago/components/button"
+import Form from "misago/components/form"
+import FormGroup from "misago/components/form-group"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import * as validators from "misago/utils/validators"
+import Header from "./header"
 
 export default class Register extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     const formValidators = {
-      email: [
-        validators.email()
-      ],
-      username: [
-        validators.usernameContent()
-      ]
-    };
-
-    if (!!misago.get('TERMS_OF_SERVICE_ID')) {
-      formValidators.termsOfService = [validators.requiredTermsOfService()];
+      email: [validators.email()],
+      username: [validators.usernameContent()]
     }
 
-    if (!!misago.get('PRIVACY_POLICY_ID')) {
-      formValidators.privacyPolicy = [validators.requiredPrivacyPolicy()];
+    if (!!misago.get("TERMS_OF_SERVICE_ID")) {
+      formValidators.termsOfService = [validators.requiredTermsOfService()]
+    }
+
+    if (!!misago.get("PRIVACY_POLICY_ID")) {
+      formValidators.privacyPolicy = [validators.requiredPrivacyPolicy()]
     }
 
     this.state = {
-      email: props.email || '',
+      email: props.email || "",
       emailProtected: !!props.email,
-      username: props.username || '',
+      username: props.username || "",
 
       termsOfService: null,
       privacyPolicy: null,
@@ -43,37 +38,37 @@ export default class Register extends Form {
       errors: {},
 
       isLoading: false
-    };
+    }
   }
 
   clean() {
-    let errors = this.validate();
+    let errors = this.validate()
     let lengths = [
       this.state.email.trim().length,
-      this.state.username.trim().length,
-    ];
+      this.state.username.trim().length
+    ]
 
     if (lengths.indexOf(0) !== -1) {
-      snackbar.error(gettext("Fill out all fields."));
-      return false;
+      snackbar.error(gettext("Fill out all fields."))
+      return false
     }
 
-    const { validators } = this.state;
+    const { validators } = this.state
 
-    const checkTermsOfService = !!misago.get('TERMS_OF_SERVICE_ID');
+    const checkTermsOfService = !!misago.get("TERMS_OF_SERVICE_ID")
     if (checkTermsOfService && this.state.termsOfService === null) {
-      snackbar.error(validators.termsOfService[0](null));
-      return false;
+      snackbar.error(validators.termsOfService[0](null))
+      return false
     }
 
-    const checkPrivacyPolicy = !!misago.get('PRIVACY_POLICY_ID');
+    const checkPrivacyPolicy = !!misago.get("PRIVACY_POLICY_ID")
     if (checkPrivacyPolicy && this.state.privacyPolicy === null) {
-      snackbar.error(validators.privacyPolicy[0](null));
-      snackbar.error(gettext("You need to accept the privacy policy."));
-      return false;
+      snackbar.error(validators.privacyPolicy[0](null))
+      snackbar.error(gettext("You need to accept the privacy policy."))
+      return false
     }
 
-    return true;
+    return true
   }
 
   send() {
@@ -82,66 +77,68 @@ export default class Register extends Form {
       username: this.state.username,
       terms_of_service: this.state.termsOfService,
       privacy_policy: this.state.privacyPolicy
-    });
+    })
   }
 
   handleSuccess(response) {
-    onRegistrationComplete(response);
+    const { onRegistrationComplete } = this.props
+    onRegistrationComplete(response)
   }
 
   handleError(rejection) {
     if (rejection.status === 200) {
       // We've entered "errored" state because response is HTML instead of exptected JSON
-      const { onRegistrationComplete } = this.props;
-      const { username } = this.state;
-      onRegistrationComplete({ activation: 'active', step: 'done', username });
+      const { onRegistrationComplete } = this.props
+      const { username } = this.state
+      onRegistrationComplete({ activation: "active", step: "done", username })
     } else if (rejection.status === 400) {
-      const stateUpdate = { errors: rejection };
+      const stateUpdate = { errors: rejection }
       if (rejection.email) {
-        stateUpdate.emailProtected = false;
+        stateUpdate.emailProtected = false
       }
-      this.setState(stateUpdate);
+      this.setState(stateUpdate)
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
   }
 
-  handlePrivacyPolicyChange = (event) => {
-    const value = event.target.value;
-    this.handleToggleAgreement('privacyPolicy', value);
-  };
+  handlePrivacyPolicyChange = event => {
+    const value = event.target.value
+    this.handleToggleAgreement("privacyPolicy", value)
+  }
 
-  handleTermsOfServiceChange = (event) => {
-    const value = event.target.value;
-    this.handleToggleAgreement('termsOfService', value);
-  };
+  handleTermsOfServiceChange = event => {
+    const value = event.target.value
+    this.handleToggleAgreement("termsOfService", value)
+  }
 
   handleToggleAgreement = (agreement, value) => {
     this.setState((prevState, props) => {
       if (prevState[agreement] === null) {
-        const errors = { ...prevState.errors, [agreement]: null };
-        return { errors, [agreement]: value };
+        const errors = { ...prevState.errors, [agreement]: null }
+        return { errors, [agreement]: value }
       }
 
-      const validator = this.state.validators[agreement][0];
-      const errors = { ...prevState.errors, [agreement]: [validator(null)] };
-      return { errors, [agreement]: null };
+      const validator = this.state.validators[agreement][0]
+      const errors = { ...prevState.errors, [agreement]: [validator(null)] }
+      return { errors, [agreement]: null }
     })
-  };
+  }
 
   render() {
-    const { backend_name } = this.props;
-    const {
-      email,
-      emailProtected,
-      username,
-      isLoading
-    } = this.state;
-
-    let emailHelpText = null;
+    const { backend_name } = this.props
+    const { email, emailProtected, username, isLoading } = this.state
+
+    let emailHelpText = null
     if (emailProtected) {
-      const emailHelpTextTpl = gettext("Your e-mail address has been verified by %(backend)s.");
-      emailHelpText = interpolate(emailHelpTextTpl, { backend: backend_name }, true);
+      const emailHelpTextTpl = gettext(
+        "Your e-mail address has been verified by %(backend)s."
+      )
+      emailHelpText = interpolate(
+        emailHelpTextTpl,
+        { backend: backend_name },
+        true
+      )
     }
 
     return (
@@ -153,7 +150,9 @@ export default class Register extends Form {
               <form onSubmit={this.handleSubmit}>
                 <div className="panel panel-default panel-form">
                   <div className="panel-heading">
-                    <h3 className="panel-title">{gettext("Complete your details")}</h3>
+                    <h3 className="panel-title">
+                      {gettext("Complete your details")}
+                    </h3>
                   </div>
                   <div className="panel-body">
                     <FormGroup
@@ -166,7 +165,7 @@ export default class Register extends Form {
                         id="id_username"
                         className="form-control"
                         disabled={isLoading}
-                        onChange={this.bindInput('username')}
+                        onChange={this.bindInput("username")}
                         value={username}
                       />
                     </FormGroup>
@@ -174,14 +173,16 @@ export default class Register extends Form {
                       for="id_email"
                       label={gettext("E-mail address")}
                       helpText={emailHelpText}
-                      validation={emailProtected ? null : this.state.errors.email}
+                      validation={
+                        emailProtected ? null : this.state.errors.email
+                      }
                     >
                       <input
                         type="email"
                         id="id_email"
                         className="form-control"
                         disabled={isLoading || emailProtected}
-                        onChange={this.bindInput('email')}
+                        onChange={this.bindInput("email")}
                         value={email}
                       />
                     </FormGroup>
@@ -194,7 +195,10 @@ export default class Register extends Form {
                     />
                   </div>
                   <div className="panel-footer">
-                    <Button className="btn-primary" loading={this.state.isLoading}>
+                    <Button
+                      className="btn-primary"
+                      loading={this.state.isLoading}
+                    >
                       {gettext("Sign in")}
                     </Button>
                   </div>
@@ -204,6 +208,6 @@ export default class Register extends Form {
           </div>
         </div>
       </div>
-    );
+    )
   }
-}
+}

+ 8 - 17
frontend/src/components/thread/header/breadcrumbs.js

@@ -1,24 +1,18 @@
-/* jshint ignore:start */
-import React from 'react';
+import React from "react"
 
 export default function(props) {
   return (
     <div className="page-breadcrumbs">
       <div className="container">
         <ol className="breadcrumb hidden-xs">
-          {props.path.map((item) => {
-            return (
-              <Breadcrumb
-                key={item.id}
-                node={item}
-              />
-            );
+          {props.path.map(item => {
+            return <Breadcrumb key={item.id} node={item} />
           })}
         </ol>
         <GoBack {...props} />
       </div>
     </div>
-  );
+  )
 }
 
 export function Breadcrumb(props) {
@@ -26,19 +20,16 @@ export function Breadcrumb(props) {
     <li>
       <a href={props.node.url.index}>{props.node.name}</a>
     </li>
-  );
+  )
 }
 
 export function GoBack(props) {
-  const lastItem = props.path[props.path.length - 1];
+  const lastItem = props.path[props.path.length - 1]
 
   return (
     <a href={lastItem.url.index} className="go-back-sm visible-xs-block">
-      <span className="material-icon">
-        chevron_left
-      </span>
+      <span className="material-icon">chevron_left</span>
       {lastItem.name}
     </a>
-  );
+  )
 }
-/* jshint ignore:end */

+ 56 - 62
frontend/src/components/thread/header/index.js

@@ -1,18 +1,17 @@
-/* jshint ignore:start */
-import React from 'react';
-import Breadcrumbs from './breadcrumbs';
-import { isModerationVisible, ModerationControls } from '../moderation/thread';
-import Stats from './stats';
-import Form from 'misago/components/form';
-import { getTitleValidators } from 'misago/components/posting/utils/validators';
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
-import * as thread from 'misago/reducers/thread';
+import React from "react"
+import Breadcrumbs from "./breadcrumbs"
+import { isModerationVisible, ModerationControls } from "../moderation/thread"
+import Stats from "./stats"
+import Form from "misago/components/form"
+import { getTitleValidators } from "misago/components/posting/utils/validators"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
+import * as thread from "misago/reducers/thread"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isEditing: false,
@@ -24,68 +23,68 @@ export default class extends Form {
         title: getTitleValidators()
       },
       errors: {}
-    };
+    }
   }
 
-  onChange = (event) => {
-    this.changeValue('title', event.target.value);
-  };
+  onChange = event => {
+    this.changeValue("title", event.target.value)
+  }
 
   onEdit = () => {
     this.setState({
       isEditing: true
-    });
-  };
+    })
+  }
 
   onCancel = () => {
     this.setState({
       title: this.props.thread.title,
 
       isEditing: false
-    });
-  };
+    })
+  }
 
   clean() {
     if (!this.state.title.trim().length) {
-      snackbar.error(gettext("You have to enter thread title."));
-      return false;
+      snackbar.error(gettext("You have to enter thread title."))
+      return false
     }
 
-    const errors = this.validate();
+    const errors = this.validate()
 
     if (errors.title) {
-      snackbar.error(errors.title[0]);
-      return false;
+      snackbar.error(errors.title[0])
+      return false
     }
 
-    return true;
+    return true
   }
 
   send() {
     return ajax.patch(this.props.thread.api.index, [
-      {op: 'replace', path: 'title', value: this.state.title}
-    ]);
+      { op: "replace", path: "title", value: this.state.title }
+    ])
   }
 
   handleSuccess(data) {
-    store.dispatch(thread.update(data));
+    store.dispatch(thread.update(data))
 
     this.setState({
-      'isEditing': false
-    });
+      isEditing: false
+    })
   }
 
   handleError(rejection) {
     if (rejection.status === 400) {
-      snackbar.error(rejection.detail[0]);
+      snackbar.error(rejection.detail[0])
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
   }
 
   render() {
-    const {thread, user} = this.props;
-    const showModeration = !!user.id && isModerationVisible(thread);
+    const { thread, user } = this.props
+    const showModeration = !!user.id && isModerationVisible(thread)
 
     if (this.state.isEditing) {
       return (
@@ -131,19 +130,25 @@ export default class extends Form {
           </div>
           <Stats thread={thread} />
         </div>
-      );
+      )
     } else if (user.id && thread.acl.can_edit) {
       return (
         <div className="page-header">
           <Breadcrumbs path={thread.path} />
           <div className="container">
             <div className="row">
-              <div className={showModeration ? "col-sm-9 col-md-8" : "col-sm-10 col-md-10"}>
-                <h1>
-                  {thread.title}
-                </h1>
+              <div
+                className={
+                  showModeration ? "col-sm-9 col-md-8" : "col-sm-10 col-md-10"
+                }
+              >
+                <h1>{thread.title}</h1>
               </div>
-              <div className={showModeration ? "col-sm-3 col-md-4" : "col-sm-3 col-md-2"}>
+              <div
+                className={
+                  showModeration ? "col-sm-3 col-md-4" : "col-sm-3 col-md-2"
+                }
+              >
                 <div className="row xs-margin-top md-margin-top-no">
                   <div className={showModeration ? "col-xs-6" : "col-xs-12"}>
                     <button
@@ -153,21 +158,17 @@ export default class extends Form {
                       type="button"
                     >
                       <span className="material-icon">edit</span>
-                      <span className="hidden-sm">
-                        {gettext("Edit")}
-                      </span>
+                      <span className="hidden-sm">{gettext("Edit")}</span>
                     </button>
                   </div>
-                  {showModeration && (
-                    <Moderation {...this.props} />
-                  )}
+                  {showModeration && <Moderation {...this.props} />}
                 </div>
               </div>
             </div>
           </div>
           <Stats thread={thread} />
         </div>
-      );
+      )
     } else if (showModeration) {
       return (
         <div className="page-header">
@@ -175,23 +176,18 @@ export default class extends Form {
           <div className="container">
             <div className="row">
               <div className="col-sm-9 col-md-10">
-                <h1>
-                  {thread.title}
-                </h1>
+                <h1>{thread.title}</h1>
               </div>
               <div className="col-sm-3 col-md-2">
                 <div className="row xs-margin-top md-margin-top-no">
-                  <Moderation
-                    isSingle={true}
-                    {...this.props}
-                  />
+                  <Moderation isSingle={true} {...this.props} />
                 </div>
               </div>
             </div>
           </div>
           <Stats thread={thread} />
         </div>
-      );
+      )
     }
 
     return (
@@ -202,7 +198,7 @@ export default class extends Form {
         </div>
         <Stats thread={thread} />
       </div>
-    );
+    )
   }
 }
 
@@ -219,9 +215,7 @@ export function Moderation(props) {
             disabled={props.thread.isBusy}
             type="button"
           >
-            <span className="material-icon">
-              settings
-            </span>
+            <span className="material-icon">settings</span>
             <span className={props.isSingle ? "" : "hidden-sm"}>
               {gettext("Moderation")}
             </span>
@@ -234,5 +228,5 @@ export function Moderation(props) {
         </div>
       </div>
     </div>
-  );
-}
+  )
+}

+ 98 - 87
frontend/src/components/thread/header/stats.js

@@ -1,126 +1,138 @@
-/* jshint ignore:start */
-import React from 'react';
-import escapeHtml from 'misago/utils/escape-html';
+import React from "react"
+import escapeHtml from "misago/utils/escape-html"
 
-const LAST_POSTER_URL = '<a href="%(url)s" class="poster-title">%(user)s</a>';
-const LAST_POSTER_SPAN = '<span class="poster-title">%(user)s</span>';
-const LAST_REPLY = '<abbr class="last-title" title="%(absolute)s">%(relative)s</abbr>';
+const LAST_POSTER_URL = '<a href="%(url)s" class="poster-title">%(user)s</a>'
+const LAST_POSTER_SPAN = '<span class="poster-title">%(user)s</span>'
+const LAST_REPLY =
+  '<abbr class="last-title" title="%(absolute)s">%(relative)s</abbr>'
 
 export function Weight(props) {
   if (props.thread.weight == 2) {
-    return <li className="thread-pinned-globally">
-      <span className="material-icon">
-        bookmark
-      </span>
-      <span className="icon-legend">
-        {gettext("Pinned globally")}
-      </span>
-    </li>;
+    return (
+      <li className="thread-pinned-globally">
+        <span className="material-icon">bookmark</span>
+        <span className="icon-legend">{gettext("Pinned globally")}</span>
+      </li>
+    )
   } else if (props.thread.weight == 1) {
-    return <li className="thread-pinned-locally">
-      <span className="material-icon">
-        bookmark_border
-      </span>
-      <span className="icon-legend">
-        {gettext("Pinned locally")}
-      </span>
-    </li>;
+    return (
+      <li className="thread-pinned-locally">
+        <span className="material-icon">bookmark_border</span>
+        <span className="icon-legend">{gettext("Pinned locally")}</span>
+      </li>
+    )
   } else {
-    return null;
+    return null
   }
 }
 
 export function Unapproved(props) {
   if (props.thread.is_unapproved) {
-    return <li className="thread-unapproved">
-      <span className="material-icon">
-        remove_circle
-      </span>
-      <span className="icon-legend">
-        {gettext("Unapproved")}
-      </span>
-    </li>;
+    return (
+      <li className="thread-unapproved">
+        <span className="material-icon">remove_circle</span>
+        <span className="icon-legend">{gettext("Unapproved")}</span>
+      </li>
+    )
   } else if (props.thread.has_unapproved_posts) {
-    return <li className="thread-unapproved-posts">
-      <span className="material-icon">
-        remove_circle_outline
-      </span>
-      <span className="icon-legend">
-        {gettext("Unapproved posts")}
-      </span>
-    </li>;
+    return (
+      <li className="thread-unapproved-posts">
+        <span className="material-icon">remove_circle_outline</span>
+        <span className="icon-legend">{gettext("Unapproved posts")}</span>
+      </li>
+    )
   } else {
-    return null;
+    return null
   }
 }
 
 export function IsHidden(props) {
   if (props.thread.is_hidden) {
-    return <li className="thread-hidden">
-      <span className="material-icon">
-        visibility_off
-      </span>
-      <span className="icon-legend">
-        {gettext("Hidden")}
-      </span>
-    </li>;
+    return (
+      <li className="thread-hidden">
+        <span className="material-icon">visibility_off</span>
+        <span className="icon-legend">{gettext("Hidden")}</span>
+      </li>
+    )
   } else {
-    return null;
+    return null
   }
 }
 
 export function IsClosed(props) {
   if (props.thread.is_closed) {
-    return <li className="thread-closed">
-      <span className="material-icon">
-        lock_outline
-      </span>
-      <span className="icon-legend">
-        {gettext("Closed")}
-      </span>
-    </li>;
+    return (
+      <li className="thread-closed">
+        <span className="material-icon">lock_outline</span>
+        <span className="icon-legend">{gettext("Closed")}</span>
+      </li>
+    )
   } else {
-    return null;
+    return null
   }
 }
 
 export function Replies(props) {
-  const message = ngettext("%(replies)s reply", "%(replies)s replies", props.thread.replies);
-  const legend = interpolate(message, {'replies': props.thread.replies}, true);
+  const message = ngettext(
+    "%(replies)s reply",
+    "%(replies)s replies",
+    props.thread.replies
+  )
+  const legend = interpolate(message, { replies: props.thread.replies }, true)
 
-  return <li className="thread-replies">
-    <span className="material-icon">
-      forum
-    </span>
-    <span className="icon-legend">
-      {legend}
-    </span>
-  </li>;
+  return (
+    <li className="thread-replies">
+      <span className="material-icon">forum</span>
+      <span className="icon-legend">{legend}</span>
+    </li>
+  )
 }
 
 export function LastReply(props) {
-  let user = null;
+  let user = null
   if (props.thread.url.last_poster) {
-    user = interpolate(LAST_POSTER_URL, {
-      url: escapeHtml(props.thread.url.last_poster),
-      user: escapeHtml(props.thread.last_poster_name)
-    }, true);
+    user = interpolate(
+      LAST_POSTER_URL,
+      {
+        url: escapeHtml(props.thread.url.last_poster),
+        user: escapeHtml(props.thread.last_poster_name)
+      },
+      true
+    )
   } else {
-    user = interpolate(LAST_POSTER_SPAN, {
-      user: escapeHtml(props.thread.last_poster_name)
-    }, true);
-  };
+    user = interpolate(
+      LAST_POSTER_SPAN,
+      {
+        user: escapeHtml(props.thread.last_poster_name)
+      },
+      true
+    )
+  }
 
-  const date = interpolate(LAST_REPLY, {
-    absolute: escapeHtml(props.thread.last_post_on.format('LLL')),
-    relative: escapeHtml(props.thread.last_post_on.fromNow())
-  }, true);
+  const date = interpolate(
+    LAST_REPLY,
+    {
+      absolute: escapeHtml(props.thread.last_post_on.format("LLL")),
+      relative: escapeHtml(props.thread.last_post_on.fromNow())
+    },
+    true
+  )
 
-  const message = interpolate(escapeHtml(gettext("last reply by %(user)s %(date)s")), {
-    date, user
-  }, true);
+  const message = interpolate(
+    escapeHtml(gettext("last reply by %(user)s %(date)s")),
+    {
+      date,
+      user
+    },
+    true
+  )
 
-  return <li className="thread-last-reply" dangerouslySetInnerHTML={{__html: message}}/>;
+  return (
+    <li
+      className="thread-last-reply"
+      dangerouslySetInnerHTML={{ __html: message }}
+    />
+  )
 }
 
 export default function(props) {
@@ -137,6 +149,5 @@ export default function(props) {
         </ul>
       </div>
     </div>
-  );
+  )
 }
-/* jshint ignore:end */

+ 167 - 156
frontend/src/components/thread/moderation/posts/actions.js

@@ -1,93 +1,85 @@
-import moment from 'moment';
-import React from 'react'; // jshint ignore:line
-import * as post from 'misago/reducers/post';
-import * as posts from 'misago/reducers/posts';
-import ajax from 'misago/services/ajax';
-import modal from 'misago/services/modal'; // jshint ignore:line
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
-import ErrorsList from './errors-list'; // jshint ignore:line
+import moment from "moment"
+import React from "react"
+import * as post from "misago/reducers/post"
+import * as posts from "misago/reducers/posts"
+import ajax from "misago/services/ajax"
+import modal from "misago/services/modal"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
+import ErrorsList from "./errors-list"
 
 export function approve(props) {
-  const { selection } = props;
+  const { selection } = props
 
-  const ops = [
-    {'op': 'replace', 'path': 'is-unapproved', 'value': false}
-  ];
+  const ops = [{ op: "replace", path: "is-unapproved", value: false }]
 
-  const newState = selection.map((post) => {
+  const newState = selection.map(post => {
     return {
       id: post.id,
       is_unapproved: false
-    };
-  });
+    }
+  })
 
-  const previousState = selection.map((post) => {
+  const previousState = selection.map(post => {
     return {
       id: post.id,
       is_unapproved: post.is_unapproved
-    };
-  });
+    }
+  })
 
-  patch(props, ops, newState, previousState);
+  patch(props, ops, newState, previousState)
 }
 
 export function protect(props) {
-  const { selection } = props;
+  const { selection } = props
 
-  const ops = [
-    {'op': 'replace', 'path': 'is-protected', 'value': true}
-  ];
+  const ops = [{ op: "replace", path: "is-protected", value: true }]
 
-  const newState = selection.map((post) => {
+  const newState = selection.map(post => {
     return {
       id: post.id,
       is_protected: true
-    };
-  });
+    }
+  })
 
-  const previousState = selection.map((post) => {
+  const previousState = selection.map(post => {
     return {
       id: post.id,
       is_protected: post.is_protected
-    };
-  });
+    }
+  })
 
-  patch(props, ops, newState, previousState);
+  patch(props, ops, newState, previousState)
 }
 
 export function unprotect(props) {
-  const { selection } = props;
+  const { selection } = props
 
-  const ops = [
-    {'op': 'replace', 'path': 'is-protected', 'value': false}
-  ];
+  const ops = [{ op: "replace", path: "is-protected", value: false }]
 
-  const newState = selection.map((post) => {
+  const newState = selection.map(post => {
     return {
       id: post.id,
       is_protected: false
-    };
-  });
+    }
+  })
 
-  const previousState = selection.map((post) => {
+  const previousState = selection.map(post => {
     return {
       id: post.id,
       is_protected: post.is_protected
-    };
-  });
+    }
+  })
 
-  patch(props, ops, newState, previousState);
+  patch(props, ops, newState, previousState)
 }
 
 export function hide(props) {
-  const { selection } = props;
+  const { selection } = props
 
-  const ops = [
-    {'op': 'replace', 'path': 'is-hidden', 'value': true}
-  ];
+  const ops = [{ op: "replace", path: "is-hidden", value: true }]
 
-  const newState = selection.map((post) => {
+  const newState = selection.map(post => {
     return {
       id: post.id,
       is_hidden: true,
@@ -96,30 +88,28 @@ export function hide(props) {
       url: Object.assign(post.url, {
         hidden_by: props.user.url
       })
-    };
-  });
+    }
+  })
 
-  const previousState = selection.map((post) => {
+  const previousState = selection.map(post => {
     return {
       id: post.id,
       is_hidden: post.is_hidden,
       hidden_on: post.hidden_on,
       hidden_by_name: post.hidden_by_name,
       url: post.url
-    };
-  });
+    }
+  })
 
-  patch(props, ops, newState, previousState);
+  patch(props, ops, newState, previousState)
 }
 
 export function unhide(props) {
-  const { selection } = props;
+  const { selection } = props
 
-  const ops = [
-    {'op': 'replace', 'path': 'is-hidden', 'value': false}
-  ];
+  const ops = [{ op: "replace", path: "is-hidden", value: false }]
 
-  const newState = selection.map((post) => {
+  const newState = selection.map(post => {
     return {
       id: post.id,
       is_hidden: false,
@@ -128,152 +118,173 @@ export function unhide(props) {
       url: Object.assign(post.url, {
         hidden_by: props.user.url
       })
-    };
-  });
+    }
+  })
 
-  const previousState = selection.map((post) => {
+  const previousState = selection.map(post => {
     return {
       id: post.id,
       is_hidden: post.is_hidden,
       hidden_on: post.hidden_on,
       hidden_by_name: post.hidden_by_name,
       url: post.url
-    };
-  });
+    }
+  })
 
-  patch(props, ops, newState, previousState);
+  patch(props, ops, newState, previousState)
 }
 
 export function patch(props, ops, newState, previousState) {
-  const { selection, thread } = props;
+  const { selection, thread } = props
 
   // patch selected items
-  newState.forEach((item) => {
-    post.patch(item, item);
-  });
+  newState.forEach(item => {
+    post.patch(item, item)
+  })
 
   // deselect all the things
-  store.dispatch(posts.deselectAll());
+  store.dispatch(posts.deselectAll())
 
   // call ajax
   const data = {
     ops,
 
-    ids: selection.map((post) => { return post.id; })
-  };
+    ids: selection.map(post => {
+      return post.id
+    })
+  }
 
   ajax.patch(thread.api.posts.index, data).then(
-    (data) => {
-      data.forEach((item) => {
-        store.dispatch(post.patch(item, item));
-      });
+    data => {
+      data.forEach(item => {
+        store.dispatch(post.patch(item, item))
+      })
     },
-    (rejection) => {
+    rejection => {
       if (rejection.status !== 400) {
         // rollback all
-        previousState.forEach((item) => {
-          store.dispatch(post.patch(item, item));
-        });
-        return snackbar.apiError(rejection);
+        previousState.forEach(item => {
+          store.dispatch(post.patch(item, item))
+        })
+        return snackbar.apiError(rejection)
       }
 
-      let errors = [];
-      let rollback = [];
+      let errors = []
+      let rollback = []
 
-      rejection.forEach((item) => {
+      rejection.forEach(item => {
         if (item.detail) {
-          errors.push(item);
-          rollback.push(item.id);
+          errors.push(item)
+          rollback.push(item.id)
         } else {
-          store.dispatch(post.patch(item, item));
+          store.dispatch(post.patch(item, item))
         }
 
-        previousState.forEach((item) => {
+        previousState.forEach(item => {
           if (rollback.indexOf(item) !== -1) {
-            store.dispatch(post.patch(item, item));
+            store.dispatch(post.patch(item, item))
           }
-        });
-      });
-
-      let posts = {};
-      selection.forEach((item) => {
-        posts[item.id] = item;
-      });
-
-      /* jshint ignore:start */
-      modal.show(
-        <ErrorsList
-          errors={errors}
-          posts={posts}
-        />
-      );
-      /* jshint ignore:end */
+        })
+      })
+
+      let posts = {}
+      selection.forEach(item => {
+        posts[item.id] = item
+      })
+
+      modal.show(<ErrorsList errors={errors} posts={posts} />)
     }
-  );
+  )
 }
 
 export function merge(props) {
-  let confirmed = confirm(gettext("Are you sure you want to merge selected posts? This action is not reversible!"));
+  let confirmed = confirm(
+    gettext(
+      "Are you sure you want to merge selected posts? This action is not reversible!"
+    )
+  )
   if (!confirmed) {
-    return;
+    return
   }
 
-  props.selection.slice(1).map((selection) => {
-    store.dispatch(post.patch(selection, {
-      isDeleted: true
-    }));
-  });
-
-  ajax.post(props.thread.api.posts.merge, {
-    posts: props.selection.map((post) => post.id)
-  }).then((data) => {
-    store.dispatch(post.patch(data, post.hydrate(data)));
-  }, (rejection) => {
-    if (rejection.status === 400) {
-      snackbar.error(rejection.detail);
-    } else {
-      snackbar.apiError(rejection);
-    }
+  props.selection.slice(1).map(selection => {
+    store.dispatch(
+      post.patch(selection, {
+        isDeleted: true
+      })
+    )
+  })
+
+  ajax
+    .post(props.thread.api.posts.merge, {
+      posts: props.selection.map(post => post.id)
+    })
+    .then(
+      data => {
+        store.dispatch(post.patch(data, post.hydrate(data)))
+      },
+      rejection => {
+        if (rejection.status === 400) {
+          snackbar.error(rejection.detail)
+        } else {
+          snackbar.apiError(rejection)
+        }
 
-    props.selection.slice(1).map((selection) => {
-      store.dispatch(post.patch(selection, {
-        isDeleted: false
-      }));
-    });
-  });
+        props.selection.slice(1).map(selection => {
+          store.dispatch(
+            post.patch(selection, {
+              isDeleted: false
+            })
+          )
+        })
+      }
+    )
 
-  store.dispatch(posts.deselectAll());
+  store.dispatch(posts.deselectAll())
 }
 
 export function remove(props) {
-  let confirmed = confirm(gettext("Are you sure you want to delete selected posts? This action is not reversible!"));
+  let confirmed = confirm(
+    gettext(
+      "Are you sure you want to delete selected posts? This action is not reversible!"
+    )
+  )
   if (!confirmed) {
-    return;
+    return
   }
 
-  props.selection.map((selection) => {
-    store.dispatch(post.patch(selection, {
-      isDeleted: true
-    }));
-  });
-
-  const ids = props.selection.map((post) => { return post.id; });
-
-  ajax.delete(props.thread.api.posts.index, ids).then(() => {
-    return;
-  }, (rejection) => {
-    if (rejection.status === 400) {
-      snackbar.error(rejection.detail);
-    } else {
-      snackbar.apiError(rejection);
-    }
+  props.selection.map(selection => {
+    store.dispatch(
+      post.patch(selection, {
+        isDeleted: true
+      })
+    )
+  })
 
-    props.selection.map((selection) => {
-      store.dispatch(post.patch(selection, {
-        isDeleted: false
-      }));
-    });
-  });
+  const ids = props.selection.map(post => {
+    return post.id
+  })
 
-  store.dispatch(posts.deselectAll());
-}
+  ajax.delete(props.thread.api.posts.index, ids).then(
+    () => {
+      return
+    },
+    rejection => {
+      if (rejection.status === 400) {
+        snackbar.error(rejection.detail)
+      } else {
+        snackbar.apiError(rejection)
+      }
+
+      props.selection.map(selection => {
+        store.dispatch(
+          post.patch(selection, {
+            isDeleted: false
+          })
+        )
+      })
+    }
+  )
+
+  store.dispatch(posts.deselectAll())
+}

+ 81 - 102
frontend/src/components/thread/moderation/posts/dropdown.js

@@ -1,9 +1,8 @@
-/* jshint ignore:start */
-import React from 'react';
-import modal from 'misago/services/modal';
-import * as moderation from './actions';
-import MoveModal from './move';
-import SplitModal from './split';
+import React from "react"
+import modal from "misago/services/modal"
+import * as moderation from "./actions"
+import MoveModal from "./move"
+import SplitModal from "./split"
 
 export default function(props) {
   return (
@@ -18,234 +17,214 @@ export default function(props) {
       <Hide {...props} />
       <Delete {...props} />
     </ul>
-  );
+  )
 }
 
 export class Approve extends React.Component {
   onClick = () => {
-    moderation.approve(this.props);
-  };
+    moderation.approve(this.props)
+  }
 
   render() {
-    const isVisible = this.props.selection.find((post) => {
-      return post.acl.can_approve && post.is_unapproved;
-    });
+    const isVisible = this.props.selection.find(post => {
+      return post.acl.can_approve && post.is_unapproved
+    })
 
-    if (!isVisible) return null;
+    if (!isVisible) return null
 
     return (
       <li>
         <button type="button" className="btn btn-link" onClick={this.onClick}>
-          <span className="material-icon">
-            done
-          </span>
+          <span className="material-icon">done</span>
           {gettext("Approve")}
         </button>
       </li>
-    );
+    )
   }
 }
 
 export class Merge extends React.Component {
   onClick = () => {
-    moderation.merge(this.props);
-  };
+    moderation.merge(this.props)
+  }
 
   render() {
-    const isVisible = this.props.selection.length > 1 && this.props.selection.find((post) => {
-      return post.acl.can_merge;
-    });
+    const isVisible =
+      this.props.selection.length > 1 &&
+      this.props.selection.find(post => {
+        return post.acl.can_merge
+      })
 
-    if (!isVisible) return null;
+    if (!isVisible) return null
 
     return (
       <li>
         <button type="button" className="btn btn-link" onClick={this.onClick}>
-          <span className="material-icon">
-            call_merge
-          </span>
+          <span className="material-icon">call_merge</span>
           {gettext("Merge")}
         </button>
       </li>
-    );
+    )
   }
 }
 
 export class Move extends React.Component {
   onClick = () => {
-    modal.show(
-      <MoveModal {...this.props} />
-    );
-  };
+    modal.show(<MoveModal {...this.props} />)
+  }
 
   render() {
-    const isVisible = this.props.selection.find((post) => {
-      return post.acl.can_move;
-    });
+    const isVisible = this.props.selection.find(post => {
+      return post.acl.can_move
+    })
 
-    if (!isVisible) return null;
+    if (!isVisible) return null
 
     return (
       <li>
         <button type="button" className="btn btn-link" onClick={this.onClick}>
-          <span className="material-icon">
-            arrow_forward
-          </span>
+          <span className="material-icon">arrow_forward</span>
           {gettext("Move")}
         </button>
       </li>
-    );
+    )
   }
 }
 
 export class Split extends React.Component {
   onClick = () => {
-    modal.show(
-      <SplitModal {...this.props} />
-    );
-  };
+    modal.show(<SplitModal {...this.props} />)
+  }
 
   render() {
-    const isVisible = this.props.selection.find((post) => {
-      return post.acl.can_move;
-    });
+    const isVisible = this.props.selection.find(post => {
+      return post.acl.can_move
+    })
 
-    if (!isVisible) return null;
+    if (!isVisible) return null
 
     return (
       <li>
         <button type="button" className="btn btn-link" onClick={this.onClick}>
-          <span className="material-icon">
-            call_split
-          </span>
+          <span className="material-icon">call_split</span>
           {gettext("Split")}
         </button>
       </li>
-    );
+    )
   }
 }
 
 export class Protect extends React.Component {
   onClick = () => {
-    moderation.protect(this.props);
-  };
+    moderation.protect(this.props)
+  }
 
   render() {
-    const isVisible = this.props.selection.find((post) => {
-      return !post.is_protected && post.acl.can_protect;
-    });
+    const isVisible = this.props.selection.find(post => {
+      return !post.is_protected && post.acl.can_protect
+    })
 
-    if (!isVisible) return null;
+    if (!isVisible) return null
 
     return (
       <li>
         <button type="button" className="btn btn-link" onClick={this.onClick}>
-          <span className="material-icon">
-            lock_outline
-          </span>
+          <span className="material-icon">lock_outline</span>
           {gettext("Protect")}
         </button>
       </li>
-    );
+    )
   }
 }
 
 export class Unprotect extends React.Component {
   onClick = () => {
-    moderation.unprotect(this.props);
-  };
+    moderation.unprotect(this.props)
+  }
 
   render() {
-    const isVisible = this.props.selection.find((post) => {
-      return post.is_protected && post.acl.can_protect;
-    });
+    const isVisible = this.props.selection.find(post => {
+      return post.is_protected && post.acl.can_protect
+    })
 
-    if (!isVisible) return null;
+    if (!isVisible) return null
 
     return (
       <li>
         <button type="button" className="btn btn-link" onClick={this.onClick}>
-          <span className="material-icon">
-            lock_open
-          </span>
+          <span className="material-icon">lock_open</span>
           {gettext("Unprotect")}
         </button>
       </li>
-    );
+    )
   }
 }
 
 export class Hide extends React.Component {
   onClick = () => {
-    moderation.hide(this.props);
-  };
+    moderation.hide(this.props)
+  }
 
   render() {
-    const isVisible = this.props.selection.find((post) => {
-      return post.acl.can_hide && !post.is_hidden;
-    });
+    const isVisible = this.props.selection.find(post => {
+      return post.acl.can_hide && !post.is_hidden
+    })
 
-    if (!isVisible) return null;
+    if (!isVisible) return null
 
     return (
       <li>
         <button type="button" className="btn btn-link" onClick={this.onClick}>
-          <span className="material-icon">
-            visibility_off
-          </span>
+          <span className="material-icon">visibility_off</span>
           {gettext("Hide")}
         </button>
       </li>
-    );
+    )
   }
 }
 
 export class Unhide extends React.Component {
   onClick = () => {
-    moderation.unhide(this.props);
-  };
+    moderation.unhide(this.props)
+  }
 
   render() {
-    const isVisible = this.props.selection.find((post) => {
-      return post.acl.can_unhide && post.is_hidden;
-    });
+    const isVisible = this.props.selection.find(post => {
+      return post.acl.can_unhide && post.is_hidden
+    })
 
-    if (!isVisible) return null;
+    if (!isVisible) return null
 
     return (
       <li>
         <button type="button" className="btn btn-link" onClick={this.onClick}>
-          <span className="material-icon">
-            visibility
-          </span>
+          <span className="material-icon">visibility</span>
           {gettext("Unhide")}
         </button>
       </li>
-    );
+    )
   }
 }
 
 export class Delete extends React.Component {
   onClick = () => {
-    moderation.remove(this.props);
-  };
+    moderation.remove(this.props)
+  }
 
   render() {
-    const isVisible = this.props.selection.find((post) => {
-      return post.acl.can_delete;
-    });
+    const isVisible = this.props.selection.find(post => {
+      return post.acl.can_delete
+    })
 
-    if (!isVisible) return null;
+    if (!isVisible) return null
 
     return (
       <li>
         <button type="button" className="btn btn-link" onClick={this.onClick}>
-          <span className="material-icon">
-            clear
-          </span>
+          <span className="material-icon">clear</span>
           {gettext("Delete")}
         </button>
       </li>
-    );
+    )
   }
-}
+}

+ 9 - 14
frontend/src/components/thread/moderation/posts/errors-list.js

@@ -1,5 +1,4 @@
-/* jshint ignore:start */
-import React from 'react';
+import React from "react"
 
 export default function({ errors, posts }) {
   return (
@@ -17,47 +16,43 @@ export default function({ errors, posts }) {
           <h4 className="modal-title">{gettext("Moderation")}</h4>
         </div>
         <div className="modal-body">
-
           <p className="lead">
             {gettext("One or more posts could not be changed:")}
           </p>
 
           <ul className="list-unstyled list-errored-items">
-            {errors.map((post) => {
+            {errors.map(post => {
               return (
                 <PostErrors
                   errors={post.detail}
                   key={post.id}
                   post={posts[post.id]}
                 />
-              );
+              )
             })}
           </ul>
-
         </div>
       </div>
     </div>
-  );
+  )
 }
 
 export function PostErrors({ errors, post }) {
   const heading = interpolate(
     gettext("%(username)s on %(posted_on)s"),
     {
-      posted_on: post.posted_on.format('LL, LT'),
+      posted_on: post.posted_on.format("LL, LT"),
       username: post.poster_name
     },
     true
-  );
+  )
 
   return (
     <li>
       <h5>{heading}:</h5>
       {errors.map((error, i) => {
-        return (
-          <p key={i}>{error}</p>
-        )
+        return <p key={i}>{error}</p>
       })}
     </li>
-  );
-}
+  )
+}

+ 15 - 17
frontend/src/components/thread/moderation/posts/index.js

@@ -1,15 +1,14 @@
-/* jshint ignore:start */
-import React from 'react';
-import Dropdown from './dropdown';
+import React from "react"
+import Dropdown from "./dropdown"
 
 export default function(props) {
   if (!props.user.id || !isVisible(props.thread, props.posts.results)) {
-    return null;
+    return null
   }
 
-  const selection = props.posts.results.filter((post) => {
-    return post.isSelected;
-  });
+  const selection = props.posts.results.filter(post => {
+    return post.isSelected
+  })
 
   return (
     <div className="dropup">
@@ -25,20 +24,20 @@ export default function(props) {
       </button>
       <Dropdown selection={selection} {...props} />
     </div>
-  );
+  )
 }
 
 export function isVisible(thread, posts) {
   if (thread.acl.can_merge_posts && posts.length > 1) {
     // fast test: show moderation menu if we can merge posts
-    return true;
+    return true
   }
 
   // slow test: show moderation if any of posts has moderation options
-  let visible = false;
-  posts.forEach((post) => {
+  let visible = false
+  posts.forEach(post => {
     if (!post.is_event) {
-      const showModeration = (
+      const showModeration =
         (post.acl.can_approve && post.is_unapproved) ||
         post.acl.can_delete ||
         (!post.is_hidden && post.acl.can_hide) ||
@@ -47,12 +46,11 @@ export function isVisible(thread, posts) {
         post.acl.can_protect ||
         (post.is_hidden && post.acl.can_unhide) ||
         post.acl.can_unprotect
-      );
 
       if (showModeration) {
-        visible = true;
+        visible = true
       }
     }
-  });
-  return visible;
-}
+  })
+  return visible
+}

+ 37 - 33
frontend/src/components/thread/moderation/posts/move.js

@@ -1,69 +1,70 @@
-// jshint ignore:start
-import React from 'react';
-import Button from 'misago/components/button';
-import Form from 'misago/components/form';
-import FormGroup from 'misago/components/form-group';
-import * as post from 'misago/reducers/post';
-import ajax from 'misago/services/ajax';
-import modal from 'misago/services/modal';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
+import React from "react"
+import Button from "misago/components/button"
+import Form from "misago/components/form"
+import FormGroup from "misago/components/form-group"
+import * as post from "misago/reducers/post"
+import ajax from "misago/services/ajax"
+import modal from "misago/services/modal"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoading: false,
 
-      url: '',
+      url: "",
 
       validators: {
         url: []
       },
       errors: {}
-    };
+    }
   }
 
   clean() {
     if (!this.state.url.trim().length) {
-      snackbar.error(gettext("You have to enter link to the other thread."));
-      return false;
+      snackbar.error(gettext("You have to enter link to the other thread."))
+      return false
     }
 
-    return true;
+    return true
   }
 
   send() {
     return ajax.post(this.props.thread.api.posts.move, {
       new_thread: this.state.url,
-      posts: this.props.selection.map((post) => post.id)
-    });
+      posts: this.props.selection.map(post => post.id)
+    })
   }
 
   handleSuccess(success) {
-    this.props.selection.forEach((selection) => {
-      store.dispatch(post.patch(selection, {
-        isDeleted: true
-      }));
-    });
+    this.props.selection.forEach(selection => {
+      store.dispatch(
+        post.patch(selection, {
+          isDeleted: true
+        })
+      )
+    })
 
-    modal.hide();
+    modal.hide()
 
-    snackbar.success(gettext("Selected posts were moved to the other thread."));
+    snackbar.success(gettext("Selected posts were moved to the other thread."))
   }
 
   handleError(rejection) {
     if (rejection.status === 400) {
-      snackbar.error(rejection.detail);
+      snackbar.error(rejection.detail)
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
   }
 
-  onUrlChange = (event) => {
-    this.changeValue('url', event.target.value);
-  };
+  onUrlChange = event => {
+    this.changeValue("url", event.target.value)
+  }
 
   render() {
     return (
@@ -94,14 +95,17 @@ export default class extends Form {
               >
                 {gettext("Cancel")}
               </button>
-              <button className="btn btn-primary" loading={this.state.isLoading}>
+              <button
+                className="btn btn-primary"
+                loading={this.state.isLoading}
+              >
                 {gettext("Move posts")}
               </button>
             </div>
           </div>
         </form>
       </div>
-    );
+    )
   }
 }
 
@@ -118,5 +122,5 @@ export function ModalHeader(props) {
       </button>
       <h4 className="modal-title">{gettext("Move posts")}</h4>
     </div>
-  );
+  )
 }

+ 197 - 178
frontend/src/components/thread/moderation/posts/split.js

@@ -1,84 +1,80 @@
-/* jshint ignore:start */
-import React from 'react';
-import Button from 'misago/components/button'; // jshint ignore:line
-import Form from 'misago/components/form';
-import FormGroup from 'misago/components/form-group'; // jshint ignore:line
-import CategorySelect from 'misago/components/category-select'; // jshint ignore:line
-import ModalLoader from 'misago/components/modal-loader'; // jshint ignore:line
-import Select from 'misago/components/select'; // jshint ignore:line
-import * as post from 'misago/reducers/post';
-import ajax from 'misago/services/ajax'; // jshint ignore:line
-import modal from 'misago/services/modal'; // jshint ignore:line
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store'; // jshint ignore:line
-import * as validators from 'misago/utils/validators';
+import React from "react"
+import Button from "misago/components/button"
+import Form from "misago/components/form"
+import FormGroup from "misago/components/form-group"
+import CategorySelect from "misago/components/category-select"
+import ModalLoader from "misago/components/modal-loader"
+import Select from "misago/components/select"
+import * as post from "misago/reducers/post"
+import ajax from "misago/services/ajax"
+import modal from "misago/services/modal"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
+import * as validators from "misago/utils/validators"
 
 export default function(props) {
-  return (
-    <PostingConfig {...props} Form={ModerationForm} />
-  );
+  return <PostingConfig {...props} Form={ModerationForm} />
 }
 
 export class PostingConfig extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoaded: false,
       isError: false,
 
       categories: []
-    };
+    }
   }
 
   componentDidMount() {
-    ajax.get(misago.get('THREAD_EDITOR_API')).then((data) => {
-      // hydrate categories, extract posting options
-      const categories = data.map((item) => {
-        return Object.assign(item, {
-          disabled: item.post === false,
-          label: item.name,
-          value: item.id,
-          post: item.post
-        });
-      });
-
-      this.setState({
-        isLoaded: true,
-        categories
-      })
-    }, (rejection) => {
-      this.setState({
-        isError: rejection.detail
-      });
-    });
+    ajax.get(misago.get("THREAD_EDITOR_API")).then(
+      data => {
+        // hydrate categories, extract posting options
+        const categories = data.map(item => {
+          return Object.assign(item, {
+            disabled: item.post === false,
+            label: item.name,
+            value: item.id,
+            post: item.post
+          })
+        })
+
+        this.setState({
+          isLoaded: true,
+          categories
+        })
+      },
+      rejection => {
+        this.setState({
+          isError: rejection.detail
+        })
+      }
+    )
   }
 
   render() {
     if (this.state.isError) {
-      return (
-        <Error message={this.state.isError} />
-      );
+      return <Error message={this.state.isError} />
     } else if (this.state.isLoaded) {
       return (
         <ModerationForm {...this.props} categories={this.state.categories} />
-      );
+      )
     } else {
-      return (
-        <Loader />
-      );
+      return <Loader />
     }
   }
 }
 
 export class ModerationForm extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoading: false,
 
-      title: '',
+      title: "",
       category: null,
       categories: props.categories,
       weight: 0,
@@ -86,65 +82,63 @@ export class ModerationForm extends Form {
       is_closed: false,
 
       validators: {
-        title: [
-          validators.required()
-        ]
+        title: [validators.required()]
       },
 
       errors: {}
-    };
+    }
 
     this.isHiddenChoices = [
       {
-        'value': 0,
-        'icon': 'visibility',
-        'label': gettext("No")
+        value: 0,
+        icon: "visibility",
+        label: gettext("No")
       },
       {
-        'value': 1,
-        'icon': 'visibility_off',
-        'label': gettext("Yes")
-      },
-    ];
+        value: 1,
+        icon: "visibility_off",
+        label: gettext("Yes")
+      }
+    ]
 
     this.isClosedChoices = [
       {
-        'value': false,
-        'icon': 'lock_outline',
-        'label': gettext("No")
+        value: false,
+        icon: "lock_outline",
+        label: gettext("No")
       },
       {
-        'value': true,
-        'icon': 'lock',
-        'label': gettext("Yes")
-      },
-    ];
+        value: true,
+        icon: "lock",
+        label: gettext("Yes")
+      }
+    ]
 
-    this.acl = {};
-    this.props.categories.forEach((category) => {
+    this.acl = {}
+    this.props.categories.forEach(category => {
       if (category.post) {
         if (!this.state.category) {
-          this.state.category = category.id;
+          this.state.category = category.id
         }
 
         this.acl[category.id] = {
           can_pin_threads: category.post.pin,
           can_close_threads: category.post.close,
-          can_hide_threads: category.post.hide,
-        };
+          can_hide_threads: category.post.hide
+        }
       }
-    });
+    })
   }
 
   clean() {
     if (this.isValid()) {
-      return true;
+      return true
     } else {
-      snackbar.error(gettext("Form contains errors."));
+      snackbar.error(gettext("Form contains errors."))
       this.setState({
         errors: this.validate()
-      });
-      return false;
+      })
+      return false
     }
   }
 
@@ -155,123 +149,146 @@ export class ModerationForm extends Form {
       weight: this.state.weight,
       is_hidden: this.state.is_hidden,
       is_closed: this.state.is_closed,
-      posts: this.props.selection.map((post) => post.id)
-    });
+      posts: this.props.selection.map(post => post.id)
+    })
   }
 
   handleSuccess(apiResponse) {
-    this.props.selection.forEach((selection) => {
-      store.dispatch(post.patch(selection, {
-        isDeleted: true
-      }));
-    });
+    this.props.selection.forEach(selection => {
+      store.dispatch(
+        post.patch(selection, {
+          isDeleted: true
+        })
+      )
+    })
 
-    modal.hide();
+    modal.hide()
 
-    snackbar.success(gettext("Selected posts were split into new thread."));
+    snackbar.success(gettext("Selected posts were split into new thread."))
   }
 
   handleError(rejection) {
     if (rejection.status === 400) {
       this.setState({
-        'errors': Object.assign({}, this.state.errors, rejection)
-      });
-      snackbar.error(gettext("Form contains errors."));
+        errors: Object.assign({}, this.state.errors, rejection)
+      })
+      snackbar.error(gettext("Form contains errors."))
     } else if (rejection.status === 403 && Array.isArray(rejection)) {
-      modal.show(<ErrorsModal errors={rejection} />);
+      modal.show(<ErrorsModal errors={rejection} />)
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
   }
 
-  onCategoryChange = (ev) => {
-    const categoryId = ev.target.value;
+  onCategoryChange = ev => {
+    const categoryId = ev.target.value
     const newState = {
       category: categoryId
-    };
+    }
 
     if (this.acl[categoryId].can_pin_threads < newState.weight) {
-      newState.weight = 0;
+      newState.weight = 0
     }
 
     if (!this.acl[categoryId].can_hide_threads) {
-      newState.is_hidden = 0;
+      newState.is_hidden = 0
     }
 
     if (!this.acl[categoryId].can_close_threads) {
-      newState.is_closed = false;
+      newState.is_closed = false
     }
 
-    this.setState(newState);
-  };
+    this.setState(newState)
+  }
 
   getWeightChoices() {
     const choices = [
       {
-        'value': 0,
-        'icon': 'remove',
-        'label': gettext("Not pinned"),
+        value: 0,
+        icon: "remove",
+        label: gettext("Not pinned")
       },
       {
-        'value': 1,
-        'icon': 'bookmark_border',
-        'label': gettext("Pinned locally"),
+        value: 1,
+        icon: "bookmark_border",
+        label: gettext("Pinned locally")
       }
-    ];
+    ]
 
     if (this.acl[this.state.category].can_pin_threads == 2) {
       choices.push({
-        'value': 2,
-        'icon': 'bookmark',
-        'label': gettext("Pinned globally"),
-      });
+        value: 2,
+        icon: "bookmark",
+        label: gettext("Pinned globally")
+      })
     }
 
-    return choices;
+    return choices
   }
 
   renderWeightField() {
     if (this.acl[this.state.category].can_pin_threads) {
-      return <FormGroup label={gettext("Thread weight")}
-                        for="id_weight"
-                        labelClass="col-sm-4" controlClass="col-sm-8">
-        <Select id="id_weight"
-                onChange={this.bindInput('weight')}
-                value={this.state.weight}
-                choices={this.getWeightChoices()} />
-      </FormGroup>;
+      return (
+        <FormGroup
+          label={gettext("Thread weight")}
+          for="id_weight"
+          labelClass="col-sm-4"
+          controlClass="col-sm-8"
+        >
+          <Select
+            id="id_weight"
+            onChange={this.bindInput("weight")}
+            value={this.state.weight}
+            choices={this.getWeightChoices()}
+          />
+        </FormGroup>
+      )
     } else {
-      return null;
+      return null
     }
   }
 
   renderHiddenField() {
     if (this.acl[this.state.category].can_hide_threads) {
-      return <FormGroup label={gettext("Hide thread")}
-                        for="id_is_hidden"
-                        labelClass="col-sm-4" controlClass="col-sm-8">
-        <Select id="id_is_closed"
-                onChange={this.bindInput('is_hidden')}
-                value={this.state.is_hidden}
-                choices={this.isHiddenChoices} />
-      </FormGroup>;
+      return (
+        <FormGroup
+          label={gettext("Hide thread")}
+          for="id_is_hidden"
+          labelClass="col-sm-4"
+          controlClass="col-sm-8"
+        >
+          <Select
+            id="id_is_closed"
+            onChange={this.bindInput("is_hidden")}
+            value={this.state.is_hidden}
+            choices={this.isHiddenChoices}
+          />
+        </FormGroup>
+      )
     } else {
-      return null;
+      return null
     }
   }
 
   renderClosedField() {
     if (this.acl[this.state.category].can_close_threads) {
-      return <FormGroup label={gettext("Close thread")}
-                        for="id_is_closed"
-                        labelClass="col-sm-4" controlClass="col-sm-8">
-        <Select id="id_is_closed"
-                onChange={this.bindInput('is_closed')}
-                value={this.state.is_closed}
-                choices={this.isClosedChoices} />
-      </FormGroup>;
+      return (
+        <FormGroup
+          label={gettext("Close thread")}
+          for="id_is_closed"
+          labelClass="col-sm-4"
+          controlClass="col-sm-8"
+        >
+          <Select
+            id="id_is_closed"
+            onChange={this.bindInput("is_closed")}
+            value={this.state.is_closed}
+            choices={this.isClosedChoices}
+          />
+        </FormGroup>
+      )
     } else {
-      return null;
+      return null
     }
   }
 
@@ -280,34 +297,42 @@ export class ModerationForm extends Form {
       <Modal className="modal-dialog">
         <form onSubmit={this.handleSubmit}>
           <div className="modal-body">
-
-            <FormGroup label={gettext("Thread title")}
-                       for="id_title"
-                       labelClass="col-sm-4" controlClass="col-sm-8"
-                       validation={this.state.errors.title}>
-              <input id="id_title"
-                     className="form-control"
-                     type="text"
-                     onChange={this.bindInput('title')}
-                     value={this.state.title} />
+            <FormGroup
+              label={gettext("Thread title")}
+              for="id_title"
+              labelClass="col-sm-4"
+              controlClass="col-sm-8"
+              validation={this.state.errors.title}
+            >
+              <input
+                id="id_title"
+                className="form-control"
+                type="text"
+                onChange={this.bindInput("title")}
+                value={this.state.title}
+              />
             </FormGroup>
-            <div className="clearfix"></div>
-
-            <FormGroup label={gettext("Category")}
-                       for="id_category"
-                       labelClass="col-sm-4" controlClass="col-sm-8"
-                       validation={this.state.errors.category}>
-              <CategorySelect id="id_category"
-                              onChange={this.onCategoryChange}
-                              value={this.state.category}
-                              choices={this.state.categories} />
+            <div className="clearfix" />
+
+            <FormGroup
+              label={gettext("Category")}
+              for="id_category"
+              labelClass="col-sm-4"
+              controlClass="col-sm-8"
+              validation={this.state.errors.category}
+            >
+              <CategorySelect
+                id="id_category"
+                onChange={this.onCategoryChange}
+                value={this.state.category}
+                choices={this.state.categories}
+              />
             </FormGroup>
-            <div className="clearfix"></div>
+            <div className="clearfix" />
 
             {this.renderWeightField()}
             {this.renderHiddenField()}
             {this.renderClosedField()}
-
           </div>
           <div className="modal-footer">
             <button
@@ -324,7 +349,7 @@ export class ModerationForm extends Form {
           </div>
         </form>
       </Modal>
-    );
+    )
   }
 }
 
@@ -333,34 +358,26 @@ export function Loader() {
     <Modal className="modal-dialog">
       <ModalLoader />
     </Modal>
-  );
+  )
 }
 
 export function Error(props) {
   return (
     <Modal className="modal-dialog modal-message">
       <div className="message-icon">
-        <span className="material-icon">
-          info_outline
-        </span>
+        <span className="material-icon">info_outline</span>
       </div>
       <div className="message-body">
         <p className="lead">
           {gettext("You can't move selected posts at the moment.")}
         </p>
-        <p>
-          {props.message}
-        </p>
-        <button
-          className="btn btn-default"
-          data-dismiss="modal"
-          type="button"
-        >
+        <p>{props.message}</p>
+        <button className="btn btn-default" data-dismiss="modal" type="button">
           {gettext("Ok")}
         </button>
       </div>
     </Modal>
-  );
+  )
 }
 
 export function Modal(props) {
@@ -376,10 +393,12 @@ export function Modal(props) {
           >
             <span aria-hidden="true">&times;</span>
           </button>
-          <h4 className="modal-title">{gettext("Split posts into new thread")}</h4>
+          <h4 className="modal-title">
+            {gettext("Split posts into new thread")}
+          </h4>
         </div>
         {props.children}
       </div>
     </div>
-  );
-}
+  )
+}

+ 184 - 218
frontend/src/components/thread/moderation/thread/controls.js

@@ -1,148 +1,172 @@
-// jshint ignore:start
-import React from 'react';
-import MergeModal from './merge'; // jshint ignore:line
-import MoveModal from './move'; // jshint ignore:line
-import * as thread from 'misago/reducers/thread';
-import ajax from 'misago/services/ajax'; // jshint ignore:line
-import modal from 'misago/services/modal'; // jshint ignore:line
-import snackbar from 'misago/services/snackbar'; // jshint ignore:line
-import store from 'misago/services/store'; // jshint ignore:line
+import React from "react"
+import MergeModal from "./merge"
+import MoveModal from "./move"
+import * as thread from "misago/reducers/thread"
+import ajax from "misago/services/ajax"
+import modal from "misago/services/modal"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export default class extends React.Component {
   callApi = (ops, successMessage) => {
-    store.dispatch(thread.busy());
+    store.dispatch(thread.busy())
 
     // by the chance update thread acl too
-    ops.push({op: 'add', path: 'acl', value: true});
-
-    ajax.patch(this.props.thread.api.index, ops).then((data) => {
-      store.dispatch(thread.update(data));
-      store.dispatch(thread.release());
-      snackbar.success(successMessage);
-    }, (rejection) => {
-      store.dispatch(thread.release());
-      if (rejection.status === 400) {
-        snackbar.error(rejection.detail[0]);
-      } else {
-        snackbar.apiError(rejection);
+    ops.push({ op: "add", path: "acl", value: true })
+
+    ajax.patch(this.props.thread.api.index, ops).then(
+      data => {
+        store.dispatch(thread.update(data))
+        store.dispatch(thread.release())
+        snackbar.success(successMessage)
+      },
+      rejection => {
+        store.dispatch(thread.release())
+        if (rejection.status === 400) {
+          snackbar.error(rejection.detail[0])
+        } else {
+          snackbar.apiError(rejection)
+        }
       }
-    });
-  };
+    )
+  }
 
   pinGlobally = () => {
-    this.callApi([
-      {
-        op: 'replace',
-        path: 'weight',
-        value: 2
-      }
-    ], gettext("Thread has been pinned globally."));
-  };
+    this.callApi(
+      [
+        {
+          op: "replace",
+          path: "weight",
+          value: 2
+        }
+      ],
+      gettext("Thread has been pinned globally.")
+    )
+  }
 
   pinLocally = () => {
-    this.callApi([
-      {
-        op: 'replace',
-        path: 'weight',
-        value: 1
-      }
-    ], gettext("Thread has been pinned locally."));
-  };
+    this.callApi(
+      [
+        {
+          op: "replace",
+          path: "weight",
+          value: 1
+        }
+      ],
+      gettext("Thread has been pinned locally.")
+    )
+  }
 
   unpin = () => {
-    this.callApi([
-      {
-        op: 'replace',
-        path: 'weight',
-        value: 0
-      }
-    ], gettext("Thread has been unpinned."));
-  };
+    this.callApi(
+      [
+        {
+          op: "replace",
+          path: "weight",
+          value: 0
+        }
+      ],
+      gettext("Thread has been unpinned.")
+    )
+  }
 
   approve = () => {
-    this.callApi([
-      {
-        op: 'replace',
-        path: 'is-unapproved',
-        value: false
-      }
-    ], gettext("Thread has been approved."));
-  };
+    this.callApi(
+      [
+        {
+          op: "replace",
+          path: "is-unapproved",
+          value: false
+        }
+      ],
+      gettext("Thread has been approved.")
+    )
+  }
 
   open = () => {
-    this.callApi([
-      {
-        op: 'replace',
-        path: 'is-closed',
-        value: false
-      }
-    ], gettext("Thread has been opened."));
-  };
+    this.callApi(
+      [
+        {
+          op: "replace",
+          path: "is-closed",
+          value: false
+        }
+      ],
+      gettext("Thread has been opened.")
+    )
+  }
 
   close = () => {
-    this.callApi([
-      {
-        op: 'replace',
-        path: 'is-closed',
-        value: true
-      }
-    ], gettext("Thread has been closed."));
-  };
+    this.callApi(
+      [
+        {
+          op: "replace",
+          path: "is-closed",
+          value: true
+        }
+      ],
+      gettext("Thread has been closed.")
+    )
+  }
 
   unhide = () => {
-    this.callApi([
-      {
-        op: 'replace',
-        path: 'is-hidden',
-        value: false
-      }
-    ], gettext("Thread has been made visible."));
-  };
+    this.callApi(
+      [
+        {
+          op: "replace",
+          path: "is-hidden",
+          value: false
+        }
+      ],
+      gettext("Thread has been made visible.")
+    )
+  }
 
   hide = () => {
-    this.callApi([
-      {
-        op: 'replace',
-        path: 'is-hidden',
-        value: true
-      }
-    ], gettext("Thread has been made hidden."));
-  };
+    this.callApi(
+      [
+        {
+          op: "replace",
+          path: "is-hidden",
+          value: true
+        }
+      ],
+      gettext("Thread has been made hidden.")
+    )
+  }
 
   move = () => {
     modal.show(
-      <MoveModal
-        posts={this.props.posts}
-        thread={this.props.thread}
-      />
-    );
-  };
+      <MoveModal posts={this.props.posts} thread={this.props.thread} />
+    )
+  }
 
   merge = () => {
-    modal.show(
-      <MergeModal thread={this.props.thread} />
-    );
-  };
+    modal.show(<MergeModal thread={this.props.thread} />)
+  }
 
   delete = () => {
     if (!confirm(gettext("Are you sure you want to delete this thread?"))) {
-      return;
+      return
     }
 
-    store.dispatch(thread.busy());
+    store.dispatch(thread.busy())
 
-    ajax.delete(this.props.thread.api.index).then((data) => {
-      snackbar.success(gettext("Thread has been deleted."))
-      window.location = this.props.thread.category.url.index;
-    }, (rejection) => {
-      store.dispatch(thread.release());
-      snackbar.apiError(rejection);
-    });
-  };
+    ajax.delete(this.props.thread.api.index).then(
+      data => {
+        snackbar.success(gettext("Thread has been deleted."))
+        window.location = this.props.thread.category.url.index
+      },
+      rejection => {
+        store.dispatch(thread.release())
+        snackbar.apiError(rejection)
+      }
+    )
+  }
 
   getPinGloballyButton() {
-    if (this.props.thread.weight === 2) return null;
-    if (!this.props.thread.acl.can_pin_globally) return null;
+    if (this.props.thread.weight === 2) return null
+    if (!this.props.thread.acl.can_pin_globally) return null
 
     return (
       <li>
@@ -151,18 +175,16 @@ export default class extends React.Component {
           onClick={this.pinGlobally}
           type="button"
         >
-          <span className="material-icon">
-            bookmark
-          </span>
+          <span className="material-icon">bookmark</span>
           {gettext("Pin globally")}
         </button>
       </li>
-    );
+    )
   }
 
   getPinLocallyButton() {
-    if (this.props.thread.weight === 1) return null;
-    if (!this.props.thread.acl.can_pin) return null;
+    if (this.props.thread.weight === 1) return null
+    if (!this.props.thread.acl.can_pin) return null
 
     return (
       <li>
@@ -171,190 +193,134 @@ export default class extends React.Component {
           onClick={this.pinLocally}
           type="button"
         >
-          <span className="material-icon">
-            bookmark_border
-          </span>
+          <span className="material-icon">bookmark_border</span>
           {gettext("Pin locally")}
         </button>
       </li>
-    );
+    )
   }
 
   getUnpinButton() {
-    if (this.props.thread.weight === 0) return null;
-    if (!this.props.thread.acl.can_pin) return null;
+    if (this.props.thread.weight === 0) return null
+    if (!this.props.thread.acl.can_pin) return null
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.unpin}
-          type="button"
-        >
-          <span className="material-icon">
-            panorama_fish_eye
-          </span>
+        <button className="btn btn-link" onClick={this.unpin} type="button">
+          <span className="material-icon">panorama_fish_eye</span>
           {gettext("Unpin")}
         </button>
       </li>
-    );
+    )
   }
 
   getMoveButton() {
-    if (!this.props.thread.acl.can_move) return null;
+    if (!this.props.thread.acl.can_move) return null
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.move}
-          type="button"
-        >
-          <span className="material-icon">
-            arrow_forward
-          </span>
+        <button className="btn btn-link" onClick={this.move} type="button">
+          <span className="material-icon">arrow_forward</span>
           {gettext("Move")}
         </button>
       </li>
-    );
+    )
   }
 
   getMergeButton() {
-    if (!this.props.thread.acl.can_merge) return null;
+    if (!this.props.thread.acl.can_merge) return null
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.merge}
-          type="button"
-        >
-          <span className="material-icon">
-            call_merge
-          </span>
+        <button className="btn btn-link" onClick={this.merge} type="button">
+          <span className="material-icon">call_merge</span>
           {gettext("Merge")}
         </button>
       </li>
-    );
+    )
   }
 
   getApproveButton() {
-    if (!this.props.thread.is_unapproved) return null;
-    if (!this.props.thread.acl.can_approve) return null;
+    if (!this.props.thread.is_unapproved) return null
+    if (!this.props.thread.acl.can_approve) return null
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.approve}
-          type="button"
-        >
-          <span className="material-icon">
-            done
-          </span>
+        <button className="btn btn-link" onClick={this.approve} type="button">
+          <span className="material-icon">done</span>
           {gettext("Approve")}
         </button>
       </li>
-    );
+    )
   }
 
   getOpenButton() {
-    if (!this.props.thread.is_closed) return null;
-    if (!this.props.thread.acl.can_close) return null;
+    if (!this.props.thread.is_closed) return null
+    if (!this.props.thread.acl.can_close) return null
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.open}
-          type="button"
-        >
-          <span className="material-icon">
-            lock_open
-          </span>
+        <button className="btn btn-link" onClick={this.open} type="button">
+          <span className="material-icon">lock_open</span>
           {gettext("Open")}
         </button>
       </li>
-    );
+    )
   }
 
   getCloseButton() {
-    if (this.props.thread.is_closed) return null;
-    if (!this.props.thread.acl.can_close) return null;
+    if (this.props.thread.is_closed) return null
+    if (!this.props.thread.acl.can_close) return null
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.close}
-          type="button"
-        >
-          <span className="material-icon">
-            lock_outline
-          </span>
+        <button className="btn btn-link" onClick={this.close} type="button">
+          <span className="material-icon">lock_outline</span>
           {gettext("Close")}
         </button>
       </li>
-    );
+    )
   }
 
   getUnhideButton() {
-    if (!this.props.thread.is_hidden) return null;
-    if (!this.props.thread.acl.can_unhide) return null;
+    if (!this.props.thread.is_hidden) return null
+    if (!this.props.thread.acl.can_unhide) return null
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.unhide}
-          type="button"
-        >
-          <span className="material-icon">
-            visibility
-          </span>
+        <button className="btn btn-link" onClick={this.unhide} type="button">
+          <span className="material-icon">visibility</span>
           {gettext("Unhide")}
         </button>
       </li>
-    );
+    )
   }
 
   getHideButton() {
-    if (this.props.thread.is_hidden) return null;
-    if (!this.props.thread.acl.can_hide) return null;
+    if (this.props.thread.is_hidden) return null
+    if (!this.props.thread.acl.can_hide) return null
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.hide}
-          type="button"
-        >
-          <span className="material-icon">
-            visibility_off
-          </span>
+        <button className="btn btn-link" onClick={this.hide} type="button">
+          <span className="material-icon">visibility_off</span>
           {gettext("Hide")}
         </button>
       </li>
-    );
+    )
   }
 
   getDeleteButton() {
-    if (!this.props.thread.acl.can_delete) return null;
+    if (!this.props.thread.acl.can_delete) return null
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.delete}
-          type="button"
-        >
-          <span className="material-icon">
-            clear
-          </span>
+        <button className="btn btn-link" onClick={this.delete} type="button">
+          <span className="material-icon">clear</span>
           {gettext("Delete")}
         </button>
       </li>
-    );
+    )
   }
 
   render() {
@@ -372,6 +338,6 @@ export default class extends React.Component {
         {this.getHideButton()}
         {this.getDeleteButton()}
       </ul>
-    );
+    )
   }
-}
+}

+ 3 - 3
frontend/src/components/thread/moderation/thread/index.js

@@ -1,4 +1,4 @@
-import ModerationControls from './controls';
-import isModerationVisible from './is-visible';
+import ModerationControls from "./controls"
+import isModerationVisible from "./is-visible"
 
-export { ModerationControls, isModerationVisible };
+export { ModerationControls, isModerationVisible }

+ 2 - 2
frontend/src/components/thread/moderation/thread/is-visible.js

@@ -9,5 +9,5 @@ export default function(thread) {
     thread.acl.can_pin ||
     (thread.acl.can_pin_globally && thread.weight !== 2) ||
     (thread.acl.can_unhide && thread.is_hidden)
-  );
-}
+  )
+}

+ 47 - 48
frontend/src/components/thread/moderation/thread/merge.js

@@ -1,64 +1,63 @@
-import React from 'react'; // jshint ignore:line
-import Form from 'misago/components/form';
-import FormGroup from 'misago/components/form-group'; // jshint ignore:line
-import MergeConflict from 'misago/components/merge-conflict'; // jshint ignore:line
-import * as thread from 'misago/reducers/thread';
-import ajax from 'misago/services/ajax'; // jshint ignore:line
-import modal from 'misago/services/modal'; // jshint ignore:line
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store'; // jshint ignore:line
+import React from "react"
+import Form from "misago/components/form"
+import FormGroup from "misago/components/form-group"
+import MergeConflict from "misago/components/merge-conflict"
+import * as thread from "misago/reducers/thread"
+import ajax from "misago/services/ajax"
+import modal from "misago/services/modal"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoading: false,
 
-      url: '',
+      url: "",
 
       validators: {
         url: []
       },
       errors: {}
-    };
+    }
   }
 
   clean() {
     if (!this.state.url.trim().length) {
-      snackbar.error(gettext("You have to enter link to the other thread."));
-      return false;
+      snackbar.error(gettext("You have to enter link to the other thread."))
+      return false
     }
 
-    return true;
+    return true
   }
 
   send() {
     // freeze thread
-    store.dispatch(thread.busy());
+    store.dispatch(thread.busy())
 
     return ajax.post(this.props.thread.api.merge, {
       other_thread: this.state.url
-    });
+    })
   }
 
-  /* jshint ignore:start */
-  handleSuccess = (success) => {
-    this.handleSuccessUnmounted(success);
+  handleSuccess = success => {
+    this.handleSuccessUnmounted(success)
 
     // keep form loading
     this.setState({
-      'isLoading': true
-    });
-  };
+      isLoading: true
+    })
+  }
 
-  handleSuccessUnmounted = (success) => {
-    snackbar.success(gettext("Thread has been merged with other one."));
-    window.location = success.url;
-  };
+  handleSuccessUnmounted = success => {
+    snackbar.success(gettext("Thread has been merged with other one."))
+    window.location = success.url
+  }
 
-  handleError = (rejection) => {
-    store.dispatch(thread.release());
+  handleError = rejection => {
+    store.dispatch(thread.release())
 
     if (rejection.status === 400) {
       if (rejection.best_answers || rejection.polls) {
@@ -66,31 +65,29 @@ export default class extends Form {
           <MergeConflict
             api={this.props.thread.api.merge}
             bestAnswers={rejection.best_answers}
-            data={{other_thread: this.state.url}}
+            data={{ other_thread: this.state.url }}
             polls={rejection.polls}
             onError={this.handleError}
             onSuccess={this.handleSuccessUnmounted}
           />
-        );
+        )
       } else if (rejection.best_answer) {
-        snackbar.error(rejection.best_answer[0]);
+        snackbar.error(rejection.best_answer[0])
       } else if (rejection.poll) {
-        snackbar.error(rejection.poll[0]);
+        snackbar.error(rejection.poll[0])
       } else {
-        snackbar.error(rejection.detail);
+        snackbar.error(rejection.detail)
       }
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
-  };
+  }
 
-  onUrlChange = (event) => {
-    this.changeValue('url', event.target.value);
-  };
-  /* jshint ignore:end */
+  onUrlChange = event => {
+    this.changeValue("url", event.target.value)
+  }
 
   render() {
-    /* jshint ignore:start */
     return (
       <div className="modal-dialog" role="document">
         <form onSubmit={this.handleSubmit}>
@@ -100,7 +97,9 @@ export default class extends Form {
               <FormGroup
                 for="id_url"
                 label={gettext("Link to thread you want to merge with")}
-                help_text={gettext("Merge will delete current thread and move its contents to the thread specified here.")}
+                help_text={gettext(
+                  "Merge will delete current thread and move its contents to the thread specified here."
+                )}
               >
                 <input
                   className="form-control"
@@ -120,19 +119,20 @@ export default class extends Form {
               >
                 {gettext("Cancel")}
               </button>
-              <button className="btn btn-primary" loading={this.state.isLoading || this.props.thread.isBusy}>
+              <button
+                className="btn btn-primary"
+                loading={this.state.isLoading || this.props.thread.isBusy}
+              >
                 {gettext("Merge thread")}
               </button>
             </div>
           </div>
         </form>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 }
 
-/* jshint ignore:start */
 export function ModalHeader(props) {
   return (
     <div className="modal-header">
@@ -146,6 +146,5 @@ export function ModalHeader(props) {
       </button>
       <h4 className="modal-title">{gettext("Merge thread")}</h4>
     </div>
-  );
+  )
 }
-/* jshint ignore:end */

+ 82 - 85
frontend/src/components/thread/moderation/thread/move.js

@@ -1,19 +1,19 @@
-import React from 'react'; // jshint ignore:line
-import Form from 'misago/components/form';
-import FormGroup from 'misago/components/form-group'; // jshint ignore:line
-import CategorySelect from 'misago/components/category-select'; // jshint ignore:line
-import ModalLoader from 'misago/components/modal-loader'; // jshint ignore:line
-import * as posts from 'misago/reducers/posts';
-import * as thread from 'misago/reducers/thread';
-import misago from 'misago'; // jshint ignore:line
-import ajax from 'misago/services/ajax'; // jshint ignore:line
-import modal from 'misago/services/modal'; // jshint ignore:line
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store'; // jshint ignore:line
+import React from "react"
+import Form from "misago/components/form"
+import FormGroup from "misago/components/form-group"
+import CategorySelect from "misago/components/category-select"
+import ModalLoader from "misago/components/modal-loader"
+import * as posts from "misago/reducers/posts"
+import * as thread from "misago/reducers/thread"
+import misago from "misago"
+import ajax from "misago/services/ajax"
+import modal from "misago/services/modal"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isReady: false,
@@ -21,81 +21,86 @@ export default class extends Form {
       isError: false,
 
       category: null,
-      categories: [],
-    };
+      categories: []
+    }
   }
 
   componentDidMount() {
-    ajax.get(misago.get('THREAD_EDITOR_API')).then((data) => {
-      let category = null;
-
-      // hydrate categories, extract posting options
-      const categories = data.map((item) => {
-        // pick first category that allows posting and if it may, override it with initial one
-        if (item.post !== false && !category) {
-          category = item.id;
-        }
-
-        return Object.assign(item, {
-          disabled: item.post === false,
-          label: item.name,
-          value: item.id
-        });
-      });
-
-      this.setState({
-        isReady: true,
-
-        category,
-        categories
-      });
-    }, (rejection) => {
-      this.setState({
-        isError: rejection.detail
-      });
-    });
+    ajax.get(misago.get("THREAD_EDITOR_API")).then(
+      data => {
+        let category = null
+
+        // hydrate categories, extract posting options
+        const categories = data.map(item => {
+          // pick first category that allows posting and if it may, override it with initial one
+          if (item.post !== false && !category) {
+            category = item.id
+          }
+
+          return Object.assign(item, {
+            disabled: item.post === false,
+            label: item.name,
+            value: item.id
+          })
+        })
+
+        this.setState({
+          isReady: true,
+
+          category,
+          categories
+        })
+      },
+      rejection => {
+        this.setState({
+          isError: rejection.detail
+        })
+      }
+    )
   }
 
   send() {
     // freeze thread
-    store.dispatch(thread.busy());
+    store.dispatch(thread.busy())
 
     return ajax.patch(this.props.thread.api.index, [
-      {op: 'replace', path: 'category', value: this.state.category},
-    ]);
+      { op: "replace", path: "category", value: this.state.category }
+    ])
   }
 
   handleSuccess() {
     // refresh thread and displayed posts
-    ajax.get(this.props.thread.api.posts.index, {page: this.props.posts.page}).then((data) => {
-      store.dispatch(thread.replace(data));
-      store.dispatch(posts.load(data.post_set));
-      store.dispatch(thread.release());
-
-      snackbar.success(gettext("Thread has been moved."));
-      modal.hide();
-    }, (rejection) => {
-      store.dispatch(thread.release());
-      snackbar.apiError(rejection);
-    });
+    ajax
+      .get(this.props.thread.api.posts.index, { page: this.props.posts.page })
+      .then(
+        data => {
+          store.dispatch(thread.replace(data))
+          store.dispatch(posts.load(data.post_set))
+          store.dispatch(thread.release())
+
+          snackbar.success(gettext("Thread has been moved."))
+          modal.hide()
+        },
+        rejection => {
+          store.dispatch(thread.release())
+          snackbar.apiError(rejection)
+        }
+      )
   }
 
   handleError(rejection) {
     if (rejection.status === 400) {
-      snackbar.error(rejection.detail[0]);
+      snackbar.error(rejection.detail[0])
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
   }
 
-  /* jshint ignore:start */
-  onCategoryChange = (event) => {
-    this.changeValue('category', event.target.value);
-  };
-  /* jshint ignore:end */
+  onCategoryChange = event => {
+    this.changeValue("category", event.target.value)
+  }
 
   render() {
-    /* jshint ignore:start */
     if (this.state.isReady) {
       return (
         <div className="modal-dialog" role="document">
@@ -122,28 +127,25 @@ export default class extends Form {
                 >
                   {gettext("Cancel")}
                 </button>
-                <button className="btn btn-primary" loading={this.state.isLoading || this.props.thread.isBusy}>
+                <button
+                  className="btn btn-primary"
+                  loading={this.state.isLoading || this.props.thread.isBusy}
+                >
                   {gettext("Move thread")}
                 </button>
               </div>
             </div>
           </form>
         </div>
-      );
+      )
     } else if (this.state.isError) {
-      return (
-        <ModalMessage message={this.state.isError} />
-      );
+      return <ModalMessage message={this.state.isError} />
     } else {
-      return (
-        <ModalLoading />
-      );
+      return <ModalLoading />
     }
-    /* jshint ignore:end */
   }
 }
 
-/* jshint ignore:start */
 export function ModalHeader(props) {
   return (
     <div className="modal-header">
@@ -157,7 +159,7 @@ export function ModalHeader(props) {
       </button>
       <h4 className="modal-title">{gettext("Move thread")}</h4>
     </div>
-  );
+  )
 }
 
 export function ModalLoading(props) {
@@ -168,7 +170,7 @@ export function ModalLoading(props) {
         <ModalLoader />
       </div>
     </div>
-  );
+  )
 }
 
 export function ModalMessage(props) {
@@ -177,17 +179,13 @@ export function ModalMessage(props) {
       <div className="modal-content">
         <ModalHeader />
         <div className="message-icon">
-          <span className="material-icon">
-            info_outline
-          </span>
+          <span className="material-icon">info_outline</span>
         </div>
         <div className="message-body">
           <p className="lead">
             {gettext("You can't move this thread at the moment.")}
           </p>
-          <p>
-            {props.message}
-          </p>
+          <p>{props.message}</p>
           <button
             className="btn btn-default"
             data-dismiss="modal"
@@ -198,6 +196,5 @@ export function ModalMessage(props) {
         </div>
       </div>
     </div>
-  );
+  )
 }
-/* jshint ignore:end */

+ 24 - 24
frontend/src/components/thread/paginator.js

@@ -1,6 +1,5 @@
-/* jshint ignore:start */
-import React from 'react';
-import { Link } from 'react-router';
+import React from "react"
+import { Link } from "react-router"
 
 export default function(props) {
   return (
@@ -8,7 +7,7 @@ export default function(props) {
       <Pager {...props} />
       <More more={props.posts.more} />
     </nav>
-  );
+  )
 }
 
 export function Pager(props) {
@@ -27,7 +26,7 @@ export function Pager(props) {
         <LastPage {...props} />
       </div>
     </div>
-  );
+  )
 }
 
 export function FirstPage(props) {
@@ -40,7 +39,7 @@ export function FirstPage(props) {
       >
         <span className="material-icon">first_page</span>
       </Link>
-    );
+    )
   } else {
     return (
       <span
@@ -49,15 +48,15 @@ export function FirstPage(props) {
       >
         <span className="material-icon">first_page</span>
       </span>
-    );
+    )
   }
 }
 
 export function PreviousPage(props) {
   if (props.posts.isLoaded && props.posts.page > 1) {
-    let previousUrl = '';
+    let previousUrl = ""
     if (props.posts.previous) {
-      previousUrl = props.posts.previous + '/';
+      previousUrl = props.posts.previous + "/"
     }
 
     return (
@@ -68,7 +67,7 @@ export function PreviousPage(props) {
       >
         <span className="material-icon">chevron_left</span>
       </Link>
-    );
+    )
   } else {
     return (
       <span
@@ -77,15 +76,15 @@ export function PreviousPage(props) {
       >
         <span className="material-icon">chevron_left</span>
       </span>
-    );
+    )
   }
 }
 
 export function NextPage(props) {
   if (props.posts.isLoaded && props.posts.more) {
-    let nextUrl = '';
+    let nextUrl = ""
     if (props.posts.next) {
-      nextUrl = props.posts.next + '/';
+      nextUrl = props.posts.next + "/"
     }
 
     return (
@@ -96,7 +95,7 @@ export function NextPage(props) {
       >
         <span className="material-icon">chevron_right</span>
       </Link>
-    );
+    )
   } else {
     return (
       <span
@@ -105,7 +104,7 @@ export function NextPage(props) {
       >
         <span className="material-icon">chevron_right</span>
       </span>
-    );
+    )
   }
 }
 
@@ -114,12 +113,12 @@ export function LastPage(props) {
     return (
       <Link
         className="btn btn-default btn-block btn-outline btn-icon"
-        to={props.thread.url.index + props.posts.last + '/'}
+        to={props.thread.url.index + props.posts.last + "/"}
         title={gettext("Go to last page")}
       >
         <span className="material-icon">last_page</span>
       </Link>
-    );
+    )
   } else {
     return (
       <span
@@ -128,21 +127,22 @@ export function LastPage(props) {
       >
         <span className="material-icon">last_page</span>
       </span>
-    );
+    )
   }
 }
 
 export function More(props) {
-  let message = null;
+  let message = null
   if (props.more) {
     message = ngettext(
       "There is %(more)s more post in this thread.",
       "There are %(more)s more posts in this thread.",
-      props.more);
-    message = interpolate(message, {'more': props.more}, true);
+      props.more
+    )
+    message = interpolate(message, { more: props.more }, true)
   } else {
-    message = gettext("There are no more posts in this thread.");
+    message = gettext("There are no more posts in this thread.")
   }
 
-  return <p>{message}</p>;
-}
+  return <p>{message}</p>
+}

+ 4 - 7
frontend/src/components/thread/reply-button.js

@@ -1,17 +1,14 @@
-/* jshint ignore:start */
-import React from 'react';
+import React from "react"
 
 export default function(props) {
   return (
     <button
-      className={props.className || 'btn btn-primary btn-outline'}
+      className={props.className || "btn btn-primary btn-outline"}
       onClick={props.onClick}
       type="button"
     >
-      <span className="material-icon">
-        chat
-      </span>
+      <span className="material-icon">chat</span>
       {gettext("Reply")}
     </button>
-  );
+  )
 }

+ 18 - 15
frontend/src/components/thread/root.js

@@ -1,21 +1,24 @@
-import { connect } from 'react-redux';
-import Route from 'misago/components/thread/route';
-import misago from 'misago/index';
+import { connect } from "react-redux"
+import Route from "misago/components/thread/route"
+import misago from "misago/index"
 
 export function select(store) {
   return {
-    'participants': store.participants,
-    'poll': store.poll,
-    'posts': store.posts,
-    'thread': store.thread,
-    'tick': store.tick.tick,
-    'user': store.auth.user
-  };
+    participants: store.participants,
+    poll: store.poll,
+    posts: store.posts,
+    thread: store.thread,
+    tick: store.tick.tick,
+    user: store.auth.user
+  }
 }
 
 export function paths() {
-  const thread = misago.get('THREAD');
-  const basePath = thread.url.index.replace(thread.slug + '-' + thread.pk, ':slug');
+  const thread = misago.get("THREAD")
+  const basePath = thread.url.index.replace(
+    thread.slug + "-" + thread.pk,
+    ":slug"
+  )
 
   return [
     {
@@ -23,8 +26,8 @@ export function paths() {
       component: connect(select)(Route)
     },
     {
-      path: basePath + ':page/',
+      path: basePath + ":page/",
       component: connect(select)(Route)
     }
-  ];
-}
+  ]
+}

+ 83 - 84
frontend/src/components/thread/route.js

@@ -1,67 +1,76 @@
-import React from 'react';
-import Participants from 'misago/components/participants'; // jshint ignore:line
-import { Poll } from 'misago/components/poll'; // jshint ignore:line
-import PostsList from 'misago/components/posts-list'; // jshint ignore:line
-import Header from './header'; // jshint ignore:line
-import ToolbarTop from './toolbar-top'; // jshint ignore:line
-import ToolbarBottom from './toolbar-bottom'; // jshint ignore:line
-import * as participants from 'misago/reducers/participants'; // jshint ignore:line
-import * as poll from 'misago/reducers/poll'; // jshint ignore:line
-import * as posts from 'misago/reducers/posts';
-import * as thread from 'misago/reducers/thread'; // jshint ignore:line
-import ajax from 'misago/services/ajax';
-import polls from 'misago/services/polls';
-import snackbar from 'misago/services/snackbar';
-import posting from 'misago/services/posting'; // jshint ignore:line
-import store from 'misago/services/store';
-import title from 'misago/services/page-title';
+import React from "react"
+import Participants from "misago/components/participants"
+import { Poll } from "misago/components/poll"
+import PostsList from "misago/components/posts-list"
+import Header from "./header"
+import ToolbarTop from "./toolbar-top"
+import ToolbarBottom from "./toolbar-bottom"
+import * as participants from "misago/reducers/participants"
+import * as poll from "misago/reducers/poll"
+import * as posts from "misago/reducers/posts"
+import * as thread from "misago/reducers/thread"
+import ajax from "misago/services/ajax"
+import polls from "misago/services/polls"
+import snackbar from "misago/services/snackbar"
+import posting from "misago/services/posting"
+import store from "misago/services/store"
+import title from "misago/services/page-title"
 
 export default class extends React.Component {
   componentDidMount() {
     if (this.shouldFetchData()) {
-      this.fetchData();
-      this.setPageTitle();
+      this.fetchData()
+      this.setPageTitle()
     }
 
-    this.startPollingApi();
+    this.startPollingApi()
   }
 
   componentDidUpdate() {
     if (this.shouldFetchData()) {
-      this.fetchData();
-      this.startPollingApi();
-      this.setPageTitle();
+      this.fetchData()
+      this.startPollingApi()
+      this.setPageTitle()
     }
   }
 
   componentWillUnmount() {
-    this.stopPollingApi();
+    this.stopPollingApi()
   }
 
   shouldFetchData() {
     if (this.props.posts.isLoaded) {
-      const page = (this.props.params.page || 1) * 1;
-      return page != this.props.posts.page;
+      const page = (this.props.params.page || 1) * 1
+      return page != this.props.posts.page
     } else {
-      return false;
+      return false
     }
   }
 
   fetchData() {
-    store.dispatch(posts.unload());
-
-    ajax.get(this.props.thread.api.posts.index, {
-      page: this.props.params.page || 1
-    }, 'posts').then((data) => {
-      this.update(data);
-    }, (rejection) => {
-      snackbar.apiError(rejection);
-    });
+    store.dispatch(posts.unload())
+
+    ajax
+      .get(
+        this.props.thread.api.posts.index,
+        {
+          page: this.props.params.page || 1
+        },
+        "posts"
+      )
+      .then(
+        data => {
+          this.update(data)
+        },
+        rejection => {
+          snackbar.apiError(rejection)
+        }
+      )
   }
 
   startPollingApi() {
     polls.start({
-      poll: 'thread-posts',
+      poll: "thread-posts",
 
       url: this.props.thread.api.posts.index,
       data: {
@@ -71,11 +80,11 @@ export default class extends React.Component {
 
       frequency: 120 * 1000,
       delayed: true
-    });
+    })
   }
 
   stopPollingApi() {
-    polls.stop('thread-posts');
+    polls.stop("thread-posts")
   }
 
   setPageTitle() {
@@ -83,70 +92,60 @@ export default class extends React.Component {
       title: this.props.thread.title,
       parent: this.props.thread.category.name,
       page: (this.props.params.page || 1) * 1
-    });
+    })
   }
 
-  /* jshint ignore:start */
-  update = (data) => {
-    store.dispatch(thread.replace(data));
-    store.dispatch(posts.load(data.post_set));
+  update = data => {
+    store.dispatch(thread.replace(data))
+    store.dispatch(posts.load(data.post_set))
 
     if (data.participants) {
-      store.dispatch(participants.replace(data.participants));
+      store.dispatch(participants.replace(data.participants))
     }
 
     if (data.poll) {
-      store.dispatch(poll.replace(data.poll));
+      store.dispatch(poll.replace(data.poll))
     }
 
-    this.setPageTitle();
-  };
+    this.setPageTitle()
+  }
 
   openReplyForm = () => {
     posting.open({
-      mode: 'REPLY',
+      mode: "REPLY",
 
       config: this.props.thread.api.editor,
       submit: this.props.thread.api.posts.index
-    });
-  };
-  /* jshint ignore:end */
+    })
+  }
 
   render() {
-    /* jshint ignore:start */
-    let className = 'page page-thread';
+    let className = "page page-thread"
     if (this.props.thread.category.css_class) {
-      className += ' page-thread-' + this.props.thread.category.css_class;
+      className += " page-thread-" + this.props.thread.category.css_class
     }
 
-    return <div className={className}>
-      <div className="page-header-bg">
-        <Header {...this.props} />
-      </div>
-      <div className="container">
-
-        <ToolbarTop
-          openReplyForm={this.openReplyForm}
-          {...this.props}
-        />
-        <Poll
-          poll={this.props.poll}
-          thread={this.props.thread}
-          user={this.props.user}
-        />
-        <Participants
-          participants={this.props.participants}
-          thread={this.props.thread}
-          user={this.props.user}
-        />
-        <PostsList {...this.props} />
-        <ToolbarBottom
-          openReplyForm={this.openReplyForm}
-          {...this.props}
-        />
-
+    return (
+      <div className={className}>
+        <div className="page-header-bg">
+          <Header {...this.props} />
+        </div>
+        <div className="container">
+          <ToolbarTop openReplyForm={this.openReplyForm} {...this.props} />
+          <Poll
+            poll={this.props.poll}
+            thread={this.props.thread}
+            user={this.props.user}
+          />
+          <Participants
+            participants={this.props.participants}
+            thread={this.props.thread}
+            user={this.props.user}
+          />
+          <PostsList {...this.props} />
+          <ToolbarBottom openReplyForm={this.openReplyForm} {...this.props} />
+        </div>
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 52 - 52
frontend/src/components/thread/subscription.js

@@ -1,12 +1,11 @@
-/* jshint ignore:start */
-import React from 'react';
-import * as actions from 'misago/reducers/thread';
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
+import React from "react"
+import * as actions from "misago/reducers/thread"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export default function(props) {
-  if (!props.user.id) return null;
+  if (!props.user.id) return null
 
   return (
     <div className={props.className}>
@@ -24,26 +23,26 @@ export default function(props) {
       </button>
       <Dropdown {...props} />
     </div>
-  );
+  )
 }
 
 export function getIcon(subscription) {
   if (subscription === true) {
-    return 'star';
+    return "star"
   } else if (subscription === false) {
-    return 'star_half';
+    return "star_half"
   } else {
-    return 'star_border';
+    return "star_border"
   }
 }
 
 export function getLabel(subscription) {
   if (subscription === true) {
-    return gettext("E-mail");
+    return gettext("E-mail")
   } else if (subscription === false) {
-    return gettext("Enabled");
+    return gettext("Enabled")
   } else {
-    return gettext("Disabled");
+    return gettext("Disabled")
   }
 }
 
@@ -54,25 +53,23 @@ export function Dropdown(props) {
       <Enable {...props} />
       <Email {...props} />
     </ul>
-  );
+  )
 }
 
 export class Disable extends React.Component {
   onClick = () => {
     if (this.props.thread.subscription === null) {
-      return;
+      return
     }
 
-    update(this.props.thread, null, 'unsubscribe');
-  };
+    update(this.props.thread, null, "unsubscribe")
+  }
 
   render() {
     return (
       <li>
         <button className="btn btn-link" onClick={this.onClick}>
-          <span className="material-icon">
-            star_border
-          </span>
+          <span className="material-icon">star_border</span>
           {gettext("Unsubscribe")}
         </button>
       </li>
@@ -83,19 +80,17 @@ export class Disable extends React.Component {
 export class Enable extends React.Component {
   onClick = () => {
     if (this.props.thread.subscription === false) {
-      return;
+      return
     }
 
-    update(this.props.thread, false, 'notify');
-  };
+    update(this.props.thread, false, "notify")
+  }
 
   render() {
     return (
       <li>
         <button className="btn btn-link" onClick={this.onClick}>
-          <span className="material-icon">
-            star_half
-          </span>
+          <span className="material-icon">star_half</span>
           {gettext("Subscribe")}
         </button>
       </li>
@@ -106,19 +101,17 @@ export class Enable extends React.Component {
 export class Email extends React.Component {
   onClick = () => {
     if (this.props.thread.subscription === true) {
-      return;
+      return
     }
 
-    update(this.props.thread, true, 'email');
-  };
+    update(this.props.thread, true, "email")
+  }
 
   render() {
     return (
       <li>
         <button className="btn btn-link" onClick={this.onClick}>
-          <span className="material-icon">
-            star
-          </span>
+          <span className="material-icon">star</span>
           {gettext("Subscribe with e-mail")}
         </button>
       </li>
@@ -129,23 +122,30 @@ export class Email extends React.Component {
 export function update(thread, newState, value) {
   const oldState = {
     subscription: thread.subscription
-  };
-
-  store.dispatch(actions.update({
-    subscription: newState
-  }));
-
-  ajax.patch(thread.api.index, [
-    {op: 'replace', path: 'subscription', value: value}
-  ]).then((finalState) => {
-    store.dispatch(actions.update(finalState));
-  }, (rejection) => {
-    if (rejection.status === 400) {
-      snackbar.error(rejection.detail[0]);
-    } else {
-      snackbar.apiError(rejection);
-    }
+  }
 
-    store.dispatch(actions.update(oldState));
-  });
-}
+  store.dispatch(
+    actions.update({
+      subscription: newState
+    })
+  )
+
+  ajax
+    .patch(thread.api.index, [
+      { op: "replace", path: "subscription", value: value }
+    ])
+    .then(
+      finalState => {
+        store.dispatch(actions.update(finalState))
+      },
+      rejection => {
+        if (rejection.status === 400) {
+          snackbar.error(rejection.detail[0])
+        } else {
+          snackbar.apiError(rejection)
+        }
+
+        store.dispatch(actions.update(oldState))
+      }
+    )
+}

+ 18 - 29
frontend/src/components/thread/toolbar-bottom.js

@@ -1,9 +1,8 @@
-/* jshint ignore:start */
-import React from 'react';
-import { Pager, More } from './paginator';
-import PostsModeration from './moderation/posts';
-import ReplyButton from './reply-button';
-import SubscriptionSwitch from './subscription';
+import React from "react"
+import { Pager, More } from "./paginator"
+import PostsModeration from "./moderation/posts"
+import ReplyButton from "./reply-button"
+import SubscriptionSwitch from "./subscription"
 
 export default function(props) {
   return (
@@ -28,28 +27,21 @@ export default function(props) {
           <Spacer {...props} />
           <Moderation {...props} />
           <Subscription {...props} />
-          <Reply
-            thread={props.thread}
-            onClick={props.openReplyForm}
-          />
+          <Reply thread={props.thread} onClick={props.openReplyForm} />
         </div>
       </Options>
     </div>
-  );
+  )
 }
 
 export function Options(props) {
-  if (!props.visible) return null;
+  if (!props.visible) return null
 
-  return (
-    <div className="col-md-5">
-      {props.children}
-    </div>
-  )
+  return <div className="col-md-5">{props.children}</div>
 }
 
 export function Moderation(props) {
-  if (!props.user.id) return null;
+  if (!props.user.id) return null
 
   return (
     <div className="col-sm-4 hidden-xs">
@@ -58,11 +50,10 @@ export function Moderation(props) {
   )
 }
 
-
 export function Subscription(props) {
-  let xsClass = "col-xs-6";
+  let xsClass = "col-xs-6"
   if (!props.thread.acl.can_reply) {
-    xsClass = 'col-xs-12';
+    xsClass = "col-xs-12"
   }
 
   return (
@@ -73,11 +64,11 @@ export function Subscription(props) {
         {...props}
       />
     </div>
-  );
+  )
 }
 
 export function Reply(props) {
-  if (!props.thread.acl.can_reply) return null;
+  if (!props.thread.acl.can_reply) return null
 
   return (
     <div className="col-xs-6 col-sm-4">
@@ -86,13 +77,11 @@ export function Reply(props) {
         onClick={props.onClick}
       />
     </div>
-  );
+  )
 }
 
 export function Spacer(props) {
-  if (props.thread.acl.can_reply) return null;
+  if (props.thread.acl.can_reply) return null
 
-  return (
-    <div className="hidden-xs hidden-sm col-sm-4"></div>
-  );
-}
+  return <div className="hidden-xs hidden-sm col-sm-4" />
+}

+ 49 - 70
frontend/src/components/thread/toolbar-top.js

@@ -1,11 +1,11 @@
-/* jshint ignore:start */
-import React from 'react';
-import ReplyButton from './reply-button';
-import Subscription from './subscription';
-import posting from 'misago/services/posting';
+import React from "react"
+import ReplyButton from "./reply-button"
+import Subscription from "./subscription"
+import posting from "misago/services/posting"
 
 export default function(props) {
-  const hiddenSpecialOption = (!props.thread.acl.can_start_poll || props.thread.poll);
+  const hiddenSpecialOption =
+    !props.thread.acl.can_start_poll || props.thread.poll
 
   return (
     <div className="row row-toolbar row-toolbar-bottom-margin">
@@ -20,15 +20,15 @@ export default function(props) {
         </div>
       </div>
     </div>
-  );
+  )
 }
 
 export function GotoMenu(props) {
-  const { user } = props;
+  const { user } = props
 
-  let className = 'col-xs-3 col-sm-3 col-md-5';
+  let className = "col-xs-3 col-sm-3 col-md-5"
   if (user.is_anonymous) {
-    className = 'col-xs-12 col-sm-3 col-md-5';
+    className = "col-xs-12 col-sm-3 col-md-5"
   }
 
   return (
@@ -41,28 +41,28 @@ export function GotoMenu(props) {
       </div>
       <CompactOptions {...props} />
     </div>
-  );
+  )
 }
 
 export function GotoNew(props) {
-  if (!props.thread.is_new) return null;
+  if (!props.thread.is_new) return null
 
   return (
     <div className="col-sm-4">
       <a
         href={props.thread.url.new_post}
         className="btn btn-default btn-block btn-outline"
-        title={gettext('Go to first new post')}
+        title={gettext("Go to first new post")}
       >
         {gettext("New")}
       </a>
     </div>
-  );
+  )
 }
 
 export function GotoBestAnswer(props) {
   if (!props.thread.best_answer) {
-    return null;
+    return null
   }
 
   return (
@@ -70,17 +70,17 @@ export function GotoBestAnswer(props) {
       <a
         href={props.thread.url.best_answer}
         className="btn btn-default btn-block btn-outline"
-        title={gettext('Go to best answer')}
+        title={gettext("Go to best answer")}
       >
         {gettext("Best answer")}
       </a>
     </div>
-  );
+  )
 }
 
 export function GotoUnapproved(props) {
   if (!props.thread.has_unapproved_posts || !props.thread.acl.can_approve) {
-    return null;
+    return null
   }
 
   return (
@@ -88,12 +88,12 @@ export function GotoUnapproved(props) {
       <a
         href={props.thread.url.unapproved_post}
         className="btn btn-default btn-block btn-outline"
-        title={gettext('Go to first unapproved post')}
+        title={gettext("Go to first unapproved post")}
       >
         {gettext("Unapproved")}
       </a>
     </div>
-  );
+  )
 }
 
 export function GotoLast(props) {
@@ -102,16 +102,16 @@ export function GotoLast(props) {
       <a
         href={props.thread.url.last_post}
         className="btn btn-default btn-block btn-outline"
-        title={gettext('Go to last post')}
+        title={gettext("Go to last post")}
       >
         {gettext("Last")}
       </a>
     </div>
-  );
+  )
 }
 
 export function CompactOptions(props) {
-  const { user } = props;
+  const { user } = props
   if (user.is_anonymous) {
     return (
       <div className="visible-xs-block visible-sm-block">
@@ -122,7 +122,7 @@ export function CompactOptions(props) {
           {gettext("Last post")}
         </a>
       </div>
-    );
+    )
   }
 
   return (
@@ -134,12 +134,8 @@ export function CompactOptions(props) {
         data-toggle="dropdown"
         type="button"
       >
-        <span className="material-icon">
-          expand_more
-        </span>
-        <span className="btn-text hidden-xs">
-          {gettext("Options")}
-        </span>
+        <span className="material-icon">expand_more</span>
+        <span className="btn-text hidden-xs">{gettext("Options")}</span>
       </button>
       <ul className="dropdown-menu">
         <StartPollCompact {...props} />
@@ -148,56 +144,47 @@ export function CompactOptions(props) {
         <GotoLastCompact {...props} />
       </ul>
     </div>
-  );
+  )
 }
 
 export function GotoNewCompact(props) {
-  if (!props.thread.is_new) return null;
+  if (!props.thread.is_new) return null
 
   return (
     <li>
-      <a
-        href={props.thread.url.new_post}
-        className="btn btn-link"
-      >
+      <a href={props.thread.url.new_post} className="btn btn-link">
         {gettext("Go to first new post")}
       </a>
     </li>
-  );
+  )
 }
 
 export function GotoUnapprovedCompact(props) {
   if (!props.thread.has_unapproved_posts || !props.thread.acl.can_approve) {
-    return null;
+    return null
   }
 
   return (
     <li>
-      <a
-        href={props.thread.url.unapproved_post}
-        className="btn btn-link"
-      >
+      <a href={props.thread.url.unapproved_post} className="btn btn-link">
         {gettext("Go to first unapproved post")}
       </a>
     </li>
-  );
+  )
 }
 
 export function GotoLastCompact(props) {
   return (
     <li>
-      <a
-        href={props.thread.url.last_post}
-        className="btn btn-link"
-      >
+      <a href={props.thread.url.last_post} className="btn btn-link">
         {gettext("Go to last post")}
       </a>
     </li>
-  );
+  )
 }
 
 export function Reply(props) {
-  if (!props.thread.acl.can_reply) return null;
+  if (!props.thread.acl.can_reply) return null
 
   return (
     <div className="col-sm-4 hidden-xs">
@@ -206,11 +193,11 @@ export function Reply(props) {
         onClick={props.openReplyForm}
       />
     </div>
-  );
+  )
 }
 
 export function SubscriptionMenu(props) {
-  if (!props.user.id) return null;
+  if (!props.user.id) return null
 
   return (
     <div className="col-xs-12 col-sm-4">
@@ -226,17 +213,17 @@ export function SubscriptionMenu(props) {
 export class StartPoll extends React.Component {
   onClick = () => {
     posting.open({
-      mode: 'POLL',
+      mode: "POLL",
       submit: this.props.thread.api.poll,
 
       thread: this.props.thread,
       poll: null
-    });
+    })
   }
 
   render() {
     if (!this.props.thread.acl.can_start_poll || this.props.thread.poll) {
-      return null;
+      return null
     }
 
     return (
@@ -246,40 +233,32 @@ export class StartPoll extends React.Component {
           onClick={this.onClick}
           type="button"
         >
-          <span className="material-icon">
-            poll
-          </span>
+          <span className="material-icon">poll</span>
           {gettext("Add poll")}
         </button>
       </div>
-    );
+    )
   }
 }
 
 export class StartPollCompact extends StartPoll {
   render() {
     if (!this.props.thread.acl.can_start_poll || this.props.thread.poll) {
-      return null;
+      return null
     }
 
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.onClick}
-          type="button"
-        >
+        <button className="btn btn-link" onClick={this.onClick} type="button">
           {gettext("Add poll")}
         </button>
       </li>
-    );
+    )
   }
 }
 
 export function Spacer(props) {
-  if (!props.visible) return null;
+  if (!props.visible) return null
 
-  return (
-    <div className="col-sm-4 hidden-xs"/>
-  );
-}
+  return <div className="col-sm-4 hidden-xs" />
+}

+ 9 - 18
frontend/src/components/threads-list/index.js

@@ -1,25 +1,19 @@
-/* jshint ignore:start */
-import React from 'react';
-import ListEmpty from 'misago/components/threads-list/list/empty';
-import ListReady from 'misago/components/threads-list/list/ready';
-import ListPreview from 'misago/components/threads-list/list/preview';
+import React from "react"
+import ListEmpty from "misago/components/threads-list/list/empty"
+import ListReady from "misago/components/threads-list/list/ready"
+import ListPreview from "misago/components/threads-list/list/preview"
 
 export default function(props) {
   if (!props.isLoaded) {
-    return (
-      <ListPreview />
-    );
+    return <ListPreview />
   }
 
   if (props.threads.length === 0) {
     return (
-      <ListEmpty
-        diffSize={props.diffSize}
-        applyDiff={props.applyDiff}
-      >
+      <ListEmpty diffSize={props.diffSize} applyDiff={props.applyDiff}>
         {props.children}
       </ListEmpty>
-    );
+    )
   }
 
   return (
@@ -28,14 +22,11 @@ export default function(props) {
       categories={props.categories}
       list={props.list}
       threads={props.threads}
-
       diffSize={props.diffSize}
       applyDiff={props.applyDiff}
-
       showOptions={props.showOptions}
       selection={props.selection}
-
       busyThreads={props.busyThreads}
     />
-  );
-}
+  )
+}

+ 16 - 16
frontend/src/components/threads-list/list/diff-message.js

@@ -1,10 +1,9 @@
-/* jshint ignore:start */
-import React from 'react';
+import React from "react"
 
 export default function(props) {
-  const { diffSize, applyDiff } = props;
+  const { diffSize, applyDiff } = props
 
-  if (diffSize === 0) return null;
+  if (diffSize === 0) return null
 
   return (
     <li className="list-group-item threads-diff-message">
@@ -13,24 +12,25 @@ export default function(props) {
         className="btn btn-block btn-default"
         onClick={applyDiff}
       >
-        <span className="material-icon">
-          cached
-        </span>
-        <span className="diff-message">
-          {getMessage(diffSize)}
-        </span>
+        <span className="material-icon">cached</span>
+        <span className="diff-message">{getMessage(diffSize)}</span>
       </button>
     </li>
-  );
+  )
 }
 
 export function getMessage(diffSize) {
   const message = ngettext(
     "There is %(threads)s new or updated thread. Click this message to show it.",
     "There are %(threads)s new or updated threads. Click this message to show them.",
-    diffSize);
+    diffSize
+  )
 
-  return interpolate(message, {
-    threads: diffSize
-  }, true);
-}
+  return interpolate(
+    message,
+    {
+      threads: diffSize
+    },
+    true
+  )
+}

+ 7 - 11
frontend/src/components/threads-list/list/empty.js

@@ -1,22 +1,19 @@
-import React from 'react';
-import DiffMessage from 'misago/components/threads-list/list/diff-message'; // jshint ignore:line
+import React from "react"
+import DiffMessage from "misago/components/threads-list/list/diff-message"
 
 export default class extends React.Component {
   getDiffMessage() {
-    if (this.props.diffSize === 0) return null;
+    if (this.props.diffSize === 0) return null
 
-    /* jshint ignore:start */
     return (
       <DiffMessage
         applyDiff={this.props.applyDiff}
         diffSize={this.props.diffSize}
       />
-    );
-    /* jshint ignore:end */
+    )
   }
 
-  render () {
-    /* jshint ignore:start */
+  render() {
     return (
       <div className="threads-list ui-ready">
         <ul className="list-group">
@@ -24,7 +21,6 @@ export default class extends React.Component {
           {this.props.children}
         </ul>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 6 - 8
frontend/src/components/threads-list/list/preview.js

@@ -1,20 +1,18 @@
-import React from 'react';
-import ThreadPreview from 'misago/components/threads-list/thread/preview'; // jshint ignore:line
+import React from "react"
+import ThreadPreview from "misago/components/threads-list/thread/preview"
 
 export default class extends React.Component {
   shouldComponentUpdate() {
-    return false;
+    return false
   }
 
-  render () {
-    /* jshint ignore:start */
+  render() {
     return (
       <div className="threads-list ui-preview">
         <ul className="list-group">
           <ThreadPreview />
         </ul>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 8 - 14
frontend/src/components/threads-list/list/ready.js

@@ -1,33 +1,27 @@
-/* jshint ignore:start */
-import React from 'react';
-import DiffMessage from 'misago/components/threads-list/list/diff-message'; // jshint ignore:line
-import Thread from 'misago/components/threads-list/thread/ready'; // jshint ignore:line
+import React from "react"
+import DiffMessage from "misago/components/threads-list/list/diff-message"
+import Thread from "misago/components/threads-list/thread/ready"
 
 export default function(props) {
   return (
     <div className="threads-list ui-ready">
       <ul className="list-group">
-        <DiffMessage
-          diffSize={props.diffSize}
-          applyDiff={props.applyDiff}
-        />
-        {props.threads.map((thread) => {
+        <DiffMessage diffSize={props.diffSize} applyDiff={props.applyDiff} />
+        {props.threads.map(thread => {
           return (
             <Thread
               activeCategory={props.activeCategory}
               categories={props.categories}
               list={props.list}
               thread={thread}
-
               showOptions={props.showOptions}
               isSelected={props.selection.indexOf(thread.id) >= 0}
-
               isBusy={props.busyThreads.indexOf(thread.id) >= 0}
               key={thread.id}
             />
-          );
+          )
         })}
       </ul>
     </div>
-  );
-}
+  )
+}

+ 48 - 81
frontend/src/components/threads-list/thread/details/bottom.js

@@ -1,28 +1,27 @@
-/* jshint ignore:start */
-import React from 'react';
-import Category from './category';
-import { OptionsXs } from '../options';
+import React from "react"
+import Category from "./category"
+import { OptionsXs } from "../options"
 
-export default function({category, isBusy, showOptions, isSelected, thread}) {
-  let className = 'col-xs-12 col-sm-12';
+export default function({ category, isBusy, showOptions, isSelected, thread }) {
+  let className = "col-xs-12 col-sm-12"
   if (showOptions) {
     if (thread.moderation.length) {
-      className = 'col-xs-6 col-sm-12';
+      className = "col-xs-6 col-sm-12"
     } else {
-      className = 'col-xs-9 col-sm-12';
+      className = "col-xs-9 col-sm-12"
     }
   }
 
-  let statusFlags = 0;
-  if (thread.is_hidden) statusFlags += 1;
-  if (thread.is_closed) statusFlags += 1;
-  if (thread.has_poll) statusFlags += 1;
+  let statusFlags = 0
+  if (thread.is_hidden) statusFlags += 1
+  if (thread.is_closed) statusFlags += 1
+  if (thread.has_poll) statusFlags += 1
 
-  let allFlagsVisible = showOptions && statusFlags === 3;
+  let allFlagsVisible = showOptions && statusFlags === 3
 
-  let textClassName = 'detail-text hidden-xs';
+  let textClassName = "detail-text hidden-xs"
   if (allFlagsVisible) {
-    textClassName += ' hidden-sm'
+    textClassName += " hidden-sm"
   }
 
   return (
@@ -32,18 +31,9 @@ export default function({category, isBusy, showOptions, isSelected, thread}) {
           className="item-title thread-detail-category hidden-xs"
           category={category}
         />
-        <HiddenLabel
-          textClassName={textClassName}
-          display={thread.is_hidden}
-        />
-        <ClosedLabel
-          textClassName={textClassName}
-          display={thread.is_closed}
-        />
-        <PollLabel
-          textClassName={textClassName}
-          display={thread.has_poll}
-        />
+        <HiddenLabel textClassName={textClassName} display={thread.is_hidden} />
+        <ClosedLabel textClassName={textClassName} display={thread.is_closed} />
+        <PollLabel textClassName={textClassName} display={thread.has_poll} />
         <BestAnswerLabel thread={thread} />
         <RepliesLabel
           forceFullText={!showOptions || statusFlags < 2}
@@ -65,56 +55,44 @@ export default function({category, isBusy, showOptions, isSelected, thread}) {
         thread={thread}
       />
     </div>
-  );;
+  )
 }
 
 export function HiddenLabel({ display, textClassName }) {
-  if (!display) return null;
+  if (!display) return null
 
   return (
     <span className="thread-detail-hidden">
-      <span className="material-icon">
-        visibility_off
-      </span>
-      <span className={textClassName}>
-        {gettext("Hidden")}
-      </span>
+      <span className="material-icon">visibility_off</span>
+      <span className={textClassName}>{gettext("Hidden")}</span>
     </span>
-  );
+  )
 }
 
 export function ClosedLabel({ display, textClassName }) {
-  if (!display) return null;
+  if (!display) return null
 
   return (
     <span className="thread-detail-closed">
-      <span className="material-icon">
-        lock_outline
-      </span>
-      <span className={textClassName}>
-        {gettext("Closed")}
-      </span>
+      <span className="material-icon">lock_outline</span>
+      <span className={textClassName}>{gettext("Closed")}</span>
     </span>
-  );
+  )
 }
 
 export function PollLabel({ display, textClassName }) {
-  if (!display) return null;
+  if (!display) return null
 
   return (
     <span className="thread-detail-poll">
-      <span className="material-icon">
-        assessment
-      </span>
-      <span className={textClassName}>
-        {gettext("Poll")}
-      </span>
+      <span className="material-icon">assessment</span>
+      <span className={textClassName}>{gettext("Poll")}</span>
     </span>
-  );
+  )
 }
 
 export function BestAnswerLabel({ thread }) {
-  if (!thread.best_answer) return null;
+  if (!thread.best_answer) return null
 
   return (
     <a
@@ -127,35 +105,28 @@ export function BestAnswerLabel({ thread }) {
 }
 
 export function RepliesLabel({ replies, forceFullText }) {
-  const text = ngettext(
-    "%(replies)s reply",
-    "%(replies)s replies",
-    replies);
+  const text = ngettext("%(replies)s reply", "%(replies)s replies", replies)
 
-  let compactClassName = '';
-  let fullClassName = '';
+  let compactClassName = ""
+  let fullClassName = ""
 
   if (forceFullText) {
-    compactClassName = 'detail-text hide';
-    fullClassName = 'detail-text';
+    compactClassName = "detail-text hide"
+    fullClassName = "detail-text"
   } else {
-    compactClassName = 'detail-text visible-xs-inline-block';
-    fullClassName = 'detail-text hidden-xs';
+    compactClassName = "detail-text visible-xs-inline-block"
+    fullClassName = "detail-text hidden-xs"
   }
 
   return (
     <span className="thread-detail-replies">
-      <span className="material-icon">
-        forum
-      </span>
-      <span className={compactClassName}>
-        {replies}
-      </span>
+      <span className="material-icon">forum</span>
+      <span className={compactClassName}>{replies}</span>
       <span className={fullClassName}>
         {interpolate(text, { replies }, true)}
       </span>
     </span>
-  );
+  )
 }
 
 export function LastReplyLabel({ datetime, url }) {
@@ -163,28 +134,24 @@ export function LastReplyLabel({ datetime, url }) {
     <a
       className="visible-sm-inline-block thread-detail-last-reply"
       href={url}
-      title={datetime.format('LLL')}
+      title={datetime.format("LLL")}
     >
       {datetime.fromNow(true)}
     </a>
-  );
+  )
 }
 
 export function LastPoster(props) {
-  const { posterName, url } = props;
-  const className = 'visible-sm-inline-block item-title thread-last-poster';
+  const { posterName, url } = props
+  const className = "visible-sm-inline-block item-title thread-last-poster"
 
   if (url) {
     return (
       <a className={className} href={url}>
         {posterName}
       </a>
-    );
+    )
   }
 
-  return (
-    <span className={className}>
-      {posterName}
-    </span>
-  );;
-}
+  return <span className={className}>{posterName}</span>
+}

+ 6 - 10
frontend/src/components/threads-list/thread/details/category.js

@@ -1,19 +1,15 @@
-/* jshint ignore:start */
-import React from 'react';
+import React from "react"
 
 export default function({ category, className }) {
-  if (!category) return null;
+  if (!category) return null
 
   if (category.css_class) {
-    className += ' thread-detail-category-' + category.css_class;
+    className += " thread-detail-category-" + category.css_class
   }
 
   return (
-    <a
-      className={className}
-      href={category.url.index}
-    >
+    <a className={className} href={category.url.index}>
       {category.name}
     </a>
-  );
-}
+  )
+}

+ 4 - 5
frontend/src/components/threads-list/thread/details/index.js

@@ -1,6 +1,5 @@
-/* jshint ignore:start */
-import BottomDetails from './bottom';
-import TopDetails from './top';
+import BottomDetails from "./bottom"
+import TopDetails from "./top"
 
-export { BottomDetails };
-export { TopDetails };
+export { BottomDetails }
+export { TopDetails }

+ 33 - 54
frontend/src/components/threads-list/thread/details/top.js

@@ -1,14 +1,10 @@
-/* jshint ignore:start */
-import React from 'react';
-import Category from './category';
+import React from "react"
+import Category from "./category"
 
 export default function({ category, thread }) {
   return (
     <div className="thread-details-top">
-      <NewLabel
-        isRead={thread.is_read}
-        url={thread.url.new_post}
-      />
+      <NewLabel isRead={thread.is_read} url={thread.url.new_post} />
       <PinnedLabel weight={thread.weight} />
       <UnapprovedLabel
         thread={thread.is_unapproved}
@@ -28,79 +24,64 @@ export default function({ category, thread }) {
         url={thread.url.last_poster}
       />
     </div>
-  );
+  )
 }
 
 export function NewLabel({ isRead, url }) {
-  if (isRead) return null;
+  if (isRead) return null
 
   return (
-    <a
-      className="thread-detail-new"
-      href={url}
-    >
-      <span className="material-icon">
-        comment
-      </span>
-      <span className="detail-text">
-        {gettext("New posts")}
-      </span>
+    <a className="thread-detail-new" href={url}>
+      <span className="material-icon">comment</span>
+      <span className="detail-text">{gettext("New posts")}</span>
     </a>
   )
 }
 
 export function PinnedLabel({ weight }) {
-  if (weight === 0) return null;
+  if (weight === 0) return null
 
-  let className = 'thread-detail-pinned-globally'
-  let icon = 'bookmark';
-  let text = gettext("Pinned globally");
+  let className = "thread-detail-pinned-globally"
+  let icon = "bookmark"
+  let text = gettext("Pinned globally")
 
   if (weight === 1) {
-    className = 'thread-detail-pinned-locally'
-    icon = 'bookmark_border';
-    text = gettext("Pinned locally");
+    className = "thread-detail-pinned-locally"
+    icon = "bookmark_border"
+    text = gettext("Pinned locally")
   }
 
   return (
     <span className={className}>
-      <span className="material-icon">
-        {icon}
-      </span>
-      <span className="detail-text">
-        {text}
-      </span>
+      <span className="material-icon">{icon}</span>
+      <span className="detail-text">{text}</span>
     </span>
   )
 }
 
 export function UnapprovedLabel({ posts, thread }) {
-  if (!posts && !thread) return null;
+  if (!posts && !thread) return null
 
-  let className = 'thread-detail-unapproved-posts'
-  let icon = 'remove_circle_outline';
-  let text = gettext("Unapproved posts");
+  let className = "thread-detail-unapproved-posts"
+  let icon = "remove_circle_outline"
+  let text = gettext("Unapproved posts")
 
   if (thread) {
-    className = 'thread-detail-unapproved'
-    icon = 'remove_circle';
-    text = gettext("Unapproved");
+    className = "thread-detail-unapproved"
+    icon = "remove_circle"
+    text = gettext("Unapproved")
   }
 
   return (
     <span className={className}>
-      <span className="material-icon">
-        {icon}
-      </span>
-      <span className="detail-text">
-        {text}
-      </span>
+      <span className="material-icon">{icon}</span>
+      <span className="detail-text">{text}</span>
     </span>
   )
 }
 
 export function BestAnswerLabel({ thread }) {
-  if (!thread.best_answer) return null;
+  if (!thread.best_answer) return null
 
   return (
     <a
@@ -108,9 +89,7 @@ export function BestAnswerLabel({ thread }) {
       href={thread.url.best_answer}
     >
       <span className="material-icon">check_box</span>
-      <span className="detail-text">
-        {gettext("Answered")}
-      </span>
+      <span className="detail-text">{gettext("Answered")}</span>
     </a>
   )
 }
@@ -120,7 +99,7 @@ export function LastReplyLabel({ datetime, url }) {
     <a
       className="visible-xs-inline-block thread-detail-last-reply"
       href={url}
-      title={datetime.format('LLL')}
+      title={datetime.format("LLL")}
     >
       {datetime.fromNow(true)}
     </a>
@@ -128,7 +107,7 @@ export function LastReplyLabel({ datetime, url }) {
 }
 
 export function LastPoster(props) {
-  const { posterName, url } = props;
+  const { posterName, url } = props
 
   if (url) {
     return (
@@ -138,12 +117,12 @@ export function LastPoster(props) {
       >
         {posterName}
       </a>
-    );
+    )
   }
 
   return (
     <span className="visible-xs-inline-block item-title thread-last-poster">
       {posterName}
     </span>
-  );
-}
+  )
+}

+ 8 - 16
frontend/src/components/threads-list/thread/last-action.js

@@ -1,7 +1,6 @@
-/* jshint ignore:start */
-import React from 'react';
-import Avatar from 'misago/components/avatar';
-import UserUrl from './user-url';
+import React from "react"
+import Avatar from "misago/components/avatar"
+import UserUrl from "./user-url"
 
 export default function({ thread }) {
   return (
@@ -26,23 +25,16 @@ export default function({ thread }) {
         >
           {thread.last_poster_name}
         </UserUrl>
-        <Timestamp
-          datetime={thread.last_post_on}
-          url={thread.url.last_post}
-        />
+        <Timestamp datetime={thread.last_post_on} url={thread.url.last_post} />
       </div>
     </div>
-  );
+  )
 }
 
 export function Timestamp({ datetime, url }) {
   return (
-    <a
-      className="thread-last-reply"
-      href={url}
-      title={datetime.format('LLL')}
-    >
+    <a className="thread-last-reply" href={url} title={datetime.format("LLL")}>
       {datetime.fromNow(true)}
     </a>
-  );
-}
+  )
+}

+ 28 - 49
frontend/src/components/threads-list/thread/options.js

@@ -1,80 +1,59 @@
-/* jshint ignore:start */
-import React from 'react';
-import SubscriptionCompact from 'misago/components/threads-list/thread/subscription/compact';
-import SubscriptionFull from 'misago/components/threads-list/thread/subscription/full';
-import * as select from 'misago/reducers/selection';
-import store from 'misago/services/store';
+import React from "react"
+import SubscriptionCompact from "misago/components/threads-list/thread/subscription/compact"
+import SubscriptionFull from "misago/components/threads-list/thread/subscription/full"
+import * as select from "misago/reducers/selection"
+import store from "misago/services/store"
 
 export function Options({ display, disabled, isSelected, thread }) {
-  if (!display) return null;
+  if (!display) return null
 
-  let className = 'col-sm-2 col-md-2 hidden-xs';
+  let className = "col-sm-2 col-md-2 hidden-xs"
   if (thread.moderation.length) {
-    className = 'col-sm-3 col-md-2 hidden-xs';
+    className = "col-sm-3 col-md-2 hidden-xs"
   }
 
   return (
     <div className={className}>
       <div className="row thread-options">
-        <SubscriptionFull
-          thread={thread}
-          disabled={disabled}
-        />
-        <SubscriptionCompact
-          thread={thread}
-          disabled={disabled}
-        />
-        <Checkbox
-          thread={thread}
-          disabled={disabled}
-          isSelected={isSelected}
-         />
+        <SubscriptionFull thread={thread} disabled={disabled} />
+        <SubscriptionCompact thread={thread} disabled={disabled} />
+        <Checkbox thread={thread} disabled={disabled} isSelected={isSelected} />
       </div>
     </div>
-  );
+  )
 }
 
 export function OptionsXs({ display, disabled, isSelected, thread }) {
-  if (!display) return null;
+  if (!display) return null
 
-  let className = ''
+  let className = ""
   if (thread.moderation.length) {
-    className += 'col-xs-6';
+    className += "col-xs-6"
   } else {
-    className += 'col-xs-3';
+    className += "col-xs-3"
   }
-  className += ' visible-xs-block thread-options-xs';
+  className += " visible-xs-block thread-options-xs"
 
   return (
     <div className={className}>
       <div className="row thread-options">
-        <SubscriptionFull
-          thread={thread}
-          disabled={disabled}
-        />
-        <SubscriptionCompact
-          thread={thread}
-          disabled={disabled}
-        />
-        <Checkbox
-          thread={thread}
-          disabled={disabled}
-          isSelected={isSelected}
-         />
+        <SubscriptionFull thread={thread} disabled={disabled} />
+        <SubscriptionCompact thread={thread} disabled={disabled} />
+        <Checkbox thread={thread} disabled={disabled} isSelected={isSelected} />
       </div>
     </div>
-  );
+  )
 }
 
 export class Checkbox extends React.Component {
   toggleSelection = () => {
-    store.dispatch(select.item(this.props.thread.id));
-  };
+    store.dispatch(select.item(this.props.thread.id))
+  }
 
   render() {
-    const { disabled, isSelected, thread } = this.props;
+    const { disabled, isSelected, thread } = this.props
 
-    if (!thread.moderation.length) return null;
+    if (!thread.moderation.length) return null
 
     return (
       <div className="col-xs-6">
@@ -84,10 +63,10 @@ export class Checkbox extends React.Component {
           disabled={disabled}
         >
           <span className="material-icon">
-            {isSelected ? 'check_box' : 'check_box_outline_blank'}
+            {isSelected ? "check_box" : "check_box_outline_blank"}
           </span>
         </button>
       </div>
-    );
+    )
   }
-}
+}

+ 15 - 17
frontend/src/components/threads-list/thread/preview.js

@@ -1,31 +1,30 @@
-import React from 'react';
-import * as random from 'misago/utils/random'; // jshint ignore:line
+import React from "react"
+import * as random from "misago/utils/random"
 
 export default class extends React.Component {
   shouldComponentUpdate() {
-    return false;
+    return false
   }
 
-  render () {
-    /* jshint ignore:start */
+  render() {
     return (
       <li className="list-group-item thread-preview">
         <div className="thread-details-top visible-xs-block">
           <span
             className="ui-preview-text"
-            style={{width: random.int(30, 80) + "px"}}
+            style={{ width: random.int(30, 80) + "px" }}
           >
             &nbsp;
           </span>
           <span
             className="ui-preview-text"
-            style={{width: random.int(30, 80) + "px"}}
+            style={{ width: random.int(30, 80) + "px" }}
           >
             &nbsp;
           </span>
           <span
             className="ui-preview-text"
-            style={{width: random.int(30, 80) + "px"}}
+            style={{ width: random.int(30, 80) + "px" }}
           >
             &nbsp;
           </span>
@@ -34,19 +33,19 @@ export default class extends React.Component {
         <span className="item-title thread-title">
           <span
             className="ui-preview-text"
-            style={{width: random.int(60, 200) + "px"}}
+            style={{ width: random.int(60, 200) + "px" }}
           >
             &nbsp;
           </span>
           <span
             className="ui-preview-text hidden-xs"
-            style={{width: random.int(60, 200) + "px"}}
+            style={{ width: random.int(60, 200) + "px" }}
           >
             &nbsp;
           </span>
           <span
             className="ui-preview-text hidden-xs"
-            style={{width: random.int(60, 200) + "px"}}
+            style={{ width: random.int(60, 200) + "px" }}
           >
             &nbsp;
           </span>
@@ -56,26 +55,25 @@ export default class extends React.Component {
           <div>
             <span
               className="ui-preview-text"
-              style={{width: random.int(30, 80) + "px"}}
+              style={{ width: random.int(30, 80) + "px" }}
             >
               &nbsp;
             </span>
             <span
               className="ui-preview-text"
-              style={{width: random.int(30, 80) + "px"}}
+              style={{ width: random.int(30, 80) + "px" }}
             >
               &nbsp;
             </span>
             <span
               className="ui-preview-text"
-              style={{width: random.int(30, 80) + "px"}}
+              style={{ width: random.int(30, 80) + "px" }}
             >
               &nbsp;
             </span>
           </div>
         </div>
       </li>
-    );
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 27 - 40
frontend/src/components/threads-list/thread/ready.js

@@ -1,10 +1,9 @@
-/* jshint ignore:start */
-import React from 'react';
-import Avatar from 'misago/components/avatar';
-import { BottomDetails, TopDetails } from './details';
-import LastAction from './last-action';
-import { Options } from './options';
-import UserUrl from './user-url';
+import React from "react"
+import Avatar from "misago/components/avatar"
+import { BottomDetails, TopDetails } from "./details"
+import LastAction from "./last-action"
+import { Options } from "./options"
+import UserUrl from "./user-url"
 
 export default function(props) {
   const {
@@ -15,53 +14,43 @@ export default function(props) {
 
     isBusy,
     isSelected,
-    showOptions,
-  } = props;
+    showOptions
+  } = props
 
-  let category = null;
+  let category = null
   if (activeCategory.id !== thread.category) {
-    category = categories[thread.category];
+    category = categories[thread.category]
   }
 
-  const flavor = category || activeCategory;
+  const flavor = category || activeCategory
 
-  let className = 'thread-main col-xs-12';
+  let className = "thread-main col-xs-12"
   if (showOptions) {
     if (thread.moderation.length) {
-      className += ' col-sm-9 col-md-7';
+      className += " col-sm-9 col-md-7"
     } else {
-      className += ' col-sm-10 col-md-7';
+      className += " col-sm-10 col-md-7"
     }
   } else {
-    className += ' col-sm-12 col-md-9';
+    className += " col-sm-12 col-md-9"
   }
 
   return (
     <li className={getClassName(thread.is_read, isBusy, isSelected, flavor)}>
-      <TopDetails
-        category={category}
-        thread={thread}
-      />
+      <TopDetails category={category} thread={thread} />
       <div className="row thread-row">
         <div className={className}>
-
           <div className="media">
             <div className="media-left hidden-xs">
-
               <UserUrl
                 className="thread-starter-avatar"
                 title={thread.starter_name}
                 url={thread.url.starter}
               >
-                <Avatar
-                  size={40}
-                  user={thread.starter}
-                />
+                <Avatar size={40} user={thread.starter} />
               </UserUrl>
-
             </div>
             <div className="media-body">
-
               <a href={thread.url.index} className="item-title thread-title">
                 {thread.title}
               </a>
@@ -73,10 +62,8 @@ export default function(props) {
                 showOptions={showOptions}
                 thread={thread}
               />
-
             </div>
           </div>
-
         </div>
         <div className="col-md-3 hidden-xs hidden-sm thread-last-action">
           <LastAction thread={thread} />
@@ -89,28 +76,28 @@ export default function(props) {
         />
       </div>
     </li>
-  );
+  )
 }
 
 export function getClassName(isRead, isBusy, isSelected, flavor) {
-  let styles = ['list-group-item'];
+  let styles = ["list-group-item"]
 
   if (flavor && flavor.css_class) {
-    styles.push('list-group-category-has-flavor');
-    styles.push('list-group-item-category-' + flavor.css_class);
+    styles.push("list-group-category-has-flavor")
+    styles.push("list-group-item-category-" + flavor.css_class)
   }
 
   if (isRead) {
-    styles.push('thread-read');
+    styles.push("thread-read")
   } else {
-    styles.push('thread-new');
+    styles.push("thread-new")
   }
 
   if (isBusy) {
-    styles.push('thread-busy');
+    styles.push("thread-busy")
   } else if (isSelected) {
-    styles.push('thread-selected');
+    styles.push("thread-selected")
   }
 
-  return styles.join(' ');
-}
+  return styles.join(" ")
+}

+ 14 - 20
frontend/src/components/threads-list/thread/subscription/compact.js

@@ -1,26 +1,23 @@
-import React from 'react'; // jshint ignore:line
-import SubscriptionFull from 'misago/components/threads-list/thread/subscription/full'; // jshint ignore:line
-import OptionsModal from 'misago/components/threads-list/thread/subscription/modal'; // jshint ignore:line
-import modal from 'misago/services/modal'; // jshint ignore:line
+import React from "react"
+import SubscriptionFull from "misago/components/threads-list/thread/subscription/full"
+import OptionsModal from "misago/components/threads-list/thread/subscription/modal"
+import modal from "misago/services/modal"
 
 export default class extends SubscriptionFull {
-  /* jshint ignore:start */
   showOptions = () => {
-    modal.show(<OptionsModal thread={this.props.thread} />);
-  };
-  /* jshint ignore:end */
+    modal.show(<OptionsModal thread={this.props.thread} />)
+  }
 
   render() {
-    /* jshint ignore:start */
-    const { moderation } = this.props.thread;
+    const { moderation } = this.props.thread
 
-    let className = ''
+    let className = ""
     if (moderation.length) {
-      className += 'col-xs-6';
+      className += "col-xs-6"
     } else {
-      className += 'col-xs-12';
+      className += "col-xs-12"
     }
-    className += ' hidden-md hidden-lg';
+    className += " hidden-md hidden-lg"
 
     return (
       <div className={className}>
@@ -30,12 +27,9 @@ export default class extends SubscriptionFull {
           disabled={this.props.disabled}
           onClick={this.showOptions}
         >
-          <span className="material-icon">
-            {this.getIcon()}
-          </span>
+          <span className="material-icon">{this.getIcon()}</span>
         </button>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 21 - 32
frontend/src/components/threads-list/thread/subscription/full.js

@@ -1,34 +1,33 @@
-/* jshint ignore:start */
-import React from 'react';
-import Options from 'misago/components/threads-list/thread/subscription/options';
+import React from "react"
+import Options from "misago/components/threads-list/thread/subscription/options"
 
 export default class extends React.Component {
   getIcon() {
     if (this.props.thread.subscription === true) {
-      return 'star';
+      return "star"
     } else if (this.props.thread.subscription === false) {
-      return 'star_half';
+      return "star_half"
     }
 
-    return 'star_border';
+    return "star_border"
   }
 
   getClassName() {
     if (this.props.thread.subscription === true) {
-      return "btn btn-default btn-icon btn-block btn-subscribe btn-subscribe-full dropdown-toggle";
+      return "btn btn-default btn-icon btn-block btn-subscribe btn-subscribe-full dropdown-toggle"
     } else if (this.props.thread.subscription === false) {
-      return "btn btn-default btn-icon btn-block btn-subscribe btn-subscribe-half dropdown-toggle";
+      return "btn btn-default btn-icon btn-block btn-subscribe btn-subscribe-half dropdown-toggle"
     }
 
-    return "btn btn-default btn-icon btn-block btn-subscribe dropdown-toggle";
+    return "btn btn-default btn-icon btn-block btn-subscribe dropdown-toggle"
   }
 
   render() {
-    const { moderation, subscription } = this.props.thread;
-    const fullwidth = !moderation.length;
+    const { moderation, subscription } = this.props.thread
+    const fullwidth = !moderation.length
 
-    let className = fullwidth ? 'col-xs-12' : 'col-xs-6';
-    className += ' hidden-xs hidden-sm';
+    let className = fullwidth ? "col-xs-12" : "col-xs-6"
+    className += " hidden-xs hidden-sm"
 
     return (
       <div className={className}>
@@ -42,40 +41,30 @@ export default class extends React.Component {
               aria-haspopup="true"
               aria-expanded="false"
             >
-              <span className="material-icon">
-                {this.getIcon()}
-              </span>
-              <Label
-                moderation={moderation}
-                subscription={subscription}
-              />
+              <span className="material-icon">{this.getIcon()}</span>
+              <Label moderation={moderation} subscription={subscription} />
             </button>
 
             <Options
               className="dropdown-menu dropdown-menu-right"
               thread={this.props.thread}
             />
-
           </div>
         </div>
       </div>
-    );
+    )
   }
 }
 
 export function Label({ moderation, subscription }) {
-  if (moderation.length) return null;
+  if (moderation.length) return null
 
-  let text = gettext("Disabled");
+  let text = gettext("Disabled")
   if (subscription === true) {
-    text = gettext("E-mail");
+    text = gettext("E-mail")
   } else if (subscription === false) {
-    text = gettext("Enabled");
+    text = gettext("Enabled")
   }
 
-  return (
-    <span className="btn-text">
-      {text}
-    </span>
-  );
-}
+  return <span className="btn-text">{text}</span>
+}

+ 20 - 18
frontend/src/components/threads-list/thread/subscription/modal.js

@@ -1,24 +1,26 @@
-import React from 'react';
-import Options from 'misago/components/threads-list/thread/subscription/options'; // jshint ignore:line
+import React from "react"
+import Options from "misago/components/threads-list/thread/subscription/options"
 
 export default class extends React.Component {
   render() {
-    /* jshint ignore:start */
-    return <div className="modal-dialog modal-sm"
-                role="document">
-      <div className="modal-content">
-        <div className="modal-header">
-          <button type="button" className="close" data-dismiss="modal"
-                  aria-label={gettext("Close")}>
-            <span aria-hidden="true">&times;</span>
-          </button>
-          <h4 className="modal-title">{gettext("Change subscription")}</h4>
-        </div>
-
-        <Options className="modal-menu" thread={this.props.thread}/>
+    return (
+      <div className="modal-dialog modal-sm" role="document">
+        <div className="modal-content">
+          <div className="modal-header">
+            <button
+              type="button"
+              className="close"
+              data-dismiss="modal"
+              aria-label={gettext("Close")}
+            >
+              <span aria-hidden="true">&times;</span>
+            </button>
+            <h4 className="modal-title">{gettext("Change subscription")}</h4>
+          </div>
 
+          <Options className="modal-menu" thread={this.props.thread} />
+        </div>
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 74 - 75
frontend/src/components/threads-list/thread/subscription/options.js

@@ -1,100 +1,99 @@
-import React from 'react';
-import Button from 'misago/components/button'; // jshint ignore:line
-import { patch } from 'misago/reducers/threads'; // jshint ignore:line
-import ajax from 'misago/services/ajax'; // jshint ignore:line
-import modal from 'misago/services/modal'; // jshint ignore:line
-import snackbar from 'misago/services/snackbar'; // jshint ignore:line
-import store from 'misago/services/store'; // jshint ignore:line
+import React from "react"
+import Button from "misago/components/button"
+import { patch } from "misago/reducers/threads"
+import ajax from "misago/services/ajax"
+import modal from "misago/services/modal"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
-/* jshint ignore:start */
 const STATE_UPDATES = {
-  'unsubscribe': null,
-  'notify': false,
-  'email': true
-};
-/* jshint ignore:end */
+  unsubscribe: null,
+  notify: false,
+  email: true
+}
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoading: false
-    };
+    }
   }
 
-  /* jshint ignore:start */
-  setSubscription = (newState) => {
-    modal.hide();
+  setSubscription = newState => {
+    modal.hide()
 
     this.setState({
       isLoading: true
-    });
+    })
 
-    let oldState = this.props.thread.subscription;
+    let oldState = this.props.thread.subscription
 
-    store.dispatch(patch(this.props.thread, {
-      subscription: STATE_UPDATES[newState]
-    }));
+    store.dispatch(
+      patch(this.props.thread, {
+        subscription: STATE_UPDATES[newState]
+      })
+    )
 
-    ajax.patch(this.props.thread.api.index, [
-      {op: 'replace', path: 'subscription', value: newState}
-    ]).then(() => {
-      this.setState({
-        isLoading: false
-      });
-    }, (rejection) => {
-      this.setState({
-        isLoading: false
-      });
-      store.dispatch(patch(this.props.thread, {
-        subscription: STATE_UPDATES[oldState]
-      }));
-      snackbar.apiError(rejection);
-    });
-  };
+    ajax
+      .patch(this.props.thread.api.index, [
+        { op: "replace", path: "subscription", value: newState }
+      ])
+      .then(
+        () => {
+          this.setState({
+            isLoading: false
+          })
+        },
+        rejection => {
+          this.setState({
+            isLoading: false
+          })
+          store.dispatch(
+            patch(this.props.thread, {
+              subscription: STATE_UPDATES[oldState]
+            })
+          )
+          snackbar.apiError(rejection)
+        }
+      )
+  }
 
   unsubscribe = () => {
-    this.setSubscription('unsubscribe');
-  };
+    this.setSubscription("unsubscribe")
+  }
 
   notify = () => {
-    this.setSubscription('notify');
-  };
+    this.setSubscription("notify")
+  }
 
   email = () => {
-    this.setSubscription('email');
-  };
-  /* jshint ignore:end */
+    this.setSubscription("email")
+  }
 
   render() {
-    /* jshint ignore:start */
-    return <ul className={this.props.className}>
-      <li>
-        <button className="btn-link" onClick={this.unsubscribe}>
-          <span className="material-icon">
-            star_border
-          </span>
-          {gettext("Unsubscribe")}
-        </button>
-      </li>
-      <li>
-        <button className="btn-link" onClick={this.notify}>
-          <span className="material-icon">
-            star_half
-          </span>
-          {gettext("Subscribe")}
-        </button>
-      </li>
-      <li>
-        <button className="btn-link" onClick={this.email}>
-          <span className="material-icon">
-            star
-          </span>
-          {gettext("Subscribe with e-mail")}
-        </button>
-      </li>
-    </ul>;
-    /* jshint ignore:end */
+    return (
+      <ul className={this.props.className}>
+        <li>
+          <button className="btn-link" onClick={this.unsubscribe}>
+            <span className="material-icon">star_border</span>
+            {gettext("Unsubscribe")}
+          </button>
+        </li>
+        <li>
+          <button className="btn-link" onClick={this.notify}>
+            <span className="material-icon">star_half</span>
+            {gettext("Subscribe")}
+          </button>
+        </li>
+        <li>
+          <button className="btn-link" onClick={this.email}>
+            <span className="material-icon">star</span>
+            {gettext("Subscribe with e-mail")}
+          </button>
+        </li>
+      </ul>
+    )
   }
-}
+}

+ 6 - 14
frontend/src/components/threads-list/thread/user-url.js

@@ -1,25 +1,17 @@
-/* jshint ignore:start */
-import React from 'react';
+import React from "react"
 
 export default function({ children, className, title, url }) {
   if (url) {
     return (
-      <a
-        className={className}
-        href={url}
-        title={title}
-      >
+      <a className={className} href={url} title={title}>
         {children}
       </a>
-    );
+    )
   }
 
   return (
-    <span
-      className={className}
-      title={title}
-    >
+    <span className={className} title={title}>
       {children}
     </span>
-  );
-}
+  )
+}

+ 11 - 17
frontend/src/components/threads/category-picker.js

@@ -1,31 +1,28 @@
-import React from 'react';
-import { Link } from 'react-router'; // jshint ignore:line
+import React from "react"
+import { Link } from "react-router"
 
 export class Subcategory extends React.Component {
   getUrl() {
     if (this.props.listPath) {
-      return this.props.category.url.index + this.props.listPath;
+      return this.props.category.url.index + this.props.listPath
     } else {
-      return this.props.category.url.index;
+      return this.props.category.url.index
     }
   }
 
   render() {
-    /* jshint ignore:start */
     return (
       <li>
         <Link to={this.getUrl()} className="btn btn-link">
           {this.props.category.name}
         </Link>
       </li>
-    );
-    /* jshint ignore:end */
+    )
   }
 }
 
 export default class extends React.Component {
   render() {
-    /* jshint ignore:start */
     return (
       <div className="dropdown category-picker">
         <button
@@ -35,13 +32,11 @@ export default class extends React.Component {
           aria-haspopup="true"
           aria-expanded="false"
         >
-          <span className="material-icon">
-            label_outline
-          </span>
+          <span className="material-icon">label_outline</span>
           <span className="hidden-xs">{gettext("Category")}</span>
         </button>
         <ul className="dropdown-menu stick-to-bottom categories-menu">
-          {this.props.choices.map((id) => {
+          {this.props.choices.map(id => {
             if (this.props.categories[id]) {
               return (
                 <Subcategory
@@ -49,14 +44,13 @@ export default class extends React.Component {
                   listPath={this.props.list.path}
                   key={id}
                 />
-              );
+              )
             } else {
-              return null;
+              return null
             }
           })}
         </ul>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 10 - 10
frontend/src/components/threads/compare.js

@@ -1,29 +1,29 @@
 export function compareLastPostAge(a, b) {
   if (a.last_post > b.last_post) {
-    return -1;
+    return -1
   } else if (a.last_post < b.last_post) {
-    return 1;
+    return 1
   } else {
-    return 0;
+    return 0
   }
 }
 
 export function compareGlobalWeight(a, b) {
   if (a.weight === 2 && a.weight > b.weight) {
-    return -1;
+    return -1
   } else if (b.weight === 2 && a.weight < b.weight) {
-    return 1;
+    return 1
   } else {
-    return compareLastPostAge(a, b);
+    return compareLastPostAge(a, b)
   }
 }
 
 export function compareWeight(a, b) {
   if (a.weight > b.weight) {
-    return -1;
+    return -1
   } else if (a.weight < b.weight) {
-    return 1;
+    return 1
   } else {
-    return compareLastPostAge(a, b);
+    return compareLastPostAge(a, b)
   }
-}
+}

+ 14 - 25
frontend/src/components/threads/container.js

@@ -1,81 +1,70 @@
-import React from 'react';
-import PageLead from 'misago/components/page-lead'; // jshint ignore:line
-import Toolbar from 'misago/components/threads/toolbar'; // jshint ignore:line
+import React from "react"
+import PageLead from "misago/components/page-lead"
+import Toolbar from "misago/components/threads/toolbar"
 
 export default class extends React.Component {
   getCategoryDescription() {
     if (this.props.pageLead) {
-      /* jshint ignore:start */
       return (
         <div className="category-description">
           <div className="page-lead">
             <p>{this.props.pageLead}</p>
           </div>
         </div>
-      );
-      /* jshint ignore:end */
+      )
     } else if (this.props.route.category.description) {
-      /* jshint ignore:start */
       return (
         <div className="category-description">
           <PageLead copy={this.props.route.category.description.html} />
         </div>
-      );
-      /* jshint ignore:end */
+      )
     } else {
-      return null;
+      return null
     }
   }
 
   getDisableToolbar() {
-    return !this.props.isLoaded || this.props.isBusy || this.props.busyThreads.length;
+    return (
+      !this.props.isLoaded || this.props.isBusy || this.props.busyThreads.length
+    )
   }
 
   getToolbar() {
-    const isVisible = this.props.subcategories.length || this.props.user.id;
+    const isVisible = this.props.subcategories.length || this.props.user.id
 
-    if (!isVisible) return null;
+    if (!isVisible) return null
 
-    /* jshint ignore:start */
     return (
       <Toolbar
         subcategories={this.props.subcategories}
         categories={this.props.route.categories}
         categoriesMap={this.props.route.categoriesMap}
         list={this.props.route.list}
-
         threads={this.props.threads}
         moderation={this.props.moderation}
         selection={this.props.selection}
         selectAllThreads={this.props.selectAllThreads}
         selectNoneThreads={this.props.selectNoneThreads}
-
         addThreads={this.props.addThreads}
         freezeThread={this.props.freezeThread}
         deleteThread={this.props.deleteThread}
         updateThread={this.props.updateThread}
-
         api={this.props.api}
         route={this.props.route}
         disabled={this.getDisableToolbar()}
         user={this.props.user}
       />
-    );
-    /* jshint ignore:end */
+    )
   }
 
   render() {
-    /* jshint ignore:start */
     return (
       <div className="container">
-
         {this.getCategoryDescription()}
         {this.getToolbar()}
 
         {this.props.children}
-
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 41 - 57
frontend/src/components/threads/header.js

@@ -1,45 +1,44 @@
-import React from 'react';
-import { Link } from 'react-router'; // jshint ignore:line
-import Button from 'misago/components/button'; // jshint ignore:line
-import DropdownToggle from 'misago/components/dropdown-toggle'; // jshint ignore:line
-import Nav from 'misago/components/threads/nav'; // jshint ignore:line
-import ajax from 'misago/services/ajax'; // jshint ignore:line
-import posting from 'misago/services/posting'; // jshint ignore:line
-import snackbar from 'misago/services/snackbar'; // jshint ignore:line
-import store from 'misago/services/store'; // jshint ignore:line
-import misago from 'misago'; // jshint ignore:line
+import React from "react"
+import { Link } from "react-router"
+import Button from "misago/components/button"
+import DropdownToggle from "misago/components/dropdown-toggle"
+import Nav from "misago/components/threads/nav"
+import ajax from "misago/services/ajax"
+import posting from "misago/services/posting"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
+import misago from "misago"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isBusy: false
-    };
+    }
   }
 
-  /* jshint ignore:start */
   startThread = () => {
-    posting.open(this.props.startThread || {
-      mode: 'START',
+    posting.open(
+      this.props.startThread || {
+        mode: "START",
 
-      config: misago.get('THREAD_EDITOR_API'),
-      submit: misago.get('THREADS_API'),
+        config: misago.get("THREAD_EDITOR_API"),
+        submit: misago.get("THREADS_API"),
 
-      category: this.props.route.category.id
-    });
-  };
-  /* jshint ignore:end */
+        category: this.props.route.category.id
+      }
+    )
+  }
 
   hasGoBackButton() {
-    return !!this.props.route.category.parent;
+    return !!this.props.route.category.parent
   }
 
   getGoBackButton() {
-    if (!this.props.route.category.parent) return null;
+    if (!this.props.route.category.parent) return null
 
-    /* jshint ignore:start */
-    const parent = this.props.categories[this.props.route.category.parent];
+    const parent = this.props.categories[this.props.route.category.parent]
 
     return (
       <div className="hidden-xs col-sm-2 col-lg-1">
@@ -47,49 +46,43 @@ export default class extends React.Component {
           className="btn btn-default btn-icon btn-aligned btn-go-back btn-block btn-outline"
           to={parent.url.index + this.props.route.list.path}
         >
-          <span className="material-icon">
-            keyboard_arrow_left
-          </span>
+          <span className="material-icon">keyboard_arrow_left</span>
         </Link>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   getStartThreadButton() {
-    if (!this.props.user.id) return null;
+    if (!this.props.user.id) return null
 
-    /* jshint ignore:start */
     return (
       <Button
         className="btn-primary btn-block btn-outline"
         onClick={this.startThread}
         disabled={this.props.disabled}
       >
-        <span className="material-icon">
-          chat
-        </span>
+        <span className="material-icon">chat</span>
         {gettext("Start thread")}
       </Button>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   render() {
-    /* jshint ignore:start */
-    let headerClassName = 'col-xs-12';
+    let headerClassName = "col-xs-12"
     if (this.hasGoBackButton()) {
-      headerClassName += ' col-sm-10 col-lg-11 sm-align-row-buttons';
+      headerClassName += " col-sm-10 col-lg-11 sm-align-row-buttons"
     }
 
-    const isAuthenticated = !!this.props.user.id;
+    const isAuthenticated = !!this.props.user.id
 
     return (
       <div className="page-header-bg">
         <div className="page-header">
           <div className="container">
             <div className="row">
-              <div className={isAuthenticated ? "col-sm-9 col-md-10" : "col-xs-12"}>
+              <div
+                className={isAuthenticated ? "col-sm-9 col-md-10" : "col-xs-12"}
+              >
                 <div className="row">
                   {this.getGoBackButton()}
                   <div className={headerClassName}>
@@ -114,30 +107,21 @@ export default class extends React.Component {
             list={this.props.route.list}
             lists={this.props.route.lists}
           />
-
         </div>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 }
 
-/* jshint ignore:start */
 export function ParentCategory({ categories, category }) {
-  if (!category) return null;
+  if (!category) return null
 
-  const parent = categories[category];
+  const parent = categories[category]
 
   return (
-    <Link
-      className="go-back-sm visible-xs-block"
-      to={parent.url.index}
-    >
-      <span className="material-icon">
-        chevron_left
-      </span>
+    <Link className="go-back-sm visible-xs-block" to={parent.url.index}>
+      <span className="material-icon">chevron_left</span>
       {parent.parent ? parent.name : gettext("Threads")}
     </Link>
-  );
+  )
 }
-/* jshint ignore:end */

+ 14 - 24
frontend/src/components/threads/list-empty.js

@@ -1,23 +1,16 @@
-import React from 'react';
+import React from "react"
 
 export default class extends React.Component {
-  render () {
-    if (this.props.list.type === 'all') {
+  render() {
+    if (this.props.list.type === "all") {
       if (this.props.emptyMessage) {
-        /* jshint ignore:start */
         return (
           <li className="list-group-item empty-message">
-            <p className="lead">
-              {this.props.emptyMessage}
-            </p>
-            <p>
-              {gettext("Why not start one yourself?")}
-            </p>
+            <p className="lead">{this.props.emptyMessage}</p>
+            <p>{gettext("Why not start one yourself?")}</p>
           </li>
-        );
-        /* jshint ignore:end */
+        )
       } else {
-        /* jshint ignore:start */
         return (
           <li className="list-group-item empty-message">
             <p className="lead">
@@ -25,19 +18,16 @@ export default class extends React.Component {
                 ? gettext("There are no threads on this forum... yet!")
                 : gettext("There are no threads in this category.")}
             </p>
-            <p>
-              {gettext("Why not start one yourself?")}
-            </p>
+            <p>{gettext("Why not start one yourself?")}</p>
           </li>
-        );
-        /* jshint ignore:end */
+        )
       }
     } else {
-      /* jshint ignore:start */
-      return <li className="list-group-item empty-message">
-        {gettext("No threads matching specified criteria were found.")}
-      </li>;
-      /* jshint ignore:end */
+      return (
+        <li className="list-group-item empty-message">
+          {gettext("No threads matching specified criteria were found.")}
+        </li>
+      )
     }
   }
-}
+}

+ 244 - 292
frontend/src/components/threads/moderation/controls.js

@@ -1,162 +1,183 @@
-import React from 'react';
-import ErrorsModal from 'misago/components/threads/moderation/errors-list'; // jshint ignore:line
-import MergeThreads from 'misago/components/threads/moderation/merge'; // jshint ignore:line
-import MoveThreads from 'misago/components/threads/moderation/move'; // jshint ignore:line
-import * as select from 'misago/reducers/selection'; // jshint ignore:line
-import ajax from 'misago/services/ajax'; // jshint ignore:line
-import modal from 'misago/services/modal'; // jshint ignore:line
-import snackbar from 'misago/services/snackbar'; // jshint ignore:line
-import store from 'misago/services/store'; // jshint ignore:line
-import Countdown from 'misago/utils/countdown'; // jshint ignore:line
+import React from "react"
+import ErrorsModal from "misago/components/threads/moderation/errors-list"
+import MergeThreads from "misago/components/threads/moderation/merge"
+import MoveThreads from "misago/components/threads/moderation/move"
+import * as select from "misago/reducers/selection"
+import ajax from "misago/services/ajax"
+import modal from "misago/services/modal"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
+import Countdown from "misago/utils/countdown"
 
 export default class extends React.Component {
-  /* jshint ignore:start */
-  callApi = (ops, successMessage, onSuccess=null) => {
+  callApi = (ops, successMessage, onSuccess = null) => {
     // freeze threads
-    this.props.threads.forEach((thread) => {
-      this.props.freezeThread(thread.id);
-    });
+    this.props.threads.forEach(thread => {
+      this.props.freezeThread(thread.id)
+    })
 
     // list ids
-    const ids = this.props.threads.map((thread) => {
-      return thread.id;
-    });
+    const ids = this.props.threads.map(thread => {
+      return thread.id
+    })
 
     // always return current acl
-    ops.push({op: 'add', path: 'acl', value: true});
+    ops.push({ op: "add", path: "acl", value: true })
 
     ajax.patch(this.props.api, { ids, ops }).then(
-      (data) => {
+      data => {
         // unfreeze
-        this.props.threads.forEach((thread) => {
-          this.props.freezeThread(thread.id);
-        });
+        this.props.threads.forEach(thread => {
+          this.props.freezeThread(thread.id)
+        })
 
         // update threads
-        data.forEach((thread) => {
-          this.props.updateThread(thread);
-        });
+        data.forEach(thread => {
+          this.props.updateThread(thread)
+        })
 
         // show success message and call callback
-        snackbar.success(successMessage);
+        snackbar.success(successMessage)
         if (onSuccess) {
-          onSuccess();
+          onSuccess()
         }
       },
-      (rejection) => {
+      rejection => {
         // unfreeze
-        this.props.threads.forEach((thread) => {
-          this.props.freezeThread(thread.id);
-        });
+        this.props.threads.forEach(thread => {
+          this.props.freezeThread(thread.id)
+        })
 
         // escape on non-400 error
         if (rejection.status !== 400) {
-          return snackbar.apiError(rejection);
+          return snackbar.apiError(rejection)
         }
 
         // build errors list
-        let errors = [];
+        let errors = []
         let threadsMap = {}
 
-        this.props.threads.forEach((thread) => {
-          threadsMap[thread.id] = thread;
-        });
+        this.props.threads.forEach(thread => {
+          threadsMap[thread.id] = thread
+        })
 
-        rejection.forEach(({id, detail }) => {
-          if (typeof threadsMap[id] !== 'undefined') {
+        rejection.forEach(({ id, detail }) => {
+          if (typeof threadsMap[id] !== "undefined") {
             errors.push({
               errors: detail,
               thread: threadsMap[id]
-            });
+            })
           }
-        });
+        })
 
-        modal.show(
-          <ErrorsModal errors={errors} />
-        );
+        modal.show(<ErrorsModal errors={errors} />)
       }
-    );
-  };
+    )
+  }
 
   pinGlobally = () => {
-    this.callApi([
-      {
-        op: 'replace',
-        path: 'weight',
-        value: 2
-      }
-    ], gettext("Selected threads were pinned globally."));
-  };
+    this.callApi(
+      [
+        {
+          op: "replace",
+          path: "weight",
+          value: 2
+        }
+      ],
+      gettext("Selected threads were pinned globally.")
+    )
+  }
 
   pinLocally = () => {
-    this.callApi([
-      {
-        op: 'replace',
-        path: 'weight',
-        value: 1
-      }
-    ], gettext("Selected threads were pinned locally."));
-  };
+    this.callApi(
+      [
+        {
+          op: "replace",
+          path: "weight",
+          value: 1
+        }
+      ],
+      gettext("Selected threads were pinned locally.")
+    )
+  }
 
   unpin = () => {
-    this.callApi([
-      {
-        op: 'replace',
-        path: 'weight',
-        value: 0
-      }
-    ], gettext("Selected threads were unpinned."));
-  };
+    this.callApi(
+      [
+        {
+          op: "replace",
+          path: "weight",
+          value: 0
+        }
+      ],
+      gettext("Selected threads were unpinned.")
+    )
+  }
 
   approve = () => {
-    this.callApi([
-      {
-        op: 'replace',
-        path: 'is-unapproved',
-        value: false
-      }
-    ], gettext("Selected threads were approved."));
-  };
+    this.callApi(
+      [
+        {
+          op: "replace",
+          path: "is-unapproved",
+          value: false
+        }
+      ],
+      gettext("Selected threads were approved.")
+    )
+  }
 
   open = () => {
-    this.callApi([
-      {
-        op: 'replace',
-        path: 'is-closed',
-        value: false
-      }
-    ], gettext("Selected threads were opened."));
-  };
+    this.callApi(
+      [
+        {
+          op: "replace",
+          path: "is-closed",
+          value: false
+        }
+      ],
+      gettext("Selected threads were opened.")
+    )
+  }
 
   close = () => {
-    this.callApi([
-      {
-        op: 'replace',
-        path: 'is-closed',
-        value: true
-      }
-    ], gettext("Selected threads were closed."));
-  };
+    this.callApi(
+      [
+        {
+          op: "replace",
+          path: "is-closed",
+          value: true
+        }
+      ],
+      gettext("Selected threads were closed.")
+    )
+  }
 
   unhide = () => {
-    this.callApi([
-      {
-        op: 'replace',
-        path: 'is-hidden',
-        value: false
-      }
-    ], gettext("Selected threads were unhidden."));
-  };
+    this.callApi(
+      [
+        {
+          op: "replace",
+          path: "is-hidden",
+          value: false
+        }
+      ],
+      gettext("Selected threads were unhidden.")
+    )
+  }
 
   hide = () => {
-    this.callApi([
-      {
-        op: 'replace',
-        path: 'is-hidden',
-        value: true
-      }
-    ], gettext("Selected threads were hidden."));
-  };
+    this.callApi(
+      [
+        {
+          op: "replace",
+          path: "is-hidden",
+          value: true
+        }
+      ],
+      gettext("Selected threads were hidden.")
+    )
+  }
 
   move = () => {
     modal.show(
@@ -167,75 +188,85 @@ export default class extends React.Component {
         route={this.props.route}
         user={this.props.user}
       />
-    );
-  };
+    )
+  }
 
   merge = () => {
-    const errors = [];
-    this.props.threads.forEach((thread) => {
+    const errors = []
+    this.props.threads.forEach(thread => {
       if (!thread.acl.can_merge) {
         errors.append({
-          'id': thread.id,
-          'title': thread.title,
-          'errors': [
-            gettext("You don't have permission to merge this thread with others.")
+          id: thread.id,
+          title: thread.title,
+          errors: [
+            gettext(
+              "You don't have permission to merge this thread with others."
+            )
           ]
-        });
+        })
       }
-    });
+    })
 
     if (this.props.threads.length < 2) {
       snackbar.info(
-        gettext("You have to select at least two threads to merge."));
+        gettext("You have to select at least two threads to merge.")
+      )
     } else if (errors.length) {
-      modal.show(<ErrorsModal errors={errors} />);
-      return;
+      modal.show(<ErrorsModal errors={errors} />)
+      return
     } else {
-      modal.show(<MergeThreads {...this.props} />);
+      modal.show(<MergeThreads {...this.props} />)
     }
-  };
+  }
 
   delete = () => {
-    if (!confirm(gettext("Are you sure you want to delete selected threads?"))) {
-      return;
+    if (
+      !confirm(gettext("Are you sure you want to delete selected threads?"))
+    ) {
+      return
     }
 
-    this.props.threads.map((thread) => {
-      this.props.freezeThread(thread.id);
-    });
+    this.props.threads.map(thread => {
+      this.props.freezeThread(thread.id)
+    })
 
-    const ids = this.props.threads.map((thread) => { return thread.id; });
+    const ids = this.props.threads.map(thread => {
+      return thread.id
+    })
 
-    ajax.delete(this.props.api, ids).then(() => {
-      this.props.threads.map((thread) => {
-        this.props.freezeThread(thread.id);
-        this.props.deleteThread(thread);
-      });
+    ajax.delete(this.props.api, ids).then(
+      () => {
+        this.props.threads.map(thread => {
+          this.props.freezeThread(thread.id)
+          this.props.deleteThread(thread)
+        })
 
-      snackbar.success(gettext("Selected threads were deleted."));
-    }, (rejection) => {
-      if (rejection.status === 400) {
-        const failedThreads = rejection.map((thread) => { return thread.id; });
-
-        this.props.threads.map((thread) => {
-          this.props.freezeThread(thread.id);
-          if (failedThreads.indexOf(thread.id) === -1) {
-            this.props.deleteThread(thread);
-          }
-        });
-
-        modal.show(<ErrorsModal errors={rejection} />);
-      } else {
-        snackbar.apiError(rejection);
+        snackbar.success(gettext("Selected threads were deleted."))
+      },
+      rejection => {
+        if (rejection.status === 400) {
+          const failedThreads = rejection.map(thread => {
+            return thread.id
+          })
+
+          this.props.threads.map(thread => {
+            this.props.freezeThread(thread.id)
+            if (failedThreads.indexOf(thread.id) === -1) {
+              this.props.deleteThread(thread)
+            }
+          })
+
+          modal.show(<ErrorsModal errors={rejection} />)
+        } else {
+          snackbar.apiError(rejection)
+        }
       }
-    });
-  };
-  /* jshint ignore:end */
+    )
+  }
 
   getPinGloballyButton() {
-    if (!this.props.moderation.can_pin_globally) return null;
+    if (!this.props.moderation.can_pin_globally) return null
 
-    /* jshint ignore:start */
     return (
       <li>
         <button
@@ -243,20 +274,16 @@ export default class extends React.Component {
           onClick={this.pinGlobally}
           type="button"
         >
-          <span className="material-icon">
-            bookmark
-          </span>
+          <span className="material-icon">bookmark</span>
           {gettext("Pin threads globally")}
         </button>
       </li>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   getPinLocallyButton() {
-    if (!this.props.moderation.can_pin) return null;
+    if (!this.props.moderation.can_pin) return null
 
-    /* jshint ignore:start */
     return (
       <li>
         <button
@@ -264,220 +291,145 @@ export default class extends React.Component {
           onClick={this.pinLocally}
           type="button"
         >
-          <span className="material-icon">
-            bookmark_border
-          </span>
+          <span className="material-icon">bookmark_border</span>
           {gettext("Pin threads locally")}
         </button>
       </li>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   getUnpinButton() {
-    if (!this.props.moderation.can_pin) return null;
+    if (!this.props.moderation.can_pin) return null
 
-    /* jshint ignore:start */
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.unpin}
-          type="button"
-        >
-          <span className="material-icon">
-            panorama_fish_eye
-          </span>
+        <button className="btn btn-link" onClick={this.unpin} type="button">
+          <span className="material-icon">panorama_fish_eye</span>
           {gettext("Unpin threads")}
         </button>
       </li>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   getMoveButton() {
-    if (!this.props.moderation.can_move) return null;
+    if (!this.props.moderation.can_move) return null
 
-    /* jshint ignore:start */
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.move}
-          type="button"
-        >
-          <span className="material-icon">
-            arrow_forward
-          </span>
+        <button className="btn btn-link" onClick={this.move} type="button">
+          <span className="material-icon">arrow_forward</span>
           {gettext("Move threads")}
         </button>
       </li>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   getMergeButton() {
-    if (!this.props.moderation.can_merge) return null;
+    if (!this.props.moderation.can_merge) return null
 
-    /* jshint ignore:start */
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.merge}
-          type="button"
-        >
-          <span className="material-icon">
-            call_merge
-          </span>
+        <button className="btn btn-link" onClick={this.merge} type="button">
+          <span className="material-icon">call_merge</span>
           {gettext("Merge threads")}
         </button>
       </li>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   getApproveButton() {
-    if (!this.props.moderation.can_approve) return null;
+    if (!this.props.moderation.can_approve) return null
 
-    /* jshint ignore:start */
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.approve}
-          type="button"
-        >
-          <span className="material-icon">
-            done
-          </span>
+        <button className="btn btn-link" onClick={this.approve} type="button">
+          <span className="material-icon">done</span>
           {gettext("Approve threads")}
         </button>
       </li>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   getOpenButton() {
-    if (!this.props.moderation.can_close) return null;
+    if (!this.props.moderation.can_close) return null
 
-    /* jshint ignore:start */
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.open}
-          type="button"
-        >
-          <span className="material-icon">
-            lock_open
-          </span>
+        <button className="btn btn-link" onClick={this.open} type="button">
+          <span className="material-icon">lock_open</span>
           {gettext("Open threads")}
         </button>
       </li>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   getCloseButton() {
-    if (!this.props.moderation.can_close) return null;
+    if (!this.props.moderation.can_close) return null
 
-    /* jshint ignore:start */
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.close}
-          type="button"
-        >
-          <span className="material-icon">
-            lock_outline
-          </span>
+        <button className="btn btn-link" onClick={this.close} type="button">
+          <span className="material-icon">lock_outline</span>
           {gettext("Close threads")}
         </button>
       </li>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   getUnhideButton() {
-    if (!this.props.moderation.can_unhide) return null;
+    if (!this.props.moderation.can_unhide) return null
 
-    /* jshint ignore:start */
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.unhide}
-          type="button"
-        >
-          <span className="material-icon">
-            visibility
-          </span>
+        <button className="btn btn-link" onClick={this.unhide} type="button">
+          <span className="material-icon">visibility</span>
           {gettext("Unhide threads")}
         </button>
       </li>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   getHideButton() {
-    if (!this.props.moderation.can_hide) return null;
+    if (!this.props.moderation.can_hide) return null
 
-    /* jshint ignore:start */
     return (
       <li>
-        <button
-          onClick={this.hide}
-          type="button"
-          className="btn btn-link"
-        >
-          <span className="material-icon">
-            visibility_off
-          </span>
+        <button onClick={this.hide} type="button" className="btn btn-link">
+          <span className="material-icon">visibility_off</span>
           {gettext("Hide threads")}
         </button>
       </li>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   getDeleteButton() {
-    if (!this.props.moderation.can_delete) return null;
+    if (!this.props.moderation.can_delete) return null
 
-    /* jshint ignore:start */
     return (
       <li>
-        <button
-          className="btn btn-link"
-          onClick={this.delete}
-          type="button"
-        >
-          <span className="material-icon">
-            clear
-          </span>
+        <button className="btn btn-link" onClick={this.delete} type="button">
+          <span className="material-icon">clear</span>
           {gettext("Delete threads")}
         </button>
       </li>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   render() {
-    /* jshint ignore:start */
-    return <ul className={this.props.className}>
-      {this.getPinGloballyButton()}
-      {this.getPinLocallyButton()}
-      {this.getUnpinButton()}
-      {this.getMoveButton()}
-      {this.getMergeButton()}
-      {this.getApproveButton()}
-      {this.getOpenButton()}
-      {this.getCloseButton()}
-      {this.getUnhideButton()}
-      {this.getHideButton()}
-      {this.getDeleteButton()}
-    </ul>;
-    /* jshint ignore:end */
+    return (
+      <ul className={this.props.className}>
+        {this.getPinGloballyButton()}
+        {this.getPinLocallyButton()}
+        {this.getUnpinButton()}
+        {this.getMoveButton()}
+        {this.getMergeButton()}
+        {this.getApproveButton()}
+        {this.getOpenButton()}
+        {this.getCloseButton()}
+        {this.getUnhideButton()}
+        {this.getHideButton()}
+        {this.getDeleteButton()}
+      </ul>
+    )
   }
-}
+}

+ 6 - 14
frontend/src/components/threads/moderation/errors-list.js

@@ -1,8 +1,7 @@
-import React from 'react';
+import React from "react"
 
 export default class extends React.Component {
   render() {
-    /* jshint ignore:start */
     return (
       <div className="modal-dialog" role="document">
         <div className="modal-content">
@@ -18,42 +17,35 @@ export default class extends React.Component {
             <h4 className="modal-title">{gettext("Threads moderation")}</h4>
           </div>
           <div className="modal-body">
-
             <p className="lead">
               {gettext("One or more threads could not be deleted:")}
             </p>
 
             <ul className="list-unstyled list-errored-items">
-              {this.props.errors.map((item) => {
+              {this.props.errors.map(item => {
                 return (
                   <ThreadErrors
                     errors={item.errors}
                     key={item.thread.id}
                     thread={item.thread}
                   />
-                );
+                )
               })}
             </ul>
-
           </div>
         </div>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 }
 
-/* jshint ignore:start */
 export function ThreadErrors({ errors, thread }) {
   return (
     <li>
       <h5>{thread.title}</h5>
       {errors.map((message, i) => {
-        return (
-          <p>{message}</p>
-        );
+        return <p>{message}</p>
       })}
     </li>
-  );
+  )
 }
-/* jshint ignore:end */

+ 229 - 212
frontend/src/components/threads/moderation/merge.js

@@ -1,366 +1,383 @@
-import React from 'react'; // jshint ignore:line
-import Button from 'misago/components/button'; // jshint ignore:line
-import Form from 'misago/components/form';
-import FormGroup from 'misago/components/form-group'; // jshint ignore:line
-import CategorySelect from 'misago/components/category-select'; // jshint ignore:line
-import Select from 'misago/components/select'; // jshint ignore:line
-import misago from 'misago/index';
-import { filterThreads } from 'misago/reducers/threads'; // jshint ignore:line
-import * as select from 'misago/reducers/selection'; // jshint ignore:line
-import ErrorsModal from 'misago/components/threads/moderation/errors-list'; // jshint ignore:line
-import MergeConflict from 'misago/components/merge-conflict'; // jshint ignore:line
-import ajax from 'misago/services/ajax'; // jshint ignore:line
-import modal from 'misago/services/modal'; // jshint ignore:line
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store'; // jshint ignore:line
-import * as validators from 'misago/utils/validators';
+import React from "react"
+import Button from "misago/components/button"
+import Form from "misago/components/form"
+import FormGroup from "misago/components/form-group"
+import CategorySelect from "misago/components/category-select"
+import Select from "misago/components/select"
+import misago from "misago/index"
+import { filterThreads } from "misago/reducers/threads"
+import * as select from "misago/reducers/selection"
+import ErrorsModal from "misago/components/threads/moderation/errors-list"
+import MergeConflict from "misago/components/merge-conflict"
+import ajax from "misago/services/ajax"
+import modal from "misago/services/modal"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
+import * as validators from "misago/utils/validators"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isLoading: false,
 
-      title: '',
+      title: "",
       category: null,
       weight: 0,
       is_hidden: 0,
       is_closed: false,
 
       validators: {
-        title: [
-          validators.required()
-        ]
+        title: [validators.required()]
       },
 
       errors: {}
-    };
+    }
 
-    this.acl = {};
+    this.acl = {}
     for (const i in props.user.acl.categories) {
       if (!props.user.acl.categories.hasOwnProperty(i)) {
-        continue;
+        continue
       }
 
-      const acl = props.user.acl.categories[i];
-      this.acl[acl.id] = acl;
+      const acl = props.user.acl.categories[i]
+      this.acl[acl.id] = acl
     }
 
-    this.categoryChoices = [];
-    props.categories.forEach((category) => {
+    this.categoryChoices = []
+    props.categories.forEach(category => {
       if (category.level > 0) {
-        const acl = this.acl[category.id];
-        const disabled = !acl.can_start_threads || (category.is_closed && !acl.can_close_threads);
+        const acl = this.acl[category.id]
+        const disabled =
+          !acl.can_start_threads ||
+          (category.is_closed && !acl.can_close_threads)
 
         this.categoryChoices.push({
           value: category.id,
           disabled: disabled,
           level: category.level - 1,
           label: category.name
-        });
+        })
 
         if (!disabled && !this.state.category) {
-          this.state.category = category.id;
+          this.state.category = category.id
         }
       }
-    });
+    })
 
     this.isHiddenChoices = [
       {
-        'value': 0,
-        'icon': 'visibility',
-        'label': gettext("No")
+        value: 0,
+        icon: "visibility",
+        label: gettext("No")
       },
       {
-        'value': 1,
-        'icon': 'visibility_off',
-        'label': gettext("Yes")
-      },
-    ];
+        value: 1,
+        icon: "visibility_off",
+        label: gettext("Yes")
+      }
+    ]
 
     this.isClosedChoices = [
       {
-        'value': false,
-        'icon': 'lock_outline',
-        'label': gettext("No")
+        value: false,
+        icon: "lock_outline",
+        label: gettext("No")
       },
       {
-        'value': true,
-        'icon': 'lock',
-        'label': gettext("Yes")
-      },
-    ];
+        value: true,
+        icon: "lock",
+        label: gettext("Yes")
+      }
+    ]
   }
 
   clean() {
     if (this.isValid()) {
-      return true;
+      return true
     } else {
-      snackbar.error(gettext("Form contains errors."));
+      snackbar.error(gettext("Form contains errors."))
       this.setState({
         errors: this.validate()
-      });
-      return false;
+      })
+      return false
     }
   }
 
   send() {
-    return ajax.post(misago.get('MERGE_THREADS_API'), this.getFormdata());
+    return ajax.post(misago.get("MERGE_THREADS_API"), this.getFormdata())
   }
 
-  /* jshint ignore:start */
   getFormdata = () => {
     return {
-      threads: this.props.threads.map((thread) => thread.id),
+      threads: this.props.threads.map(thread => thread.id),
       title: this.state.title,
       category: this.state.category,
       weight: this.state.weight,
       is_hidden: this.state.is_hidden,
       is_closed: this.state.is_closed
-    };
-  };
+    }
+  }
 
-  handleSuccess = (apiResponse) => {
+  handleSuccess = apiResponse => {
     // unfreeze and remove merged threads
-    this.props.threads.forEach((thread) => {
-      this.props.freezeThread(thread.id);
-      this.props.deleteThread(thread);
-    });
+    this.props.threads.forEach(thread => {
+      this.props.freezeThread(thread.id)
+      this.props.deleteThread(thread)
+    })
 
     // deselect all threads
-    store.dispatch(select.none());
+    store.dispatch(select.none())
 
     // append merged thread, filter threads
-    this.props.addThreads([apiResponse]);
-    store.dispatch(filterThreads(this.props.route.category, this.props.categoriesMap));
+    this.props.addThreads([apiResponse])
+    store.dispatch(
+      filterThreads(this.props.route.category, this.props.categoriesMap)
+    )
 
     // hide modal
-    modal.hide();
-  };
+    modal.hide()
+  }
 
-  handleError = (rejection) => {
+  handleError = rejection => {
     if (rejection.status === 400) {
       if (rejection.best_answers || rejection.polls) {
         modal.show(
           <MergeConflict
-            api={misago.get('MERGE_THREADS_API')}
+            api={misago.get("MERGE_THREADS_API")}
             bestAnswers={rejection.best_answers}
             data={this.getFormdata()}
             polls={rejection.polls}
             onError={this.handleError}
             onSuccess={this.handleSuccess}
           />
-        );
+        )
       } else {
         this.setState({
-          'errors': Object.assign({}, this.state.errors, rejection)
-        });
-        snackbar.error(gettext("Form contains errors."));
+          errors: Object.assign({}, this.state.errors, rejection)
+        })
+        snackbar.error(gettext("Form contains errors."))
       }
     } else if (rejection.status === 403 && Array.isArray(rejection)) {
-      modal.show(<ErrorsModal errors={rejection} />);
+      modal.show(<ErrorsModal errors={rejection} />)
     } else if (rejection.best_answer) {
-      snackbar.error(rejection.best_answer[0]);
+      snackbar.error(rejection.best_answer[0])
     } else if (rejection.poll) {
-      snackbar.error(rejection.poll[0]);
+      snackbar.error(rejection.poll[0])
     } else {
-      snackbar.apiError(rejection);
+      snackbar.apiError(rejection)
     }
-  };
+  }
 
-  onCategoryChange = (ev) => {
-    const categoryId = ev.target.value;
+  onCategoryChange = ev => {
+    const categoryId = ev.target.value
     const newState = {
       category: categoryId
-    };
+    }
 
     if (this.acl[categoryId].can_pin_threads < newState.weight) {
-      newState.weight = 0;
+      newState.weight = 0
     }
 
     if (!this.acl[categoryId].can_hide_threads) {
-      newState.is_hidden = 0;
+      newState.is_hidden = 0
     }
 
     if (!this.acl[categoryId].can_close_threads) {
-      newState.is_closed = false;
+      newState.is_closed = false
     }
 
-    this.setState(newState);
-  };
-  /* jshint ignore:end */
+    this.setState(newState)
+  }
 
   getWeightChoices() {
     const choices = [
       {
-        'value': 0,
-        'icon': 'remove',
-        'label': gettext("Not pinned"),
+        value: 0,
+        icon: "remove",
+        label: gettext("Not pinned")
       },
       {
-        'value': 1,
-        'icon': 'bookmark_border',
-        'label': gettext("Pinned locally"),
+        value: 1,
+        icon: "bookmark_border",
+        label: gettext("Pinned locally")
       }
-    ];
+    ]
 
     if (this.acl[this.state.category].can_pin_threads == 2) {
       choices.push({
-        'value': 2,
-        'icon': 'bookmark',
-        'label': gettext("Pinned globally"),
-      });
+        value: 2,
+        icon: "bookmark",
+        label: gettext("Pinned globally")
+      })
     }
 
-    return choices;
+    return choices
   }
 
   renderWeightField() {
     if (this.acl[this.state.category].can_pin_threads) {
-      /* jshint ignore:start */
-      return <FormGroup label={gettext("Thread weight")}
-                        for="id_weight">
-        <Select id="id_weight"
-                onChange={this.bindInput('weight')}
-                value={this.state.weight}
-                choices={this.getWeightChoices()} />
-      </FormGroup>;
-      /* jshint ignore:end */
+      return (
+        <FormGroup label={gettext("Thread weight")} for="id_weight">
+          <Select
+            id="id_weight"
+            onChange={this.bindInput("weight")}
+            value={this.state.weight}
+            choices={this.getWeightChoices()}
+          />
+        </FormGroup>
+      )
     } else {
-      return null;
+      return null
     }
   }
 
   renderHiddenField() {
     if (this.acl[this.state.category].can_hide_threads) {
-      /* jshint ignore:start */
-      return <FormGroup label={gettext("Hide thread")}
-                        for="id_is_hidden">
-        <Select id="id_is_closed"
-                onChange={this.bindInput('is_hidden')}
-                value={this.state.is_hidden}
-                choices={this.isHiddenChoices} />
-      </FormGroup>;
-      /* jshint ignore:end */
+      return (
+        <FormGroup label={gettext("Hide thread")} for="id_is_hidden">
+          <Select
+            id="id_is_closed"
+            onChange={this.bindInput("is_hidden")}
+            value={this.state.is_hidden}
+            choices={this.isHiddenChoices}
+          />
+        </FormGroup>
+      )
     } else {
-      return null;
+      return null
     }
   }
 
   renderClosedField() {
     if (this.acl[this.state.category].can_close_threads) {
-      /* jshint ignore:start */
-      return <FormGroup label={gettext("Close thread")}
-                        for="id_is_closed">
-        <Select id="id_is_closed"
-                onChange={this.bindInput('is_closed')}
-                value={this.state.is_closed}
-                choices={this.isClosedChoices} />
-      </FormGroup>;
-      /* jshint ignore:end */
+      return (
+        <FormGroup label={gettext("Close thread")} for="id_is_closed">
+          <Select
+            id="id_is_closed"
+            onChange={this.bindInput("is_closed")}
+            value={this.state.is_closed}
+            choices={this.isClosedChoices}
+          />
+        </FormGroup>
+      )
     } else {
-      return null;
+      return null
     }
   }
 
   renderForm() {
-    /* jshint ignore:start */
-    return <form onSubmit={this.handleSubmit}>
-      <div className="modal-body">
-
-        <FormGroup label={gettext("Thread title")}
-                   for="id_title"
-                   validation={this.state.errors.title}>
-          <input id="id_title"
-                 className="form-control"
-                 type="text"
-                 onChange={this.bindInput('title')}
-                 value={this.state.title} />
-        </FormGroup>
-        <div className="clearfix"></div>
-
-        <FormGroup label={gettext("Category")}
-                   for="id_category"
-                   validation={this.state.errors.category}>
-          <CategorySelect id="id_category"
-                          onChange={this.onCategoryChange}
-                          value={this.state.category}
-                          choices={this.categoryChoices} />
-        </FormGroup>
-        <div className="clearfix"></div>
-
-        {this.renderWeightField()}
-        {this.renderHiddenField()}
-        {this.renderClosedField()}
-
-      </div>
-      <div className="modal-footer">
-        <button
-          className="btn btn-default"
-          data-dismiss="modal"
-          disabled={this.state.isLoading}
-          type="button"
-        >
-          {gettext("Cancel")}
-        </button>
-        <Button className="btn-primary" loading={this.state.isLoading}>
-          {gettext("Merge threads")}
-        </Button>
-      </div>
-    </form>;
-    /* jshint ignore:end */
+    return (
+      <form onSubmit={this.handleSubmit}>
+        <div className="modal-body">
+          <FormGroup
+            label={gettext("Thread title")}
+            for="id_title"
+            validation={this.state.errors.title}
+          >
+            <input
+              id="id_title"
+              className="form-control"
+              type="text"
+              onChange={this.bindInput("title")}
+              value={this.state.title}
+            />
+          </FormGroup>
+          <div className="clearfix" />
+
+          <FormGroup
+            label={gettext("Category")}
+            for="id_category"
+            validation={this.state.errors.category}
+          >
+            <CategorySelect
+              id="id_category"
+              onChange={this.onCategoryChange}
+              value={this.state.category}
+              choices={this.categoryChoices}
+            />
+          </FormGroup>
+          <div className="clearfix" />
+
+          {this.renderWeightField()}
+          {this.renderHiddenField()}
+          {this.renderClosedField()}
+        </div>
+        <div className="modal-footer">
+          <button
+            className="btn btn-default"
+            data-dismiss="modal"
+            disabled={this.state.isLoading}
+            type="button"
+          >
+            {gettext("Cancel")}
+          </button>
+          <Button className="btn-primary" loading={this.state.isLoading}>
+            {gettext("Merge threads")}
+          </Button>
+        </div>
+      </form>
+    )
   }
 
   renderCantMergeMessage() {
-    /* jshint ignore:start */
-    return <div className="modal-body">
-      <div className="message-icon">
-        <span className="material-icon">
-          info_outline
-        </span>
-      </div>
-      <div className="message-body">
-        <p className="lead">
-          {gettext("You can't move threads because there are no categories you are allowed to move them to.")}
-        </p>
-        <p>
-          {gettext("You need permission to start threads in category to be able to merge threads to it.")}
-        </p>
-        <button
-          className="btn btn-default"
-          data-dismiss="modal"
-          type="button"
-        >
-          {gettext("Ok")}
-        </button>
+    return (
+      <div className="modal-body">
+        <div className="message-icon">
+          <span className="material-icon">info_outline</span>
+        </div>
+        <div className="message-body">
+          <p className="lead">
+            {gettext(
+              "You can't move threads because there are no categories you are allowed to move them to."
+            )}
+          </p>
+          <p>
+            {gettext(
+              "You need permission to start threads in category to be able to merge threads to it."
+            )}
+          </p>
+          <button
+            className="btn btn-default"
+            data-dismiss="modal"
+            type="button"
+          >
+            {gettext("Ok")}
+          </button>
+        </div>
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
 
   getClassName() {
     if (!this.state.category) {
-      return 'modal-dialog modal-message';
+      return "modal-dialog modal-message"
     } else {
-      return 'modal-dialog';
+      return "modal-dialog"
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className={this.getClassName()} role="document">
-      <div className="modal-content">
-        <div className="modal-header">
-          <button type="button" className="close" data-dismiss="modal"
-                  aria-label={gettext("Close")}>
-            <span aria-hidden="true">&times;</span>
-          </button>
-          <h4 className="modal-title">{gettext("Merge threads")}</h4>
+    return (
+      <div className={this.getClassName()} role="document">
+        <div className="modal-content">
+          <div className="modal-header">
+            <button
+              type="button"
+              className="close"
+              data-dismiss="modal"
+              aria-label={gettext("Close")}
+            >
+              <span aria-hidden="true">&times;</span>
+            </button>
+            <h4 className="modal-title">{gettext("Merge threads")}</h4>
+          </div>
+          {this.state.category
+            ? this.renderForm()
+            : this.renderCantMergeMessage()}
         </div>
-        {this.state.category ? this.renderForm() : this.renderCantMergeMessage()}
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
 }

+ 120 - 106
frontend/src/components/threads/moderation/move.js

@@ -1,157 +1,171 @@
-import React from 'react'; // jshint ignore:line
-import Form from 'misago/components/form';
-import FormGroup from 'misago/components/form-group'; // jshint ignore:line
-import CategorySelect from 'misago/components/category-select'; // jshint ignore:line
-import * as select from 'misago/reducers/selection'; // jshint ignore:line
-import { filterThreads } from 'misago/reducers/threads'; // jshint ignore:line
-import modal from 'misago/services/modal'; // jshint ignore:line
-import store from 'misago/services/store'; // jshint ignore:line
+import React from "react"
+import Form from "misago/components/form"
+import FormGroup from "misago/components/form-group"
+import CategorySelect from "misago/components/category-select"
+import * as select from "misago/reducers/selection"
+import { filterThreads } from "misago/reducers/threads"
+import modal from "misago/services/modal"
+import store from "misago/services/store"
 
 export default class extends Form {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
-      category: null,
-    };
+      category: null
+    }
 
-    const acls = {};
+    const acls = {}
     for (const i in props.user.acl.categories) {
       if (!props.user.acl.categories.hasOwnProperty(i)) {
-        continue;
+        continue
       }
 
-      const acl = props.user.acl.categories[i];
-      acls[acl.id] = acl;
+      const acl = props.user.acl.categories[i]
+      acls[acl.id] = acl
     }
 
-    this.categoryChoices = [];
-    props.categories.forEach((category) => {
+    this.categoryChoices = []
+    props.categories.forEach(category => {
       if (category.level > 0) {
-        const acl = acls[category.id];
-        const disabled = !acl.can_start_threads || (category.is_closed && !acl.can_close_threads);
+        const acl = acls[category.id]
+        const disabled =
+          !acl.can_start_threads ||
+          (category.is_closed && !acl.can_close_threads)
 
         this.categoryChoices.push({
           value: category.id,
           disabled: disabled,
           level: category.level - 1,
           label: category.name
-        });
+        })
 
         if (!disabled && !this.state.category) {
-          this.state.category = category.id;
+          this.state.category = category.id
         }
       }
-    });
+    })
   }
 
-  /* jshint ignore:start */
-  handleSubmit = (event) => {
+  handleSubmit = event => {
     // we don't reload page on submissions
-    event.preventDefault();
+    event.preventDefault()
 
-    modal.hide();
+    modal.hide()
 
     const onSuccess = () => {
       store.dispatch(
-        filterThreads(this.props.route.category, this.props.categoriesMap));
+        filterThreads(this.props.route.category, this.props.categoriesMap)
+      )
 
       // deselect threads moved outside of visible scope
-      const storeState = store.getState();
-      const leftThreads = storeState.threads.map((thread) => (thread.id));
-      store.dispatch(select.all(storeState.selection.filter((thread) => {
-        return leftThreads.indexOf(thread) !== -1;
-      })));
-    };
-
-    this.props.callApi([
-      {op: 'replace', path: 'category', value: this.state.category},
-      {op: 'replace', path: 'flatten-categories', value: null},
-      {op: 'add', path: 'acl', value: true}
-    ], gettext("Selected threads were moved."), onSuccess);
-  };
-  /* jshint ignore:end */
+      const storeState = store.getState()
+      const leftThreads = storeState.threads.map(thread => thread.id)
+      store.dispatch(
+        select.all(
+          storeState.selection.filter(thread => {
+            return leftThreads.indexOf(thread) !== -1
+          })
+        )
+      )
+    }
+
+    this.props.callApi(
+      [
+        { op: "replace", path: "category", value: this.state.category },
+        { op: "replace", path: "flatten-categories", value: null },
+        { op: "add", path: "acl", value: true }
+      ],
+      gettext("Selected threads were moved."),
+      onSuccess
+    )
+  }
 
   getClassName() {
     if (!this.state.category) {
-      return 'modal-dialog modal-message';
+      return "modal-dialog modal-message"
     } else {
-      return 'modal-dialog';
+      return "modal-dialog"
     }
   }
 
   renderForm() {
-    /* jshint ignore:start */
-    return <form onSubmit={this.handleSubmit}>
-      <div className="modal-body">
-
-        <FormGroup label={gettext("New category")}
-                   for="id_new_category">
-          <CategorySelect id="id_new_category"
-                          onChange={this.bindInput('category')}
-                          value={this.state.category}
-                          choices={this.categoryChoices} />
-        </FormGroup>
-
-      </div>
-      <div className="modal-footer">
-        <button
-          className="btn btn-default"
-          data-dismiss="modal"
-          disabled={this.state.isLoading}
-          type="button"
-        >
-          {gettext("Cancel")}
-        </button>
-        <button className="btn btn-primary">
-          {gettext("Move threads")}
-        </button>
-      </div>
-    </form>;
-    /* jshint ignore:end */
+    return (
+      <form onSubmit={this.handleSubmit}>
+        <div className="modal-body">
+          <FormGroup label={gettext("New category")} for="id_new_category">
+            <CategorySelect
+              id="id_new_category"
+              onChange={this.bindInput("category")}
+              value={this.state.category}
+              choices={this.categoryChoices}
+            />
+          </FormGroup>
+        </div>
+        <div className="modal-footer">
+          <button
+            className="btn btn-default"
+            data-dismiss="modal"
+            disabled={this.state.isLoading}
+            type="button"
+          >
+            {gettext("Cancel")}
+          </button>
+          <button className="btn btn-primary">{gettext("Move threads")}</button>
+        </div>
+      </form>
+    )
   }
 
   renderCantMoveMessage() {
-    /* jshint ignore:start */
-    return <div className="modal-body">
-      <div className="message-icon">
-        <span className="material-icon">
-          info_outline
-        </span>
-      </div>
-      <div className="message-body">
-        <p className="lead">
-          {gettext("You can't move threads because there are no categories you are allowed to move them to.")}
-        </p>
-        <p>
-          {gettext("You need permission to start threads in category to be able to move threads to it.")}
-        </p>
-        <button
-          className="btn btn-default"
-          data-dismiss="modal"
-          type="button"
-        >
-          {gettext("Ok")}
-        </button>
+    return (
+      <div className="modal-body">
+        <div className="message-icon">
+          <span className="material-icon">info_outline</span>
+        </div>
+        <div className="message-body">
+          <p className="lead">
+            {gettext(
+              "You can't move threads because there are no categories you are allowed to move them to."
+            )}
+          </p>
+          <p>
+            {gettext(
+              "You need permission to start threads in category to be able to move threads to it."
+            )}
+          </p>
+          <button
+            className="btn btn-default"
+            data-dismiss="modal"
+            type="button"
+          >
+            {gettext("Ok")}
+          </button>
+        </div>
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className={this.getClassName()} role="document">
-      <div className="modal-content">
-        <div className="modal-header">
-          <button type="button" className="close" data-dismiss="modal"
-                  aria-label={gettext("Close")}>
-            <span aria-hidden="true">&times;</span>
-          </button>
-          <h4 className="modal-title">{gettext("Move threads")}</h4>
+    return (
+      <div className={this.getClassName()} role="document">
+        <div className="modal-content">
+          <div className="modal-header">
+            <button
+              type="button"
+              className="close"
+              data-dismiss="modal"
+              aria-label={gettext("Close")}
+            >
+              <span aria-hidden="true">&times;</span>
+            </button>
+            <h4 className="modal-title">{gettext("Move threads")}</h4>
+          </div>
+          {this.state.category
+            ? this.renderForm()
+            : this.renderCantMoveMessage()}
         </div>
-        {this.state.category ? this.renderForm() : this.renderCantMoveMessage()}
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
 }

+ 15 - 15
frontend/src/components/threads/moderation/selection.js

@@ -1,22 +1,23 @@
-import React from 'react';
-import * as select from 'misago/reducers/selection'; // jshint ignore:line
-import store from 'misago/services/store'; // jshint ignore:line
+import React from "react"
+import * as select from "misago/reducers/selection"
+import store from "misago/services/store"
 
 export default class extends React.Component {
-  /* jshint ignore:start */
   selectAll = () => {
-    store.dispatch(select.all(this.props.threads.map(function(thread) {
-      return thread.id;
-    })));
-  };
+    store.dispatch(
+      select.all(
+        this.props.threads.map(function(thread) {
+          return thread.id
+        })
+      )
+    )
+  }
 
   selectNone = () => {
-    store.dispatch(select.none());
-  };
-  /* jshint ignore:end */
+    store.dispatch(select.none())
+  }
 
   render() {
-    /* jshint ignore:start */
     return (
       <ul className={this.props.className}>
         <li>
@@ -40,7 +41,6 @@ export default class extends React.Component {
           </button>
         </li>
       </ul>
-    );
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 10 - 13
frontend/src/components/threads/nav.js

@@ -1,30 +1,27 @@
-// jshint ignore:start
-import React from 'react';
-import { Link } from 'react-router';
-import Li from 'misago/components/li';
+import React from "react"
+import { Link } from "react-router"
+import Li from "misago/components/li"
 
-export default function({baseUrl, list, lists}) {
-  if (lists.length < 2) return null;
+export default function({ baseUrl, list, lists }) {
+  if (lists.length < 2) return null
 
   return (
     <div className="page-tabs">
       <div className="container">
         <ul className="nav nav-pills">
-          {lists.map((item) => {
+          {lists.map(item => {
             return (
               <Li
                 isControlled={true}
                 isActive={item.path === list.path}
                 key={baseUrl + item.path}
               >
-                <Link to={baseUrl + item.path}>
-                  {item.name}
-                </Link>
+                <Link to={baseUrl + item.path}>{item.name}</Link>
               </Li>
-            );
+            )
           })}
         </ul>
       </div>
     </div>
-  );
-}
+  )
+}

+ 40 - 40
frontend/src/components/threads/root.js

@@ -1,90 +1,90 @@
-import { connect } from 'react-redux';
-import Route from 'misago/components/threads/route';
-import misago from 'misago/index';
+import { connect } from "react-redux"
+import Route from "misago/components/threads/route"
+import misago from "misago/index"
 
 export function getSelect(options) {
   return function(store) {
     return {
-      'options': options,
-      'selection': store.selection,
-      'threads': store.threads,
-      'tick': store.tick.tick,
-      'user': store.auth.user
-    };
-  };
+      options: options,
+      selection: store.selection,
+      threads: store.threads,
+      tick: store.tick.tick,
+      user: store.auth.user
+    }
+  }
 }
 
 export function getLists(user) {
   let lists = [
     {
-      type: 'all',
-      path: '',
+      type: "all",
+      path: "",
       name: gettext("All"),
       longName: gettext("All threads")
     }
-  ];
+  ]
 
   if (user.id) {
     lists.push({
-      type: 'my',
-      path: 'my/',
+      type: "my",
+      path: "my/",
       name: gettext("My"),
       longName: gettext("My threads")
-    });
+    })
     lists.push({
-      type: 'new',
-      path: 'new/',
+      type: "new",
+      path: "new/",
       name: gettext("New"),
       longName: gettext("New threads")
-    });
+    })
     lists.push({
-      type: 'unread',
-      path: 'unread/',
+      type: "unread",
+      path: "unread/",
       name: gettext("Unread"),
       longName: gettext("Unread threads")
-    });
+    })
     lists.push({
-      type: 'subscribed',
-      path: 'subscribed/',
+      type: "subscribed",
+      path: "subscribed/",
       name: gettext("Subscribed"),
       longName: gettext("Subscribed threads")
-    });
+    })
 
     if (user.acl.can_see_unapproved_content_lists) {
       lists.push({
-        type: 'unapproved',
-        path: 'unapproved/',
+        type: "unapproved",
+        path: "unapproved/",
         name: gettext("Unapproved"),
         longName: gettext("Unapproved content")
-      });
+      })
     }
   }
 
-  return lists;
+  return lists
 }
 
 export function paths(user, mode) {
-  let lists = getLists(user);
-  let routes = [];
-  let categoriesMap = {};
+  let lists = getLists(user)
+  let routes = []
+  let categoriesMap = {}
 
-  misago.get('CATEGORIES').forEach(function(category) {
+  misago.get("CATEGORIES").forEach(function(category) {
     lists.forEach(function(list) {
-      categoriesMap[category.id] = category;
+      categoriesMap[category.id] = category
 
       routes.push({
         path: category.url.index + list.path,
         component: connect(getSelect(mode))(Route),
 
-        categories: misago.get('CATEGORIES'),
+        categories: misago.get("CATEGORIES"),
         categoriesMap,
         category,
 
         lists,
         list
-      });
-    });
-  });
+      })
+    })
+  })
 
-  return routes;
-}
+  return routes
+}

+ 138 - 138
frontend/src/components/threads/route.js

@@ -1,25 +1,33 @@
-import React from 'react'; // jshint ignore:line
-import Button from 'misago/components/button'; // jshint ignore:line
-import { compareGlobalWeight, compareWeight } from 'misago/components/threads/compare'; // jshint ignore:line
-import Container from 'misago/components/threads/container'; // jshint ignore:line
-import Header from 'misago/components/threads/header'; // jshint ignore:line
-import { diffThreads, getModerationActions, getPageTitle, getTitle } from 'misago/components/threads/utils'; // jshint ignore:line
-import ThreadsList from 'misago/components/threads-list'; // jshint ignore:line
-import ThreadsListEmpty from 'misago/components/threads/list-empty'; // jshint ignore:line
-import WithDropdown from 'misago/components/with-dropdown'; // jshint ignore:line
-import misago from 'misago/index';
-import * as select from 'misago/reducers/selection'; // jshint ignore:line
-import { append, deleteThread, hydrate, patch } from 'misago/reducers/threads'; // jshint ignore:line
-import ajax from 'misago/services/ajax';
-import polls from 'misago/services/polls';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
-import title from 'misago/services/page-title';
-import * as sets from 'misago/utils/sets'; // jshint ignore:line
+import React from "react"
+import Button from "misago/components/button"
+import {
+  compareGlobalWeight,
+  compareWeight
+} from "misago/components/threads/compare"
+import Container from "misago/components/threads/container"
+import Header from "misago/components/threads/header"
+import {
+  diffThreads,
+  getModerationActions,
+  getPageTitle,
+  getTitle
+} from "misago/components/threads/utils"
+import ThreadsList from "misago/components/threads-list"
+import ThreadsListEmpty from "misago/components/threads/list-empty"
+import WithDropdown from "misago/components/with-dropdown"
+import misago from "misago/index"
+import * as select from "misago/reducers/selection"
+import { append, deleteThread, hydrate, patch } from "misago/reducers/threads"
+import ajax from "misago/services/ajax"
+import polls from "misago/services/polls"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
+import title from "misago/services/page-title"
+import * as sets from "misago/utils/sets"
 
 export default class extends WithDropdown {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       isMounted: true,
@@ -42,22 +50,22 @@ export default class extends WithDropdown {
 
       page: 1,
       pages: 1
-    };
+    }
 
-    let category = this.getCategory();
+    let category = this.getCategory()
 
-    if (misago.has('THREADS')) {
-      this.initWithPreloadedData(category, misago.get('THREADS'));
+    if (misago.has("THREADS")) {
+      this.initWithPreloadedData(category, misago.get("THREADS"))
     } else {
-      this.initWithoutPreloadedData(category);
+      this.initWithoutPreloadedData(category)
     }
   }
 
   getCategory() {
     if (!this.props.route.category.special_role) {
-      return this.props.route.category.id;
+      return this.props.route.category.id
     } else {
-      return null;
+      return null
     }
   }
 
@@ -72,56 +80,65 @@ export default class extends WithDropdown {
 
       page: data.page,
       pages: data.pages
-    });
+    })
 
-    this.startPolling(category);
+    this.startPolling(category)
   }
 
   initWithoutPreloadedData(category) {
-    this.loadThreads(category);
+    this.loadThreads(category)
   }
 
-  loadThreads(category, page=1) {
-    ajax.get(this.props.options.api, {
-      category: category,
-      list: this.props.route.list.type,
-      page: page || 1
-    }, 'threads').then((data) => {
-      if (!this.state.isMounted) {
-        // user changed route before loading completion
-        return;
-      }
-
-      if (page === 1) {
-        store.dispatch(hydrate(data.results));
-      } else {
-        store.dispatch(append(data.results, this.getSorting()));
-      }
-
-      this.setState({
-        isLoaded: true,
-        isBusy: false,
-
-        moderation: getModerationActions(store.getState().threads),
-
-        subcategories: data.subcategories,
-
-        count: data.count,
-        more: data.more,
-
-        page: data.page,
-        pages: data.pages
-      });
-
-      this.startPolling(category);
-    }, (rejection) => {
-      snackbar.apiError(rejection);
-    });
+  loadThreads(category, page = 1) {
+    ajax
+      .get(
+        this.props.options.api,
+        {
+          category: category,
+          list: this.props.route.list.type,
+          page: page || 1
+        },
+        "threads"
+      )
+      .then(
+        data => {
+          if (!this.state.isMounted) {
+            // user changed route before loading completion
+            return
+          }
+
+          if (page === 1) {
+            store.dispatch(hydrate(data.results))
+          } else {
+            store.dispatch(append(data.results, this.getSorting()))
+          }
+
+          this.setState({
+            isLoaded: true,
+            isBusy: false,
+
+            moderation: getModerationActions(store.getState().threads),
+
+            subcategories: data.subcategories,
+
+            count: data.count,
+            more: data.more,
+
+            page: data.page,
+            pages: data.pages
+          })
+
+          this.startPolling(category)
+        },
+        rejection => {
+          snackbar.apiError(rejection)
+        }
+      )
   }
 
   startPolling(category) {
     polls.start({
-      poll: 'threads',
+      poll: "threads",
       url: this.props.options.api,
       data: {
         category: category,
@@ -129,118 +146,116 @@ export default class extends WithDropdown {
       },
       frequency: 120 * 1000,
       update: this.pollResponse
-    });
+    })
   }
 
   componentDidMount() {
-    this.setPageTitle();
+    this.setPageTitle()
 
-    if (misago.has('THREADS')) {
+    if (misago.has("THREADS")) {
       // unlike in other components, routes are root components for threads
       // so we can't dispatch store action from constructor
-      store.dispatch(hydrate(misago.pop('THREADS').results));
+      store.dispatch(hydrate(misago.pop("THREADS").results))
 
       this.setState({
         isLoaded: true
-      });
+      })
     }
 
-    store.dispatch(select.none());
+    store.dispatch(select.none())
   }
 
   componentWillUnmount() {
-    this.state.isMounted = false;
-    polls.stop('threads');
+    this.state.isMounted = false
+    polls.stop("threads")
   }
 
   getTitle() {
     if (this.props.options.title) {
-      return this.props.options.title;
+      return this.props.options.title
     }
 
-    return getTitle(this.props.route);
+    return getTitle(this.props.route)
   }
 
   setPageTitle() {
-    if (this.props.route.category.level || !misago.get('THREADS_ON_INDEX')) {
-      title.set(getPageTitle(this.props.route));
+    if (this.props.route.category.level || !misago.get("THREADS_ON_INDEX")) {
+      title.set(getPageTitle(this.props.route))
     } else if (this.props.options.title) {
-        title.set(this.props.options.title);
+      title.set(this.props.options.title)
     } else {
-      if (misago.get('SETTINGS').forum_index_title) {
-        document.title = misago.get('SETTINGS').forum_index_title;
+      if (misago.get("SETTINGS").forum_index_title) {
+        document.title = misago.get("SETTINGS").forum_index_title
       } else {
-        document.title = misago.get('SETTINGS').forum_name;
+        document.title = misago.get("SETTINGS").forum_name
       }
     }
   }
 
   getSorting() {
     if (this.props.route.category.level) {
-      return compareWeight;
+      return compareWeight
     } else {
-      return compareGlobalWeight;
+      return compareGlobalWeight
     }
   }
 
-  /* jshint ignore:start */
-
   // AJAX
 
   loadMore = () => {
     this.setState({
       isBusy: true
-    });
+    })
 
-    this.loadThreads(this.getCategory(), this.state.page + 1);
-  };
+    this.loadThreads(this.getCategory(), this.state.page + 1)
+  }
 
-  pollResponse = (data) => {
+  pollResponse = data => {
     this.setState({
       diff: Object.assign({}, data, {
         results: diffThreads(this.props.threads, data.results)
       })
-    });
-  };
+    })
+  }
 
-  addThreads = (threads) => {
-    store.dispatch(append(threads, this.getSorting()));
-  };
+  addThreads = threads => {
+    store.dispatch(append(threads, this.getSorting()))
+  }
 
   applyDiff = () => {
-    this.addThreads(this.state.diff.results);
+    this.addThreads(this.state.diff.results)
 
-    this.setState(Object.assign({}, this.state.diff, {
-      moderation: getModerationActions(store.getState().threads),
+    this.setState(
+      Object.assign({}, this.state.diff, {
+        moderation: getModerationActions(store.getState().threads),
 
-      diff: {
-        results: []
-      }
-    }));
-  };
+        diff: {
+          results: []
+        }
+      })
+    )
+  }
 
   // Thread state utils
 
-  freezeThread = (thread) => {
+  freezeThread = thread => {
     this.setState(function(currentState) {
       return {
         busyThreads: sets.toggle(currentState.busyThreads, thread)
-      };
-    });
-  };
+      }
+    })
+  }
 
-  updateThread = (thread) => {
-    store.dispatch(patch(thread, thread, this.getSorting()));
-  };
+  updateThread = thread => {
+    store.dispatch(patch(thread, thread, this.getSorting()))
+  }
 
-  deleteThread = (thread) => {
-    store.dispatch(deleteThread(thread));
-  };
-  /* jshint ignore:end */
+  deleteThread = thread => {
+    store.dispatch(deleteThread(thread))
+  }
 
   getMoreButton() {
     if (this.state.more) {
-      /* jshint ignore:start */
       return (
         <div className="pager-more">
           <Button
@@ -251,24 +266,22 @@ export default class extends WithDropdown {
             {gettext("Show more")}
           </Button>
         </div>
-      );
-      /* jshint ignore:end */
+      )
     } else {
-      return null;
+      return null
     }
   }
 
   getClassName() {
-    let className = 'page page-threads';
-    className += ' page-threads-' + this.props.route.list.type;
+    let className = "page page-threads"
+    className += " page-threads-" + this.props.route.list.type
     if (this.props.route.category.css_class) {
-      className += ' page-threads-' + this.props.route.category.css_class;
+      className += " page-threads-" + this.props.route.category.css_class
     }
-    return className;
+    return className
   }
 
   render() {
-    /* jshint ignore:start */
     return (
       <div className={this.getClassName()}>
         <Header
@@ -284,41 +297,31 @@ export default class extends WithDropdown {
 
         <Container
           api={this.props.options.api}
-
           route={this.props.route}
           subcategories={this.state.subcategories}
           user={this.props.user}
-
           pageLead={this.props.options.pageLead}
-
           threads={this.props.threads}
           threadsCount={this.state.count}
-
           moderation={this.state.moderation}
           selection={this.props.selection}
-
           busyThreads={this.state.busyThreads}
           addThreads={this.addThreads}
           freezeThread={this.freezeThread}
           deleteThread={this.deleteThread}
           updateThread={this.updateThread}
-
           isLoaded={this.state.isLoaded}
           isBusy={this.state.isBusy}
         >
-
           <ThreadsList
             category={this.props.route.category}
             categories={this.props.route.categoriesMap}
             list={this.props.route.list}
             selection={this.props.selection}
             threads={this.props.threads}
-
             diffSize={this.state.diff.results.length}
             applyDiff={this.applyDiff}
-
             showOptions={!!this.props.user.id}
-
             isLoaded={this.state.isLoaded}
             busyThreads={this.state.busyThreads}
           >
@@ -330,11 +333,8 @@ export default class extends WithDropdown {
           </ThreadsList>
 
           {this.getMoreButton()}
-
         </Container>
-
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 18 - 30
frontend/src/components/threads/toolbar.js

@@ -1,37 +1,34 @@
-import React from 'react'; // jshint ignore:line
-import CategoryPicker from 'misago/components/threads/category-picker'; // jshint ignore:line
-import ModerationControls from 'misago/components/threads/moderation/controls'; // jshint ignore:line
-import SelectionControls from 'misago/components/threads/moderation/selection'; // jshint ignore:line
+import React from "react"
+import CategoryPicker from "misago/components/threads/category-picker"
+import ModerationControls from "misago/components/threads/moderation/controls"
+import SelectionControls from "misago/components/threads/moderation/selection"
 
 export default class extends React.Component {
   getCategoryPicker() {
-    if (!this.props.subcategories.length) return null;
+    if (!this.props.subcategories.length) return null
 
-    /* jshint ignore:start */
     return (
       <CategoryPicker
         categories={this.props.categoriesMap}
         choices={this.props.subcategories}
         list={this.props.list}
       />
-    );
-    /* jshint ignore:end */
+    )
   }
 
   showModerationOptions() {
-    return this.props.user.id && this.props.moderation.allow;
+    return this.props.user.id && this.props.moderation.allow
   }
 
   getSelectedThreads() {
-    return this.props.threads.filter((thread) => {
-      return this.props.selection.indexOf(thread.id) >= 0;
-    });
+    return this.props.threads.filter(thread => {
+      return this.props.selection.indexOf(thread.id) >= 0
+    })
   }
 
   getModerationButton() {
-    if (!this.showModerationOptions()) return null;
+    if (!this.showModerationOptions()) return null
 
-    /* jshint ignore:start */
     return (
       <div className="col-xs-6 col-sm-3 col-md-2">
         <div className="btn-group btn-group-justified">
@@ -44,9 +41,7 @@ export default class extends React.Component {
               aria-expanded="false"
               disabled={this.props.disabled || !this.props.selection.length}
             >
-              <span className="material-icon">
-                settings
-              </span>
+              <span className="material-icon">settings</span>
               {gettext("Options")}
             </button>
 
@@ -67,14 +62,12 @@ export default class extends React.Component {
           </div>
         </div>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   getSelectionButton() {
-    if (!this.showModerationOptions()) return null;
+    if (!this.showModerationOptions()) return null
 
-    /* jshint ignore:start */
     return (
       <div className="col-xs-3 col-sm-2 col-md-1">
         <div className="btn-group btn-group-justified">
@@ -87,9 +80,7 @@ export default class extends React.Component {
               aria-expanded="false"
               disabled={this.props.disabled}
             >
-              <span className="material-icon">
-                select_all
-              </span>
+              <span className="material-icon">select_all</span>
             </button>
 
             <SelectionControls
@@ -99,12 +90,10 @@ export default class extends React.Component {
           </div>
         </div>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   render() {
-    /* jshint ignore:start */
     return (
       <div className="row row-toolbar row-toolbar-bottom-margin">
         <div className="col-xs-3 col-sm-3 col-md-2 dropdown">
@@ -114,7 +103,6 @@ export default class extends React.Component {
         {this.getModerationButton()}
         {this.getSelectionButton()}
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 47 - 43
frontend/src/components/threads/utils.js

@@ -1,4 +1,4 @@
-import misago from 'misago/index';
+import misago from "misago/index"
 
 export function getPageTitle(route) {
   if (route.category.level) {
@@ -6,71 +6,73 @@ export function getPageTitle(route) {
       return {
         title: route.list.longName,
         parent: route.category.name
-      };
+      }
     } else {
       return {
         title: route.category.name
-      };
+      }
     }
-  } else if (misago.get('THREADS_ON_INDEX')) {
+  } else if (misago.get("THREADS_ON_INDEX")) {
     if (route.list.path) {
       return {
         title: route.list.longName
-      };
+      }
     } else {
-      return null;
+      return null
     }
   } else {
     if (route.list.path) {
       return {
         title: route.list.longName,
         parent: gettext("Threads")
-      };
+      }
     } else {
       return {
         title: gettext("Threads")
-      };
+      }
     }
   }
 }
 
 export function getTitle(route) {
   if (route.category.level) {
-    return route.category.name;
-  } else if (misago.get('THREADS_ON_INDEX')) {
-    if (misago.get('SETTINGS').forum_index_title) {
-      return misago.get('SETTINGS').forum_index_title;
+    return route.category.name
+  } else if (misago.get("THREADS_ON_INDEX")) {
+    if (misago.get("SETTINGS").forum_index_title) {
+      return misago.get("SETTINGS").forum_index_title
     } else {
-      return misago.get('SETTINGS').forum_name;
+      return misago.get("SETTINGS").forum_name
     }
   } else {
-    return gettext("Threads");
+    return gettext("Threads")
   }
 }
 
 export function isThreadChanged(current, fromDb) {
-  return [
-    current.title === fromDb.title,
-    current.weight === fromDb.weight,
-    current.category === fromDb.category,
-    current.last_post === fromDb.last_post,
-    current.last_poster_name === fromDb.last_poster_name
-  ].indexOf(false) >= 0;
+  return (
+    [
+      current.title === fromDb.title,
+      current.weight === fromDb.weight,
+      current.category === fromDb.category,
+      current.last_post === fromDb.last_post,
+      current.last_poster_name === fromDb.last_poster_name
+    ].indexOf(false) >= 0
+  )
 }
 
 export function diffThreads(current, fromDb) {
-  let currentMap = {};
+  let currentMap = {}
   current.forEach(function(thread) {
-    currentMap[thread.id] = thread;
-  });
+    currentMap[thread.id] = thread
+  })
 
   return fromDb.filter(function(thread) {
     if (currentMap[thread.id]) {
-      return isThreadChanged(currentMap[thread.id], thread);
+      return isThreadChanged(currentMap[thread.id], thread)
     } else {
-      return true;
+      return true
     }
-  });
+  })
 }
 
 export function getModerationActions(threads) {
@@ -86,46 +88,49 @@ export function getModerationActions(threads) {
     can_pin: 0,
     can_pin_globally: 0,
     can_unhide: 0
-  };
+  }
 
   threads.forEach(function(thread) {
-    if (thread.is_unapproved && thread.acl.can_approve > moderation.can_approve) {
-      moderation.can_approve = thread.acl.can_approve;
+    if (
+      thread.is_unapproved &&
+      thread.acl.can_approve > moderation.can_approve
+    ) {
+      moderation.can_approve = thread.acl.can_approve
     }
 
     if (thread.acl.can_close > moderation.can_close) {
-      moderation.can_close = thread.acl.can_close;
+      moderation.can_close = thread.acl.can_close
     }
 
     if (thread.acl.can_delete > moderation.can_delete) {
-      moderation.can_delete = thread.acl.can_delete;
+      moderation.can_delete = thread.acl.can_delete
     }
 
     if (thread.acl.can_hide > moderation.can_hide) {
-      moderation.can_hide = thread.acl.can_hide;
+      moderation.can_hide = thread.acl.can_hide
     }
 
     if (thread.acl.can_merge > moderation.can_merge) {
-      moderation.can_merge = thread.acl.can_merge;
+      moderation.can_merge = thread.acl.can_merge
     }
 
     if (thread.acl.can_move > moderation.can_move) {
-      moderation.can_move = thread.acl.can_move;
+      moderation.can_move = thread.acl.can_move
     }
 
     if (thread.acl.can_pin > moderation.can_pin) {
-      moderation.can_pin = thread.acl.can_pin;
+      moderation.can_pin = thread.acl.can_pin
     }
 
     if (thread.acl.can_pin_globally > moderation.can_pin_globally) {
-      moderation.can_pin_globally = thread.acl.can_pin_globally;
+      moderation.can_pin_globally = thread.acl.can_pin_globally
     }
 
     if (thread.is_hidden && thread.acl.can_unhide > moderation.can_unhide) {
-      moderation.can_unhide = thread.acl.can_unhide;
+      moderation.can_unhide = thread.acl.can_unhide
     }
 
-    moderation.allow = (
+    moderation.allow =
       moderation.can_approve ||
       moderation.can_close ||
       moderation.can_delete ||
@@ -135,8 +140,7 @@ export function getModerationActions(threads) {
       moderation.can_pin ||
       moderation.can_pin_globally ||
       moderation.can_unhide
-    );
-  });
+  })
 
-  return moderation;
-}
+  return moderation
+}

+ 15 - 23
frontend/src/components/user-menu/guest-nav.js

@@ -1,18 +1,17 @@
-import React from 'react';
-import Avatar from 'misago/components/avatar'; // jshint ignore:line
-import NavbarSearch from 'misago/components/navbar-search'; // jshint ignore:line
-import RegisterButton from 'misago/components/register-button'; // jshint ignore:line
-import SignInModal from 'misago/components/sign-in.js';
-import dropdown from 'misago/services/mobile-navbar-dropdown';
-import modal from 'misago/services/modal';
+import React from "react"
+import Avatar from "misago/components/avatar"
+import NavbarSearch from "misago/components/navbar-search"
+import RegisterButton from "misago/components/register-button"
+import SignInModal from "misago/components/sign-in.js"
+import dropdown from "misago/services/mobile-navbar-dropdown"
+import modal from "misago/services/modal"
 
 export class GuestMenu extends React.Component {
   showSignInModal() {
-    modal.show(SignInModal);
+    modal.show(SignInModal)
   }
 
   render() {
-    /* jshint ignore:start */
     return (
       <ul
         className="dropdown-menu user-dropdown dropdown-menu-right"
@@ -21,11 +20,12 @@ export class GuestMenu extends React.Component {
         <li className="guest-preview">
           <h4>{gettext("You are browsing as guest.")}</h4>
           <p>
-            {gettext('Sign in or register to start and participate in discussions.')}
+            {gettext(
+              "Sign in or register to start and participate in discussions."
+            )}
           </p>
           <div className="row">
             <div className="col-xs-6">
-
               <button
                 className="btn btn-default btn-sign-in btn-block"
                 onClick={this.showSignInModal}
@@ -33,26 +33,21 @@ export class GuestMenu extends React.Component {
               >
                 {gettext("Sign in")}
               </button>
-
             </div>
             <div className="col-xs-6">
-
               <RegisterButton className="btn-primary btn-register btn-block">
                 {gettext("Register")}
               </RegisterButton>
-
             </div>
           </div>
         </li>
       </ul>
-    );
-    /* jshint ignore:end */
+    )
   }
 }
 
 export class GuestNav extends GuestMenu {
   render() {
-    /* jshint ignore:start */
     return (
       <div className="nav nav-guest">
         <button
@@ -69,23 +64,20 @@ export class GuestNav extends GuestMenu {
           <NavbarSearch />
         </div>
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 }
 
 export class CompactGuestNav extends React.Component {
   showGuestMenu() {
-    dropdown.show(GuestMenu);
+    dropdown.show(GuestMenu)
   }
 
   render() {
-    /* jshint ignore:start */
     return (
       <button type="button" onClick={this.showGuestMenu}>
         <Avatar size="64" />
       </button>
-    );
-    /* jshint ignore:end */
+    )
   }
 }

+ 8 - 20
frontend/src/components/user-menu/root.js

@@ -1,39 +1,27 @@
-import React from 'react';
-import { GuestNav, CompactGuestNav } from './guest-nav'; // jshint ignore:line
-import { UserNav, CompactUserNav} from './user-nav'; // jshint ignore:line
+import React from "react"
+import { GuestNav, CompactGuestNav } from "./guest-nav"
+import { UserNav, CompactUserNav } from "./user-nav"
 
 export class UserMenu extends React.Component {
   render() {
-    /* jshint ignore:start */
     if (this.props.isAuthenticated) {
-      return (
-        <UserNav user={this.props.user} />
-      );
+      return <UserNav user={this.props.user} />
     } else {
-      return (
-        <GuestNav />
-      );
+      return <GuestNav />
     }
-    /* jshint ignore:end */
   }
 }
 
 export class CompactUserMenu extends React.Component {
   render() {
-    /* jshint ignore:start */
     if (this.props.isAuthenticated) {
-      return (
-        <CompactUserNav user={this.props.user} />
-      );
+      return <CompactUserNav user={this.props.user} />
     } else {
-      return (
-        <CompactGuestNav />
-      );
+      return <CompactGuestNav />
     }
-    /* jshint ignore:end */
   }
 }
 
 export function select(state) {
-  return state.auth;
+  return state.auth
 }

+ 35 - 61
frontend/src/components/user-menu/user-nav.js

@@ -1,27 +1,26 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import Avatar from 'misago/components/avatar'; // jshint ignore:line
-import ChangeAvatarModal, { select } from 'misago/components/change-avatar/root'; // jshint ignore:line
-import NavbarSearch from 'misago/components/navbar-search'; // jshint ignore:line
-import misago from 'misago'; // jshint ignore:line
-import dropdown from 'misago/services/mobile-navbar-dropdown';
-import modal from 'misago/services/modal';
+import React from "react"
+import { connect } from "react-redux"
+import Avatar from "misago/components/avatar"
+import ChangeAvatarModal, { select } from "misago/components/change-avatar/root"
+import NavbarSearch from "misago/components/navbar-search"
+import misago from "misago"
+import dropdown from "misago/services/mobile-navbar-dropdown"
+import modal from "misago/services/modal"
 
 export class UserMenu extends React.Component {
   logout() {
-    let decision = confirm(gettext("Are you sure you want to sign out?"));
+    let decision = confirm(gettext("Are you sure you want to sign out?"))
     if (decision) {
-      $('#hidden-logout-form').submit();
+      $("#hidden-logout-form").submit()
     }
   }
 
   changeAvatar() {
-    modal.show(connect(select)(ChangeAvatarModal));
+    modal.show(connect(select)(ChangeAvatarModal))
   }
 
   render() {
-    /* jshint ignore:start */
-    const { user } = this.props;
+    const { user } = this.props
 
     return (
       <ul
@@ -32,27 +31,19 @@ export class UserMenu extends React.Component {
           <strong>{user.username}</strong>
           <ul className="list-unstyled list-inline user-stats">
             <li>
-              <span className="material-icon">
-                message
-              </span>
+              <span className="material-icon">message</span>
               {user.posts}
             </li>
             <li>
-              <span className="material-icon">
-                forum
-              </span>
+              <span className="material-icon">forum</span>
               {user.threads}
             </li>
             <li>
-              <span className="material-icon">
-                favorite
-              </span>
+              <span className="material-icon">favorite</span>
               {user.followers}
             </li>
             <li>
-              <span className="material-icon">
-                favorite_outline
-              </span>
+              <span className="material-icon">favorite_outline</span>
               {user.following}
             </li>
           </ul>
@@ -65,7 +56,7 @@ export class UserMenu extends React.Component {
           </a>
         </li>
         <li>
-          <a href={misago.get('USERCP_URL')}>
+          <a href={misago.get("USERCP_URL")}>
             <span className="material-icon">done_all</span>
             {gettext("Change options")}
           </a>
@@ -82,7 +73,7 @@ export class UserMenu extends React.Component {
         </li>
         {!!user.acl.can_use_private_threads && (
           <li>
-            <a href={misago.get('PRIVATE_THREADS_URL')}>
+            <a href={misago.get("PRIVATE_THREADS_URL")}>
               <span className="material-icon">message</span>
               {gettext("Private threads")}
               <PrivateThreadsBadge user={user} />
@@ -100,25 +91,16 @@ export class UserMenu extends React.Component {
           </button>
         </li>
       </ul>
-    );
-    /* jshint ignore:end */
+    )
   }
 }
 
 export function PrivateThreadsBadge({ user }) {
-  if (!user.unread_private_threads) return null;
-
-  /* jshint ignore:start */
-  return (
-    <span className="badge">
-      {user.unread_private_threads}
-    </span>
-  );
-  /* jshint ignore:end */
+  if (!user.unread_private_threads) return null
 
+  return <span className="badge">{user.unread_private_threads}</span>
 }
 
-/* jshint ignore:start */
 export function UserNav({ user }) {
   return (
     <ul className="ul nav navbar-nav nav-user">
@@ -140,59 +122,51 @@ export function UserNav({ user }) {
         <UserMenu user={user} />
       </li>
     </ul>
-  );
+  )
 }
-/* jshint ignore:end */
 
 export function UserPrivateThreadsLink({ user }) {
-  if (!user.acl.can_use_private_threads) return null;
+  if (!user.acl.can_use_private_threads) return null
 
-  let title = null;
+  let title = null
   if (user.unread_private_threads) {
-    title = gettext("You have unread private threads!");
+    title = gettext("You have unread private threads!")
   } else {
-    title = gettext("Private threads");
+    title = gettext("Private threads")
   }
 
-  /* jshint ignore:start */
   return (
     <li>
       <a
         className="navbar-icon"
-        href={misago.get('PRIVATE_THREADS_URL')}
-        title={title}>
-        <span className="material-icon">
-          message
-        </span>
+        href={misago.get("PRIVATE_THREADS_URL")}
+        title={title}
+      >
+        <span className="material-icon">message</span>
         {user.unread_private_threads > 0 && (
-          <span className="badge">
-            {user.unread_private_threads}
-          </span>
+          <span className="badge">{user.unread_private_threads}</span>
         )}
       </a>
     </li>
-  );
-  /* jshint ignore:end */
+  )
 }
 
 export function selectUserMenu(state) {
   return {
     user: state.auth.user
-  };
+  }
 }
 
 export class CompactUserNav extends React.Component {
   showUserMenu() {
-    dropdown.showConnected('user-menu', connect(selectUserMenu)(UserMenu));
+    dropdown.showConnected("user-menu", connect(selectUserMenu)(UserMenu))
   }
 
   render() {
-    /* jshint ignore:start */
     return (
       <button type="button" onClick={this.showUserMenu}>
         <Avatar user={this.props.user} size="50" />
       </button>
-    );
-    /* jshint ignore:end */
+    )
   }
 }

+ 86 - 67
frontend/src/components/user-status.js

@@ -1,131 +1,150 @@
-import React from 'react';
+import React from "react"
 
 export default class extends React.Component {
   getClass() {
-    return getStatusClassName(this.props.status);
+    return getStatusClassName(this.props.status)
   }
 
   render() {
-    /* jshint ignore:start */
-    return (
-      <span className={this.getClass()}>
-        {this.props.children}
-      </span>
-    );
-    /* jshint ignore:end */
+    return <span className={this.getClass()}>{this.props.children}</span>
   }
 }
 
 export class StatusIcon extends React.Component {
   getIcon() {
     if (this.props.status.is_banned) {
-      return 'remove_circle_outline';
+      return "remove_circle_outline"
     } else if (this.props.status.is_hidden) {
-      return 'help_outline';
+      return "help_outline"
     } else if (this.props.status.is_online_hidden) {
-      return 'label';
+      return "label"
     } else if (this.props.status.is_offline_hidden) {
-      return 'label_outline';
+      return "label_outline"
     } else if (this.props.status.is_online) {
-      return 'lens';
+      return "lens"
     } else if (this.props.status.is_offline) {
-      return 'panorama_fish_eye';
+      return "panorama_fish_eye"
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <span className="material-icon status-icon">
-      {this.getIcon()}
-    </span>;
-    /* jshint ignore:end */
+    return <span className="material-icon status-icon">{this.getIcon()}</span>
   }
-
 }
 
 export class StatusLabel extends React.Component {
   getHelp() {
-    return getStatusDescription(this.props.user, this.props.status);
+    return getStatusDescription(this.props.user, this.props.status)
   }
 
   getLabel() {
     if (this.props.status.is_banned) {
-      return gettext("Banned");
+      return gettext("Banned")
     } else if (this.props.status.is_hidden) {
-      return gettext("Hidden");
+      return gettext("Hidden")
     } else if (this.props.status.is_online_hidden) {
-      return gettext("Online (hidden)");
+      return gettext("Online (hidden)")
     } else if (this.props.status.is_offline_hidden) {
-      return gettext("Offline (hidden)");
+      return gettext("Offline (hidden)")
     } else if (this.props.status.is_online) {
-      return gettext("Online");
+      return gettext("Online")
     } else if (this.props.status.is_offline) {
-      return gettext("Offline");
+      return gettext("Offline")
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <span className={this.props.className || "status-label"}
-                 title={this.getHelp()}>
-      {this.getLabel()}
-    </span>;
-    /* jshint ignore:end */
+    return (
+      <span
+        className={this.props.className || "status-label"}
+        title={this.getHelp()}
+      >
+        {this.getLabel()}
+      </span>
+    )
   }
 }
 
 export function getStatusClassName(status) {
-  let className = '';
+  let className = ""
   if (status.is_banned) {
-    className = 'banned';
+    className = "banned"
   } else if (status.is_hidden) {
-    className = 'offline';
+    className = "offline"
   } else if (status.is_online_hidden) {
-    className = 'online';
+    className = "online"
   } else if (status.is_offline_hidden) {
-    className = 'offline';
+    className = "offline"
   } else if (status.is_online) {
-    className = 'online';
+    className = "online"
   } else if (status.is_offline) {
-    className = 'offline';
+    className = "offline"
   }
 
-  return 'user-status user-' + className;
+  return "user-status user-" + className
 }
 
 export function getStatusDescription(user, status) {
   if (status.is_banned) {
     if (status.banned_until) {
-      return interpolate(gettext("%(username)s is banned until %(ban_expires)s"), {
-        username: user.username,
-        ban_expires: status.banned_until.format('LL, LT')
-      }, true);
+      return interpolate(
+        gettext("%(username)s is banned until %(ban_expires)s"),
+        {
+          username: user.username,
+          ban_expires: status.banned_until.format("LL, LT")
+        },
+        true
+      )
     } else {
-      return interpolate(gettext("%(username)s is banned"), {
-        username: user.username
-      }, true);
+      return interpolate(
+        gettext("%(username)s is banned"),
+        {
+          username: user.username
+        },
+        true
+      )
     }
   } else if (status.is_hidden) {
-    return interpolate(gettext("%(username)s is hiding presence"), {
-      username: user.username
-    }, true);
+    return interpolate(
+      gettext("%(username)s is hiding presence"),
+      {
+        username: user.username
+      },
+      true
+    )
   } else if (status.is_online_hidden) {
-    return interpolate(gettext("%(username)s is online (hidden)"), {
-      username: user.username
-    }, true);
+    return interpolate(
+      gettext("%(username)s is online (hidden)"),
+      {
+        username: user.username
+      },
+      true
+    )
   } else if (status.is_offline_hidden) {
-    return interpolate(gettext("%(username)s was last seen %(last_click)s (hidden)"), {
-      username: user.username,
-      last_click: status.last_click.fromNow()
-    }, true);
+    return interpolate(
+      gettext("%(username)s was last seen %(last_click)s (hidden)"),
+      {
+        username: user.username,
+        last_click: status.last_click.fromNow()
+      },
+      true
+    )
   } else if (status.is_online) {
-    return interpolate(gettext("%(username)s is online"), {
-      username: user.username
-    }, true);
+    return interpolate(
+      gettext("%(username)s is online"),
+      {
+        username: user.username
+      },
+      true
+    )
   } else if (status.is_offline) {
-    return interpolate(gettext("%(username)s was last seen %(last_click)s"), {
-      username: user.username,
-      last_click: status.last_click.fromNow()
-    }, true);
+    return interpolate(
+      gettext("%(username)s was last seen %(last_click)s"),
+      {
+        username: user.username,
+        last_click: status.last_click.fromNow()
+      },
+      true
+    )
   }
-}
+}

+ 47 - 29
frontend/src/components/username-history/change-preview.js

@@ -1,42 +1,60 @@
-import React from 'react';
-import Avatar from 'misago/components/avatar'; // jshint ignore:line
-import * as random from 'misago/utils/random'; // jshint ignore:line
+import React from "react"
+import Avatar from "misago/components/avatar"
+import * as random from "misago/utils/random"
 
 export default class extends React.Component {
   shouldComponentUpdate() {
-    return false;
+    return false
   }
 
   getClassName() {
     if (this.props.hiddenOnMobile) {
-      return 'list-group-item hidden-xs hidden-sm';
+      return "list-group-item hidden-xs hidden-sm"
     } else {
-      return 'list-group-item';
+      return "list-group-item"
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <li className={this.getClassName()}>
-      <div className="change-avatar">
-        <span className="user-avatar">
-          <Avatar size="100" />
-        </span>
-      </div>
-      <div className="change-author">
-        <span className="ui-preview-text" style={{width: random.int(30, 100) + "px"}}>&nbsp;</span>
-      </div>
-      <div className="change">
-        <span className="ui-preview-text" style={{width: random.int(30, 70) + "px"}}>&nbsp;</span>
-        <span className="material-icon">
-          arrow_forward
-        </span>
-        <span className="ui-preview-text" style={{width: random.int(30, 70) + "px"}}>&nbsp;</span>
-      </div>
-      <div className="change-date">
-        <span className="ui-preview-text" style={{width: random.int(80, 140) + "px"}}>&nbsp;</span>
-      </div>
-    </li>;
-    /* jshint ignore:end */
+    return (
+      <li className={this.getClassName()}>
+        <div className="change-avatar">
+          <span className="user-avatar">
+            <Avatar size="100" />
+          </span>
+        </div>
+        <div className="change-author">
+          <span
+            className="ui-preview-text"
+            style={{ width: random.int(30, 100) + "px" }}
+          >
+            &nbsp;
+          </span>
+        </div>
+        <div className="change">
+          <span
+            className="ui-preview-text"
+            style={{ width: random.int(30, 70) + "px" }}
+          >
+            &nbsp;
+          </span>
+          <span className="material-icon">arrow_forward</span>
+          <span
+            className="ui-preview-text"
+            style={{ width: random.int(30, 70) + "px" }}
+          >
+            &nbsp;
+          </span>
+        </div>
+        <div className="change-date">
+          <span
+            className="ui-preview-text"
+            style={{ width: random.int(80, 140) + "px" }}
+          >
+            &nbsp;
+          </span>
+        </div>
+      </li>
+    )
   }
-}
+}

+ 42 - 49
frontend/src/components/username-history/change.js

@@ -1,65 +1,58 @@
-import React from 'react';
-import Avatar from 'misago/components/avatar'; // jshint ignore:line
+import React from "react"
+import Avatar from "misago/components/avatar"
 
 export default class extends React.Component {
   renderUserAvatar() {
     if (this.props.change.changed_by) {
-      /* jshint ignore:start */
-      return <a href={this.props.change.changed_by.url} className="user-avatar-wrapper">
-        <Avatar user={this.props.change.changed_by} size="100" />
-      </a>;
-      /* jshint ignore:end */
+      return (
+        <a
+          href={this.props.change.changed_by.url}
+          className="user-avatar-wrapper"
+        >
+          <Avatar user={this.props.change.changed_by} size="100" />
+        </a>
+      )
     } else {
-      /* jshint ignore:start */
-      return <span className="user-avatar-wrapper">
-        <Avatar size="100" />
-      </span>;
-      /* jshint ignore:end */
+      return (
+        <span className="user-avatar-wrapper">
+          <Avatar size="100" />
+        </span>
+      )
     }
   }
 
   renderUsername() {
     if (this.props.change.changed_by) {
-      /* jshint ignore:start */
-      return <a href={this.props.change.changed_by.url} className="item-title">
-        {this.props.change.changed_by.username}
-      </a>;
-      /* jshint ignore:end */
+      return (
+        <a href={this.props.change.changed_by.url} className="item-title">
+          {this.props.change.changed_by.username}
+        </a>
+      )
     } else {
-      /* jshint ignore:start */
-      return <span className="item-title">
-        {this.props.change.changed_by_username}
-      </span>;
-      /* jshint ignore:end */
+      return (
+        <span className="item-title">
+          {this.props.change.changed_by_username}
+        </span>
+      )
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <li className="list-group-item" key={this.props.change.id}>
-      <div className="change-avatar">
-        {this.renderUserAvatar()}
-      </div>
-      <div className="change-author">
-        {this.renderUsername()}
-      </div>
-      <div className="change">
-        <span className="old-username">
-          {this.props.change.old_username}
-        </span>
-        <span className="material-icon">
-          arrow_forward
-        </span>
-        <span className="new-username">
-          {this.props.change.new_username}
-        </span>
-      </div>
-      <div className="change-date">
-        <abbr title={this.props.change.changed_on.format('LLL')}>
-          {this.props.change.changed_on.fromNow()}
-        </abbr>
-      </div>
-    </li>;
-    /* jshint ignore:end */
+    return (
+      <li className="list-group-item" key={this.props.change.id}>
+        <div className="change-avatar">{this.renderUserAvatar()}</div>
+        <div className="change-author">{this.renderUsername()}</div>
+        <div className="change">
+          <span className="old-username">{this.props.change.old_username}</span>
+          <span className="material-icon">arrow_forward</span>
+          <span className="new-username">{this.props.change.new_username}</span>
+        </div>
+        <div className="change-date">
+          <abbr title={this.props.change.changed_on.format("LLL")}>
+            {this.props.change.changed_on.fromNow()}
+          </abbr>
+        </div>
+      </li>
+    )
   }
-}
+}

+ 13 - 13
frontend/src/components/username-history/list-empty.js

@@ -1,23 +1,23 @@
-import React from 'react';
+import React from "react"
 
 export default class extends React.Component {
   getEmptyMessage() {
     if (this.props.emptyMessage) {
-      return this.props.emptyMessage;
+      return this.props.emptyMessage
     } else {
-      return gettext("No name changes have been recorded for your account.");
+      return gettext("No name changes have been recorded for your account.")
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className="username-history ui-ready">
-      <ul className="list-group">
-        <li className="list-group-item empty-message">
-          {this.getEmptyMessage()}
-        </li>
-      </ul>
-    </div>;
-    /* jshint ignore:end */
+    return (
+      <div className="username-history ui-ready">
+        <ul className="list-group">
+          <li className="list-group-item empty-message">
+            {this.getEmptyMessage()}
+          </li>
+        </ul>
+      </div>
+    )
   }
-}
+}

+ 14 - 14
frontend/src/components/username-history/list-preview.js

@@ -1,20 +1,20 @@
-import React from 'react';
-import ChangePreview from 'misago/components/username-history/change-preview'; // jshint ignore:line
+import React from "react"
+import ChangePreview from "misago/components/username-history/change-preview"
 
 export default class extends React.Component {
   shouldComponentUpdate() {
-    return false;
+    return false
   }
 
-  render () {
-    /* jshint ignore:start */
-    return <div className="username-history ui-preview">
-      <ul className="list-group">
-        {[0, 1, 2].map((i) => {
-          return <ChangePreview hiddenOnMobile={i > 0} key={i} />
-        })}
-      </ul>
-    </div>;
-    /* jshint ignore:end */
+  render() {
+    return (
+      <div className="username-history ui-preview">
+        <ul className="list-group">
+          {[0, 1, 2].map(i => {
+            return <ChangePreview hiddenOnMobile={i > 0} key={i} />
+          })}
+        </ul>
+      </div>
+    )
   }
-}
+}

+ 12 - 12
frontend/src/components/username-history/list-ready.js

@@ -1,16 +1,16 @@
-import React from 'react';
-import Change from 'misago/components/username-history/change'; // jshint ignore:line
+import React from "react"
+import Change from "misago/components/username-history/change"
 
 export default class extends React.Component {
   render() {
-    /* jshint ignore:start */
-    return <div className="username-history ui-ready">
-      <ul className="list-group">
-        {this.props.changes.map((change) => {
-          return <Change change={change} key={change.id} />;
-        })}
-      </ul>
-    </div>;
-    /* jshint ignore:end */
+    return (
+      <div className="username-history ui-ready">
+        <ul className="list-group">
+          {this.props.changes.map(change => {
+            return <Change change={change} key={change.id} />
+          })}
+        </ul>
+      </div>
+    )
   }
-}
+}

+ 8 - 14
frontend/src/components/username-history/root.js

@@ -1,24 +1,18 @@
-import React from 'react';
-import ListEmpty from 'misago/components/username-history/list-empty'; // jshint ignore:line
-import ListReady from 'misago/components/username-history/list-ready'; // jshint ignore:line
-import ListPreview from 'misago/components/username-history/list-preview'; // jshint ignore:line
+import React from "react"
+import ListEmpty from "misago/components/username-history/list-empty"
+import ListReady from "misago/components/username-history/list-ready"
+import ListPreview from "misago/components/username-history/list-preview"
 
 export default class extends React.Component {
   render() {
     if (this.props.isLoaded) {
       if (this.props.changes.length) {
-        /* jshint ignore:start */
-        return <ListReady changes={this.props.changes} />;
-        /* jshint ignore:end */
+        return <ListReady changes={this.props.changes} />
       } else {
-        /* jshint ignore:start */
-        return <ListEmpty emptyMessage={this.props.emptyMessage} />;
-        /* jshint ignore:end */
+        return <ListEmpty emptyMessage={this.props.emptyMessage} />
       }
     } else {
-      /* jshint ignore:start */
-      return <ListPreview />;
-      /* jshint ignore:end */
+      return <ListPreview />
     }
   }
-}
+}

+ 14 - 33
frontend/src/components/users-list/card/index.js

@@ -1,15 +1,14 @@
-// jshint ignore:start
-import React from 'react';
-import Avatar from 'misago/components/avatar';
-import Stats from './stats';
-import UserTitle from './user-title';
+import React from "react"
+import Avatar from "misago/components/avatar"
+import Stats from "./stats"
+import UserTitle from "./user-title"
 
 export default function({ showStatus, user }) {
-  const { rank } = user;
+  const { rank } = user
 
-  let className = 'panel user-card';
+  let className = "panel user-card"
   if (rank.css_class) {
-    className += ' user-card-' + rank.css_class;
+    className += " user-card-" + rank.css_class
   }
 
   return (
@@ -19,48 +18,30 @@ export default function({ showStatus, user }) {
           <div className="col-xs-3 user-card-left">
             <div className="user-card-small-avatar">
               <a href={user.url}>
-                <Avatar
-                  size="50"
-                  size2x="80"
-                  user={user}
-                />
+                <Avatar size="50" size2x="80" user={user} />
               </a>
             </div>
           </div>
           <div className="col-xs-9 col-sm-12 user-card-body">
-
             <div className="user-card-avatar">
               <a href={user.url}>
-                <Avatar
-                  size="150"
-                  size2x="200"
-                  user={user}
-                />
+                <Avatar size="150" size2x="200" user={user} />
               </a>
             </div>
 
             <div className="user-card-username">
-              <a href={user.url}>
-                {user.username}
-              </a>
+              <a href={user.url}>{user.username}</a>
             </div>
             <div className="user-card-title">
-              <UserTitle
-                rank={rank}
-                title={user.title}
-              />
+              <UserTitle rank={rank} title={user.title} />
             </div>
 
             <div className="user-card-stats">
-              <Stats
-                showStatus={showStatus}
-                user={user}
-              />
+              <Stats showStatus={showStatus} user={user} />
             </div>
-
           </div>
         </div>
       </div>
     </div>
-  );
-}
+  )
+}

+ 57 - 50
frontend/src/components/users-list/card/stats.js

@@ -1,112 +1,119 @@
-// jshint ignore:start
-import React from 'react';
-import UserStatus, { StatusLabel } from 'misago/components/user-status';
+import React from "react"
+import UserStatus, { StatusLabel } from "misago/components/user-status"
 
 export default function({ showStatus, user }) {
   return (
     <ul className="list-unstyled">
-      <Status
-        showStatus={showStatus}
-        user={user}
-      />
+      <Status showStatus={showStatus} user={user} />
       <JoinDate user={user} />
       <li className="user-stat-divider" />
       <Posts user={user} />
       <Threads user={user} />
       <Followers user={user} />
     </ul>
-  );
+  )
 }
 
 export function Status({ showStatus, user }) {
-  if (!showStatus) return null;
+  if (!showStatus) return null
 
   return (
     <li className="user-stat-status">
       <UserStatus status={user.status}>
-        <StatusLabel
-          status={user.status}
-          user={user}
-        />
+        <StatusLabel status={user.status} user={user} />
       </UserStatus>
     </li>
-  );
+  )
 }
 
 export function JoinDate({ user }) {
-  const { joined_on } = user;
+  const { joined_on } = user
 
-  let title = interpolate(gettext("Joined on %(joined_on)s"), {
-    'joined_on': joined_on.format('LL, LT')
-  }, true);
+  let title = interpolate(
+    gettext("Joined on %(joined_on)s"),
+    {
+      joined_on: joined_on.format("LL, LT")
+    },
+    true
+  )
 
-  let message = interpolate(gettext("Joined %(joined_on)s"), {
-    'joined_on': joined_on.fromNow()
-  }, true);
+  let message = interpolate(
+    gettext("Joined %(joined_on)s"),
+    {
+      joined_on: joined_on.fromNow()
+    },
+    true
+  )
 
   return (
     <li className="user-stat-join-date">
-      <abbr title={title}>
-        {message}
-      </abbr>
+      <abbr title={title}>{message}</abbr>
     </li>
-  );
+  )
 }
 
 export function Posts({ user }) {
-  const className = getStatClassName("user-stat-posts", user.posts);
-  const message = ngettext(
-    "%(posts)s post",
-    "%(posts)s posts",
-    user.posts
-  );
+  const className = getStatClassName("user-stat-posts", user.posts)
+  const message = ngettext("%(posts)s post", "%(posts)s posts", user.posts)
 
   return (
     <li className={className}>
-      {interpolate(message, {
-        'posts': user.posts
-      }, true)}
+      {interpolate(
+        message,
+        {
+          posts: user.posts
+        },
+        true
+      )}
     </li>
-  );
+  )
 }
 
 export function Threads({ user }) {
-  const className = getStatClassName("user-stat-threads", user.threads);
+  const className = getStatClassName("user-stat-threads", user.threads)
   const message = ngettext(
     "%(threads)s thread",
     "%(threads)s threads",
     user.threads
-  );
+  )
 
   return (
     <li className={className}>
-      {interpolate(message, {
-        'threads': user.threads
-      }, true)}
+      {interpolate(
+        message,
+        {
+          threads: user.threads
+        },
+        true
+      )}
     </li>
-  );
+  )
 }
 
 export function Followers({ user }) {
-  const className = getStatClassName("user-stat-followers", user.followers);
+  const className = getStatClassName("user-stat-followers", user.followers)
   const message = ngettext(
     "%(followers)s follower",
     "%(followers)s followers",
     user.followers
-  );
+  )
 
   return (
     <li className={className}>
-      {interpolate(message, {
-        'followers': user.followers
-      }, true)}
+      {interpolate(
+        message,
+        {
+          followers: user.followers
+        },
+        true
+      )}
     </li>
-  );
+  )
 }
 
 export function getStatClassName(className, stat) {
   if (stat === 0) {
-    return className + ' user-stat-empty';
+    return className + " user-stat-empty"
   }
-  return className;
-}
+  return className
+}

+ 7 - 12
frontend/src/components/users-list/card/user-title.js

@@ -1,12 +1,11 @@
-/* jshint ignore:start */
-import React from 'react';
+import React from "react"
 
 export default function({ rank, title }) {
-  let userTitle = title || rank.title || rank.name;
+  let userTitle = title || rank.title || rank.name
 
-  let className = 'user-title';
+  let className = "user-title"
   if (rank.css_class) {
-    className += ' user-title-' + rank.css_class;
+    className += " user-title-" + rank.css_class
   }
 
   if (rank.is_tab) {
@@ -14,12 +13,8 @@ export default function({ rank, title }) {
       <a className={className} href={rank.url}>
         {userTitle}
       </a>
-    );
+    )
   }
 
-  return (
-    <span className={className}>
-      {userTitle}
-    </span>
-  );
-}
+  return <span className={className}>{userTitle}</span>
+}

+ 11 - 24
frontend/src/components/users-list/index.js

@@ -1,41 +1,28 @@
-// jshint ignore:start
-import React from 'react';
-import Card from './card';
-import Preview from './preview';
-
+import React from "react"
+import Card from "./card"
+import Preview from "./preview"
 
 export default function({ cols, isReady, showStatus, users }) {
-  let colClassName = 'col-xs-12 col-sm-4';
+  let colClassName = "col-xs-12 col-sm-4"
   if (cols === 4) {
-    colClassName += ' col-md-3';
+    colClassName += " col-md-3"
   }
 
   if (!isReady) {
-    return (
-      <Preview
-        colClassName={colClassName}
-        cols={cols}
-      />
-    );
+    return <Preview colClassName={colClassName} cols={cols} />
   }
 
   return (
     <div className="users-cards-list ui-ready">
       <div className="row">
-        {users.map((user) => {
+        {users.map(user => {
           return (
-            <div
-              className={colClassName}
-              key={user.id}
-            >
-              <Card
-                showStatus={showStatus}
-                user={user}
-              />
+            <div className={colClassName} key={user.id}>
+              <Card showStatus={showStatus} user={user} />
             </div>
-          );
+          )
         })}
       </div>
     </div>
-  );
+  )
 }

+ 14 - 23
frontend/src/components/users-list/preview/card.js

@@ -1,11 +1,10 @@
-// jshint ignore:start
-import React from 'react';
-import Avatar from 'misago/components/avatar';
-import * as random from 'misago/utils/random';
+import React from "react"
+import Avatar from "misago/components/avatar"
+import * as random from "misago/utils/random"
 
 export default class extends React.Component {
   shouldComponentUpdate() {
-    return false;
+    return false
   }
 
   render() {
@@ -16,28 +15,21 @@ export default class extends React.Component {
             <div className="col-xs-3 user-card-left">
               <div className="user-card-small-avatar">
                 <span>
-                  <Avatar
-                    size="50"
-                    size2x="80"
-                  />
+                  <Avatar size="50" size2x="80" />
                 </span>
               </div>
             </div>
             <div className="col-xs-9 col-sm-12 user-card-body">
-
               <div className="user-card-avatar">
                 <span>
-                  <Avatar
-                    size="150"
-                    size2x="200"
-                  />
+                  <Avatar size="150" size2x="200" />
                 </span>
               </div>
 
               <div className="user-card-username">
                 <span
                   className="ui-preview-text"
-                  style={{width: random.int(60, 150) + "px"}}
+                  style={{ width: random.int(60, 150) + "px" }}
                 >
                   &nbsp;
                 </span>
@@ -45,7 +37,7 @@ export default class extends React.Component {
               <div className="user-card-title">
                 <span
                   className="ui-preview-text"
-                  style={{width: random.int(60, 150) + "px"}}
+                  style={{ width: random.int(60, 150) + "px" }}
                 >
                   &nbsp;
                 </span>
@@ -56,7 +48,7 @@ export default class extends React.Component {
                   <li>
                     <span
                       className="ui-preview-text"
-                      style={{width: random.int(30, 70) + "px"}}
+                      style={{ width: random.int(30, 70) + "px" }}
                     >
                       &nbsp;
                     </span>
@@ -64,7 +56,7 @@ export default class extends React.Component {
                   <li>
                     <span
                       className="ui-preview-text"
-                      style={{width: random.int(30, 70) + "px"}}
+                      style={{ width: random.int(30, 70) + "px" }}
                     >
                       &nbsp;
                     </span>
@@ -73,7 +65,7 @@ export default class extends React.Component {
                   <li>
                     <span
                       className="ui-preview-text"
-                      style={{width: random.int(30, 70) + "px"}}
+                      style={{ width: random.int(30, 70) + "px" }}
                     >
                       &nbsp;
                     </span>
@@ -81,18 +73,17 @@ export default class extends React.Component {
                   <li>
                     <span
                       className="ui-preview-text"
-                      style={{width: random.int(30, 70) + "px"}}
+                      style={{ width: random.int(30, 70) + "px" }}
                     >
                       &nbsp;
                     </span>
                   </li>
                 </ul>
               </div>
-
             </div>
           </div>
         </div>
       </div>
-    );
+    )
   }
-}
+}

+ 11 - 16
frontend/src/components/users-list/preview/index.js

@@ -1,29 +1,24 @@
-// jshint ignore:start
-import React from 'react';
-import Card from './card';
-
+import React from "react"
+import Card from "./card"
 
 export default function({ colClassName, cols }) {
-  const list = Array.apply(null, {length: cols}).map(Number.call, Number);
+  const list = Array.apply(null, { length: cols }).map(Number.call, Number)
 
   return (
     <div className="users-cards-list ui-preview">
       <div className="row">
-        {list.map((i) => {
-          let className = colClassName;
-          if (i !== 0) className += ' hidden-xs';
-          if (i === 3) className += ' hidden-sm';
+        {list.map(i => {
+          let className = colClassName
+          if (i !== 0) className += " hidden-xs"
+          if (i === 3) className += " hidden-sm"
 
           return (
-            <div
-              className={className}
-              key={i}
-            >
+            <div className={className} key={i}>
               <Card />
             </div>
-          );
+          )
         })}
       </div>
     </div>
-  );
-}
+  )
+}

+ 14 - 12
frontend/src/components/users/active-posters/list-empty.js

@@ -1,21 +1,23 @@
-import React from 'react';
+import React from "react"
 
 export default class extends React.Component {
   getEmptyMessage() {
     return interpolate(
-      gettext("No users have posted any new messages during last %(days)s days."),
-      {'days': this.props.trackedPeriod}, true);
+      gettext(
+        "No users have posted any new messages during last %(days)s days."
+      ),
+      { days: this.props.trackedPeriod },
+      true
+    )
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className="active-posters-list">
-      <div className="container">
-        <p className="lead">
-          {this.getEmptyMessage()}
-        </p>
+    return (
+      <div className="active-posters-list">
+        <div className="container">
+          <p className="lead">{this.getEmptyMessage()}</p>
+        </div>
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 18 - 22
frontend/src/components/users/active-posters/list-item-preview.js

@@ -1,22 +1,21 @@
-import React from 'react';
-import Avatar from 'misago/components/avatar'; // jshint ignore:line
-import * as random from 'misago/utils/random'; // jshint ignore:line
+import React from "react"
+import Avatar from "misago/components/avatar"
+import * as random from "misago/utils/random"
 
 export default class extends React.Component {
   shouldComponentUpdate() {
-    return false;
+    return false
   }
 
   getClassName() {
     if (this.props.hiddenOnMobile) {
-      return 'list-group-item hidden-xs hidden-sm';
+      return "list-group-item hidden-xs hidden-sm"
     } else {
-      return 'list-group-item';
+      return "list-group-item"
     }
   }
 
   render() {
-    /* jshint ignore:start */
     return (
       <li className={this.getClassName()}>
         <div className="rank-user-avatar">
@@ -30,7 +29,7 @@ export default class extends React.Component {
             <span className="item-title">
               <span
                 className="ui-preview-text"
-                style={{width: random.int(30, 80) + "px"}}
+                style={{ width: random.int(30, 80) + "px" }}
               >
                 &nbsp;
               </span>
@@ -39,12 +38,10 @@ export default class extends React.Component {
 
           <div className="user-details">
             <span className="user-status">
-              <span className="status-icon ui-preview-text">
-                &nbsp;
-              </span>
+              <span className="status-icon ui-preview-text">&nbsp;</span>
               <span
                 className="status-label ui-preview-text hidden-xs hidden-sm"
-                style={{width: random.int(30, 50) + "px"}}
+                style={{ width: random.int(30, 50) + "px" }}
               >
                 &nbsp;
               </span>
@@ -52,7 +49,7 @@ export default class extends React.Component {
             <span className="rank-name">
               <span
                 className="ui-preview-text"
-                style={{width: random.int(30, 50) + "px"}}
+                style={{ width: random.int(30, 50) + "px" }}
               >
                 &nbsp;
               </span>
@@ -60,7 +57,7 @@ export default class extends React.Component {
             <span className="user-title hidden-xs hidden-sm">
               <span
                 className="ui-preview-text"
-                style={{width: random.int(30, 50) + "px"}}
+                style={{ width: random.int(30, 50) + "px" }}
               >
                 &nbsp;
               </span>
@@ -71,7 +68,7 @@ export default class extends React.Component {
               <strong>
                 <span
                   className="ui-preview-text"
-                  style={{width: random.int(20, 30) + "px"}}
+                  style={{ width: random.int(20, 30) + "px" }}
                 >
                   &nbsp;
                 </span>
@@ -82,7 +79,7 @@ export default class extends React.Component {
               <strong>
                 <span
                   className="ui-preview-text"
-                  style={{width: random.int(20, 30) + "px"}}
+                  style={{ width: random.int(20, 30) + "px" }}
                 >
                   &nbsp;
                 </span>
@@ -96,7 +93,7 @@ export default class extends React.Component {
           <strong>
             <span
               className="ui-preview-text"
-              style={{width: random.int(20, 30) + "px"}}
+              style={{ width: random.int(20, 30) + "px" }}
             >
               &nbsp;
             </span>
@@ -108,7 +105,7 @@ export default class extends React.Component {
           <strong>
             <span
               className="ui-preview-text"
-              style={{width: random.int(20, 30) + "px"}}
+              style={{ width: random.int(20, 30) + "px" }}
             >
               &nbsp;
             </span>
@@ -120,7 +117,7 @@ export default class extends React.Component {
           <strong>
             <span
               className="ui-preview-text"
-              style={{width: random.int(20, 30) + "px"}}
+              style={{ width: random.int(20, 30) + "px" }}
             >
               &nbsp;
             </span>
@@ -128,7 +125,6 @@ export default class extends React.Component {
           <small>{gettext("Total posts")}</small>
         </div>
       </li>
-    );
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 29 - 42
frontend/src/components/users/active-posters/list-item.js

@@ -1,84 +1,72 @@
-import React from 'react';
-import { Link } from 'react-router'; // jshint ignore:line
-import Avatar from 'misago/components/avatar'; // jshint ignore:line
-import Status, { StatusIcon, StatusLabel } from 'misago/components/user-status'; // jshint ignore:line
-import misago from 'misago/index'; // jshint ignore:line
-import * as random from 'misago/utils/random'; // jshint ignore:line
+import React from "react"
+import { Link } from "react-router"
+import Avatar from "misago/components/avatar"
+import Status, { StatusIcon, StatusLabel } from "misago/components/user-status"
+import misago from "misago/index"
+import * as random from "misago/utils/random"
 
 export default class extends React.Component {
   getClassName() {
     if (this.props.rank.css_class) {
-      return "list-group-item list-group-rank-" + this.props.rank.css_class;
+      return "list-group-item list-group-rank-" + this.props.rank.css_class
     } else {
-      return "list-group-item";
+      return "list-group-item"
     }
   }
 
   getUserStatus() {
     if (this.props.user.status) {
-      /* jshint ignore:start */
       return (
         <Status user={this.props.user} status={this.props.user.status}>
-          <StatusIcon user={this.props.user}
-                      status={this.props.user.status} />
-          <StatusLabel user={this.props.user}
-                       status={this.props.user.status}
-                       className="status-label hidden-xs hidden-sm" />
+          <StatusIcon user={this.props.user} status={this.props.user.status} />
+          <StatusLabel
+            user={this.props.user}
+            status={this.props.user.status}
+            className="status-label hidden-xs hidden-sm"
+          />
         </Status>
-      );
-      /* jshint ignore:end */
+      )
     }
 
-    /* jshint ignore:start */
     return (
       <span className="user-status">
-        <span className="status-icon ui-preview-text">
-          &nbsp;
-        </span>
-        <span className="status-label ui-preview-text hidden-xs hidden-sm"
-              style={{width: random.int(30, 50) + "px"}}>
+        <span className="status-icon ui-preview-text">&nbsp;</span>
+        <span
+          className="status-label ui-preview-text hidden-xs hidden-sm"
+          style={{ width: random.int(30, 50) + "px" }}
+        >
           &nbsp;
         </span>
       </span>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   getRankName() {
     if (!this.props.rank.is_tab) {
-      /* jshint ignore:start */
       return (
-        <span className="rank-name item-title">
-          {this.props.rank.name}
-        </span>
-      );
-      /* jshint ignore:end */
+        <span className="rank-name item-title">{this.props.rank.name}</span>
+      )
     }
 
-    /* jshint ignore:start */
-    let rankUrl = misago.get('USERS_LIST_URL') + this.props.rank.slug + '/';
+    let rankUrl = misago.get("USERS_LIST_URL") + this.props.rank.slug + "/"
     return (
       <Link to={rankUrl} className="rank-name item-title">
         {this.props.rank.name}
       </Link>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   getUserTitle() {
-    if (!this.props.user.title) return null;
+    if (!this.props.user.title) return null
 
-    /* jshint ignore:start */
     return (
       <span className="user-title hidden-xs hidden-sm">
         {this.props.user.title}
       </span>
-    );
-    /* jshint ignore:end */
+    )
   }
 
   render() {
-    /* jshint ignore:start */
     return (
       <li className={this.getClassName()}>
         <div className="rank-user-avatar">
@@ -126,7 +114,6 @@ export default class extends React.Component {
           <small>{gettext("Total posts")}</small>
         </div>
       </li>
-    );
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 24 - 22
frontend/src/components/users/active-posters/list-preview.js

@@ -1,32 +1,34 @@
-import React from 'react';
-import ItemPreview from 'misago/components/users/active-posters/list-item-preview'; // jshint ignore:line
-import * as random from 'misago/utils/random'; // jshint ignore:line
+import React from "react"
+import ItemPreview from "misago/components/users/active-posters/list-item-preview"
+import * as random from "misago/utils/random"
 
 export default class extends React.Component {
   shouldComponentUpdate() {
-    return false;
+    return false
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className="active-posters-list">
-      <div className="container">
-        <p className="lead ui-preview">
-          <span className="ui-preview-text"
-                         style={{width: random.int(50, 220) + "px"}}>
-            &nbsp;
-          </span>
-        </p>
+    return (
+      <div className="active-posters-list">
+        <div className="container">
+          <p className="lead ui-preview">
+            <span
+              className="ui-preview-text"
+              style={{ width: random.int(50, 220) + "px" }}
+            >
+              &nbsp;
+            </span>
+          </p>
 
-        <div className="active-posters ui-preview">
-          <ul className="list-group">
-            {[0, 1, 2].map((i) => {
-              return <ItemPreview hiddenOnMobile={i > 0} key={i} />
-            })}
-          </ul>
+          <div className="active-posters ui-preview">
+            <ul className="list-group">
+              {[0, 1, 2].map(i => {
+                return <ItemPreview hiddenOnMobile={i > 0} key={i} />
+              })}
+            </ul>
+          </div>
         </div>
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 34 - 27
frontend/src/components/users/active-posters/list-ready.js

@@ -1,39 +1,46 @@
-import React from 'react';
-import ListItem from 'misago/components/users/active-posters/list-item'; // jshint ignore:line
+import React from "react"
+import ListItem from "misago/components/users/active-posters/list-item"
 
 export default class extends React.Component {
   getLeadMessage() {
     let message = ngettext(
-        "%(posters)s most active poster from last %(days)s days.",
-        "%(posters)s most active posters from last %(days)s days.",
-        this.props.count);
+      "%(posters)s most active poster from last %(days)s days.",
+      "%(posters)s most active posters from last %(days)s days.",
+      this.props.count
+    )
 
-    return interpolate(message, {
-      posters: this.props.count,
-      days: this.props.trackedPeriod
-    }, true);
+    return interpolate(
+      message,
+      {
+        posters: this.props.count,
+        days: this.props.trackedPeriod
+      },
+      true
+    )
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className="active-posters-list">
-      <div className="container">
-        <p className="lead">
-          {this.getLeadMessage()}
-        </p>
+    return (
+      <div className="active-posters-list">
+        <div className="container">
+          <p className="lead">{this.getLeadMessage()}</p>
 
-        <div className="active-posters ui-ready">
-          <ul className="list-group">
-            {this.props.users.map((user, i) => {
-              return <ListItem user={user}
-                               rank={user.rank}
-                               counter={i + 1}
-                               key={user.id} />;
-            })}
-          </ul>
+          <div className="active-posters ui-ready">
+            <ul className="list-group">
+              {this.props.users.map((user, i) => {
+                return (
+                  <ListItem
+                    user={user}
+                    rank={user.rank}
+                    counter={i + 1}
+                    key={user.id}
+                  />
+                )
+              })}
+            </ul>
+          </div>
         </div>
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 37 - 41
frontend/src/components/users/active-posters/root.js

@@ -1,24 +1,24 @@
-import React from 'react';
-import ListEmpty from 'misago/components/users/active-posters/list-empty'; // jshint ignore:line
-import ListPreview from 'misago/components/users/active-posters/list-preview'; // jshint ignore:line
-import ListReady from 'misago/components/users/active-posters/list-ready'; // jshint ignore:line
-import misago from 'misago/index';
-import { hydrate } from 'misago/reducers/users';
-import polls from 'misago/services/polls';
-import store from 'misago/services/store';
-import title from 'misago/services/page-title';
+import React from "react"
+import ListEmpty from "misago/components/users/active-posters/list-empty"
+import ListPreview from "misago/components/users/active-posters/list-preview"
+import ListReady from "misago/components/users/active-posters/list-ready"
+import misago from "misago/index"
+import { hydrate } from "misago/reducers/users"
+import polls from "misago/services/polls"
+import store from "misago/services/store"
+import title from "misago/services/page-title"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
-    if (misago.has('USERS')) {
-      this.initWithPreloadedData(misago.pop('USERS'));
+    if (misago.has("USERS")) {
+      this.initWithPreloadedData(misago.pop("USERS"))
     } else {
-      this.initWithoutPreloadedData();
+      this.initWithoutPreloadedData()
     }
 
-    this.startPolling();
+    this.startPolling()
   }
 
   initWithPreloadedData(data) {
@@ -27,70 +27,66 @@ export default class extends React.Component {
 
       trackedPeriod: data.tracked_period,
       count: data.count
-    };
+    }
 
-    store.dispatch(hydrate(data.results));
+    store.dispatch(hydrate(data.results))
   }
 
   initWithoutPreloadedData() {
     this.state = {
       isLoaded: false
-    };
+    }
   }
 
   startPolling() {
     polls.start({
-      poll: 'active-posters',
-      url: misago.get('USERS_API'),
+      poll: "active-posters",
+      url: misago.get("USERS_API"),
       data: {
-        list: 'active'
+        list: "active"
       },
       frequency: 90 * 1000,
       update: this.update
-    });
+    })
   }
 
-  /* jshint ignore:start */
-  update = (data) => {
-    store.dispatch(hydrate(data.results));
+  update = data => {
+    store.dispatch(hydrate(data.results))
 
     this.setState({
       isLoaded: true,
 
       trackedPeriod: data.tracked_period,
       count: data.count
-    });
-  };
-  /* jshint ignore:end */
+    })
+  }
 
   componentDidMount() {
     title.set({
       title: this.props.route.extra.name,
       parent: gettext("Users")
-    });
+    })
   }
 
   componentWillUnmount() {
-    polls.stop('active-posters');
+    polls.stop("active-posters")
   }
 
   render() {
     if (this.state.isLoaded) {
       if (this.state.count > 0) {
-        /* jshint ignore:start */
-        return <ListReady users={this.props.users}
-                          trackedPeriod={this.state.trackedPeriod}
-                          count={this.state.count} />;
-        /* jshint ignore:end */
+        return (
+          <ListReady
+            users={this.props.users}
+            trackedPeriod={this.state.trackedPeriod}
+            count={this.state.count}
+          />
+        )
       } else {
-        /* jshint ignore:start */
-        return <ListEmpty trackedPeriod={this.state.trackedPeriod} />;
-        /* jshint ignore:end */
+        return <ListEmpty trackedPeriod={this.state.trackedPeriod} />
       }
     } else {
-      /* jshint ignore:start */
-      return <ListPreview />;
-      /* jshint ignore:end */
+      return <ListPreview />
     }
   }
-}
+}

+ 15 - 18
frontend/src/components/users/nav.js

@@ -1,32 +1,29 @@
-// jshint ignore:start
-import React from 'react';
-import { Link } from 'react-router';
-import Li from 'misago/components/li';
-import misago from 'misago/index';
+import React from "react"
+import { Link } from "react-router"
+import Li from "misago/components/li"
+import misago from "misago/index"
 
 export default function({ baseUrl, lists }) {
   return (
     <ul className="nav nav-pills">
-      {lists.map((list) => {
-        const url = listUrl(baseUrl, list);
+      {lists.map(list => {
+        const url = listUrl(baseUrl, list)
         return (
           <Li path={url} key={url}>
-            <Link to={url}>
-              {list.name}
-            </Link>
+            <Link to={url}>{list.name}</Link>
           </Li>
-        );
+        )
       })}
     </ul>
-  );
+  )
 }
 
 const listUrl = function(baseUrl, list) {
-  let url = baseUrl;
-  if (list.component === 'rank') {
-    url += list.slug;
+  let url = baseUrl
+  if (list.component === "rank") {
+    url += list.slug
   } else {
-    url += list.component;
+    url += list.component
   }
-  return url + '/';
-};
+  return url + "/"
+}

+ 5 - 10
frontend/src/components/users/rank/list-loading.js

@@ -1,21 +1,16 @@
-import React from 'react';
-import UsersList from 'misago/components/users-list' // jshint ignore:line
+import React from "react"
+import UsersList from "misago/components/users-list"
 
 export default class extends React.Component {
   shouldComponentUpdate() {
-    return false;
+    return false
   }
 
   render() {
-    /* jshint ignore:start */
     return (
       <div>
-        <UsersList
-          cols={4}
-          isReady={false}
-        />
+        <UsersList cols={4} isReady={false} />
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 }

+ 5 - 6
frontend/src/components/users/rank/list.js

@@ -1,7 +1,6 @@
-/* jshint ignore:start */
-import React from 'react';
-import Pager from 'misago/components/users/rank/pager';
-import UsersList from 'misago/components/users-list';
+import React from "react"
+import Pager from "misago/components/users/rank/pager"
+import UsersList from "misago/components/users-list"
 
 export default function(props) {
   return (
@@ -14,5 +13,5 @@ export default function(props) {
       />
       <Pager {...props} />
     </div>
-  );
-}
+  )
+}

+ 26 - 26
frontend/src/components/users/rank/pager.js

@@ -1,10 +1,9 @@
-/* jshint ignore:start */
-import React from 'react';
-import { Link } from 'react-router';
-import resetScroll from 'misago/utils/reset-scroll';
+import React from "react"
+import { Link } from "react-router"
+import resetScroll from "misago/utils/reset-scroll"
 
 export default function(props) {
-  if (props.pages === 1) return null;
+  if (props.pages === 1) return null
 
   return (
     <div className="row row-toolbar">
@@ -23,7 +22,7 @@ export default function(props) {
         </div>
       </div>
     </div>
-  );
+  )
 }
 
 export function Pager(props) {
@@ -42,7 +41,7 @@ export function Pager(props) {
         <LastPage {...props} />
       </div>
     </div>
-  );
+  )
 }
 
 export function FirstPage(props) {
@@ -56,7 +55,7 @@ export function FirstPage(props) {
       >
         <span className="material-icon">first_page</span>
       </Link>
-    );
+    )
   } else {
     return (
       <span
@@ -65,15 +64,15 @@ export function FirstPage(props) {
       >
         <span className="material-icon">first_page</span>
       </span>
-    );
+    )
   }
 }
 
 export function PreviousPage(props) {
   if (props.isLoaded && props.page > 1) {
-    let previousUrl = '';
+    let previousUrl = ""
     if (props.previous) {
-      previousUrl = props.previous + '/';
+      previousUrl = props.previous + "/"
     }
 
     return (
@@ -85,7 +84,7 @@ export function PreviousPage(props) {
       >
         <span className="material-icon">chevron_left</span>
       </Link>
-    );
+    )
   } else {
     return (
       <span
@@ -94,15 +93,15 @@ export function PreviousPage(props) {
       >
         <span className="material-icon">chevron_left</span>
       </span>
-    );
+    )
   }
 }
 
 export function NextPage(props) {
   if (props.isLoaded && props.more) {
-    let nextUrl = '';
+    let nextUrl = ""
     if (props.next) {
-      nextUrl = props.next + '/';
+      nextUrl = props.next + "/"
     }
 
     return (
@@ -114,7 +113,7 @@ export function NextPage(props) {
       >
         <span className="material-icon">chevron_right</span>
       </Link>
-    );
+    )
   } else {
     return (
       <span
@@ -123,7 +122,7 @@ export function NextPage(props) {
       >
         <span className="material-icon">chevron_right</span>
       </span>
-    );
+    )
   }
 }
 
@@ -133,12 +132,12 @@ export function LastPage(props) {
       <Link
         className="btn btn-default btn-block btn-icon btn-outline"
         onClick={resetScroll}
-        to={props.baseUrl + props.last + '/'}
+        to={props.baseUrl + props.last + "/"}
         title={gettext("Go to last page")}
       >
         <span className="material-icon">last_page</span>
       </Link>
-    );
+    )
   } else {
     return (
       <span
@@ -147,21 +146,22 @@ export function LastPage(props) {
       >
         <span className="material-icon">last_page</span>
       </span>
-    );
+    )
   }
 }
 
 export function More(props) {
-  let message = null;
+  let message = null
   if (props.more) {
     message = ngettext(
       "There is %(more)s more member with this role.",
       "There are %(more)s more members with this role.",
-      props.more);
-    message = interpolate(message, {'more': props.more}, true);
+      props.more
+    )
+    message = interpolate(message, { more: props.more }, true)
   } else {
-    message = gettext("There are no more members with this role.");
+    message = gettext("There are no more members with this role.")
   }
 
-  return <p>{message}</p>;
-}
+  return <p>{message}</p>
+}

+ 58 - 63
frontend/src/components/users/rank/root.js

@@ -1,71 +1,69 @@
-import React from 'react';
-import PageLead from 'misago/components/page-lead' // jshint ignore:line
-import List from 'misago/components/users/rank/list' // jshint ignore:line
-import ListLoading from 'misago/components/users/rank/list-loading' // jshint ignore:line
-import misago from 'misago/index';
-import { hydrate } from 'misago/reducers/users';
-import polls from 'misago/services/polls';
-import store from 'misago/services/store';
-import title from 'misago/services/page-title';
+import React from "react"
+import PageLead from "misago/components/page-lead"
+import List from "misago/components/users/rank/list"
+import ListLoading from "misago/components/users/rank/list-loading"
+import misago from "misago/index"
+import { hydrate } from "misago/reducers/users"
+import polls from "misago/services/polls"
+import store from "misago/services/store"
+import title from "misago/services/page-title"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
-    if (misago.has('USERS')) {
-      this.initWithPreloadedData(misago.pop('USERS'));
+    if (misago.has("USERS")) {
+      this.initWithPreloadedData(misago.pop("USERS"))
     } else {
-      this.initWithoutPreloadedData();
+      this.initWithoutPreloadedData()
     }
 
-    this.startPolling(props.params.page || 1);
+    this.startPolling(props.params.page || 1)
   }
 
   initWithPreloadedData(data) {
     this.state = Object.assign(data, {
       isLoaded: true
-    });
-    store.dispatch(hydrate(data.results));
+    })
+    store.dispatch(hydrate(data.results))
   }
 
   initWithoutPreloadedData() {
     this.state = {
       isLoaded: false
-    };
+    }
   }
 
   startPolling(page) {
     polls.start({
-      poll: 'rank-users',
-      url: misago.get('USERS_API'),
+      poll: "rank-users",
+      url: misago.get("USERS_API"),
       data: {
         rank: this.props.route.rank.id,
         page: page
       },
       frequency: 90 * 1000,
       update: this.update
-    });
+    })
   }
 
-  /* jshint ignore:start */
-  update = (data) => {
-    store.dispatch(hydrate(data.results));
+  update = data => {
+    store.dispatch(hydrate(data.results))
 
-    data.isLoaded = true;
-    this.setState(data);
-  };
-  /* jshint ignore:end */
+    data.isLoaded = true
+    this.setState(data)
+  }
 
   componentDidMount() {
     title.set({
       title: this.props.route.rank.name,
       page: this.props.params.page || null,
       parent: gettext("Users")
-    });
+    })
   }
 
   componentWillUnmount() {
-    polls.stop('rank-users');
+    polls.stop("rank-users")
   }
 
   componentWillReceiveProps(nextProps) {
@@ -74,68 +72,65 @@ export default class extends React.Component {
         title: this.props.route.rank.name,
         page: nextProps.params.page || null,
         parent: gettext("Users")
-      });
+      })
 
       this.setState({
         isLoaded: false
-      });
+      })
 
-      polls.stop('rank-users');
-      this.startPolling(nextProps.params.page);
+      polls.stop("rank-users")
+      this.startPolling(nextProps.params.page)
     }
   }
 
   getClassName() {
     if (this.props.route.rank.css_class) {
-      return 'rank-users-list rank-users-' + this.props.route.rank.css_class;
+      return "rank-users-list rank-users-" + this.props.route.rank.css_class
     } else {
-      return 'rank-users-list';
+      return "rank-users-list"
     }
   }
 
   getRankDescription() {
     if (this.props.route.rank.description) {
-      /* jshint ignore:start */
-      return <div className="rank-description">
-        <PageLead copy={this.props.route.rank.description.html} />
-      </div>;
-      /* jshint ignore:end */
+      return (
+        <div className="rank-description">
+          <PageLead copy={this.props.route.rank.description.html} />
+        </div>
+      )
     } else {
-      return null;
+      return null
     }
   }
 
   getComponent() {
     if (this.state.isLoaded) {
       if (this.state.count > 0) {
-        /* jshint ignore:start */
-        let baseUrl = misago.get('USERS_LIST_URL') + this.props.route.rank.slug + '/';
-        return <List baseUrl={baseUrl}
-                     users={this.props.users}
-                     {...this.state} />;
-        /* jshint ignore:end */
+        let baseUrl =
+          misago.get("USERS_LIST_URL") + this.props.route.rank.slug + "/"
+        return (
+          <List baseUrl={baseUrl} users={this.props.users} {...this.state} />
+        )
       } else {
-        /* jshint ignore:start */
-        return <p className="lead">
-          {gettext("There are no users with this rank at the moment.")}
-        </p>;
-        /* jshint ignore:end */
+        return (
+          <p className="lead">
+            {gettext("There are no users with this rank at the moment.")}
+          </p>
+        )
       }
     } else {
-      /* jshint ignore:start */
-      return <ListLoading />;
-      /* jshint ignore:end */
+      return <ListLoading />
     }
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div className={this.getClassName()}>
-      <div className="container">
-        {this.getRankDescription()}
-        {this.getComponent()}
+    return (
+      <div className={this.getClassName()}>
+        <div className="container">
+          {this.getRankDescription()}
+          {this.getComponent()}
+        </div>
       </div>
-    </div>;
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 27 - 31
frontend/src/components/users/root.js

@@ -1,15 +1,14 @@
-import React from 'react'; // jshint ignore:line
-import { connect } from 'react-redux';
-import DropdownToggle from 'misago/components/dropdown-toggle'; // jshint ignore:line
-import Nav from 'misago/components/users/nav'; // jshint ignore:line
-import ActivePosters from 'misago/components/users/active-posters/root'; // jshint ignore:line
-import Rank from 'misago/components/users/rank/root';
-import WithDropdown from 'misago/components/with-dropdown';
-import misago from 'misago/index';
+import React from "react"
+import { connect } from "react-redux"
+import DropdownToggle from "misago/components/dropdown-toggle"
+import Nav from "misago/components/users/nav"
+import ActivePosters from "misago/components/users/active-posters/root"
+import Rank from "misago/components/users/rank/root"
+import WithDropdown from "misago/components/with-dropdown"
+import misago from "misago/index"
 
 export default class extends WithDropdown {
   render() {
-    /* jshint ignore:start */
     return (
       <div className="page page-users-lists">
         <div className="page-header-bg">
@@ -19,12 +18,10 @@ export default class extends WithDropdown {
             </div>
             <div className="page-tabs">
               <div className="container">
-
                 <Nav
-                  lists={misago.get('USERS_LISTS')}
-                  baseUrl={misago.get('USERS_LIST_URL')}
+                  lists={misago.get("USERS_LISTS")}
+                  baseUrl={misago.get("USERS_LIST_URL")}
                 />
-
               </div>
             </div>
           </div>
@@ -32,44 +29,43 @@ export default class extends WithDropdown {
 
         {this.props.children}
       </div>
-    );
-    /* jshint ignore:end */
+    )
   }
 }
 
 export function select(store) {
   return {
-    'tick': store.tick.tick,
-    'user': store.auth.user,
-    'users': store.users
-  };
+    tick: store.tick.tick,
+    user: store.auth.user,
+    users: store.users
+  }
 }
 
 export function paths() {
-  let paths = [];
+  let paths = []
 
-  misago.get('USERS_LISTS').forEach(function(item) {
-    if (item.component === 'rank') {
+  misago.get("USERS_LISTS").forEach(function(item) {
+    if (item.component === "rank") {
       paths.push({
-        path: misago.get('USERS_LIST_URL') + item.slug + '/:page/',
+        path: misago.get("USERS_LIST_URL") + item.slug + "/:page/",
         component: connect(select)(Rank),
         rank: item
-      });
+      })
       paths.push({
-        path: misago.get('USERS_LIST_URL') + item.slug + '/',
+        path: misago.get("USERS_LIST_URL") + item.slug + "/",
         component: connect(select)(Rank),
         rank: item
-      });
-    } else if (item.component === 'active-posters'){
+      })
+    } else if (item.component === "active-posters") {
       paths.push({
-        path: misago.get('USERS_LIST_URL') + item.component + '/',
+        path: misago.get("USERS_LIST_URL") + item.component + "/",
         component: connect(select)(ActivePosters),
         extra: {
           name: item.name
         }
-      });
+      })
     }
-  });
+  })
 
-  return paths;
+  return paths
 }

+ 10 - 12
frontend/src/components/with-dropdown.js

@@ -1,33 +1,31 @@
-import React from 'react';
+import React from "react"
 
 export default class extends React.Component {
   constructor(props) {
-    super(props);
+    super(props)
 
     this.state = {
       dropdown: false
-    };
+    }
   }
 
-  /* jshint ignore:start */
   toggleNav = () => {
     this.setState({
       dropdown: !this.state.dropdown
-    });
-  };
+    })
+  }
 
   hideNav = () => {
     this.setState({
       dropdown: false
-    });
-  };
-  /* jshint ignore:end */
+    })
+  }
 
   getCompactNavClassName() {
     if (this.state.dropdown) {
-      return 'compact-nav open';
+      return "compact-nav open"
     } else {
-      return 'compact-nav';
+      return "compact-nav"
     }
   }
-}
+}

+ 14 - 22
frontend/src/components/yes-no-switch.js

@@ -1,59 +1,51 @@
-import React from 'react';
+import React from "react"
 
 export default class extends React.Component {
   getClassName() {
     if (this.props.value) {
-      return "btn btn-yes-no btn-yes-no-on";
+      return "btn btn-yes-no btn-yes-no-on"
     } else {
-      return "btn btn-yes-no btn-yes-no-off";
+      return "btn btn-yes-no btn-yes-no-off"
     }
   }
 
   getIcon() {
     if (!!this.props.value) {
-      return this.props.iconOn || 'check_box';
+      return this.props.iconOn || "check_box"
     } else {
-      return this.props.iconOff || 'check_box_outline_blank';
+      return this.props.iconOff || "check_box_outline_blank"
     }
   }
 
   getLabel() {
     if (!!this.props.value) {
-      return this.props.labelOn || gettext("yes");
+      return this.props.labelOn || gettext("yes")
     } else {
-      return this.props.labelOff || gettext("no");
+      return this.props.labelOff || gettext("no")
     }
   }
 
-  /* jshint ignore:start */
   toggle = () => {
     this.props.onChange({
       target: {
         value: !this.props.value
       }
-    });
-  };
-  /* jshint ignore:end */
+    })
+  }
 
   render() {
-    /* jshint ignore:start */
     return (
       <button
         type="button"
         onClick={this.toggle}
         className={this.getClassName()}
         id={this.props.id || null}
-        aria-describedby={this.props['aria-describedby'] || null}
+        aria-describedby={this.props["aria-describedby"] || null}
         disabled={this.props.disabled || false}
       >
-        <span className="material-icon">
-          {this.getIcon()}
-        </span>
-        <span className="btn-text">
-          {this.getLabel()}
-        </span>
+        <span className="material-icon">{this.getIcon()}</span>
+        <span className="btn-text">{this.getLabel()}</span>
       </button>
-    );
-    /* jshint ignore:end */
+    )
   }
-}
+}

+ 12 - 13
frontend/src/data/profile-details.js

@@ -1,25 +1,24 @@
-/* jshint ignore:start */
-import React from 'react';
-import { load } from 'misago/reducers/profile-details';
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
+import React from "react"
+import { load } from "misago/reducers/profile-details"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
 
 export default class extends React.Component {
   componentDidMount() {
-    const { data, dispatch, user } = this.props;
-    if (data && data.id === user.id) return;
+    const { data, dispatch, user } = this.props
+    if (data && data.id === user.id) return
 
     ajax.get(this.props.user.api.details).then(
-      (data) => {
-        dispatch(load(data));
+      data => {
+        dispatch(load(data))
       },
-      (rejection) => {
-        snackbar.apiError(rejection);
+      rejection => {
+        snackbar.apiError(rejection)
       }
     )
   }
 
   render() {
-    return this.props.children;
+    return this.props.children
   }
-}
+}

+ 18 - 18
frontend/src/index.js

@@ -1,9 +1,9 @@
-import OrderedList from 'misago/utils/ordered-list';
+import OrderedList from "misago/utils/ordered-list"
 
 export class Misago {
   constructor() {
-    this._initializers = [];
-    this._context = {};
+    this._initializers = []
+    this._context = {}
   }
 
   addInitializer(initializer) {
@@ -14,47 +14,47 @@ export class Misago {
 
       after: initializer.after,
       before: initializer.before
-    });
+    })
   }
 
   init(context) {
-    this._context = context;
+    this._context = context
 
-    var initOrder = new OrderedList(this._initializers).orderedValues();
+    var initOrder = new OrderedList(this._initializers).orderedValues()
     initOrder.forEach(initializer => {
-      initializer(this);
-    });
+      initializer(this)
+    })
   }
 
   // context accessors
   has(key) {
-    return !!this._context[key];
+    return !!this._context[key]
   }
 
   get(key, fallback) {
     if (this.has(key)) {
-      return this._context[key];
+      return this._context[key]
     } else {
-      return fallback || undefined;
+      return fallback || undefined
     }
   }
 
   pop(key) {
     if (this.has(key)) {
-      let value = this._context[key];
-      this._context[key] = null;
-      return value;
+      let value = this._context[key]
+      this._context[key] = null
+      return value
     } else {
-      return undefined;
+      return undefined
     }
   }
 }
 
 // create  singleton
-var misago = new Misago();
+var misago = new Misago()
 
 // expose it globally
-global.misago = misago;
+global.misago = misago
 
 // and export it for tests and stuff
-export default misago;
+export default misago

+ 5 - 5
frontend/src/initializers/ajax.js

@@ -1,11 +1,11 @@
-import misago from 'misago/index';
-import ajax from 'misago/services/ajax';
+import misago from "misago/index"
+import ajax from "misago/services/ajax"
 
 export default function initializer() {
-  ajax.init(misago.get('CSRF_COOKIE_NAME'));
+  ajax.init(misago.get("CSRF_COOKIE_NAME"))
 }
 
 misago.addInitializer({
-  name: 'ajax',
+  name: "ajax",
   initializer: initializer
-});
+})

+ 19 - 16
frontend/src/initializers/auth-sync.js

@@ -1,25 +1,28 @@
-import misago from 'misago/index';
-import { patch } from 'misago/reducers/auth';
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
+import misago from "misago/index"
+import { patch } from "misago/reducers/auth"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
-const AUTH_SYNC_RATE = 45; // sync user with backend every 45 seconds
+const AUTH_SYNC_RATE = 45 // sync user with backend every 45 seconds
 
 export default function initializer(context) {
-  if (context.get('isAuthenticated')) {
+  if (context.get("isAuthenticated")) {
     window.setInterval(function() {
-      ajax.get(context.get('AUTH_API')).then(function(data) {
-        store.dispatch(patch(data));
-      }, function(rejection) {
-        snackbar.apiError(rejection);
-      });
-    }, AUTH_SYNC_RATE * 1000);
+      ajax.get(context.get("AUTH_API")).then(
+        function(data) {
+          store.dispatch(patch(data))
+        },
+        function(rejection) {
+          snackbar.apiError(rejection)
+        }
+      )
+    }, AUTH_SYNC_RATE * 1000)
   }
 }
 
 misago.addInitializer({
-  name: 'auth-sync',
+  name: "auth-sync",
   initializer: initializer,
-  after: 'auth'
-});
+  after: "auth"
+})

+ 9 - 9
frontend/src/initializers/auth.js

@@ -1,15 +1,15 @@
-import misago from 'misago/index';
-import auth from 'misago/services/auth';
-import modal from 'misago/services/modal';
-import store from 'misago/services/store';
-import storage from 'misago/services/local-storage';
+import misago from "misago/index"
+import auth from "misago/services/auth"
+import modal from "misago/services/modal"
+import store from "misago/services/store"
+import storage from "misago/services/local-storage"
 
 export default function initializer() {
-  auth.init(store, storage, modal);
+  auth.init(store, storage, modal)
 }
 
 misago.addInitializer({
-  name: 'auth',
+  name: "auth",
   initializer: initializer,
-  after: 'store'
-});
+  after: "store"
+})

+ 8 - 8
frontend/src/initializers/captcha.js

@@ -1,14 +1,14 @@
-import misago from 'misago/index';
-import ajax from 'misago/services/ajax';
-import captcha from 'misago/services/captcha';
-import include from 'misago/services/include';
-import snackbar from 'misago/services/snackbar';
+import misago from "misago/index"
+import ajax from "misago/services/ajax"
+import captcha from "misago/services/captcha"
+import include from "misago/services/include"
+import snackbar from "misago/services/snackbar"
 
 export default function initializer(context) {
-  captcha.init(context, ajax, include, snackbar);
+  captcha.init(context, ajax, include, snackbar)
 }
 
 misago.addInitializer({
-  name: 'captcha',
+  name: "captcha",
   initializer: initializer
-});
+})

+ 11 - 12
frontend/src/initializers/components/accept-agreement.js

@@ -1,21 +1,20 @@
-/* jshint ignore:start */
-import React from 'react';
-import misago from 'misago/index';
-import AcceptAgreement from 'misago/components/accept-agreement';
-import mount from 'misago/utils/mount-component';
+import React from "react"
+import misago from "misago/index"
+import AcceptAgreement from "misago/components/accept-agreement"
+import mount from "misago/utils/mount-component"
 
 export default function initializer(context) {
-  if (document.getElementById('required-agreement-mount')) {
+  if (document.getElementById("required-agreement-mount")) {
     mount(
-      <AcceptAgreement api={context.get('REQUIRED_AGREEMENT_API')} />,
-      'required-agreement-mount',
+      <AcceptAgreement api={context.get("REQUIRED_AGREEMENT_API")} />,
+      "required-agreement-mount",
       false
-    );
+    )
   }
 }
 
 misago.addInitializer({
-  name: 'component:accept-agreement',
+  name: "component:accept-agreement",
   initializer: initializer,
-  after: 'store'
-});
+  after: "store"
+})

+ 8 - 8
frontend/src/initializers/components/auth-message.js

@@ -1,14 +1,14 @@
-import { connect } from 'react-redux';
-import misago from 'misago/index';
-import AuthMessage, { select } from 'misago/components/auth-message';
-import mount from 'misago/utils/mount-component';
+import { connect } from "react-redux"
+import misago from "misago/index"
+import AuthMessage, { select } from "misago/components/auth-message"
+import mount from "misago/utils/mount-component"
 
 export default function initializer() {
-  mount(connect(select)(AuthMessage), 'auth-message-mount');
+  mount(connect(select)(AuthMessage), "auth-message-mount")
 }
 
 misago.addInitializer({
-  name: 'component:auth-message',
+  name: "component:auth-message",
   initializer: initializer,
-  after: 'store'
-});
+  after: "store"
+})

+ 7 - 7
frontend/src/initializers/components/banned-page.js

@@ -1,14 +1,14 @@
-import misago from 'misago/index';
-import showBannedPage from 'misago/utils/banned-page';
+import misago from "misago/index"
+import showBannedPage from "misago/utils/banned-page"
 
 export default function initializer(context) {
-  if (context.has('BAN_MESSAGE')) {
-    showBannedPage(context.get('BAN_MESSAGE'), false);
+  if (context.has("BAN_MESSAGE")) {
+    showBannedPage(context.get("BAN_MESSAGE"), false)
   }
 }
 
 misago.addInitializer({
-  name: 'component:banmed-page',
+  name: "component:banmed-page",
   initializer: initializer,
-  after: 'store'
-});
+  after: "store"
+})

+ 9 - 9
frontend/src/initializers/components/categories.js

@@ -1,16 +1,16 @@
-import { connect } from 'react-redux';
-import Categories, { select } from 'misago/components/categories';
-import misago from 'misago/index';
-import mount from 'misago/utils/mount-component';
+import { connect } from "react-redux"
+import Categories, { select } from "misago/components/categories"
+import misago from "misago/index"
+import mount from "misago/utils/mount-component"
 
 export default function initializer() {
-  if (document.getElementById('categories-mount')) {
-    mount(connect(select)(Categories), 'categories-mount');
+  if (document.getElementById("categories-mount")) {
+    mount(connect(select)(Categories), "categories-mount")
   }
 }
 
 misago.addInitializer({
-  name: 'component:categories',
+  name: "component:categories",
   initializer: initializer,
-  after: 'store'
-});
+  after: "store"
+})

+ 9 - 9
frontend/src/initializers/components/options.js

@@ -1,19 +1,19 @@
-import Options, { paths } from 'misago/components/options/root';
-import misago from 'misago/index';
-import mount from 'misago/utils/routed-component';
+import Options, { paths } from "misago/components/options/root"
+import misago from "misago/index"
+import mount from "misago/utils/routed-component"
 
 export default function initializer(context) {
-  if (context.has('USER_OPTIONS')) {
+  if (context.has("USER_OPTIONS")) {
     mount({
-      root: misago.get('USERCP_URL'),
+      root: misago.get("USERCP_URL"),
       component: Options,
       paths: paths()
-    });
+    })
   }
 }
 
 misago.addInitializer({
-  name: 'component:options',
+  name: "component:options",
   initializer: initializer,
-  after: 'store'
-});
+  after: "store"
+})

+ 10 - 10
frontend/src/initializers/components/profile.js

@@ -1,20 +1,20 @@
-import { connect } from 'react-redux';
-import Profile, { paths, select } from 'misago/components/profile/root';
-import misago from 'misago/index';
-import mount from 'misago/utils/routed-component';
+import { connect } from "react-redux"
+import Profile, { paths, select } from "misago/components/profile/root"
+import misago from "misago/index"
+import mount from "misago/utils/routed-component"
 
 export default function initializer(context) {
-  if (context.has('PROFILE') && context.has('PROFILE_PAGES')) {
+  if (context.has("PROFILE") && context.has("PROFILE_PAGES")) {
     mount({
-      root: misago.get('PROFILE').url,
+      root: misago.get("PROFILE").url,
       component: connect(select)(Profile),
       paths: paths()
-    });
+    })
   }
 }
 
 misago.addInitializer({
-  name: 'component:profile',
+  name: "component:profile",
   initializer: initializer,
-  after: 'reducer:profile-hydrate'
-});
+  after: "reducer:profile-hydrate"
+})

+ 8 - 8
frontend/src/initializers/components/request-activation-link.js

@@ -1,15 +1,15 @@
-import misago from 'misago/index';
-import RequestActivationLink from 'misago/components/request-activation-link';
-import mount from 'misago/utils/mount-component';
+import misago from "misago/index"
+import RequestActivationLink from "misago/components/request-activation-link"
+import mount from "misago/utils/mount-component"
 
 export default function initializer() {
-  if (document.getElementById('request-activation-link-mount')) {
-    mount(RequestActivationLink, 'request-activation-link-mount', false);
+  if (document.getElementById("request-activation-link-mount")) {
+    mount(RequestActivationLink, "request-activation-link-mount", false)
   }
 }
 
 misago.addInitializer({
-  name: 'component:request-activation-link',
+  name: "component:request-activation-link",
   initializer: initializer,
-  after: 'store'
-});
+  after: "store"
+})

+ 8 - 8
frontend/src/initializers/components/request-password-reset.js

@@ -1,15 +1,15 @@
-import misago from 'misago/index';
-import RequestPasswordReset from 'misago/components/request-password-reset';
-import mount from 'misago/utils/mount-component';
+import misago from "misago/index"
+import RequestPasswordReset from "misago/components/request-password-reset"
+import mount from "misago/utils/mount-component"
 
 export default function initializer() {
-  if (document.getElementById('request-password-reset-mount')) {
-    mount(RequestPasswordReset, 'request-password-reset-mount', false);
+  if (document.getElementById("request-password-reset-mount")) {
+    mount(RequestPasswordReset, "request-password-reset-mount", false)
   }
 }
 
 misago.addInitializer({
-  name: 'component:request-password-reset',
+  name: "component:request-password-reset",
   initializer: initializer,
-  after: 'store'
-});
+  after: "store"
+})

+ 8 - 8
frontend/src/initializers/components/reset-password-form.js

@@ -1,15 +1,15 @@
-import misago from 'misago';
-import ResetPasswordForm from 'misago/components/reset-password-form';
-import mount from 'misago/utils/mount-component';
+import misago from "misago"
+import ResetPasswordForm from "misago/components/reset-password-form"
+import mount from "misago/utils/mount-component"
 
 export default function initializer() {
-  if (document.getElementById('reset-password-form-mount')) {
-    mount(ResetPasswordForm, 'reset-password-form-mount', false);
+  if (document.getElementById("reset-password-form-mount")) {
+    mount(ResetPasswordForm, "reset-password-form-mount", false)
   }
 }
 
 misago.addInitializer({
-  name: 'component:reset-password-form',
+  name: "component:reset-password-form",
   initializer: initializer,
-  after: 'store'
-});
+  after: "store"
+})

+ 9 - 9
frontend/src/initializers/components/search.js

@@ -1,17 +1,17 @@
-import paths from 'misago/components/search';
-import misago from 'misago';
-import mount from 'misago/utils/routed-component';
+import paths from "misago/components/search"
+import misago from "misago"
+import mount from "misago/utils/routed-component"
 
 export default function initializer(context) {
-  if (context.get('CURRENT_LINK') === 'misago:search') {
+  if (context.get("CURRENT_LINK") === "misago:search") {
     mount({
-      paths: paths(misago.get('SEARCH_PROVIDERS'))
-    });
+      paths: paths(misago.get("SEARCH_PROVIDERS"))
+    })
   }
 }
 
 misago.addInitializer({
-  name: 'component:search',
+  name: "component:search",
   initializer: initializer,
-  after: 'store'
-});
+  after: "store"
+})

+ 8 - 8
frontend/src/initializers/components/snackbar.js

@@ -1,14 +1,14 @@
-import { connect } from 'react-redux';
-import misago from 'misago/index';
-import { Snackbar, select } from 'misago/components/snackbar';
-import mount from 'misago/utils/mount-component';
+import { connect } from "react-redux"
+import misago from "misago/index"
+import { Snackbar, select } from "misago/components/snackbar"
+import mount from "misago/utils/mount-component"
 
 export default function initializer() {
-  mount(connect(select)(Snackbar), 'snackbar-mount');
+  mount(connect(select)(Snackbar), "snackbar-mount")
 }
 
 misago.addInitializer({
-  name: 'component:snackbar',
+  name: "component:snackbar",
   initializer: initializer,
-  after: 'snackbar'
-});
+  after: "snackbar"
+})

+ 10 - 11
frontend/src/initializers/components/social-auth.js

@@ -1,18 +1,17 @@
-// jshint ignore:start
-import React from 'react';
-import SocialAuth from 'misago/components/social-auth';
-import misago from 'misago';
-import mount from 'misago/utils/mount-component';
+import React from "react"
+import SocialAuth from "misago/components/social-auth"
+import misago from "misago"
+import mount from "misago/utils/mount-component"
 
 export default function initializer(context) {
-  if (context.get('CURRENT_LINK') === 'social:complete') {
-    const props = context.get('SOCIAL_AUTH');
-    mount(<SocialAuth {...props} />, 'page-mount');
+  if (context.get("CURRENT_LINK") === "social:complete") {
+    const props = context.get("SOCIAL_AUTH")
+    mount(<SocialAuth {...props} />, "page-mount")
   }
 }
 
 misago.addInitializer({
-  name: 'component:social-auth',
+  name: "component:social-auth",
   initializer: initializer,
-  after: 'store'
-});
+  after: "store"
+})

+ 8 - 8
frontend/src/initializers/components/thread.js

@@ -1,17 +1,17 @@
-import { paths } from 'misago/components/thread/root';
-import misago from 'misago/index';
-import mount from 'misago/utils/routed-component';
+import { paths } from "misago/components/thread/root"
+import misago from "misago/index"
+import mount from "misago/utils/routed-component"
 
 export default function initializer(context) {
-  if (context.has('THREAD') && context.has('POSTS')) {
+  if (context.has("THREAD") && context.has("POSTS")) {
     mount({
       paths: paths()
-    });
+    })
   }
 }
 
 misago.addInitializer({
-  name: 'component:thread',
+  name: "component:thread",
   initializer: initializer,
-  after: 'store'
-});
+  after: "store"
+})

+ 23 - 19
frontend/src/initializers/components/threads.js

@@ -1,39 +1,43 @@
-import { paths } from 'misago/components/threads/root';
-import misago from 'misago/index';
-import mount from 'misago/utils/routed-component';
+import { paths } from "misago/components/threads/root"
+import misago from "misago/index"
+import mount from "misago/utils/routed-component"
 
-const PRIVATE_THREADS_LIST = 'misago:private-threads';
+const PRIVATE_THREADS_LIST = "misago:private-threads"
 
 export default function initializer(context) {
-  if (context.has('THREADS') && context.has('CATEGORIES')) {
+  if (context.has("THREADS") && context.has("CATEGORIES")) {
     mount({
-      paths: paths(context.get('user'), getListOptions(context))
-    });
+      paths: paths(context.get("user"), getListOptions(context))
+    })
   }
 }
 
 export function getListOptions(context) {
-  const currentLink = context.get('CURRENT_LINK');
-  if (currentLink.substr(0, PRIVATE_THREADS_LIST.length) === PRIVATE_THREADS_LIST) {
+  const currentLink = context.get("CURRENT_LINK")
+  if (
+    currentLink.substr(0, PRIVATE_THREADS_LIST.length) === PRIVATE_THREADS_LIST
+  ) {
     return {
-      api: context.get('PRIVATE_THREADS_API'),
+      api: context.get("PRIVATE_THREADS_API"),
       startThread: {
-        mode: 'START_PRIVATE',
-        submit: misago.get('PRIVATE_THREADS_API')
+        mode: "START_PRIVATE",
+        submit: misago.get("PRIVATE_THREADS_API")
       },
       title: gettext("Private threads"),
-      pageLead: gettext("Private threads are threads which only those that started them and those they have invited may see and participate in."),
+      pageLead: gettext(
+        "Private threads are threads which only those that started them and those they have invited may see and participate in."
+      ),
       emptyMessage: gettext("You aren't participating in any private threads.")
-    };
+    }
   }
 
   return {
-    'api': context.get('THREADS_API')
-  };
+    api: context.get("THREADS_API")
+  }
 }
 
 misago.addInitializer({
-  name: 'component:threads',
+  name: "component:threads",
   initializer: initializer,
-  after: 'store'
-});
+  after: "store"
+})

+ 13 - 9
frontend/src/initializers/components/user-menu.js

@@ -1,15 +1,19 @@
-import { connect } from 'react-redux';
-import misago from 'misago/index';
-import { UserMenu, CompactUserMenu, select } from 'misago/components/user-menu/root';
-import mount from 'misago/utils/mount-component';
+import { connect } from "react-redux"
+import misago from "misago/index"
+import {
+  UserMenu,
+  CompactUserMenu,
+  select
+} from "misago/components/user-menu/root"
+import mount from "misago/utils/mount-component"
 
 export default function initializer() {
-  mount(connect(select)(UserMenu), 'user-menu-mount');
-  mount(connect(select)(CompactUserMenu), 'user-menu-compact-mount');
+  mount(connect(select)(UserMenu), "user-menu-mount")
+  mount(connect(select)(CompactUserMenu), "user-menu-compact-mount")
 }
 
 misago.addInitializer({
-  name: 'component:user-menu',
+  name: "component:user-menu",
   initializer: initializer,
-  after: 'store'
-});
+  after: "store"
+})

+ 9 - 9
frontend/src/initializers/components/users.js

@@ -1,19 +1,19 @@
-import Users, { paths } from 'misago/components/users/root';
-import misago from 'misago/index';
-import mount from 'misago/utils/routed-component';
+import Users, { paths } from "misago/components/users/root"
+import misago from "misago/index"
+import mount from "misago/utils/routed-component"
 
 export default function initializer(context) {
-  if (context.has('USERS_LISTS')) {
+  if (context.has("USERS_LISTS")) {
     mount({
-      root: misago.get('USERS_LIST_URL'),
+      root: misago.get("USERS_LIST_URL"),
       component: Users,
       paths: paths()
-    });
+    })
   }
 }
 
 misago.addInitializer({
-  name: 'component:users',
+  name: "component:users",
   initializer: initializer,
-  after: 'store'
-});
+  after: "store"
+})

+ 5 - 5
frontend/src/initializers/include.js

@@ -1,11 +1,11 @@
-import misago from 'misago/index';
-import include from 'misago/services/include';
+import misago from "misago/index"
+import include from "misago/services/include"
 
 export default function initializer(context) {
-  include.init(context.get('STATIC_URL'));
+  include.init(context.get("STATIC_URL"))
 }
 
 misago.addInitializer({
-  name: 'include',
+  name: "include",
   initializer: initializer
-});
+})

+ 5 - 5
frontend/src/initializers/local-storage.js

@@ -1,11 +1,11 @@
-import misago from 'misago/index';
-import storage from 'misago/services/local-storage';
+import misago from "misago/index"
+import storage from "misago/services/local-storage"
 
 export default function initializer() {
-  storage.init('misago_');
+  storage.init("misago_")
 }
 
 misago.addInitializer({
-  name: 'local-storage',
+  name: "local-storage",
   initializer: initializer
-});
+})

+ 7 - 7
frontend/src/initializers/mobile-navbar-dropdown.js

@@ -1,15 +1,15 @@
-import misago from 'misago/index';
-import dropdown from 'misago/services/mobile-navbar-dropdown';
+import misago from "misago/index"
+import dropdown from "misago/services/mobile-navbar-dropdown"
 
 export default function initializer() {
-  let element = document.getElementById('mobile-navbar-dropdown-mount');
+  let element = document.getElementById("mobile-navbar-dropdown-mount")
   if (element) {
-    dropdown.init(element);
+    dropdown.init(element)
   }
 }
 
 misago.addInitializer({
-  name: 'dropdown',
+  name: "dropdown",
   initializer: initializer,
-  before: 'store'
-});
+  before: "store"
+})

+ 7 - 7
frontend/src/initializers/modal.js

@@ -1,15 +1,15 @@
-import misago from 'misago/index';
-import modal from 'misago/services/modal';
+import misago from "misago/index"
+import modal from "misago/services/modal"
 
 export default function initializer() {
-  let element = document.getElementById('modal-mount');
+  let element = document.getElementById("modal-mount")
   if (element) {
-    modal.init(element);
+    modal.init(element)
   }
 }
 
 misago.addInitializer({
-  name: 'modal',
+  name: "modal",
   initializer: initializer,
-  before: 'store'
-});
+  before: "store"
+})

+ 5 - 5
frontend/src/initializers/moment-locale.js

@@ -1,11 +1,11 @@
-import moment from 'moment';
-import misago from 'misago/index';
+import moment from "moment"
+import misago from "misago/index"
 
 export default function initializer() {
-  moment.locale($('html').attr('lang'));
+  moment.locale($("html").attr("lang"))
 }
 
 misago.addInitializer({
-  name: 'moment',
+  name: "moment",
   initializer: initializer
-});
+})

+ 7 - 7
frontend/src/initializers/page-title.js

@@ -1,14 +1,14 @@
-import misago from 'misago/index';
-import title from 'misago/services/page-title';
+import misago from "misago/index"
+import title from "misago/services/page-title"
 
 export default function initializer(context) {
   title.init(
-    context.get('SETTINGS').forum_index_title,
-    context.get('SETTINGS').forum_name
-  );
+    context.get("SETTINGS").forum_index_title,
+    context.get("SETTINGS").forum_name
+  )
 }
 
 misago.addInitializer({
-  name: 'page-title',
+  name: "page-title",
   initializer: initializer
-});
+})

+ 7 - 7
frontend/src/initializers/polls.js

@@ -1,13 +1,13 @@
-import misago from 'misago/index';
-import ajax from 'misago/services/ajax';
-import snackbar from 'misago/services/snackbar';
-import polls from 'misago/services/polls';
+import misago from "misago/index"
+import ajax from "misago/services/ajax"
+import snackbar from "misago/services/snackbar"
+import polls from "misago/services/polls"
 
 export default function initializer() {
-  polls.init(ajax, snackbar);
+  polls.init(ajax, snackbar)
 }
 
 misago.addInitializer({
-  name: 'polls',
+  name: "polls",
   initializer: initializer
-});
+})

+ 7 - 7
frontend/src/initializers/posting.js

@@ -1,13 +1,13 @@
-import misago from 'misago/index';
-import ajax from 'misago/services/ajax';
-import posting from 'misago/services/posting';
-import snackbar from 'misago/services/snackbar';
+import misago from "misago/index"
+import ajax from "misago/services/ajax"
+import posting from "misago/services/posting"
+import snackbar from "misago/services/snackbar"
 
 export default function initializer() {
-  posting.init(ajax, snackbar, document.getElementById('posting-placeholder'));
+  posting.init(ajax, snackbar, document.getElementById("posting-placeholder"))
 }
 
 misago.addInitializer({
-  name: 'posting',
+  name: "posting",
   initializer: initializer
-});
+})

+ 18 - 11
frontend/src/initializers/reducers/auth.js

@@ -1,18 +1,25 @@
-import misago from 'misago/index';
-import reducer, { initialState } from 'misago/reducers/auth';
-import store from 'misago/services/store';
+import misago from "misago/index"
+import reducer, { initialState } from "misago/reducers/auth"
+import store from "misago/services/store"
 
 export default function initializer(context) {
-  store.addReducer('auth', reducer, Object.assign({
-    isAuthenticated: context.get('isAuthenticated'),
-    isAnonymous: !context.get('isAuthenticated'),
+  store.addReducer(
+    "auth",
+    reducer,
+    Object.assign(
+      {
+        isAuthenticated: context.get("isAuthenticated"),
+        isAnonymous: !context.get("isAuthenticated"),
 
-    user: context.get('user')
-  }, initialState));
+        user: context.get("user")
+      },
+      initialState
+    )
+  )
 }
 
 misago.addInitializer({
-  name: 'reducer:auth',
+  name: "reducer:auth",
   initializer: initializer,
-  before: 'store'
-});
+  before: "store"
+})

+ 10 - 10
frontend/src/initializers/reducers/participants.js

@@ -1,18 +1,18 @@
-import misago from 'misago/index';
-import reducer from 'misago/reducers/participants';
-import store from 'misago/services/store';
+import misago from "misago/index"
+import reducer from "misago/reducers/participants"
+import store from "misago/services/store"
 
 export default function initializer() {
-  let initialState = null;
-  if (misago.has('THREAD')) {
-    initialState = misago.get('THREAD').participants;
+  let initialState = null
+  if (misago.has("THREAD")) {
+    initialState = misago.get("THREAD").participants
   }
 
-  store.addReducer('participants', reducer, initialState || []);
+  store.addReducer("participants", reducer, initialState || [])
 }
 
 misago.addInitializer({
-  name: 'reducer:participants',
+  name: "reducer:participants",
   initializer: initializer,
-  before: 'store'
-});
+  before: "store"
+})

+ 11 - 11
frontend/src/initializers/reducers/poll.js

@@ -1,22 +1,22 @@
-import misago from 'misago/index';
-import reducer, { hydrate } from 'misago/reducers/poll';
-import store from 'misago/services/store';
+import misago from "misago/index"
+import reducer, { hydrate } from "misago/reducers/poll"
+import store from "misago/services/store"
 
 export default function initializer() {
-  let initialState = null;
-  if (misago.has('THREAD') && misago.get('THREAD').poll) {
-    initialState = hydrate(misago.get('THREAD').poll);
+  let initialState = null
+  if (misago.has("THREAD") && misago.get("THREAD").poll) {
+    initialState = hydrate(misago.get("THREAD").poll)
   } else {
     initialState = {
       isBusy: false
-    };
+    }
   }
 
-  store.addReducer('poll', reducer, initialState);
+  store.addReducer("poll", reducer, initialState)
 }
 
 misago.addInitializer({
-  name: 'reducer:poll',
+  name: "reducer:poll",
   initializer: initializer,
-  before: 'store'
-});
+  before: "store"
+})

+ 11 - 11
frontend/src/initializers/reducers/posts.js

@@ -1,23 +1,23 @@
-import misago from 'misago/index';
-import reducer, { hydrate } from 'misago/reducers/posts';
-import store from 'misago/services/store';
+import misago from "misago/index"
+import reducer, { hydrate } from "misago/reducers/posts"
+import store from "misago/services/store"
 
 export default function initializer() {
-  let initialState = null;
-  if (misago.has('POSTS')) {
-    initialState = hydrate(misago.get('POSTS'));
+  let initialState = null
+  if (misago.has("POSTS")) {
+    initialState = hydrate(misago.get("POSTS"))
   } else {
     initialState = {
       isLoaded: false,
       isBusy: false
-    };
+    }
   }
 
-  store.addReducer('posts', reducer, initialState);
+  store.addReducer("posts", reducer, initialState)
 }
 
 misago.addInitializer({
-  name: 'reducer:posts',
+  name: "reducer:posts",
   initializer: initializer,
-  before: 'store'
-});
+  before: "store"
+})

+ 10 - 10
frontend/src/initializers/reducers/profile-details.js

@@ -1,18 +1,18 @@
-import misago from 'misago/index';
-import reducer from 'misago/reducers/profile-details';
-import store from 'misago/services/store';
+import misago from "misago/index"
+import reducer from "misago/reducers/profile-details"
+import store from "misago/services/store"
 
 export default function initializer() {
-  let initialState = null;
-  if (misago.has('PROFILE_DETAILS')) {
-    initialState = misago.get('PROFILE_DETAILS');
+  let initialState = null
+  if (misago.has("PROFILE_DETAILS")) {
+    initialState = misago.get("PROFILE_DETAILS")
   }
 
-  store.addReducer('profile-details', reducer, initialState || {});
+  store.addReducer("profile-details", reducer, initialState || {})
 }
 
 misago.addInitializer({
-  name: 'reducer:profile-details',
+  name: "reducer:profile-details",
   initializer: initializer,
-  before: 'store'
-});
+  before: "store"
+})

+ 8 - 8
frontend/src/initializers/reducers/profile-hydrate.js

@@ -1,15 +1,15 @@
-import misago from 'misago/index';
-import { hydrate } from 'misago/reducers/profile';
-import store from 'misago/services/store';
+import misago from "misago/index"
+import { hydrate } from "misago/reducers/profile"
+import store from "misago/services/store"
 
 export default function initializer() {
-  if (misago.has('PROFILE')) {
-    store.dispatch(hydrate(misago.get('PROFILE')));
+  if (misago.has("PROFILE")) {
+    store.dispatch(hydrate(misago.get("PROFILE")))
   }
 }
 
 misago.addInitializer({
-  name: 'reducer:profile-hydrate',
+  name: "reducer:profile-hydrate",
   initializer: initializer,
-  after: 'store'
-});
+  after: "store"
+})

+ 7 - 7
frontend/src/initializers/reducers/profile.js

@@ -1,13 +1,13 @@
-import misago from 'misago/index';
-import reducer from 'misago/reducers/profile';
-import store from 'misago/services/store';
+import misago from "misago/index"
+import reducer from "misago/reducers/profile"
+import store from "misago/services/store"
 
 export default function initializer() {
-  store.addReducer('profile', reducer, {});
+  store.addReducer("profile", reducer, {})
 }
 
 misago.addInitializer({
-  name: 'reducer:profile',
+  name: "reducer:profile",
   initializer: initializer,
-  before: 'store'
-});
+  before: "store"
+})

+ 14 - 10
frontend/src/initializers/reducers/search.js

@@ -1,16 +1,20 @@
-import misago from 'misago';
-import reducer, { initialState } from 'misago/reducers/search';
-import store from 'misago/services/store';
+import misago from "misago"
+import reducer, { initialState } from "misago/reducers/search"
+import store from "misago/services/store"
 
 export default function initializer() {
-  store.addReducer('search', reducer, Object.assign({}, initialState, {
-    providers: misago.get('SEARCH_PROVIDERS') || [],
-    query: misago.get('SEARCH_QUERY') || ''
-  }));
+  store.addReducer(
+    "search",
+    reducer,
+    Object.assign({}, initialState, {
+      providers: misago.get("SEARCH_PROVIDERS") || [],
+      query: misago.get("SEARCH_QUERY") || ""
+    })
+  )
 }
 
 misago.addInitializer({
-  name: 'reducer:search',
+  name: "reducer:search",
   initializer: initializer,
-  before: 'store'
-});
+  before: "store"
+})

+ 7 - 7
frontend/src/initializers/reducers/selection.js

@@ -1,13 +1,13 @@
-import misago from 'misago/index';
-import reducer from 'misago/reducers/selection';
-import store from 'misago/services/store';
+import misago from "misago/index"
+import reducer from "misago/reducers/selection"
+import store from "misago/services/store"
 
 export default function initializer() {
-  store.addReducer('selection', reducer, []);
+  store.addReducer("selection", reducer, [])
 }
 
 misago.addInitializer({
-  name: 'reducer:selection',
+  name: "reducer:selection",
   initializer: initializer,
-  before: 'store'
-});
+  before: "store"
+})

+ 7 - 7
frontend/src/initializers/reducers/snackbar.js

@@ -1,13 +1,13 @@
-import misago from 'misago/index';
-import reducer, { initialState } from 'misago/reducers/snackbar';
-import store from 'misago/services/store';
+import misago from "misago/index"
+import reducer, { initialState } from "misago/reducers/snackbar"
+import store from "misago/services/store"
 
 export default function initializer() {
-  store.addReducer('snackbar', reducer, initialState);
+  store.addReducer("snackbar", reducer, initialState)
 }
 
 misago.addInitializer({
-  name: 'reducer:snackbar',
+  name: "reducer:snackbar",
   initializer: initializer,
-  before: 'store'
-});
+  before: "store"
+})

+ 11 - 11
frontend/src/initializers/reducers/thread.js

@@ -1,22 +1,22 @@
-import misago from 'misago/index';
-import reducer, { hydrate } from 'misago/reducers/thread';
-import store from 'misago/services/store';
+import misago from "misago/index"
+import reducer, { hydrate } from "misago/reducers/thread"
+import store from "misago/services/store"
 
 export default function initializer() {
-  let initialState = null;
-  if (misago.has('THREAD')) {
-    initialState = hydrate(misago.get('THREAD'));
+  let initialState = null
+  if (misago.has("THREAD")) {
+    initialState = hydrate(misago.get("THREAD"))
   } else {
     initialState = {
       isBusy: false
-    };
+    }
   }
 
-  store.addReducer('thread', reducer, initialState);
+  store.addReducer("thread", reducer, initialState)
 }
 
 misago.addInitializer({
-  name: 'reducer:thread',
+  name: "reducer:thread",
   initializer: initializer,
-  before: 'store'
-});
+  before: "store"
+})

+ 7 - 7
frontend/src/initializers/reducers/threads.js

@@ -1,13 +1,13 @@
-import misago from 'misago/index';
-import reducer from 'misago/reducers/threads';
-import store from 'misago/services/store';
+import misago from "misago/index"
+import reducer from "misago/reducers/threads"
+import store from "misago/services/store"
 
 export default function initializer() {
-  store.addReducer('threads', reducer, []);
+  store.addReducer("threads", reducer, [])
 }
 
 misago.addInitializer({
-  name: 'reducer:threads',
+  name: "reducer:threads",
   initializer: initializer,
-  before: 'store'
-});
+  before: "store"
+})

+ 7 - 7
frontend/src/initializers/reducers/tick.js

@@ -1,13 +1,13 @@
-import misago from 'misago/index';
-import reducer, { initialState } from 'misago/reducers/tick';
-import store from 'misago/services/store';
+import misago from "misago/index"
+import reducer, { initialState } from "misago/reducers/tick"
+import store from "misago/services/store"
 
 export default function initializer() {
-  store.addReducer('tick', reducer, initialState);
+  store.addReducer("tick", reducer, initialState)
 }
 
 misago.addInitializer({
-  name: 'reducer:tick',
+  name: "reducer:tick",
   initializer: initializer,
-  before: 'store'
-});
+  before: "store"
+})

+ 7 - 7
frontend/src/initializers/reducers/username-history.js

@@ -1,13 +1,13 @@
-import misago from 'misago/index';
-import reducer from 'misago/reducers/username-history';
-import store from 'misago/services/store';
+import misago from "misago/index"
+import reducer from "misago/reducers/username-history"
+import store from "misago/services/store"
 
 export default function initializer() {
-  store.addReducer('username-history', reducer, []);
+  store.addReducer("username-history", reducer, [])
 }
 
 misago.addInitializer({
-  name: 'reducer:username-history',
+  name: "reducer:username-history",
   initializer: initializer,
-  before: 'store'
-});
+  before: "store"
+})

+ 7 - 7
frontend/src/initializers/reducers/users.js

@@ -1,13 +1,13 @@
-import misago from 'misago/index';
-import reducer from 'misago/reducers/users';
-import store from 'misago/services/store';
+import misago from "misago/index"
+import reducer from "misago/reducers/users"
+import store from "misago/services/store"
 
 export default function initializer() {
-  store.addReducer('users', reducer, []);
+  store.addReducer("users", reducer, [])
 }
 
 misago.addInitializer({
-  name: 'reducer:users',
+  name: "reducer:users",
   initializer: initializer,
-  before: 'store'
-});
+  before: "store"
+})

+ 7 - 7
frontend/src/initializers/snackbar.js

@@ -1,13 +1,13 @@
-import misago from 'misago/index';
-import snackbar from 'misago/services/snackbar';
-import store from 'misago/services/store';
+import misago from "misago/index"
+import snackbar from "misago/services/snackbar"
+import store from "misago/services/store"
 
 export default function initializer() {
-  snackbar.init(store);
+  snackbar.init(store)
 }
 
 misago.addInitializer({
-  name: 'snackbar',
+  name: "snackbar",
   initializer: initializer,
-  after: 'store'
-});
+  after: "store"
+})

+ 6 - 6
frontend/src/initializers/store.js

@@ -1,12 +1,12 @@
-import misago from 'misago/index';
-import store from 'misago/services/store';
+import misago from "misago/index"
+import store from "misago/services/store"
 
 export default function initializer() {
-  store.init();
+  store.init()
 }
 
 misago.addInitializer({
-  name: 'store',
+  name: "store",
   initializer: initializer,
-  before: '_end'
-});
+  before: "_end"
+})

+ 9 - 9
frontend/src/initializers/tick-start.js

@@ -1,17 +1,17 @@
-import misago from 'misago/index';
-import { doTick } from 'misago/reducers/tick';
-import store from 'misago/services/store';
+import misago from "misago/index"
+import { doTick } from "misago/reducers/tick"
+import store from "misago/services/store"
 
-const TICK_PERIOD = 50 * 1000; //do the tick every 50s
+const TICK_PERIOD = 50 * 1000 //do the tick every 50s
 
 export default function initializer() {
   window.setInterval(function() {
-    store.dispatch(doTick());
-  }, TICK_PERIOD);
+    store.dispatch(doTick())
+  }, TICK_PERIOD)
 }
 
 misago.addInitializer({
-  name: 'tick-start',
+  name: "tick-start",
   initializer: initializer,
-  after: 'store'
-});
+  after: "store"
+})

+ 6 - 6
frontend/src/initializers/zxcvbn.js

@@ -1,12 +1,12 @@
-import misago from 'misago/index';
-import include from 'misago/services/include';
-import zxcvbn from 'misago/services/zxcvbn';
+import misago from "misago/index"
+import include from "misago/services/include"
+import zxcvbn from "misago/services/zxcvbn"
 
 export default function initializer() {
-  zxcvbn.init(include);
+  zxcvbn.init(include)
 }
 
 misago.addInitializer({
-  name: 'zxcvbn',
+  name: "zxcvbn",
   initializer: initializer
-});
+})

+ 25 - 25
frontend/src/reducers/auth.js

@@ -1,76 +1,76 @@
-import { UPDATE_AVATAR, UPDATE_USERNAME } from 'misago/reducers/users';
+import { UPDATE_AVATAR, UPDATE_USERNAME } from "misago/reducers/users"
 
 export var initialState = {
   signedIn: false,
   signedOut: false
-};
+}
 
-export const PATCH_USER = 'PATCH_USER';
-export const SIGN_IN = 'SIGN_IN';
-export const SIGN_OUT = 'SIGN_OUT';
+export const PATCH_USER = "PATCH_USER"
+export const SIGN_IN = "SIGN_IN"
+export const SIGN_OUT = "SIGN_OUT"
 
 export function patch(patch) {
   return {
     type: PATCH_USER,
     patch
-  };
+  }
 }
 
 export function signIn(user) {
   return {
     type: SIGN_IN,
     user
-  };
+  }
 }
 
-export function signOut(soft=false) {
+export function signOut(soft = false) {
   return {
     type: SIGN_OUT,
     soft
-  };
+  }
 }
 
-export default function auth(state=initialState, action=null) {
+export default function auth(state = initialState, action = null) {
   switch (action.type) {
     case PATCH_USER:
-        let newState = Object.assign({}, state);
-        newState.user = Object.assign({}, state.user, action.patch);
-        return newState;
+      let newState = Object.assign({}, state)
+      newState.user = Object.assign({}, state.user, action.patch)
+      return newState
 
     case SIGN_IN:
       return Object.assign({}, state, {
         signedIn: action.user
-      });
+      })
 
     case SIGN_OUT:
       return Object.assign({}, state, {
         isAuthenticated: false,
         isAnonymous: true,
         signedOut: !action.soft
-      });
+      })
 
     case UPDATE_AVATAR:
       if (state.isAuthenticated && state.user.id === action.userId) {
-        let newState = Object.assign({}, state);
+        let newState = Object.assign({}, state)
         newState.user = Object.assign({}, state.user, {
-          'avatars': action.avatars
-        });
-        return newState;
+          avatars: action.avatars
+        })
+        return newState
       }
-      return state;
+      return state
 
     case UPDATE_USERNAME:
       if (state.isAuthenticated && state.user.id === action.userId) {
-        let newState = Object.assign({}, state);
+        let newState = Object.assign({}, state)
         newState.user = Object.assign({}, state.user, {
           username: action.username,
           slug: action.slug
-        });
-        return newState;
+        })
+        return newState
       }
-      return state;
+      return state
 
     default:
-      return state;
+      return state
   }
 }

+ 6 - 6
frontend/src/reducers/participants.js

@@ -1,18 +1,18 @@
-export const REPLACE_PARTICIPANTS = 'REPLACE_PARTICIPANTS';
+export const REPLACE_PARTICIPANTS = "REPLACE_PARTICIPANTS"
 
 export function replace(newState) {
   return {
     type: REPLACE_PARTICIPANTS,
     state: newState
-  };
+  }
 }
 
-export default function participants(state=[], action=null) {
+export default function participants(state = [], action = null) {
   switch (action.type) {
     case REPLACE_PARTICIPANTS:
-      return action.state;
+      return action.state
 
     default:
-      return state;
+      return state
   }
-}
+}

+ 28 - 26
frontend/src/reducers/poll.js

@@ -1,18 +1,18 @@
-import moment from 'moment';
+import moment from "moment"
 
-export const BUSY_POLL = 'BUSY_POLL';
-export const RELEASE_POLL = 'RELEASE_POLL';
-export const REMOVE_POLL = 'REMOVE_POLL';
-export const REPLACE_POLL = 'REPLACE_POLL';
-export const UPDATE_POLL = 'UPDATE_POLL';
+export const BUSY_POLL = "BUSY_POLL"
+export const RELEASE_POLL = "RELEASE_POLL"
+export const REMOVE_POLL = "REMOVE_POLL"
+export const REPLACE_POLL = "REPLACE_POLL"
+export const UPDATE_POLL = "UPDATE_POLL"
 
 export function hydrate(json) {
-  let hasSelectedChoices = false;
+  let hasSelectedChoices = false
   for (const i in json.choices) {
-    const choice = json.choices[i];
+    const choice = json.choices[i]
     if (choice.selected) {
-      hasSelectedChoices = true;
-      break;
+      hasSelectedChoices = true
+      break
     }
   }
 
@@ -20,64 +20,66 @@ export function hydrate(json) {
     posted_on: moment(json.posted_on),
 
     hasSelectedChoices,
-    endsOn: (json.length ? moment(json.posted_on).add(json.length, 'days') : null),
+    endsOn: json.length
+      ? moment(json.posted_on).add(json.length, "days")
+      : null,
 
     isBusy: false
-  });
+  })
 }
 
 export function busy() {
   return {
     type: BUSY_POLL
-  };
+  }
 }
 
 export function release() {
   return {
     type: RELEASE_POLL
-  };
+  }
 }
 
-export function replace(newState, hydrated=false) {
+export function replace(newState, hydrated = false) {
   return {
     type: REPLACE_POLL,
     state: hydrated ? newState : hydrate(newState)
-  };
+  }
 }
 
 export function update(data) {
   return {
     type: UPDATE_POLL,
     data
-  };
+  }
 }
 
 export function remove() {
   return {
     type: REMOVE_POLL
-  };
+  }
 }
 
-export default function poll(state={}, action=null) {
+export default function poll(state = {}, action = null) {
   switch (action.type) {
     case BUSY_POLL:
-      return Object.assign({}, state, {isBusy: true});
+      return Object.assign({}, state, { isBusy: true })
 
     case RELEASE_POLL:
-      return Object.assign({}, state, {isBusy: false});
+      return Object.assign({}, state, { isBusy: false })
 
     case REMOVE_POLL:
       return {
         isBusy: false
-      };
+      }
 
     case REPLACE_POLL:
-      return action.state;
+      return action.state
 
     case UPDATE_POLL:
-      return Object.assign({}, state, action.data);
+      return Object.assign({}, state, action.data)
 
     default:
-      return state;
+      return state
   }
-}
+}

+ 14 - 12
frontend/src/reducers/post.js

@@ -1,7 +1,7 @@
-import moment from 'moment';
-import { hydrateUser } from './users';
+import moment from "moment"
+import { hydrateUser } from "./users"
 
-export const PATCH_POST = 'PATCH_POST';
+export const PATCH_POST = "PATCH_POST"
 
 export function hydrate(json) {
   return Object.assign({}, json, {
@@ -9,19 +9,21 @@ export function hydrate(json) {
     updated_on: moment(json.updated_on),
     hidden_on: moment(json.hidden_on),
 
-    attachments: json.attachments ? json.attachments.map(hydrateAttachment) : null,
+    attachments: json.attachments
+      ? json.attachments.map(hydrateAttachment)
+      : null,
     poster: json.poster ? hydrateUser(json.poster) : null,
 
     isSelected: false,
     isBusy: false,
     isDeleted: false
-  });
+  })
 }
 
 export function hydrateAttachment(json) {
   return Object.assign({}, json, {
     uploaded_on: moment(json.uploaded_on)
-  });
+  })
 }
 
 export function patch(post, patch) {
@@ -29,18 +31,18 @@ export function patch(post, patch) {
     type: PATCH_POST,
     post,
     patch
-  };
+  }
 }
 
-export default function post(state={}, action=null) {
+export default function post(state = {}, action = null) {
   switch (action.type) {
     case PATCH_POST:
       if (state.id == action.post.id) {
-        return Object.assign({}, state, action.patch);
+        return Object.assign({}, state, action.patch)
       }
-      return state;
+      return state
 
     default:
-      return state;
+      return state
   }
-}
+}

+ 56 - 53
frontend/src/reducers/posts.js

@@ -1,31 +1,34 @@
-import postReducer, { PATCH_POST, hydrate as hydratePost } from 'misago/reducers/post';
-
-export const APPEND_POSTS = 'APPEND_POSTS';
-export const SELECT_POST = 'SELECT_POST';
-export const DESELECT_POST = 'DESELECT_POST';
-export const DESELECT_POSTS = 'DESELECT_POSTS';
-export const LOAD_POSTS = 'LOAD_POSTS';
-export const UNLOAD_POSTS = 'UNLOAD_POSTS';
-export const UPDATE_POSTS = 'UPDATE_POSTS';
+import postReducer, {
+  PATCH_POST,
+  hydrate as hydratePost
+} from "misago/reducers/post"
+
+export const APPEND_POSTS = "APPEND_POSTS"
+export const SELECT_POST = "SELECT_POST"
+export const DESELECT_POST = "DESELECT_POST"
+export const DESELECT_POSTS = "DESELECT_POSTS"
+export const LOAD_POSTS = "LOAD_POSTS"
+export const UNLOAD_POSTS = "UNLOAD_POSTS"
+export const UPDATE_POSTS = "UPDATE_POSTS"
 
 export function select(post) {
   return {
     type: SELECT_POST,
     post
-  };
+  }
 }
 
 export function deselect(post) {
   return {
     type: DESELECT_POST,
     post
-  };
+  }
 }
 
 export function deselectAll() {
   return {
-    type: DESELECT_POSTS,
-  };
+    type: DESELECT_POSTS
+  }
 }
 
 export function hydrate(json) {
@@ -34,116 +37,116 @@ export function hydrate(json) {
     isLoaded: true,
     isBusy: false,
     isSelected: false
-  });
+  })
 }
 
-export function load(newState, hydrated=false) {
+export function load(newState, hydrated = false) {
   return {
     type: LOAD_POSTS,
     state: hydrated ? newState : hydrate(newState)
-  };
+  }
 }
 
-export function append(newState, hydrated=false) {
+export function append(newState, hydrated = false) {
   return {
     type: APPEND_POSTS,
     state: hydrated ? newState : hydrate(newState)
-  };
+  }
 }
 
 export function unload() {
   return {
     type: UNLOAD_POSTS
-  };
+  }
 }
 
 export function update(newState) {
   return {
     type: UPDATE_POSTS,
     update: newState
-  };
+  }
 }
 
-export default function posts(state={}, action=null) {
+export default function posts(state = {}, action = null) {
   switch (action.type) {
     case SELECT_POST:
-      const selectedPosts = state.results.map((post) => {
+      const selectedPosts = state.results.map(post => {
         if (post.id == action.post.id) {
           return Object.assign({}, post, {
             isSelected: true
-          });
+          })
         } else {
-          return post;
+          return post
         }
-      });
+      })
 
       return Object.assign({}, state, {
         results: selectedPosts
-      });
+      })
 
     case DESELECT_POST:
-      const deseletedPosts = state.results.map((post) => {
+      const deseletedPosts = state.results.map(post => {
         if (post.id == action.post.id) {
           return Object.assign({}, post, {
             isSelected: false
-          });
+          })
         } else {
-          return post;
+          return post
         }
-      });
+      })
 
       return Object.assign({}, state, {
         results: deseletedPosts
-      });
+      })
 
     case DESELECT_POSTS:
-      const deseletedAllPosts = state.results.map((post) => {
+      const deseletedAllPosts = state.results.map(post => {
         return Object.assign({}, post, {
           isSelected: false
-        });
-      });
+        })
+      })
 
       return Object.assign({}, state, {
         results: deseletedAllPosts
-      });
+      })
 
     case APPEND_POSTS:
-      let results = state.results.slice();
-      const resultsIds = state.results.map((post) => {
-        return post.id;
-      });
+      let results = state.results.slice()
+      const resultsIds = state.results.map(post => {
+        return post.id
+      })
 
-      action.state.results.map((post) => {
+      action.state.results.map(post => {
         if (resultsIds.indexOf(post.id) === -1) {
-          results.push(post);
+          results.push(post)
         }
-      });
+      })
 
       return Object.assign({}, action.state, {
         results
-      });
+      })
 
     case LOAD_POSTS:
-      return action.state;
+      return action.state
 
     case UNLOAD_POSTS:
       return Object.assign({}, state, {
-        isLoaded: false,
-      });
+        isLoaded: false
+      })
 
     case UPDATE_POSTS:
-      return Object.assign({}, state, action.update);
+      return Object.assign({}, state, action.update)
 
     case PATCH_POST:
-      const reducedPosts = state.results.map((post) => {
-        return postReducer(post, action);
-      });
+      const reducedPosts = state.results.map(post => {
+        return postReducer(post, action)
+      })
 
       return Object.assign({}, state, {
         results: reducedPosts
-      });
+      })
 
     default:
-      return state;
+      return state
   }
-}
+}

+ 6 - 6
frontend/src/reducers/profile-details.js

@@ -1,19 +1,19 @@
-export const LOAD_DETAILS = 'LOAD_DETAILS';
+export const LOAD_DETAILS = "LOAD_DETAILS"
 
 export function load(newState) {
   return {
     type: LOAD_DETAILS,
 
     newState
-  };
+  }
 }
 
-export default function details(state={}, action=null) {
+export default function details(state = {}, action = null) {
   switch (action.type) {
     case LOAD_DETAILS:
-      return action.newState;
+      return action.newState
 
     default:
-      return state;
+      return state
   }
-}
+}

+ 18 - 14
frontend/src/reducers/profile.js

@@ -1,52 +1,56 @@
-import moment from 'moment';
-import { UPDATE_AVATAR, UPDATE_USERNAME, hydrateStatus } from 'misago/reducers/users';
+import moment from "moment"
+import {
+  UPDATE_AVATAR,
+  UPDATE_USERNAME,
+  hydrateStatus
+} from "misago/reducers/users"
 
-export const HYDRATE_PROFILE = 'HYDRATE_PROFILE';
-export const PATCH_PROFILE = 'PATCH_PROFILE';
+export const HYDRATE_PROFILE = "HYDRATE_PROFILE"
+export const PATCH_PROFILE = "PATCH_PROFILE"
 
 export function hydrate(profile) {
   return {
     type: HYDRATE_PROFILE,
     profile
-  };
+  }
 }
 
 export function patch(patch) {
   return {
     type: PATCH_PROFILE,
     patch
-  };
+  }
 }
 
-export default function auth(state={}, action=null) {
+export default function auth(state = {}, action = null) {
   switch (action.type) {
     case HYDRATE_PROFILE:
       return Object.assign({}, action.profile, {
         joined_on: moment(action.profile.joined_on),
         status: hydrateStatus(action.profile.status)
-      });
+      })
 
     case PATCH_PROFILE:
-      return Object.assign({}, state, action.patch);
+      return Object.assign({}, state, action.patch)
 
     case UPDATE_AVATAR:
       if (state.id === action.userId) {
         return Object.assign({}, state, {
           avatars: action.avatars
-        });
+        })
       }
-      return state;
+      return state
 
     case UPDATE_USERNAME:
       if (state.id === action.userId) {
         return Object.assign({}, state, {
           username: action.username,
           slug: action.slug
-        });
+        })
       }
-      return state;
+      return state
 
     default:
-      return state;
+      return state
   }
 }

+ 17 - 17
frontend/src/reducers/search.js

@@ -1,12 +1,12 @@
-export const REPLACE_SEARCH = 'REPLACE_SEARCH';
-export const UPDATE_SEARCH = 'UPDATE_SEARCH';
-export const UPDATE_SEARCH_PROVIDER = 'UPDATE_SEARCH_PROVIDER';
+export const REPLACE_SEARCH = "REPLACE_SEARCH"
+export const UPDATE_SEARCH = "UPDATE_SEARCH"
+export const UPDATE_SEARCH_PROVIDER = "UPDATE_SEARCH_PROVIDER"
 
 export const initialState = {
   isLoading: false,
-  query: '',
+  query: "",
   providers: []
-};
+}
 
 export function replace(newState) {
   return {
@@ -15,43 +15,43 @@ export function replace(newState) {
       isLoading: false,
       providers: newState
     }
-  };
+  }
 }
 
 export function update(newState) {
   return {
     type: UPDATE_SEARCH,
     update: newState
-  };
+  }
 }
 
 export function updateProvider(provider) {
   return {
     type: UPDATE_SEARCH_PROVIDER,
     provider: provider
-  };
+  }
 }
 
-export default function participants(state={}, action=null) {
+export default function participants(state = {}, action = null) {
   switch (action.type) {
     case REPLACE_SEARCH:
-      return action.state;
+      return action.state
 
     case UPDATE_SEARCH:
-      return Object.assign({}, state, action.update);
+      return Object.assign({}, state, action.update)
 
     case UPDATE_SEARCH_PROVIDER:
       return Object.assign({}, state, {
-        providers: state.providers.map((provider) => {
+        providers: state.providers.map(provider => {
           if (provider.id === action.provider.id) {
-            return action.provider;
+            return action.provider
           } else {
-            return provider;
+            return provider
           }
         })
-      });
+      })
 
     default:
-      return state;
+      return state
   }
-}
+}

+ 13 - 13
frontend/src/reducers/selection.js

@@ -1,41 +1,41 @@
-import { toggle } from 'misago/utils/sets';
+import { toggle } from "misago/utils/sets"
 
-export const SELECT_ALL = 'SELECT_ALL';
-export const SELECT_NONE = 'SELECT_NONE';
-export const SELECT_ITEM = 'SELECT_ITEM';
+export const SELECT_ALL = "SELECT_ALL"
+export const SELECT_NONE = "SELECT_NONE"
+export const SELECT_ITEM = "SELECT_ITEM"
 
 export function all(itemsIds) {
   return {
     type: SELECT_ALL,
     items: itemsIds
-  };
+  }
 }
 
 export function none() {
   return {
     type: SELECT_NONE
-  };
+  }
 }
 
 export function item(itemId) {
   return {
     type: SELECT_ITEM,
     item: itemId
-  };
+  }
 }
 
-export default function selection(state=[], action=null) {
+export default function selection(state = [], action = null) {
   switch (action.type) {
     case SELECT_ALL:
-      return action.items;
+      return action.items
 
     case SELECT_NONE:
-      return [];
+      return []
 
     case SELECT_ITEM:
-      return toggle(state, action.item);
+      return toggle(state, action.item)
 
     default:
-      return state;
+      return state
   }
-}
+}

+ 12 - 12
frontend/src/reducers/snackbar.js

@@ -1,38 +1,38 @@
 export var initialState = {
-  type: 'info',
-  message: '',
+  type: "info",
+  message: "",
   isVisible: false
-};
+}
 
-export const SHOW_SNACKBAR = 'SHOW_SNACKBAR';
-export const HIDE_SNACKBAR = 'HIDE_SNACKBAR';
+export const SHOW_SNACKBAR = "SHOW_SNACKBAR"
+export const HIDE_SNACKBAR = "HIDE_SNACKBAR"
 
 export function showSnackbar(message, type) {
   return {
     type: SHOW_SNACKBAR,
     message,
     messageType: type
-  };
+  }
 }
 
 export function hideSnackbar() {
   return {
     type: HIDE_SNACKBAR
-  };
+  }
 }
 
-export default function snackbar(state=initialState, action=null) {
+export default function snackbar(state = initialState, action = null) {
   if (action.type === SHOW_SNACKBAR) {
     return {
       type: action.messageType,
       message: action.message,
       isVisible: true
-    };
+    }
   } else if (action.type === HIDE_SNACKBAR) {
     return Object.assign({}, state, {
-        isVisible: false
-    });
+      isVisible: false
+    })
   } else {
-    return state;
+    return state
   }
 }

+ 28 - 26
frontend/src/reducers/thread.js

@@ -1,80 +1,82 @@
-import moment from 'moment';
-import { REMOVE_POLL, REPLACE_POLL } from './poll';
+import moment from "moment"
+import { REMOVE_POLL, REPLACE_POLL } from "./poll"
 
-export const BUSY_THREAD = 'BUSY_THREAD';
-export const RELEASE_THREAD = 'RELEASE_THREAD';
-export const REPLACE_THREAD = 'REPLACE_THREAD';
-export const UPDATE_THREAD = 'UPDATE_THREAD';
-export const UPDATE_THREAD_ACL = 'UPDATE_THREAD_ACL';
+export const BUSY_THREAD = "BUSY_THREAD"
+export const RELEASE_THREAD = "RELEASE_THREAD"
+export const REPLACE_THREAD = "REPLACE_THREAD"
+export const UPDATE_THREAD = "UPDATE_THREAD"
+export const UPDATE_THREAD_ACL = "UPDATE_THREAD_ACL"
 
 export function hydrate(json) {
   return Object.assign({}, json, {
     started_on: moment(json.started_on),
     last_post_on: moment(json.last_post_on),
-    best_answer_marked_on: json.best_answer_marked_on ? moment(json.best_answer_marked_on) : null,
+    best_answer_marked_on: json.best_answer_marked_on
+      ? moment(json.best_answer_marked_on)
+      : null,
 
     isBusy: false
-  });
+  })
 }
 
 export function busy() {
   return {
     type: BUSY_THREAD
-  };
+  }
 }
 
 export function release() {
   return {
     type: RELEASE_THREAD
-  };
+  }
 }
 
-export function replace(newState, hydrated=false) {
+export function replace(newState, hydrated = false) {
   return {
     type: REPLACE_THREAD,
     state: hydrated ? newState : hydrate(newState)
-  };
+  }
 }
 
 export function update(data) {
   return {
     type: UPDATE_THREAD,
     data
-  };
+  }
 }
 
 export function updateAcl(data) {
   return {
     type: UPDATE_THREAD_ACL,
     data
-  };
+  }
 }
 
-export default function thread(state={}, action=null) {
+export default function thread(state = {}, action = null) {
   switch (action.type) {
     case BUSY_THREAD:
-      return Object.assign({}, state, {isBusy: true});
+      return Object.assign({}, state, { isBusy: true })
 
     case RELEASE_THREAD:
-      return Object.assign({}, state, {isBusy: false});
+      return Object.assign({}, state, { isBusy: false })
 
     case REMOVE_POLL:
-      return Object.assign({}, state, {poll: null});
+      return Object.assign({}, state, { poll: null })
 
     case REPLACE_POLL:
-      return Object.assign({}, state, {poll: action.state});
+      return Object.assign({}, state, { poll: action.state })
 
     case REPLACE_THREAD:
-      return action.state;
+      return action.state
 
     case UPDATE_THREAD:
-      return Object.assign({}, state, action.data);
+      return Object.assign({}, state, action.data)
 
     case UPDATE_THREAD_ACL:
-      const acl = Object.assign({}, state.acl, action.data);
-      return Object.assign({}, state, {acl});
+      const acl = Object.assign({}, state.acl, action.data)
+      return Object.assign({}, state, { acl })
 
     default:
-      return state;
+      return state
   }
-}
+}

+ 53 - 50
frontend/src/reducers/threads.js

@@ -1,37 +1,37 @@
-import moment from 'moment';
-import concatUnique from 'misago/utils/concat-unique';
+import moment from "moment"
+import concatUnique from "misago/utils/concat-unique"
 
-export const APPEND_THREADS = 'APPEND_THREADS';
-export const DELETE_THREAD = 'DELETE_THREAD';
-export const FILTER_THREADS = 'FILTER_THREADS';
-export const HYDRATE_THREADS = 'HYDRATE_THREADS';
-export const PATCH_THREAD = 'PATCH_THREAD';
-export const SORT_THREADS = 'SORT_THREADS';
+export const APPEND_THREADS = "APPEND_THREADS"
+export const DELETE_THREAD = "DELETE_THREAD"
+export const FILTER_THREADS = "FILTER_THREADS"
+export const HYDRATE_THREADS = "HYDRATE_THREADS"
+export const PATCH_THREAD = "PATCH_THREAD"
+export const SORT_THREADS = "SORT_THREADS"
 
 export const MODERATION_PERMISSIONS = [
-  'can_announce',
-  'can_approve',
-  'can_close',
-  'can_hide',
-  'can_move',
-  'can_merge',
-  'can_pin',
-  'can_review'
-];
+  "can_announce",
+  "can_approve",
+  "can_close",
+  "can_hide",
+  "can_move",
+  "can_merge",
+  "can_pin",
+  "can_review"
+]
 
 export function append(items, sorting) {
- return {
+  return {
     type: APPEND_THREADS,
     items,
     sorting
-  };
+  }
 }
 
 export function deleteThread(thread) {
   return {
     type: DELETE_THREAD,
     thread
-  };
+  }
 }
 
 export function filterThreads(category, categoriesMap) {
@@ -39,40 +39,40 @@ export function filterThreads(category, categoriesMap) {
     type: FILTER_THREADS,
     category,
     categoriesMap
-  };
+  }
 }
 
 export function hydrate(items) {
   return {
     type: HYDRATE_THREADS,
     items
-  };
+  }
 }
 
-export function patch(thread, patch, sorting=null) {
+export function patch(thread, patch, sorting = null) {
   return {
     type: PATCH_THREAD,
     thread,
     patch,
     sorting
-  };
+  }
 }
 
 export function sort(sorting) {
   return {
     type: SORT_THREADS,
     sorting
-  };
+  }
 }
 
 export function getThreadModerationOptions(thread_acl) {
-  let options = [];
+  let options = []
   MODERATION_PERMISSIONS.forEach(function(perm) {
     if (thread_acl[perm]) {
-      options.push(perm);
+      options.push(perm)
     }
-  });
-  return options;
+  })
+  return options
 }
 
 export function hydrateThread(thread) {
@@ -80,56 +80,59 @@ export function hydrateThread(thread) {
     started_on: moment(thread.started_on),
     last_post_on: moment(thread.last_post_on),
     moderation: getThreadModerationOptions(thread.acl)
-  });
+  })
 }
 
-export default function thread(state=[], action=null) {
+export default function thread(state = [], action = null) {
   switch (action.type) {
     case APPEND_THREADS:
-      const mergedState = concatUnique(action.items.map(hydrateThread), state);
-      return mergedState.sort(action.sorting);
+      const mergedState = concatUnique(action.items.map(hydrateThread), state)
+      return mergedState.sort(action.sorting)
 
     case DELETE_THREAD:
       return state.filter(function(item) {
-        return item.id !== action.thread.id;
-      });
+        return item.id !== action.thread.id
+      })
 
     case FILTER_THREADS:
       return state.filter(function(item) {
-        const itemCategory = action.categoriesMap[item.category];
-        if (itemCategory.lft >= action.category.lft && itemCategory.rght <= action.category.rght) {
+        const itemCategory = action.categoriesMap[item.category]
+        if (
+          itemCategory.lft >= action.category.lft &&
+          itemCategory.rght <= action.category.rght
+        ) {
           // same or sub category
-          return true;
+          return true
         } else if (item.weight == 2) {
           // globally pinned
-          return true;
+          return true
         } else {
           // thread moved outside displayed scope, hide it
-          return false;
+          return false
         }
-      });
+      })
 
     case HYDRATE_THREADS:
-      return action.items.map(hydrateThread);
+      return action.items.map(hydrateThread)
 
     case PATCH_THREAD:
       const patchedState = state.map(function(item) {
         if (item.id === action.thread.id) {
-          return Object.assign({}, item, action.patch);
+          return Object.assign({}, item, action.patch)
         } else {
-          return item;
+          return item
         }
-      });
+      })
 
       if (action.sorting) {
-        return patchedState.sort(action.sorting);
+        return patchedState.sort(action.sorting)
       }
-      return patchedState;
+      return patchedState
 
     case SORT_THREADS:
-      return state.sort(action.sorting);
+      return state.sort(action.sorting)
 
     default:
-      return state;
+      return state
   }
-}
+}

+ 7 - 7
frontend/src/reducers/tick.js

@@ -1,21 +1,21 @@
 export var initialState = {
   tick: 0
-};
+}
 
-export const TICK = 'TICK';
+export const TICK = "TICK"
 
 export function doTick() {
   return {
     type: TICK
-  };
+  }
 }
 
-export default function tick(state=initialState, action=null) {
+export default function tick(state = initialState, action = null) {
   if (action.type === TICK) {
     return Object.assign({}, state, {
-        tick: state.tick + 1
-    });
+      tick: state.tick + 1
+    })
   } else {
-    return state;
+    return state
   }
 }

+ 26 - 26
frontend/src/reducers/username-history.js

@@ -1,10 +1,10 @@
-import moment from 'moment';
-import { UPDATE_AVATAR, UPDATE_USERNAME } from 'misago/reducers/users';
-import concatUnique from 'misago/utils/concat-unique';
+import moment from "moment"
+import { UPDATE_AVATAR, UPDATE_USERNAME } from "misago/reducers/users"
+import concatUnique from "misago/utils/concat-unique"
 
-export const ADD_NAME_CHANGE = 'ADD_NAME_CHANGE';
-export const APPEND_HISTORY = 'APPEND_HISTORY';
-export const HYDRATE_HISTORY = 'HYDRATE_HISTORY';
+export const ADD_NAME_CHANGE = "ADD_NAME_CHANGE"
+export const APPEND_HISTORY = "APPEND_HISTORY"
+export const HYDRATE_HISTORY = "HYDRATE_HISTORY"
 
 export function addNameChange(change, user, changedBy) {
   return {
@@ -12,33 +12,33 @@ export function addNameChange(change, user, changedBy) {
     change,
     user,
     changedBy
-  };
+  }
 }
 
 export function append(items) {
   return {
     type: APPEND_HISTORY,
     items: items
-  };
+  }
 }
 
 export function hydrate(items) {
   return {
     type: HYDRATE_HISTORY,
     items: items
-  };
+  }
 }
 
 export function hydrateNamechange(namechange) {
   return Object.assign({}, namechange, {
     changed_on: moment(namechange.changed_on)
-  });
+  })
 }
 
-export default function username(state=[], action=null) {
+export default function username(state = [], action = null) {
   switch (action.type) {
     case ADD_NAME_CHANGE:
-      let newState = state.slice();
+      let newState = state.slice()
       newState.unshift({
         id: Math.floor(Date.now() / 1000), // just small hax for getting id
         changed_by: action.changedBy,
@@ -46,41 +46,41 @@ export default function username(state=[], action=null) {
         changed_on: moment(),
         new_username: action.change.username,
         old_username: action.user.username
-      });
-      return newState;
+      })
+      return newState
 
     case APPEND_HISTORY:
-      return concatUnique(state, action.items.map(hydrateNamechange));
+      return concatUnique(state, action.items.map(hydrateNamechange))
 
     case HYDRATE_HISTORY:
-      return action.items.map(hydrateNamechange);
+      return action.items.map(hydrateNamechange)
 
     case UPDATE_AVATAR:
       return state.map(function(item) {
-        item = Object.assign({}, item);
+        item = Object.assign({}, item)
         if (item.changed_by && item.changed_by.id === action.userId) {
           item.changed_by = Object.assign({}, item.changed_by, {
             avatars: action.avatars
-          });
+          })
         }
 
-        return item;
-      });
+        return item
+      })
 
     case UPDATE_USERNAME:
       return state.map(function(item) {
-        item = Object.assign({}, item);
+        item = Object.assign({}, item)
         if (item.changed_by && item.changed_by.id === action.userId) {
           item.changed_by = Object.assign({}, item.changed_by, {
             username: action.username,
             slug: action.slug
-          });
+          })
         }
 
-        return Object.assign({}, item);
-      });
+        return Object.assign({}, item)
+      })
 
     default:
-      return state;
+      return state
   }
-}
+}

+ 22 - 22
frontend/src/reducers/users.js

@@ -1,23 +1,23 @@
-import moment from 'moment';
-import concatUnique from 'misago/utils/concat-unique';
+import moment from "moment"
+import concatUnique from "misago/utils/concat-unique"
 
-export const APPEND_USERS = 'APPEND_USERS';
-export const HYDRATE_USERS = 'HYDRATE_USERS';
-export const UPDATE_AVATAR = 'UPDATE_AVATAR';
-export const UPDATE_USERNAME = 'UPDATE_USERNAME';
+export const APPEND_USERS = "APPEND_USERS"
+export const HYDRATE_USERS = "HYDRATE_USERS"
+export const UPDATE_AVATAR = "UPDATE_AVATAR"
+export const UPDATE_USERNAME = "UPDATE_USERNAME"
 
 export function append(items) {
   return {
     type: APPEND_USERS,
     items
-  };
+  }
 }
 
 export function hydrate(items) {
   return {
     type: HYDRATE_USERS,
     items
-  };
+  }
 }
 
 export function hydrateStatus(status) {
@@ -25,9 +25,9 @@ export function hydrateStatus(status) {
     return Object.assign({}, status, {
       last_click: status.last_click ? moment(status.last_click) : null,
       banned_until: status.banned_until ? moment(status.banned_until) : null
-    });
+    })
   } else {
-    return null;
+    return null
   }
 }
 
@@ -35,7 +35,7 @@ export function hydrateUser(user) {
   return Object.assign({}, user, {
     joined_on: moment(user.joined_on),
     status: hydrateStatus(user.status)
-  });
+  })
 }
 
 export function updateAvatar(user, avatars) {
@@ -43,7 +43,7 @@ export function updateAvatar(user, avatars) {
     type: UPDATE_AVATAR,
     userId: user.id,
     avatars
-  };
+  }
 }
 
 export function updateUsername(user, username, slug) {
@@ -52,28 +52,28 @@ export function updateUsername(user, username, slug) {
     userId: user.id,
     username,
     slug
-  };
+  }
 }
 
-export default function user(state=[], action=null) {
+export default function user(state = [], action = null) {
   switch (action.type) {
     case APPEND_USERS:
-      return concatUnique(state, action.items.map(hydrateUser));
+      return concatUnique(state, action.items.map(hydrateUser))
 
     case HYDRATE_USERS:
-      return action.items.map(hydrateUser);
+      return action.items.map(hydrateUser)
 
     case UPDATE_AVATAR:
       return state.map(function(item) {
-        item = Object.assign({}, item);
+        item = Object.assign({}, item)
         if (item.id === action.userId) {
-          item.avatars = action.avatars;
+          item.avatars = action.avatars
         }
 
-        return item;
-      });
+        return item
+      })
 
     default:
-      return state;
+      return state
   }
-}
+}

+ 109 - 97
frontend/src/services/ajax.js

@@ -1,82 +1,82 @@
 export class Ajax {
   constructor() {
-    this._cookieName = null;
-    this._csrfToken = null;
-    this._locks = {};
+    this._cookieName = null
+    this._csrfToken = null
+    this._locks = {}
   }
 
   init(cookieName) {
-    this._cookieName = cookieName;
+    this._cookieName = cookieName
   }
 
   getCsrfToken() {
     if (document.cookie.indexOf(this._cookieName) !== -1) {
-      let cookieRegex = new RegExp(this._cookieName + '\=([^;]*)');
-      let cookie = document.cookie.match(cookieRegex)[0];
-      return cookie ? cookie.split('=')[1] : null;
+      let cookieRegex = new RegExp(this._cookieName + "=([^;]*)")
+      let cookie = document.cookie.match(cookieRegex)[0]
+      return cookie ? cookie.split("=")[1] : null
     } else {
-      return null;
+      return null
     }
   }
 
   request(method, url, data) {
-    let self = this;
+    let self = this
     return new Promise(function(resolve, reject) {
       let xhr = {
         url: url,
         method: method,
         headers: {
-          'X-CSRFToken': self.getCsrfToken()
+          "X-CSRFToken": self.getCsrfToken()
         },
 
-        data: (data ? JSON.stringify(data) : null),
+        data: data ? JSON.stringify(data) : null,
         contentType: "application/json; charset=utf-8",
-        dataType: 'json',
+        dataType: "json",
 
         success: function(data) {
-          resolve(data);
+          resolve(data)
         },
 
         error: function(jqXHR) {
-          let rejection = jqXHR.responseJSON || {};
+          let rejection = jqXHR.responseJSON || {}
 
-          rejection.status = jqXHR.status;
+          rejection.status = jqXHR.status
 
           if (rejection.status === 0) {
-            rejection.detail = gettext("Lost connection with application.");
+            rejection.detail = gettext("Lost connection with application.")
           }
 
           if (rejection.status === 404) {
-            if (!rejection.detail || rejection.detail === 'NOT FOUND') {
-              rejection.detail = gettext("Action link is invalid.");
+            if (!rejection.detail || rejection.detail === "NOT FOUND") {
+              rejection.detail = gettext("Action link is invalid.")
             }
           }
 
           if (rejection.status === 500 && !rejection.detail) {
-            rejection.detail = gettext("Unknown error has occured.");
+            rejection.detail = gettext("Unknown error has occured.")
           }
 
-          rejection.statusText = jqXHR.statusText;
+          rejection.statusText = jqXHR.statusText
 
-          reject(rejection);
+          reject(rejection)
         }
-      };
+      }
 
-      $.ajax(xhr);
-    });
+      $.ajax(xhr)
+    })
   }
 
   get(url, params, lock) {
     if (params) {
-      url += '?' + $.param(params);
+      url += "?" + $.param(params)
     }
 
     if (lock) {
-      let self = this;
+      let self = this
 
       // update url in existing lock?
       if (this._locks[lock]) {
-        this._locks[lock].url = url;
+        this._locks[lock].url = url
       }
 
       // immediately dereference promise handlers without doing anything
@@ -84,103 +84,109 @@ export class Ajax {
       if (this._locks[lock] && this._locks[lock].waiter) {
         return {
           then: function() {
-            return;
+            return
           }
-        };
+        }
 
-      // return promise that will begin when original one resolves
+        // return promise that will begin when original one resolves
       } else if (this._locks[lock] && this._locks[lock].wait) {
-        this._locks[lock].waiter = true;
+        this._locks[lock].waiter = true
 
         return new Promise(function(resolve, reject) {
           let wait = function(url) {
             // keep waiting on promise
             if (self._locks[lock].wait) {
               window.setTimeout(function() {
-                wait(url);
-              }, 300);
+                wait(url)
+              }, 300)
 
-            // poll for new url
+              // poll for new url
             } else if (self._locks[lock].url !== url) {
-              wait(self._locks[lock].url);
+              wait(self._locks[lock].url)
 
-            // ajax backend for response
+              // ajax backend for response
             } else {
-              self._locks[lock].waiter = false;
-              self.request('GET', self._locks[lock].url).then(function(data) {
-                if (self._locks[lock].url === url) {
-                  resolve(data);
-                } else {
-                  self._locks[lock].waiter = true;
-                  wait(self._locks[lock].url);
+              self._locks[lock].waiter = false
+              self.request("GET", self._locks[lock].url).then(
+                function(data) {
+                  if (self._locks[lock].url === url) {
+                    resolve(data)
+                  } else {
+                    self._locks[lock].waiter = true
+                    wait(self._locks[lock].url)
+                  }
+                },
+                function(rejection) {
+                  if (self._locks[lock].url === url) {
+                    reject(rejection)
+                  } else {
+                    self._locks[lock].waiter = true
+                    wait(self._locks[lock].url)
+                  }
                 }
-              }, function(rejection) {
-                if (self._locks[lock].url === url) {
-                  reject(rejection);
-                } else {
-                  self._locks[lock].waiter = true;
-                  wait(self._locks[lock].url);
-                }
-              });
+              )
             }
-          };
+          }
 
           window.setTimeout(function() {
-            wait(url);
-          }, 300);
-        });
+            wait(url)
+          }, 300)
+        })
 
-      // setup new lock without waiter
+        // setup new lock without waiter
       } else {
         this._locks[lock] = {
           url,
           wait: true,
           waiter: false
-        };
+        }
 
         return new Promise(function(resolve, reject) {
-          self.request('GET', url).then(function(data) {
-            self._locks[lock].wait = false;
-            if (self._locks[lock].url === url) {
-              resolve(data);
-            }
-          }, function(rejection) {
-            self._locks[lock].wait = false;
-            if (self._locks[lock].url === url) {
-              reject(rejection);
+          self.request("GET", url).then(
+            function(data) {
+              self._locks[lock].wait = false
+              if (self._locks[lock].url === url) {
+                resolve(data)
+              }
+            },
+            function(rejection) {
+              self._locks[lock].wait = false
+              if (self._locks[lock].url === url) {
+                reject(rejection)
+              }
             }
-          });
-        });
+          )
+        })
       }
     } else {
-      return this.request('GET', url);
+      return this.request("GET", url)
     }
   }
 
   post(url, data) {
-    return this.request('POST', url, data);
+    return this.request("POST", url, data)
   }
 
   patch(url, data) {
-    return this.request('PATCH', url, data);
+    return this.request("PATCH", url, data)
   }
 
   put(url, data) {
-    return this.request('PUT', url, data);
+    return this.request("PUT", url, data)
   }
 
   delete(url, data) {
-    return this.request('DELETE', url, data);
+    return this.request("DELETE", url, data)
   }
 
   upload(url, data, progress) {
-    let self = this;
+    let self = this
     return new Promise(function(resolve, reject) {
       let xhr = {
         url: url,
-        method: 'POST',
+        method: "POST",
         headers: {
-          'X-CSRFToken': self.getCsrfToken()
+          "X-CSRFToken": self.getCsrfToken()
         },
 
         data: data,
@@ -188,51 +194,57 @@ export class Ajax {
         processData: false,
 
         xhr: function() {
-          let xhr = new window.XMLHttpRequest();
-          xhr.upload.addEventListener("progress", function(evt) {
-            if (evt.lengthComputable) {
-              progress(Math.round(evt.loaded / evt.total * 100));
-            }
-          }, false);
-          return xhr;
+          let xhr = new window.XMLHttpRequest()
+          xhr.upload.addEventListener(
+            "progress",
+            function(evt) {
+              if (evt.lengthComputable) {
+                progress(Math.round((evt.loaded / evt.total) * 100))
+              }
+            },
+            false
+          )
+          return xhr
         },
 
         success: function(response) {
-          resolve(response);
+          resolve(response)
         },
 
         error: function(jqXHR) {
-          let rejection = jqXHR.responseJSON || {};
+          let rejection = jqXHR.responseJSON || {}
 
-          rejection.status = jqXHR.status;
+          rejection.status = jqXHR.status
 
           if (rejection.status === 0) {
-            rejection.detail = gettext("Lost connection with application.");
+            rejection.detail = gettext("Lost connection with application.")
           }
 
           if (rejection.status === 413 && !rejection.detail) {
-            rejection.detail = gettext("Upload was rejected by server as too large.");
+            rejection.detail = gettext(
+              "Upload was rejected by server as too large."
+            )
           }
 
           if (rejection.status === 404) {
-            if (!rejection.detail || rejection.detail === 'NOT FOUND') {
-              rejection.detail = gettext("Action link is invalid.");
+            if (!rejection.detail || rejection.detail === "NOT FOUND") {
+              rejection.detail = gettext("Action link is invalid.")
             }
           }
 
           if (rejection.status === 500 && !rejection.detail) {
-            rejection.detail = gettext("Unknown error has occured.");
+            rejection.detail = gettext("Unknown error has occured.")
           }
 
-          rejection.statusText = jqXHR.statusText;
+          rejection.statusText = jqXHR.statusText
 
-          reject(rejection);
+          reject(rejection)
         }
-      };
+      }
 
-      $.ajax(xhr);
-    });
+      $.ajax(xhr)
+    })
   }
 }
 
-export default new Ajax();
+export default new Ajax()

+ 34 - 32
frontend/src/services/auth.js

@@ -1,74 +1,76 @@
-import { signIn, signOut } from 'misago/reducers/auth'; // jshint ignore:line
+import { signIn, signOut } from "misago/reducers/auth"
 
 export class Auth {
   init(store, local, modal) {
-    this._store = store;
-    this._local = local;
-    this._modal = modal;
+    this._store = store
+    this._local = local
+    this._modal = modal
 
     // tell other tabs what auth state is because we are most current with it
-    this.syncSession();
+    this.syncSession()
 
     // listen for other tabs to tell us that state changed
-    this.watchState();
+    this.watchState()
   }
 
   syncSession() {
-    const state = this._store.getState().auth;
+    const state = this._store.getState().auth
     if (state.isAuthenticated) {
-      this._local.set('auth', {
+      this._local.set("auth", {
         isAuthenticated: true,
         username: state.user.username
-      });
+      })
     } else {
-      this._local.set('auth', {
+      this._local.set("auth", {
         isAuthenticated: false
-      });
+      })
     }
   }
 
   watchState() {
-    const state = this._store.getState().auth;
-    this._local.watch('auth', (newState) => {
+    const state = this._store.getState().auth
+    this._local.watch("auth", newState => {
       if (newState.isAuthenticated) {
-        this._store.dispatch(signIn({
-          username: newState.username
-        }));
+        this._store.dispatch(
+          signIn({
+            username: newState.username
+          })
+        )
       } else if (state.isAuthenticated) {
         // check if we are authenticated in this tab
         // because some browser plugins prune local store
         // aggressively, forcing erroneous message to display here
         // tracking bug #955
-        this._store.dispatch(signOut());
+        this._store.dispatch(signOut())
       }
-    });
-    this._modal.hide();
+    })
+    this._modal.hide()
   }
 
   signIn(user) {
-    this._store.dispatch(signIn(user));
-    this._local.set('auth', {
+    this._store.dispatch(signIn(user))
+    this._local.set("auth", {
       isAuthenticated: true,
       username: user.username
-    });
-    this._modal.hide();
+    })
+    this._modal.hide()
   }
 
   signOut() {
-    this._store.dispatch(signOut());
-    this._local.set('auth', {
+    this._store.dispatch(signOut())
+    this._local.set("auth", {
       isAuthenticated: false
-    });
-    this._modal.hide();
+    })
+    this._modal.hide()
   }
 
   softSignOut() {
-    this._store.dispatch(signOut(true));
-    this._local.set('auth', {
+    this._store.dispatch(signOut(true))
+    this._local.set("auth", {
       isAuthenticated: false
-    });
-    this._modal.hide();
+    })
+    this._modal.hide()
   }
 }
 
-export default new Auth();
+export default new Auth()

+ 63 - 66
frontend/src/services/captcha.js

@@ -1,13 +1,13 @@
 /* global grecaptcha */
-import React from 'react'; // jshint ignore:line
-import FormGroup from 'misago/components/form-group'; // jshint ignore:line
+import React from "react"
+import FormGroup from "misago/components/form-group"
 
 export class BaseCaptcha {
   init(context, ajax, include, snackbar) {
-    this._context = context;
-    this._ajax = ajax;
-    this._include = include;
-    this._snackbar = snackbar;
+    this._context = context
+    this._ajax = ajax
+    this._include = include
+    this._snackbar = snackbar
   }
 }
 
@@ -15,40 +15,41 @@ export class NoCaptcha extends BaseCaptcha {
   load() {
     return new Promise(function(resolve) {
       // immediately resolve as we don't have anything to validate
-      resolve();
-    });
+      resolve()
+    })
   }
 
   validator() {
-    return null;
+    return null
   }
 
   component() {
-    return null;
+    return null
   }
 }
 
 export class QACaptcha extends BaseCaptcha {
   load() {
-    var self = this;
+    var self = this
     return new Promise((resolve, reject) => {
-      self._ajax.get(self._context.get('CAPTCHA_API')).then(
-      function(data) {
-        self.question = data.question;
-        self.helpText = data.help_text;
-        resolve();
-      }, function() {
-        self._snackbar.error(gettext("Failed to load CAPTCHA."));
-        reject();
-      });
-    });
+      self._ajax.get(self._context.get("CAPTCHA_API")).then(
+        function(data) {
+          self.question = data.question
+          self.helpText = data.help_text
+          resolve()
+        },
+        function() {
+          self._snackbar.error(gettext("Failed to load CAPTCHA."))
+          reject()
+        }
+      )
+    })
   }
 
   validator() {
-    return [];
+    return []
   }
 
-  /* jshint ignore:start */
   component(kwargs) {
     return (
       <FormGroup
@@ -64,62 +65,57 @@ export class QACaptcha extends BaseCaptcha {
           className="form-control"
           disabled={kwargs.form.state.isLoading}
           id="id_captcha"
-          onChange={kwargs.form.bindInput('captcha')}
+          onChange={kwargs.form.bindInput("captcha")}
           type="text"
           value={kwargs.form.state.captcha}
         />
       </FormGroup>
-    );
+    )
   }
-  /* jshint ignore:end */
 }
 
-
 export class ReCaptchaComponent extends React.Component {
   componentDidMount() {
-    grecaptcha.render('recaptcha', {
-      'sitekey': this.props.siteKey,
-      'callback': (response) => {
+    grecaptcha.render("recaptcha", {
+      sitekey: this.props.siteKey,
+      callback: response => {
         // fire fakey event to binding
         this.props.binding({
           target: {
             value: response
           }
-        });
+        })
       }
-    });
+    })
   }
 
   render() {
-    /* jshint ignore:start */
-    return <div id="recaptcha" />;
-    /* jshint ignore:end */
+    return <div id="recaptcha" />
   }
 }
 
 export class ReCaptcha extends BaseCaptcha {
   load() {
-    this._include.include('https://www.google.com/recaptcha/api.js', true);
+    this._include.include("https://www.google.com/recaptcha/api.js", true)
 
     return new Promise(function(resolve) {
       var wait = function() {
         if (typeof grecaptcha === "undefined") {
           window.setTimeout(function() {
-            wait();
-          }, 200);
+            wait()
+          }, 200)
         } else {
-          resolve();
+          resolve()
         }
-      };
-      wait();
-    });
+      }
+      wait()
+    })
   }
 
   validator() {
-    return [];
+    return []
   }
 
-  /* jshint ignore:start */
   component(kwargs) {
     return (
       <FormGroup
@@ -128,50 +124,51 @@ export class ReCaptcha extends BaseCaptcha {
         labelClass={kwargs.labelClass || ""}
         controlClass={kwargs.controlClass || ""}
         validation={kwargs.form.state.errors.captcha}
-        helpText={gettext("This test helps us prevent automated spam registrations on our site.")}
+        helpText={gettext(
+          "This test helps us prevent automated spam registrations on our site."
+        )}
       >
         <ReCaptchaComponent
-          binding={kwargs.form.bindInput('captcha')}
-          siteKey={this._context.get('SETTINGS').recaptcha_site_key}
+          binding={kwargs.form.bindInput("captcha")}
+          siteKey={this._context.get("SETTINGS").recaptcha_site_key}
         />
       </FormGroup>
-    );
+    )
   }
-  /* jshint ignore:end */
 }
 
 export class Captcha {
   init(context, ajax, include, snackbar) {
-    switch(context.get('SETTINGS').captcha_type) {
-      case 'no':
-        this._captcha = new NoCaptcha();
-        break;
-
-      case 'qa':
-        this._captcha = new QACaptcha();
-        break;
-
-      case 're':
-        this._captcha = new ReCaptcha();
-        break;
+    switch (context.get("SETTINGS").captcha_type) {
+      case "no":
+        this._captcha = new NoCaptcha()
+        break
+
+      case "qa":
+        this._captcha = new QACaptcha()
+        break
+
+      case "re":
+        this._captcha = new ReCaptcha()
+        break
     }
 
-    this._captcha.init(context, ajax, include, snackbar);
+    this._captcha.init(context, ajax, include, snackbar)
   }
 
   // accessors for underlying strategy
 
   load() {
-    return this._captcha.load();
+    return this._captcha.load()
   }
 
   validator() {
-    return this._captcha.validator();
+    return this._captcha.validator()
   }
 
   component(kwargs) {
-    return this._captcha.component(kwargs);
+    return this._captcha.component(kwargs)
   }
 }
 
-export default new Captcha();
+export default new Captcha()

+ 9 - 9
frontend/src/services/include.js

@@ -1,23 +1,23 @@
 export class Include {
   init(staticUrl) {
-    this._staticUrl = staticUrl;
-    this._included = [];
+    this._staticUrl = staticUrl
+    this._included = []
   }
 
-  include(script, remote=false) {
+  include(script, remote = false) {
     if (this._included.indexOf(script) === -1) {
-      this._included.push(script);
-      this._include(script, remote);
+      this._included.push(script)
+      this._include(script, remote)
     }
   }
 
   _include(script, remote) {
     $.ajax({
-      url: (!remote ? this._staticUrl : '') + script,
+      url: (!remote ? this._staticUrl : "") + script,
       cache: true,
-      dataType: 'script'
-    });
+      dataType: "script"
+    })
   }
 }
 
-export default new Include();
+export default new Include()

+ 14 - 14
frontend/src/services/local-storage.js

@@ -1,30 +1,30 @@
-let storage = window.localStorage;
+let storage = window.localStorage
 
 export class LocalStorage {
   init(prefix) {
-    this._prefix = prefix;
-    this._watchers = [];
+    this._prefix = prefix
+    this._watchers = []
 
-    window.addEventListener('storage', (e) => {
-      let newValueJson = JSON.parse(e.newValue);
+    window.addEventListener("storage", e => {
+      let newValueJson = JSON.parse(e.newValue)
       this._watchers.forEach(function(watcher) {
         if (watcher.key === e.key && e.oldValue !== e.newValue) {
-          watcher.callback(newValueJson);
+          watcher.callback(newValueJson)
         }
-      });
-    });
+      })
+    })
   }
 
   set(key, value) {
-    storage.setItem(this._prefix + key, JSON.stringify(value));
+    storage.setItem(this._prefix + key, JSON.stringify(value))
   }
 
   get(key) {
-    let itemString = storage.getItem(this._prefix + key);
+    let itemString = storage.getItem(this._prefix + key)
     if (itemString) {
-      return JSON.parse(itemString);
+      return JSON.parse(itemString)
     } else {
-      return null;
+      return null
     }
   }
 
@@ -32,8 +32,8 @@ export class LocalStorage {
     this._watchers.push({
       key: this._prefix + key,
       callback: callback
-    });
+    })
   }
 }
 
-export default new LocalStorage();
+export default new LocalStorage()

+ 14 - 14
frontend/src/services/mobile-navbar-dropdown.js

@@ -1,35 +1,35 @@
-import mount from 'misago/utils/mount-component';
+import mount from "misago/utils/mount-component"
 
 export class MobileNavbarDropdown {
   init(element) {
-    this._element = element;
-    this._component = null;
+    this._element = element
+    this._component = null
   }
 
   show(component) {
     if (this._component === component) {
-      this.hide();
+      this.hide()
     } else {
-      this._component = component;
-      mount(component, this._element.id);
-      $(this._element).addClass('open');
+      this._component = component
+      mount(component, this._element.id)
+      $(this._element).addClass("open")
     }
   }
 
   showConnected(name, component) {
     if (this._component === name) {
-      this.hide();
+      this.hide()
     } else {
-      this._component = name;
-      mount(component, this._element.id, true);
-      $(this._element).addClass('open');
+      this._component = name
+      mount(component, this._element.id, true)
+      $(this._element).addClass("open")
     }
   }
 
   hide() {
-    $(this._element).removeClass('open');
-    this._component = null;
+    $(this._element).removeClass("open")
+    this._component = null
   }
 }
 
-export default new MobileNavbarDropdown();
+export default new MobileNavbarDropdown()

+ 11 - 11
frontend/src/services/modal.js

@@ -1,25 +1,25 @@
-import ReactDOM from 'react-dom';
-import mount from 'misago/utils/mount-component';
+import ReactDOM from "react-dom"
+import mount from "misago/utils/mount-component"
 
 export class Modal {
   init(element) {
-    this._element = element;
+    this._element = element
 
-    this._modal = $(element).modal({show: false});
+    this._modal = $(element).modal({ show: false })
 
-    this._modal.on('hidden.bs.modal', () => {
-      ReactDOM.unmountComponentAtNode(this._element);
-    });
+    this._modal.on("hidden.bs.modal", () => {
+      ReactDOM.unmountComponentAtNode(this._element)
+    })
   }
 
   show(component) {
-    mount(component, this._element.id);
-    this._modal.modal('show');
+    mount(component, this._element.id)
+    this._modal.modal("show")
   }
 
   hide() {
-    this._modal.modal('hide');
+    this._modal.modal("hide")
   }
 }
 
-export default new Modal();
+export default new Modal()

+ 62 - 58
frontend/src/services/one-box.js

@@ -1,81 +1,85 @@
-const ytRegExp = new RegExp('^.*(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*');
+const ytRegExp = new RegExp(
+  "^.*(?:(?:youtu.be/|v/|vi/|u/w/|embed/)|(?:(?:watch)??v(?:i)?=|&v(?:i)?=))([^#&?]*).*"
+)
 
 export class OneBox {
   constructor() {
-    this._youtube = {};
+    this._youtube = {}
   }
 
-  // jshint ignore:start
-  render = (domnode) => {
-    if (!domnode) return;
-    this.highlightCode(domnode);
-    this.embedYoutubePlayers(domnode);
-  };
-  // jshint ignore:end
+  render = domnode => {
+    if (!domnode) return
+    this.highlightCode(domnode)
+    this.embedYoutubePlayers(domnode)
+  }
 
   highlightCode(domnode) {
-    const codeblocks = domnode.querySelectorAll('pre>code');
-    for(let i = 0; i < codeblocks.length; i++ ) {
-      const code = codeblocks[i];
-      hljs.highlightBlock(code);
+    const codeblocks = domnode.querySelectorAll("pre>code")
+    for (let i = 0; i < codeblocks.length; i++) {
+      const code = codeblocks[i]
+      hljs.highlightBlock(code)
     }
   }
 
   embedYoutubePlayers(domnode) {
-    const anchors = domnode.querySelectorAll('p>a');
-    for(let i = 0; i < anchors.length; i++ ) {
-      const a = anchors[i];
-      const p = a.parentNode;
-      const onlyChild = p.childNodes.length === 1;
+    const anchors = domnode.querySelectorAll("p>a")
+    for (let i = 0; i < anchors.length; i++) {
+      const a = anchors[i]
+      const p = a.parentNode
+      const onlyChild = p.childNodes.length === 1
 
       if (!this._youtube[a.href]) {
-        this._youtube[a.href] = parseYoutubeUrl(a.href);
+        this._youtube[a.href] = parseYoutubeUrl(a.href)
       }
 
-      const youtubeMovie = this._youtube[a.href];
+      const youtubeMovie = this._youtube[a.href]
       if (onlyChild && !!youtubeMovie && youtubeMovie.data !== false) {
-        this.swapYoutubePlayer(a, youtubeMovie);
+        this.swapYoutubePlayer(a, youtubeMovie)
       }
     }
   }
 
   swapYoutubePlayer(element, youtube) {
-    let url = 'https://www.youtube.com/embed/';
-    url += youtube.video;
-    url += '?rel=0';
+    let url = "https://www.youtube.com/embed/"
+    url += youtube.video
+    url += "?rel=0"
     if (youtube.start) {
-      url += '&start=' + youtube.start;
+      url += "&start=" + youtube.start
     }
 
-    const player = $('<iframe class="embed-responsive-item" src="' + url + '" allowfullscreen></iframe>');
-    $(element).replaceWith(player);
-    player.wrap('<div class="embed-responsive embed-responsive-16by9"></div>');
+    const player = $(
+      '<iframe class="embed-responsive-item" src="' +
+        url +
+        '" allowfullscreen></iframe>'
+    )
+    $(element).replaceWith(player)
+    player.wrap('<div class="embed-responsive embed-responsive-16by9"></div>')
   }
 }
 
-export default new OneBox();
+export default new OneBox()
 
 export function parseYoutubeUrl(url) {
-  const cleanedUrl = cleanUrl(url);
-  const video = getVideoIdFromUrl(cleanedUrl);
+  const cleanedUrl = cleanUrl(url)
+  const video = getVideoIdFromUrl(cleanedUrl)
 
-  if (!video) return null;
+  if (!video) return null
 
-  let start = 0;
-  if (cleanedUrl.indexOf('?') > 0){
-    const query = cleanedUrl.substr(cleanedUrl.indexOf('?') + 1);
-    const timebit = query.split('&').filter((i) => {
-      return i.substr(0, 2) === 't=';
-    })[0];
+  let start = 0
+  if (cleanedUrl.indexOf("?") > 0) {
+    const query = cleanedUrl.substr(cleanedUrl.indexOf("?") + 1)
+    const timebit = query.split("&").filter(i => {
+      return i.substr(0, 2) === "t="
+    })[0]
 
     if (timebit) {
-      const bits = timebit.substr(2).split('m');
-      if (bits[0].substr(-1) === 's') {
-        start += parseInt(bits[0].substr(0, bits[0].length - 1));
+      const bits = timebit.substr(2).split("m")
+      if (bits[0].substr(-1) === "s") {
+        start += parseInt(bits[0].substr(0, bits[0].length - 1))
       } else {
-        start += parseInt(bits[0]) * 60;
-        if (!!bits[1] && bits[1].substr(-1) === 's') {
-          start += parseInt(bits[1].substr(0, bits[1].length - 1));
+        start += parseInt(bits[0]) * 60
+        if (!!bits[1] && bits[1].substr(-1) === "s") {
+          start += parseInt(bits[1].substr(0, bits[1].length - 1))
         }
       }
     }
@@ -84,31 +88,31 @@ export function parseYoutubeUrl(url) {
   return {
     start,
     video
-  };
+  }
 }
 
 export function cleanUrl(url) {
-  let clean = url;
+  let clean = url
 
-  if (url.substr(0, 8) === 'https://') {
-    clean = clean.substr(8);
-  } else if (url.substr(0, 7) === 'http://') {
-    clean = clean.substr(7);
+  if (url.substr(0, 8) === "https://") {
+    clean = clean.substr(8)
+  } else if (url.substr(0, 7) === "http://") {
+    clean = clean.substr(7)
   }
 
-  if (clean.substr(0, 4) === 'www.') {
-    clean = clean.substr(4);
+  if (clean.substr(0, 4) === "www.") {
+    clean = clean.substr(4)
   }
 
-  return clean;
+  return clean
 }
 
 export function getVideoIdFromUrl(url) {
-  if (url.indexOf('youtu') === -1) return null;
+  if (url.indexOf("youtu") === -1) return null
 
-  const video = url.match(ytRegExp);
+  const video = url.match(ytRegExp)
   if (video) {
-    return video[1];
+    return video[1]
   }
-  return null;
-}
+  return null
+}

+ 19 - 15
frontend/src/services/page-title.js

@@ -1,35 +1,39 @@
 export class PageTitle {
   init(indexTitle, forumName) {
-    this._indexTitle = indexTitle;
-    this._forumName = forumName;
+    this._indexTitle = indexTitle
+    this._forumName = forumName
   }
 
   set(title) {
     if (!title) {
-      document.title = this._indexTitle || this._forumName;
-      return;
+      document.title = this._indexTitle || this._forumName
+      return
     }
 
-    if (typeof title === 'string') {
-      title = {title: title};
+    if (typeof title === "string") {
+      title = { title: title }
     }
 
-    let finalTitle = title.title;
+    let finalTitle = title.title
 
     if (title.page > 1) {
-      const pageLabel = interpolate(gettext('page: %(page)s'), {
-        page: title.page
-      }, true);
-
-      finalTitle += ' (' + pageLabel + ')';
+      const pageLabel = interpolate(
+        gettext("page: %(page)s"),
+        {
+          page: title.page
+        },
+        true
+      )
+
+      finalTitle += " (" + pageLabel + ")"
     }
 
     if (title.parent) {
-      finalTitle += ' | ' + title.parent;
+      finalTitle += " | " + title.parent
     }
 
-    document.title = finalTitle + ' | ' + this._forumName;
+    document.title = finalTitle + " | " + this._forumName
   }
 }
 
-export default new PageTitle();
+export default new PageTitle()

+ 29 - 23
frontend/src/services/polls.js

@@ -1,49 +1,55 @@
 export class Polls {
   init(ajax, snackbar) {
-    this._ajax = ajax;
-    this._snackbar = snackbar;
+    this._ajax = ajax
+    this._snackbar = snackbar
 
-    this._polls = {};
+    this._polls = {}
   }
 
   start(kwargs) {
-    this.stop(kwargs.poll);
+    this.stop(kwargs.poll)
 
     const poolServer = () => {
-      this._polls[kwargs.poll] = kwargs;
+      this._polls[kwargs.poll] = kwargs
 
-      this._ajax.get(kwargs.url, kwargs.data || null).then((data) => {
-        if (!this._polls[kwargs.poll]._stopped) {
-          kwargs.update(data);
+      this._ajax.get(kwargs.url, kwargs.data || null).then(
+        data => {
+          if (!this._polls[kwargs.poll]._stopped) {
+            kwargs.update(data)
 
-          this._polls[kwargs.poll].timeout = window.setTimeout(poolServer, kwargs.frequency);
-        }
-      }, (rejection) => {
-        if (!this._polls[kwargs.poll]._stopped) {
-          if (kwargs.error) {
-            kwargs.error(rejection);
-          } else {
-            this._snackbar.apiError(rejection);
+            this._polls[kwargs.poll].timeout = window.setTimeout(
+              poolServer,
+              kwargs.frequency
+            )
+          }
+        },
+        rejection => {
+          if (!this._polls[kwargs.poll]._stopped) {
+            if (kwargs.error) {
+              kwargs.error(rejection)
+            } else {
+              this._snackbar.apiError(rejection)
+            }
           }
         }
-      });
-    };
+      )
+    }
 
     if (kwargs.delayed) {
       this._polls[kwargs.poll] = {
         timeout: window.setTimeout(poolServer, kwargs.frequency)
-      };
+      }
     } else {
-      poolServer();
+      poolServer()
     }
   }
 
   stop(pollId) {
     if (this._polls[pollId]) {
-      window.clearTimeout(this._polls[pollId].timeout);
-      this._polls[pollId]._stopped = true;
+      window.clearTimeout(this._polls[pollId].timeout)
+      this._polls[pollId]._stopped = true
     }
   }
 }
 
-export default new Polls();
+export default new Polls()

+ 47 - 46
frontend/src/services/posting.js

@@ -1,77 +1,78 @@
-import React from 'react'; // jshint ignore:line
-import ReactDOM from 'react-dom'; // jshint ignore:line
-import { PollForm } from 'misago/components/poll'; // jshint ignore:line
-import PostingComponent from 'misago/components/posting'; // jshint ignore:line
-import mount from 'misago/utils/mount-component'; // jshint ignore:line
+import React from "react"
+import ReactDOM from "react-dom"
+import { PollForm } from "misago/components/poll"
+import PostingComponent from "misago/components/posting"
+import mount from "misago/utils/mount-component"
 
 export class Posting {
   init(ajax, snackbar, placeholder) {
-    this._ajax = ajax;
-    this._snackbar = snackbar;
-    this._placeholder = $(placeholder);
+    this._ajax = ajax
+    this._snackbar = snackbar
+    this._placeholder = $(placeholder)
 
-    this._mode = null;
+    this._mode = null
 
-    this._isOpen = false;
-    this._isClosing = false;
+    this._isOpen = false
+    this._isClosing = false
   }
 
   open(props) {
     if (this._isOpen === false) {
-      this._mode = props.mode;
-      this._isOpen = props.submit;
-      this._realOpen(props);
+      this._mode = props.mode
+      this._isOpen = props.submit
+      this._realOpen(props)
     } else if (this._isOpen !== props.submit) {
-      let message = gettext("You are already working on other message. Do you want to discard it?");
-      if (this._mode == 'POLL') {
-        message = gettext("You are already working on a poll. Do you want to discard it?");
+      let message = gettext(
+        "You are already working on other message. Do you want to discard it?"
+      )
+      if (this._mode == "POLL") {
+        message = gettext(
+          "You are already working on a poll. Do you want to discard it?"
+        )
       }
 
-      const changeForm = confirm(message);
+      const changeForm = confirm(message)
       if (changeForm) {
-        this._mode = props.mode;
-        this._isOpen = props.submit;
-        this._realOpen(props);
+        this._mode = props.mode
+        this._isOpen = props.submit
+        this._realOpen(props)
       }
-    } else if (this._mode == 'REPLY' && props.mode == 'REPLY') {
-      this._realOpen(props);
+    } else if (this._mode == "REPLY" && props.mode == "REPLY") {
+      this._realOpen(props)
     }
   }
 
-  // jshint ignore:start
   _realOpen(props) {
-    if (props.mode == 'POLL') {
-      mount(
-        <PollForm {...props} />,
-        'posting-mount'
-      );
+    if (props.mode == "POLL") {
+      mount(<PollForm {...props} />, "posting-mount")
     } else {
-      mount(
-        <PostingComponent {...props} />,
-        'posting-mount'
-      );
+      mount(<PostingComponent {...props} />, "posting-mount")
     }
 
-    this._placeholder.addClass('slide-in');
+    this._placeholder.addClass("slide-in")
 
-    $('html, body').animate({
-      scrollTop: this._placeholder.offset().top
-    }, 1000);
+    $("html, body").animate(
+      {
+        scrollTop: this._placeholder.offset().top
+      },
+      1000
+    )
   }
 
   close = () => {
     if (this._isOpen && !this._isClosing) {
-      this._isClosing = true;
-      this._placeholder.removeClass('slide-in');
+      this._isClosing = true
+      this._placeholder.removeClass("slide-in")
 
       window.setTimeout(() => {
-        ReactDOM.unmountComponentAtNode(document.getElementById('posting-mount'));
-        this._isClosing = false;
-        this._isOpen = false;
-      }, 300);
+        ReactDOM.unmountComponentAtNode(
+          document.getElementById("posting-mount")
+        )
+        this._isClosing = false
+        this._isOpen = false
+      }, 300)
     }
-  };
-  // jshint ignore:end
+  }
 }
 
-export default new Posting();
+export default new Posting()

+ 24 - 24
frontend/src/services/snackbar.js

@@ -1,69 +1,69 @@
-import { showSnackbar, hideSnackbar } from 'misago/reducers/snackbar';
+import { showSnackbar, hideSnackbar } from "misago/reducers/snackbar"
 
-const HIDE_ANIMATION_LENGTH = 300;
-const MESSAGE_SHOW_LENGTH = 5000;
+const HIDE_ANIMATION_LENGTH = 300
+const MESSAGE_SHOW_LENGTH = 5000
 
 export class Snackbar {
   init(store) {
-    this._store = store;
-    this._timeout = null;
+    this._store = store
+    this._timeout = null
   }
 
   alert(message, type) {
     if (this._timeout) {
-      window.clearTimeout(this._timeout);
-      this._store.dispatch(hideSnackbar());
+      window.clearTimeout(this._timeout)
+      this._store.dispatch(hideSnackbar())
 
       this._timeout = window.setTimeout(() => {
-        this._timeout = null;
-        this.alert(message, type);
-      }, HIDE_ANIMATION_LENGTH);
+        this._timeout = null
+        this.alert(message, type)
+      }, HIDE_ANIMATION_LENGTH)
     } else {
-      this._store.dispatch(showSnackbar(message, type));
+      this._store.dispatch(showSnackbar(message, type))
       this._timeout = window.setTimeout(() => {
-        this._store.dispatch(hideSnackbar());
-        this._timeout = null;
-      }, MESSAGE_SHOW_LENGTH);
+        this._store.dispatch(hideSnackbar())
+        this._timeout = null
+      }, MESSAGE_SHOW_LENGTH)
     }
   }
 
   // shorthands for message types
 
   info(message) {
-    this.alert(message, 'info');
+    this.alert(message, "info")
   }
 
   success(message) {
-    this.alert(message, 'success');
+    this.alert(message, "success")
   }
 
   warning(message) {
-    this.alert(message, 'warning');
+    this.alert(message, "warning")
   }
 
   error(message) {
-    this.alert(message, 'error');
+    this.alert(message, "error")
   }
 
   // shorthand for api errors
 
   apiError(rejection) {
-    let message = rejection.detail;
+    let message = rejection.detail
 
     if (!message) {
       if (rejection.status === 404) {
-        message = gettext("Action link is invalid.");
+        message = gettext("Action link is invalid.")
       } else {
-        message = gettext("Unknown error has occured.");
+        message = gettext("Unknown error has occured.")
       }
     }
 
     if (rejection.status === 403 && message === "Permission denied") {
-      message = gettext("You don't have permission to perform this action.");
+      message = gettext("You don't have permission to perform this action.")
     }
 
-    this.error(message);
+    this.error(message)
   }
 }
 
-export default new Snackbar();
+export default new Snackbar()

+ 13 - 11
frontend/src/services/store.js

@@ -1,35 +1,37 @@
-import { combineReducers, createStore } from 'redux';
+import { combineReducers, createStore } from "redux"
 
 export class StoreWrapper {
   constructor() {
-    this._store = null;
-    this._reducers = {};
-    this._initialState = {};
+    this._store = null
+    this._reducers = {}
+    this._initialState = {}
   }
 
   addReducer(name, reducer, initialState) {
-    this._reducers[name] = reducer;
-    this._initialState[name] = initialState;
+    this._reducers[name] = reducer
+    this._initialState[name] = initialState
   }
 
   init() {
     this._store = createStore(
-      combineReducers(this._reducers), this._initialState);
+      combineReducers(this._reducers),
+      this._initialState
+    )
   }
 
   getStore() {
-    return this._store;
+    return this._store
   }
 
   // Store API
 
   getState() {
-    return this._store.getState();
+    return this._store.getState()
   }
 
   dispatch(action) {
-    return this._store.dispatch(action);
+    return this._store.dispatch(action)
   }
 }
 
-export default new StoreWrapper();
+export default new StoreWrapper()

+ 21 - 21
frontend/src/services/zxcvbn.js

@@ -1,55 +1,55 @@
 /* global zxcvbn */
 export class Zxcvbn {
   init(include) {
-    this._include = include;
-    this._isLoaded = false;
+    this._include = include
+    this._isLoaded = false
   }
 
   scorePassword(password, inputs) {
     // 0-4 score, the more the stronger password
     if (this._isLoaded) {
-      return zxcvbn(password, inputs).score;
+      return zxcvbn(password, inputs).score
     }
 
-    return 0;
+    return 0
   }
 
   load() {
     if (!this._isLoaded) {
-      this._include.include('misago/js/zxcvbn.js');
-      return this._loadingPromise();
+      this._include.include("misago/js/zxcvbn.js")
+      return this._loadingPromise()
     } else {
-      return this._loadedPromise();
+      return this._loadedPromise()
     }
   }
 
   _loadingPromise() {
-    const self = this;
+    const self = this
 
     return new Promise(function(resolve, reject) {
-      var wait = function(tries=0) {
-        tries += 1;
+      var wait = function(tries = 0) {
+        tries += 1
         if (tries > 200) {
-          reject();
+          reject()
         } else if (typeof zxcvbn === "undefined") {
           window.setTimeout(function() {
-            wait(tries);
-          }, 200);
+            wait(tries)
+          }, 200)
         } else {
-          self._isLoaded = true;
-          resolve();
+          self._isLoaded = true
+          resolve()
         }
-      };
-      wait();
-    });
+      }
+      wait()
+    })
   }
 
   _loadedPromise() {
     // we have already loaded zxcvbn.js, resolve away!
     return new Promise(function(resolve) {
-      resolve();
-    });
+      resolve()
+    })
   }
 }
 
-export default new Zxcvbn();
+export default new Zxcvbn()

+ 80 - 77
frontend/src/test-setup.js

@@ -1,64 +1,77 @@
-var jQuery = require('jQuery'); // jshint ignore:line
+var jQuery = require("jQuery")
 
-global.$ = jQuery;
-global.jQuery = jQuery;
+global.$ = jQuery
+global.jQuery = jQuery
 
-require('bootstrap-transition');
-require('bootstrap-affix');
-require('bootstrap-modal');
-require('bootstrap-dropdown');
+require("bootstrap-transition")
+require("bootstrap-affix")
+require("bootstrap-modal")
+require("bootstrap-dropdown")
 
-require('dropzone');
-require('cropit');
+require("dropzone")
+require("cropit")
 
-require('jquery-mockjax')(jQuery, window);
-$.mockjaxSettings.logging = false;
-$.mockjaxSettings.responseTime = 50;
+require("jquery-mockjax")(jQuery, window)
+$.mockjaxSettings.logging = false
+$.mockjaxSettings.responseTime = 50
 
 // polyfill es6 features in phantom.js
-require("babel-polyfill");
+require("babel-polyfill")
 
 // Mock base href element
-$('head').append('<base href="/test-runner/">');
+$("head").append('<base href="/test-runner/">')
 
 // Bootstrap's modal (we'll need it anyway for tests);
-$('body').append('<div class="modal fade" id="modal-mount" tabindex="-1" role="dialog" aria-labelledby="misago-modal-label"></div>');
-$('body').append('<div id="dropdown-mount"></div>');
-$('body').append('<div id="page-mount"></div>');
-$('body').append('<div id="test-mount"></div>');
-
+$("body").append(
+  '<div class="modal fade" id="modal-mount" tabindex="-1" role="dialog" aria-labelledby="misago-modal-label"></div>'
+)
+$("body").append('<div id="dropdown-mount"></div>')
+$("body").append('<div id="page-mount"></div>')
+$("body").append('<div id="test-mount"></div>')
 
 // inlined gettext functions form Django
-// jshint ignore: start
-(function (globals) {
-
-  var django = globals.django || (globals.django = {});
+;(function(globals) {
+  var django = globals.django || (globals.django = {})
 
-  django.pluralidx = function (count) { return (count == 1) ? 0 : 1; };
+  django.pluralidx = function(count) {
+    return count == 1 ? 0 : 1
+  }
 
   /* gettext identity library */
 
-  django.gettext = function (msgid) { return msgid; };
-  django.ngettext = function (singular, plural, count) { return (count == 1) ? singular : plural; };
-  django.gettext_noop = function (msgid) { return msgid; };
-  django.pgettext = function (context, msgid) { return msgid; };
-  django.npgettext = function (context, singular, plural, count) { return (count == 1) ? singular : plural; };
-
-
-  django.interpolate = function (fmt, obj, named) {
+  django.gettext = function(msgid) {
+    return msgid
+  }
+  django.ngettext = function(singular, plural, count) {
+    return count == 1 ? singular : plural
+  }
+  django.gettext_noop = function(msgid) {
+    return msgid
+  }
+  django.pgettext = function(context, msgid) {
+    return msgid
+  }
+  django.npgettext = function(context, singular, plural, count) {
+    return count == 1 ? singular : plural
+  }
+
+  django.interpolate = function(fmt, obj, named) {
     if (named) {
-      return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])});
+      return fmt.replace(/%\(\w+\)s/g, function(match) {
+        return String(obj[match.slice(2, -2)])
+      })
     } else {
-      return fmt.replace(/%s/g, function(match){return String(obj.shift())});
+      return fmt.replace(/%s/g, function(match) {
+        return String(obj.shift())
+      })
     }
-  };
-
+  }
 
   /* formatting library */
 
   django.formats = {
-    "DATETIME_FORMAT": "N j, Y, P",
-    "DATETIME_INPUT_FORMATS": [
+    DATETIME_FORMAT: "N j, Y, P",
+    DATETIME_INPUT_FORMATS: [
       "%Y-%m-%d %H:%M:%S",
       "%Y-%m-%d %H:%M:%S.%f",
       "%Y-%m-%d %H:%M",
@@ -72,46 +85,36 @@ $('body').append('<div id="test-mount"></div>');
       "%m/%d/%y %H:%M",
       "%m/%d/%y"
     ],
-    "DATE_FORMAT": "N j, Y",
-    "DATE_INPUT_FORMATS": [
-      "%Y-%m-%d",
-      "%m/%d/%Y",
-      "%m/%d/%y"
-    ],
-    "DECIMAL_SEPARATOR": ".",
-    "FIRST_DAY_OF_WEEK": "0",
-    "MONTH_DAY_FORMAT": "F j",
-    "NUMBER_GROUPING": "3",
-    "SHORT_DATETIME_FORMAT": "m/d/Y P",
-    "SHORT_DATE_FORMAT": "m/d/Y",
-    "THOUSAND_SEPARATOR": ",",
-    "TIME_FORMAT": "P",
-    "TIME_INPUT_FORMATS": [
-      "%H:%M:%S",
-      "%H:%M:%S.%f",
-      "%H:%M"
-    ],
-    "YEAR_MONTH_FORMAT": "F Y"
-  };
-
-  django.get_format = function (format_type) {
-    var value = django.formats[format_type];
-    if (typeof(value) == 'undefined') {
-      return format_type;
+    DATE_FORMAT: "N j, Y",
+    DATE_INPUT_FORMATS: ["%Y-%m-%d", "%m/%d/%Y", "%m/%d/%y"],
+    DECIMAL_SEPARATOR: ".",
+    FIRST_DAY_OF_WEEK: "0",
+    MONTH_DAY_FORMAT: "F j",
+    NUMBER_GROUPING: "3",
+    SHORT_DATETIME_FORMAT: "m/d/Y P",
+    SHORT_DATE_FORMAT: "m/d/Y",
+    THOUSAND_SEPARATOR: ",",
+    TIME_FORMAT: "P",
+    TIME_INPUT_FORMATS: ["%H:%M:%S", "%H:%M:%S.%f", "%H:%M"],
+    YEAR_MONTH_FORMAT: "F Y"
+  }
+
+  django.get_format = function(format_type) {
+    var value = django.formats[format_type]
+    if (typeof value == "undefined") {
+      return format_type
     } else {
-      return value;
+      return value
     }
-  };
+  }
 
   /* add to global namespace */
-  window.pluralidx = django.pluralidx;
-  window.gettext = django.gettext;
-  window.ngettext = django.ngettext;
-  window.gettext_noop = django.gettext_noop;
-  window.pgettext = django.pgettext;
-  window.npgettext = django.npgettext;
-  window.interpolate = django.interpolate;
-  window.get_format = django.get_format;
-
-}(global));
-// jshint ignore: end
+  window.pluralidx = django.pluralidx
+  window.gettext = django.gettext
+  window.ngettext = django.ngettext
+  window.gettext_noop = django.gettext_noop
+  window.pgettext = django.pgettext
+  window.npgettext = django.npgettext
+  window.interpolate = django.interpolate
+  window.get_format = django.get_format
+})(global)

+ 22 - 23
frontend/src/utils/banned-page.js

@@ -1,33 +1,32 @@
-import moment from 'moment'; // jshint ignore:line
-import React from 'react'; // jshint ignore:line
-import ReactDOM from 'react-dom';
-import { Provider, connect } from 'react-redux'; // jshint ignore:line
-import BannedPage from 'misago/components/banned-page'; // jshint ignore:line
-import misago from 'misago/index';
-import store from 'misago/services/store'; // jshint ignore:line
+import moment from "moment"
+import React from "react"
+import ReactDOM from "react-dom"
+import { Provider, connect } from "react-redux"
+import BannedPage from "misago/components/banned-page"
+import misago from "misago/index"
+import store from "misago/services/store"
 
-/* jshint ignore:start */
 let select = function(state) {
-  return state.tick;
-};
+  return state.tick
+}
 
-let RedrawedBannedPage = connect(select)(BannedPage);
-/* jshint ignore:end */
+let RedrawedBannedPage = connect(select)(BannedPage)
 
 export default function(ban, changeState) {
   ReactDOM.render(
-    /* jshint ignore:start */
     <Provider store={store.getStore()}>
-      <RedrawedBannedPage message={ban.message}
-                          expires={ban.expires_on ? moment(ban.expires_on) : null} />
+      <RedrawedBannedPage
+        message={ban.message}
+        expires={ban.expires_on ? moment(ban.expires_on) : null}
+      />
     </Provider>,
-    /* jshint ignore:end */
-    document.getElementById('page-mount')
-  );
 
-  if (typeof changeState === 'undefined' || changeState) {
-    let forumName = misago.get('SETTINGS').forum_name;
-    document.title = gettext("You are banned") + ' | ' + forumName;
-    window.history.pushState({}, "", misago.get('BANNED_URL'));
+    document.getElementById("page-mount")
+  )
+
+  if (typeof changeState === "undefined" || changeState) {
+    let forumName = misago.get("SETTINGS").forum_name
+    document.title = gettext("You are banned") + " | " + forumName
+    window.history.pushState({}, "", misago.get("BANNED_URL"))
   }
-}
+}

+ 12 - 12
frontend/src/utils/batch.js

@@ -1,25 +1,25 @@
-export default function(list, rowWidth, padding=false) {
-  let rows = [];
-  let row = [];
+export default function(list, rowWidth, padding = false) {
+  let rows = []
+  let row = []
 
   list.forEach(function(element) {
-    row.push(element);
+    row.push(element)
     if (row.length === rowWidth) {
-      rows.push(row);
-      row = [];
+      rows.push(row)
+      row = []
     }
-  });
+  })
 
   // pad row to required length?
   if (padding !== false && row.length > 0 && row.length < rowWidth) {
-    for (let i = row.length; i < rowWidth; i ++) {
-      row.push(padding);
+    for (let i = row.length; i < rowWidth; i++) {
+      row.push(padding)
     }
   }
 
   if (row.length) {
-    rows.push(row);
+    rows.push(row)
   }
 
-  return rows;
-}
+  return rows
+}

+ 6 - 6
frontend/src/utils/concat-unique.js

@@ -1,11 +1,11 @@
 export default function(a, b) {
-  let ids = [];
+  let ids = []
   return a.concat(b).filter(function(item) {
     if (ids.indexOf(item.id) === -1) {
-      ids.push(item.id);
-      return true;
+      ids.push(item.id)
+      return true
     } else {
-      return false;
+      return false
     }
-  });
-}
+  })
+}

+ 5 - 5
frontend/src/utils/countdown.js

@@ -1,13 +1,13 @@
 export default class {
   constructor(callback, count) {
-    this._callback = callback;
-    this._count = count;
+    this._callback = callback
+    this._count = count
   }
 
   count() {
-    this._count -= 1;
+    this._count -= 1
     if (this._count === 0) {
-      this._callback();
+      this._callback()
     }
   }
-}
+}

+ 10 - 8
frontend/src/utils/escape-html.js

@@ -1,11 +1,13 @@
 const map = {
-  '&': '&amp;',
-  '<': '&lt;',
-  '>': '&gt;',
-  '"': '&quot;',
-  "'": '&#039;'
-};
+  "&": "&amp;",
+  "<": "&lt;",
+  ">": "&gt;",
+  '"': "&quot;",
+  "'": "&#039;"
+}
 
 export default function(text) {
-  return text.replace(/[&<>"']/g, function(m) { return map[m]; });
-}
+  return text.replace(/[&<>"']/g, function(m) {
+    return map[m]
+  })
+}

+ 6 - 6
frontend/src/utils/file-size.js

@@ -1,15 +1,15 @@
 export default function(bytes) {
   if (bytes > 1024 * 1024 * 1024) {
-    return roundSize(bytes / (1024 * 1024 * 1024)) + ' GB';
+    return roundSize(bytes / (1024 * 1024 * 1024)) + " GB"
   } else if (bytes > 1024 * 1024) {
-    return roundSize(bytes / (1024 * 1024)) + ' MB';
+    return roundSize(bytes / (1024 * 1024)) + " MB"
   } else if (bytes > 1024) {
-    return roundSize(bytes / 1024) + ' KB';
+    return roundSize(bytes / 1024) + " KB"
   } else {
-    return roundSize(bytes) + ' B';
+    return roundSize(bytes) + " B"
   }
 }
 
 export function roundSize(value) {
-  return value.toFixed(1);
-}
+  return value.toFixed(1)
+}

+ 6 - 3
frontend/src/utils/is-url.js

@@ -1,5 +1,8 @@
-const URL_PATTERN = new RegExp('^(https?:\\/\\/)?((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|((\\d{1,3}\\.){3}\\d{1,3}))(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*(\\?[;&a-z\\d%_.~+=-]*)?(\\#[-a-z\\d_]*)?$','i');
+const URL_PATTERN = new RegExp(
+  "^(https?:\\/\\/)?((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|((\\d{1,3}\\.){3}\\d{1,3}))(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*(\\?[;&a-z\\d%_.~+=-]*)?(\\#[-a-z\\d_]*)?$",
+  "i"
+)
 
 export default function(str) {
-  return URL_PATTERN.test($.trim(str));
-}
+  return URL_PATTERN.test($.trim(str))
+}

+ 11 - 18
frontend/src/utils/mount-component.js

@@ -1,29 +1,22 @@
-import React from 'react'; // jshint ignore:line
-import ReactDOM from 'react-dom';
-import { Provider } from 'react-redux'; // jshint ignore:line
-import store from 'misago/services/store'; // jshint ignore:line
+import React from "react"
+import ReactDOM from "react-dom"
+import { Provider } from "react-redux"
+import store from "misago/services/store"
 
-export default function(Component, rootElementId, connected=true) {
-  let rootElement = document.getElementById(rootElementId);
+export default function(Component, rootElementId, connected = true) {
+  let rootElement = document.getElementById(rootElementId)
 
-  /* jshint ignore:start */
-  let finalComponent = Component.props ? Component : <Component />;
-  /* jshint ignore:end */
+  let finalComponent = Component.props ? Component : <Component />
 
   if (rootElement) {
     if (connected) {
       ReactDOM.render(
-        /* jshint ignore:start */
-        <Provider store={store.getStore()}>
-          {finalComponent}
-        </Provider>,
-        /* jshint ignore:end */
+        <Provider store={store.getStore()}>{finalComponent}</Provider>,
+
         rootElement
-      );
+      )
     } else {
-      /* jshint ignore:start */
-      ReactDOM.render(finalComponent, rootElement);
-      /* jshint ignore:end */
+      ReactDOM.render(finalComponent, rootElement)
     }
   }
 }

+ 93 - 93
frontend/src/utils/ordered-list.js

@@ -1,117 +1,117 @@
 class OrderedList {
-    constructor(items) {
-      this.isOrdered = false;
-      this._items = items || [];
-    }
+  constructor(items) {
+    this.isOrdered = false
+    this._items = items || []
+  }
 
-    add(key, item, order) {
-      this._items.push({
-        key: key,
-        item: item,
+  add(key, item, order) {
+    this._items.push({
+      key: key,
+      item: item,
 
-        after: order ? order.after || null : null,
-        before: order ? order.before || null : null
-      });
-    }
+      after: order ? order.after || null : null,
+      before: order ? order.before || null : null
+    })
+  }
 
-    get(key, value) {
-      for (var i = 0; i < this._items.length; i++) {
-        if (this._items[i].key === key) {
-          return this._items[i].item;
-        }
+  get(key, value) {
+    for (var i = 0; i < this._items.length; i++) {
+      if (this._items[i].key === key) {
+        return this._items[i].item
       }
-
-      return value;
     }
 
-    has(key) {
-      return this.get(key) !== undefined;
-    }
+    return value
+  }
 
-    values() {
-      var values = [];
-      for (var i = 0; i < this._items.length; i++) {
-        values.push(this._items[i].item);
-      }
-      return values;
-    }
+  has(key) {
+    return this.get(key) !== undefined
+  }
 
-    order(values_only) {
-      if (!this.isOrdered) {
-        this._items = this._order(this._items);
-        this.isOrdered = true;
-      }
+  values() {
+    var values = []
+    for (var i = 0; i < this._items.length; i++) {
+      values.push(this._items[i].item)
+    }
+    return values
+  }
 
-      if (values_only || typeof values_only === 'undefined') {
-        return this.values();
-      } else {
-        return this._items;
-      }
+  order(values_only) {
+    if (!this.isOrdered) {
+      this._items = this._order(this._items)
+      this.isOrdered = true
     }
 
-    orderedValues() {
-      return this.order(true);
+    if (values_only || typeof values_only === "undefined") {
+      return this.values()
+    } else {
+      return this._items
     }
+  }
 
-    _order(unordered) {
-      // Index of unordered items
-      var index = [];
-      unordered.forEach(function (item) {
-        index.push(item.key);
-      });
-
-      // Ordered items
-      var ordered = [];
-      var ordering = [];
-
-      // First pass: register items that
-      // don't specify their order
-      unordered.forEach(function (item) {
-        if (!item.after && !item.before) {
-          ordered.push(item);
-          ordering.push(item.key);
-        }
-      });
-
-      // Second pass: register items that
-      // specify their before to "_end"
-      unordered.forEach(function (item) {
-        if (item.before === "_end") {
-          ordered.push(item);
-          ordering.push(item.key);
-        }
-      });
-
-      // Third pass: keep iterating items
-      // until we hit iterations limit or finish
-      // ordering list
-      function insertItem(item) {
-        var insertAt = -1;
-        if (ordering.indexOf(item.key) === -1) {
-          if (item.after) {
-            insertAt = ordering.indexOf(item.after);
-            if (insertAt !== -1) {
-              insertAt += 1;
-            }
-          } else if (item.before) {
-            insertAt = ordering.indexOf(item.before);
-          }
+  orderedValues() {
+    return this.order(true)
+  }
 
+  _order(unordered) {
+    // Index of unordered items
+    var index = []
+    unordered.forEach(function(item) {
+      index.push(item.key)
+    })
+
+    // Ordered items
+    var ordered = []
+    var ordering = []
+
+    // First pass: register items that
+    // don't specify their order
+    unordered.forEach(function(item) {
+      if (!item.after && !item.before) {
+        ordered.push(item)
+        ordering.push(item.key)
+      }
+    })
+
+    // Second pass: register items that
+    // specify their before to "_end"
+    unordered.forEach(function(item) {
+      if (item.before === "_end") {
+        ordered.push(item)
+        ordering.push(item.key)
+      }
+    })
+
+    // Third pass: keep iterating items
+    // until we hit iterations limit or finish
+    // ordering list
+    function insertItem(item) {
+      var insertAt = -1
+      if (ordering.indexOf(item.key) === -1) {
+        if (item.after) {
+          insertAt = ordering.indexOf(item.after)
           if (insertAt !== -1) {
-            ordered.splice(insertAt, 0, item);
-            ordering.splice(insertAt, 0, item.key);
+            insertAt += 1
           }
+        } else if (item.before) {
+          insertAt = ordering.indexOf(item.before)
         }
-      }
 
-      var iterations = 200;
-      while (iterations > 0 && index.length !== ordering.length) {
-        iterations -= 1;
-        unordered.forEach(insertItem);
+        if (insertAt !== -1) {
+          ordered.splice(insertAt, 0, item)
+          ordering.splice(insertAt, 0, item.key)
+        }
       }
+    }
 
-      return ordered;
+    var iterations = 200
+    while (iterations > 0 && index.length !== ordering.length) {
+      iterations -= 1
+      unordered.forEach(insertItem)
     }
+
+    return ordered
   }
+}
 
-  export default OrderedList;
+export default OrderedList

+ 6 - 6
frontend/src/utils/random.js

@@ -1,12 +1,12 @@
 export function int(min, max) {
-  return Math.floor((Math.random() * (max - min + 1))) + min;
+  return Math.floor(Math.random() * (max - min + 1)) + min
 }
 
 export function range(min, max) {
-  let array = new Array(int(min, max));
-  for(let i=0; i<array.length; i++){
-    array[i] = i;
+  let array = new Array(int(min, max))
+  for (let i = 0; i < array.length; i++) {
+    array[i] = i
   }
 
-  return array;
-}
+  return array
+}

+ 2 - 2
frontend/src/utils/reset-scroll.js

@@ -1,3 +1,3 @@
 export default function() {
-  window.scrollTo(0, 0);
-}
+  window.scrollTo(0, 0)
+}

+ 11 - 12
frontend/src/utils/routed-component.js

@@ -1,29 +1,28 @@
-// jshint ignore:start
-import React from 'react';
-import ReactDOM from 'react-dom';
-import { Provider } from 'react-redux';
-import { Router, browserHistory } from 'react-router';
-import store from 'misago/services/store';
+import React from "react"
+import ReactDOM from "react-dom"
+import { Provider } from "react-redux"
+import { Router, browserHistory } from "react-router"
+import store from "misago/services/store"
 
-const rootElement = document.getElementById('page-mount');
+const rootElement = document.getElementById("page-mount")
 
 export default function(options) {
   let routes = {
     component: options.component || null,
     childRoutes: []
-  };
+  }
 
   if (options.root) {
     routes.childRoutes = [
       {
         path: options.root,
         onEnter: function(nextState, replaceState) {
-          replaceState(null, options.paths[0].path);
+          replaceState(null, options.paths[0].path)
         }
       }
-    ].concat(options.paths);
+    ].concat(options.paths)
   } else {
-    routes.childRoutes = options.paths;
+    routes.childRoutes = options.paths
   }
 
   ReactDOM.render(
@@ -31,5 +30,5 @@ export default function(options) {
       <Router routes={routes} history={browserHistory} />
     </Provider>,
     rootElement
-  );
+  )
 }

+ 13 - 13
frontend/src/utils/sets.js

@@ -1,31 +1,31 @@
 export function push(array, value) {
   if (array.indexOf(value) === -1) {
-    let copy = array.slice();
-    copy.push(value);
-    return copy;
+    let copy = array.slice()
+    copy.push(value)
+    return copy
   } else {
-    return array;
+    return array
   }
 }
 
 export function remove(array, value) {
   if (array.indexOf(value) >= 0) {
     return array.filter(function(i) {
-      return i !== value;
-    });
+      return i !== value
+    })
   } else {
-    return array;
+    return array
   }
 }
 
 export function toggle(array, value) {
   if (array.indexOf(value) === -1) {
-    let copy = array.slice();
-    copy.push(value);
-    return copy;
+    let copy = array.slice()
+    copy.push(value)
+    return copy
   } else {
     return array.filter(function(i) {
-      return i !== value;
-    });
+      return i !== value
+    })
   }
-}
+}

+ 12 - 12
frontend/src/utils/string-count.js

@@ -1,22 +1,22 @@
 export default function(string, subString) {
-  string = (string + "").toLowerCase();
-  subString = (subString + "").toLowerCase();
+  string = (string + "").toLowerCase()
+  subString = (subString + "").toLowerCase()
 
-  if (subString.length <= 0) return 0;
+  if (subString.length <= 0) return 0
 
-  let n = 0;
-  let pos = 0;
-  let step = subString.length;
+  let n = 0
+  let pos = 0
+  let step = subString.length
 
   while (true) {
-    pos = string.indexOf(subString, pos);
+    pos = string.indexOf(subString, pos)
     if (pos >= 0) {
-      n += 1;
-      pos += step;
+      n += 1
+      pos += step
     } else {
-      break;
+      break
     }
   }
 
-  return n;
-}
+  return n
+}

+ 59 - 48
frontend/src/utils/test-utils.js

@@ -1,33 +1,37 @@
-import React from 'react'; // jshint ignore:line
-import ReactDOM from 'react-dom';
-import ReactTestUtils from 'react-addons-test-utils';
+import React from "react"
+import ReactDOM from "react-dom"
+import ReactTestUtils from "react-addons-test-utils"
 
 // clean test mounts from components
 export function render(containerOrComponent, Component) {
   if (Component) {
     return ReactDOM.render(
-      Component, document.getElementById(containerOrComponent + '-mount'));
+      Component,
+      document.getElementById(containerOrComponent + "-mount")
+    )
   } else {
     return ReactDOM.render(
-      containerOrComponent, document.getElementById('test-mount'));
+      containerOrComponent,
+      document.getElementById("test-mount")
+    )
   }
 }
 
 export function unmountComponents() {
-  ReactDOM.unmountComponentAtNode(document.getElementById('dropdown-mount'));
-  ReactDOM.unmountComponentAtNode(document.getElementById('modal-mount'));
-  ReactDOM.unmountComponentAtNode(document.getElementById('page-mount'));
-  ReactDOM.unmountComponentAtNode(document.getElementById('test-mount'));
+  ReactDOM.unmountComponentAtNode(document.getElementById("dropdown-mount"))
+  ReactDOM.unmountComponentAtNode(document.getElementById("modal-mount"))
+  ReactDOM.unmountComponentAtNode(document.getElementById("page-mount"))
+  ReactDOM.unmountComponentAtNode(document.getElementById("test-mount"))
 }
 
 // global utility for mocking context
 export function contextClear(misago) {
-  misago._context = {};
+  misago._context = {}
 }
 
 export function mockUser(overrides) {
   let user = {
-    id : 42,
+    id: 42,
     absolute_url: "/user/loremipsum-42/",
     api_url: {
       avatar: "/test-api/users/42/avatar/",
@@ -47,7 +51,8 @@ export function mockUser(overrides) {
       id: 1,
 
       css_class: "team",
-      description: '<p>Lorem ipsum dolor met sit amet elit, si vis pacem para bellum.</p>\n<p>To help see <a href="http://wololo.com/something.php?page=2131">http://wololo.com/something.php?page=2131</a></p>',
+      description:
+        '<p>Lorem ipsum dolor met sit amet elit, si vis pacem para bellum.</p>\n<p>To help see <a href="http://wololo.com/something.php?page=2131">http://wololo.com/something.php?page=2131</a></p>',
       is_tab: true,
       name: "Forum team",
       slug: "forum-team",
@@ -64,12 +69,12 @@ export function mockUser(overrides) {
     status: null,
 
     acl: {}
-  };
+  }
 
   if (overrides) {
-    return Object.assign(user, overrides);
+    return Object.assign(user, overrides)
   } else {
-    return user;
+    return user
   }
 }
 
@@ -78,11 +83,11 @@ export function contextGuest(misago) {
     isAuthenticated: false,
 
     user: {
-      id : null,
+      id: null,
 
       acl: {}
     }
-  });
+  })
 }
 
 export function contextAuthenticated(misago, overrides) {
@@ -90,14 +95,20 @@ export function contextAuthenticated(misago, overrides) {
     isAuthenticated: true,
 
     user: mockUser(overrides)
-  });
+  })
 }
 
 // global utility function for store mocking
 export function initEmptyStore(store) {
-  store.constructor();
-  store.addReducer('tick', function(state={}, action=null) { return {}; }, {}); // jshint ignore:line
-  store.init();
+  store.constructor()
+  store.addReducer(
+    "tick",
+    function(state = {}, action = null) {
+      return {}
+    },
+    {}
+  )
+  store.init()
 }
 
 export function snackbarStoreMock() {
@@ -106,34 +117,34 @@ export function snackbarStoreMock() {
     _callback: null,
 
     callback: function(callback) {
-      this._callback = callback;
+      this._callback = callback
     },
 
     dispatch: function(action) {
-      if (action.type === 'SHOW_SNACKBAR') {
+      if (action.type === "SHOW_SNACKBAR") {
         this.message = {
           message: action.message,
           type: action.messageType
-        };
+        }
 
         if (this._callback) {
           window.setTimeout(() => {
-            this._callback(this.message);
-          }, 100);
+            this._callback(this.message)
+          }, 100)
         }
       }
     }
-  };
+  }
 }
 
 // global init function for modal and dropdown services
 export function initModal(modal) {
-  $('#modal-mount').off();
-  modal.init(document.getElementById('modal-mount'));
+  $("#modal-mount").off()
+  modal.init(document.getElementById("modal-mount"))
 }
 
 export function initDropdown(dropdown) {
-  dropdown.init(document.getElementById('dropdown-mount'));
+  dropdown.init(document.getElementById("dropdown-mount"))
 }
 
 // global util for reseting snackbar
@@ -143,54 +154,54 @@ export function snackbarClear(snackbar) {
   // suite where one tests check's snackbar state before it has reopened with
   // new message set by current test
   if (snackbar._timeout) {
-    window.clearTimeout(snackbar._timeout);
-    snackbar._timeout = null;
+    window.clearTimeout(snackbar._timeout)
+    snackbar._timeout = null
   }
 }
 
 // global util functions for events
 export function simulateClick(selector) {
   if ($(selector).length) {
-    ReactTestUtils.Simulate.click($(selector).get(0));
+    ReactTestUtils.Simulate.click($(selector).get(0))
   } else {
-    throw 'selector "' + selector + '" did not match anything';
+    throw 'selector "' + selector + '" did not match anything'
   }
 }
 
 export function simulateSubmit(selector) {
   if ($(selector).length) {
-    ReactTestUtils.Simulate.submit($(selector).get(0));
+    ReactTestUtils.Simulate.submit($(selector).get(0))
   } else {
-    throw 'selector "' + selector + '" did not match anything';
+    throw 'selector "' + selector + '" did not match anything'
   }
 }
 
 export function simulateChange(selector, value) {
   if ($(selector).length) {
-    $(selector).val(value);
-    ReactTestUtils.Simulate.change($(selector).get(0));
+    $(selector).val(value)
+    ReactTestUtils.Simulate.change($(selector).get(0))
   } else {
-    throw 'selector "' + selector + '" did not match anything';
+    throw 'selector "' + selector + '" did not match anything'
   }
 }
 
 export function afterAjax(callback) {
   window.setTimeout(function() {
-    callback();
-  }, 200);
+    callback()
+  }, 200)
 }
 
 export function onElement(selector, callback) {
   let _getElement = function() {
     window.setTimeout(function() {
-      let element = $(selector);
+      let element = $(selector)
       if (element.length >= 1) {
-        callback(element);
+        callback(element)
       } else {
-        _getElement();
+        _getElement()
       }
-    }, 50);
-  };
+    }, 50)
+  }
 
-  _getElement();
-}
+  _getElement()
+}

+ 63 - 44
frontend/src/utils/validators.js

@@ -1,74 +1,84 @@
-const EMAIL = /^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
-const USERNAME = new RegExp('^[0-9a-z]+$', 'i');
+const EMAIL = /^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i
+const USERNAME = new RegExp("^[0-9a-z]+$", "i")
 
 export function required(message) {
   return function(value) {
     if (value === false || value === null || $.trim(value).length === 0) {
-      return message || gettext("This field is required.");
+      return message || gettext("This field is required.")
     }
-  };
+  }
 }
 
 export function requiredTermsOfService(message) {
-  const error = gettext("You have to accept the terms of service.");
-  return required(message || error);
+  const error = gettext("You have to accept the terms of service.")
+  return required(message || error)
 }
 
 export function requiredPrivacyPolicy(message) {
-  const error = gettext("You have to accept the privacy policy.");
-  return required(message || error);
+  const error = gettext("You have to accept the privacy policy.")
+  return required(message || error)
 }
 
 export function email(message) {
   return function(value) {
     if (!EMAIL.test(value)) {
-      return message || gettext("Enter a valid email address.");
+      return message || gettext("Enter a valid email address.")
     }
-  };
+  }
 }
 
 export function minLength(limitValue, message) {
   return function(value) {
-    var returnMessage = '';
-    var length = $.trim(value).length;
+    var returnMessage = ""
+    var length = $.trim(value).length
 
     if (length < limitValue) {
       if (message) {
-        returnMessage = message(limitValue, length);
+        returnMessage = message(limitValue, length)
       } else {
         returnMessage = ngettext(
           "Ensure this value has at least %(limit_value)s character (it has %(show_value)s).",
           "Ensure this value has at least %(limit_value)s characters (it has %(show_value)s).",
-          limitValue);
+          limitValue
+        )
       }
-      return interpolate(returnMessage, {
-        limit_value: limitValue,
-        show_value: length
-      }, true);
+      return interpolate(
+        returnMessage,
+        {
+          limit_value: limitValue,
+          show_value: length
+        },
+        true
+      )
     }
-  };
+  }
 }
 
 export function maxLength(limitValue, message) {
   return function(value) {
-    var returnMessage = '';
-    var length = $.trim(value).length;
+    var returnMessage = ""
+    var length = $.trim(value).length
 
     if (length > limitValue) {
       if (message) {
-        returnMessage = message(limitValue, length);
+        returnMessage = message(limitValue, length)
       } else {
         returnMessage = ngettext(
           "Ensure this value has at most %(limit_value)s character (it has %(show_value)s).",
           "Ensure this value has at most %(limit_value)s characters (it has %(show_value)s).",
-          limitValue);
+          limitValue
+        )
       }
-      return interpolate(returnMessage, {
-        limit_value: limitValue,
-        show_value: length
-      }, true);
+      return interpolate(
+        returnMessage,
+        {
+          limit_value: limitValue,
+          show_value: length
+        },
+        true
+      )
     }
-  };
+  }
 }
 
 export function usernameMinLength(lengthMin) {
@@ -76,9 +86,10 @@ export function usernameMinLength(lengthMin) {
     return ngettext(
       "Username must be at least %(limit_value)s character long.",
       "Username must be at least %(limit_value)s characters long.",
-      lengthMin);
-  };
-  return minLength(lengthMin, message);
+      lengthMin
+    )
+  }
+  return minLength(lengthMin, message)
 }
 
 export function usernameMaxLength(lengthMax) {
@@ -86,33 +97,41 @@ export function usernameMaxLength(lengthMax) {
     return ngettext(
       "Username cannot be longer than %(limit_value)s character.",
       "Username cannot be longer than %(limit_value)s characters.",
-      lengthMax);
-  };
-  return maxLength(lengthMax, message);
+      lengthMax
+    )
+  }
+  return maxLength(lengthMax, message)
 }
 
 export function usernameContent() {
   return function(value) {
     if (!USERNAME.test($.trim(value))) {
-      return gettext("Username can only contain latin alphabet letters and digits.");
+      return gettext(
+        "Username can only contain latin alphabet letters and digits."
+      )
     }
-  };
+  }
 }
 
 export function passwordMinLength(limitValue) {
   return function(value) {
-    const length = value.length;
+    const length = value.length
 
     if (length < limitValue) {
       const returnMessage = ngettext(
         "Valid password must be at least %(limit_value)s character long.",
         "Valid password must be at least %(limit_value)s characters long.",
-        limitValue);
+        limitValue
+      )
 
-      return interpolate(returnMessage, {
-        limit_value: limitValue,
-        show_value: length
-      }, true);
+      return interpolate(
+        returnMessage,
+        {
+          limit_value: limitValue,
+          show_value: length
+        },
+        true
+      )
     }
-  };
-}
+  }
+}

+ 14 - 14
frontend/src/vendor.js

@@ -1,18 +1,18 @@
-var jQuery = require('jquery'); // jshint ignore:line
-var moment = require('moment');
+var jQuery = require("jquery")
+var moment = require("moment")
 
-global.$ = jQuery;
-global.jQuery = jQuery;
-global.moment = moment;
+global.$ = jQuery
+global.jQuery = jQuery
+global.moment = moment
 
-require('bootstrap-transition');
-require('bootstrap-affix');
-require('bootstrap-modal');
-require('bootstrap-dropdown');
-require('at-js');
+require("bootstrap-transition")
+require("bootstrap-affix")
+require("bootstrap-modal")
+require("bootstrap-dropdown")
+require("at-js")
 
-require('cropit');
-require('waypoints');
+require("cropit")
+require("waypoints")
 
-require('jquery-caret');
-require('highlight');
+require("jquery-caret")
+require("highlight")

Some files were not shown because too many files changed in this diff