threddedDANIEL 0.14.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (367) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +744 -0
  4. data/app/assets/images/favicons/README.md +3 -0
  5. data/app/assets/images/favicons/amazon.png +0 -0
  6. data/app/assets/images/favicons/github.png +0 -0
  7. data/app/assets/images/favicons/google_branding/logo_calendar_128px.png +0 -0
  8. data/app/assets/images/favicons/google_branding/logo_docs_48px.png +0 -0
  9. data/app/assets/images/favicons/google_branding/logo_drive_48px.png +0 -0
  10. data/app/assets/images/favicons/google_branding/logo_forms_48px.png +0 -0
  11. data/app/assets/images/favicons/google_branding/logo_sheets_48px.png +0 -0
  12. data/app/assets/images/favicons/google_branding/logo_slides_48px.png +0 -0
  13. data/app/assets/images/favicons/stackexchange.png +0 -0
  14. data/app/assets/images/favicons/twitter.png +0 -0
  15. data/app/assets/images/favicons/wikipedia.png +0 -0
  16. data/app/assets/images/thredded/breadcrumb-chevron.svg +1 -0
  17. data/app/assets/images/thredded/follow.svg +1 -0
  18. data/app/assets/images/thredded/lock.svg +1 -0
  19. data/app/assets/images/thredded/moderation.svg +1 -0
  20. data/app/assets/images/thredded/private-messages.svg +1 -0
  21. data/app/assets/images/thredded/settings.svg +1 -0
  22. data/app/assets/images/thredded/three-dot-menu.svg +3 -0
  23. data/app/assets/images/thredded/unfollow.svg +1 -0
  24. data/app/assets/javascripts/thredded.es6 +2 -0
  25. data/app/assets/javascripts/thredded/components/currently_online.es6 +32 -0
  26. data/app/assets/javascripts/thredded/components/flash_messages.es6 +9 -0
  27. data/app/assets/javascripts/thredded/components/mention_autocompletion.es6 +40 -0
  28. data/app/assets/javascripts/thredded/components/post_form.es6 +37 -0
  29. data/app/assets/javascripts/thredded/components/preview_area.es6 +56 -0
  30. data/app/assets/javascripts/thredded/components/quote_post.es6 +49 -0
  31. data/app/assets/javascripts/thredded/components/time_stamps.es6 +25 -0
  32. data/app/assets/javascripts/thredded/components/topic_form.es6 +94 -0
  33. data/app/assets/javascripts/thredded/components/topics.es6 +46 -0
  34. data/app/assets/javascripts/thredded/components/turboforms.es6 +25 -0
  35. data/app/assets/javascripts/thredded/components/user_preferences_form.es6 +66 -0
  36. data/app/assets/javascripts/thredded/components/user_textcomplete.es6 +50 -0
  37. data/app/assets/javascripts/thredded/components/users_select.es6 +122 -0
  38. data/app/assets/javascripts/thredded/core/csrf_tokens.es6 +9 -0
  39. data/app/assets/javascripts/thredded/core/debounce.es6 +32 -0
  40. data/app/assets/javascripts/thredded/core/escape_html.es6 +7 -0
  41. data/app/assets/javascripts/thredded/core/hide_soft_keyboard.es6 +7 -0
  42. data/app/assets/javascripts/thredded/core/on_page_load.es6 +54 -0
  43. data/app/assets/javascripts/thredded/core/serialize_form.es6 +9 -0
  44. data/app/assets/javascripts/thredded/core/thredded.es6 +1 -0
  45. data/app/assets/javascripts/thredded/dependencies.js +8 -0
  46. data/app/assets/javascripts/thredded/dependencies/autosize.js +1 -0
  47. data/app/assets/javascripts/thredded/dependencies/textcomplete.js +1 -0
  48. data/app/assets/javascripts/thredded/dependencies/timeago.js +1 -0
  49. data/app/assets/javascripts/thredded/dependencies/ujs.js +1 -0
  50. data/app/assets/javascripts/thredded/thredded.es6 +2 -0
  51. data/app/assets/stylesheets/thredded.scss +3 -0
  52. data/app/assets/stylesheets/thredded/_base.scss +13 -0
  53. data/app/assets/stylesheets/thredded/_dependencies.scss +0 -0
  54. data/app/assets/stylesheets/thredded/_email.scss +51 -0
  55. data/app/assets/stylesheets/thredded/_thredded.scss +33 -0
  56. data/app/assets/stylesheets/thredded/base/_alerts.scss +44 -0
  57. data/app/assets/stylesheets/thredded/base/_buttons.scss +73 -0
  58. data/app/assets/stylesheets/thredded/base/_dropdown-menu.scss +83 -0
  59. data/app/assets/stylesheets/thredded/base/_forms.scss +84 -0
  60. data/app/assets/stylesheets/thredded/base/_grid.scss +58 -0
  61. data/app/assets/stylesheets/thredded/base/_lists.scss +31 -0
  62. data/app/assets/stylesheets/thredded/base/_nav.scss +67 -0
  63. data/app/assets/stylesheets/thredded/base/_tables.scss +17 -0
  64. data/app/assets/stylesheets/thredded/base/_typography.scss +72 -0
  65. data/app/assets/stylesheets/thredded/base/_variables.scss +141 -0
  66. data/app/assets/stylesheets/thredded/components/_alerts.scss +19 -0
  67. data/app/assets/stylesheets/thredded/components/_base.scss +25 -0
  68. data/app/assets/stylesheets/thredded/components/_currently-online.scss +54 -0
  69. data/app/assets/stylesheets/thredded/components/_empty.scss +11 -0
  70. data/app/assets/stylesheets/thredded/components/_flash-message.scss +19 -0
  71. data/app/assets/stylesheets/thredded/components/_following.scss +14 -0
  72. data/app/assets/stylesheets/thredded/components/_form-list.scss +39 -0
  73. data/app/assets/stylesheets/thredded/components/_icons.scss +3 -0
  74. data/app/assets/stylesheets/thredded/components/_main-section.scss +9 -0
  75. data/app/assets/stylesheets/thredded/components/_mention-autocomplete.scss +57 -0
  76. data/app/assets/stylesheets/thredded/components/_messageboard.scss +155 -0
  77. data/app/assets/stylesheets/thredded/components/_onebox.scss +275 -0
  78. data/app/assets/stylesheets/thredded/components/_pagination.scss +35 -0
  79. data/app/assets/stylesheets/thredded/components/_post-form.scss +27 -0
  80. data/app/assets/stylesheets/thredded/components/_post.scss +138 -0
  81. data/app/assets/stylesheets/thredded/components/_preferences.scss +19 -0
  82. data/app/assets/stylesheets/thredded/components/_preview_area.scss +11 -0
  83. data/app/assets/stylesheets/thredded/components/_topic-delete.scss +8 -0
  84. data/app/assets/stylesheets/thredded/components/_topic-header.scss +111 -0
  85. data/app/assets/stylesheets/thredded/components/_topics.scss +186 -0
  86. data/app/assets/stylesheets/thredded/layout/_main-container.scss +16 -0
  87. data/app/assets/stylesheets/thredded/layout/_main-navigation.scss +79 -0
  88. data/app/assets/stylesheets/thredded/layout/_moderation.scss +97 -0
  89. data/app/assets/stylesheets/thredded/layout/_navigation.scss +87 -0
  90. data/app/assets/stylesheets/thredded/layout/_search-navigation.scss +126 -0
  91. data/app/assets/stylesheets/thredded/layout/_user-navigation.scss +20 -0
  92. data/app/assets/stylesheets/thredded/layout/_user.scss +10 -0
  93. data/app/assets/stylesheets/thredded/utilities/_is-compact.scss +26 -0
  94. data/app/assets/stylesheets/thredded/utilities/_is-expanded.scss +16 -0
  95. data/app/commands/thredded/at_notification_extractor.rb +23 -0
  96. data/app/commands/thredded/autofollow_users.rb +62 -0
  97. data/app/commands/thredded/create_messageboard.rb +42 -0
  98. data/app/commands/thredded/mark_all_read.rb +22 -0
  99. data/app/commands/thredded/moderate_post.rb +48 -0
  100. data/app/commands/thredded/notify_following_users.rb +78 -0
  101. data/app/commands/thredded/notify_private_topic_users.rb +34 -0
  102. data/app/controllers/concerns/thredded/new_post_params.rb +21 -0
  103. data/app/controllers/concerns/thredded/new_private_post_params.rb +21 -0
  104. data/app/controllers/concerns/thredded/new_private_topic_params.rb +25 -0
  105. data/app/controllers/concerns/thredded/new_topic_params.rb +19 -0
  106. data/app/controllers/concerns/thredded/render_preview.rb +16 -0
  107. data/app/controllers/thredded/application_controller.rb +145 -0
  108. data/app/controllers/thredded/autocomplete_users_controller.rb +52 -0
  109. data/app/controllers/thredded/messageboard_groups_controller.rb +31 -0
  110. data/app/controllers/thredded/messageboards_controller.rb +53 -0
  111. data/app/controllers/thredded/moderation_controller.rb +103 -0
  112. data/app/controllers/thredded/post_permalinks_controller.rb +11 -0
  113. data/app/controllers/thredded/post_previews_controller.rb +29 -0
  114. data/app/controllers/thredded/posts_controller.rb +97 -0
  115. data/app/controllers/thredded/preferences_controller.rb +42 -0
  116. data/app/controllers/thredded/private_post_permalinks_controller.rb +12 -0
  117. data/app/controllers/thredded/private_post_previews_controller.rb +29 -0
  118. data/app/controllers/thredded/private_posts_controller.rb +96 -0
  119. data/app/controllers/thredded/private_topic_previews_controller.rb +15 -0
  120. data/app/controllers/thredded/private_topics_controller.rb +102 -0
  121. data/app/controllers/thredded/read_states_controller.rb +13 -0
  122. data/app/controllers/thredded/theme_previews_controller.rb +34 -0
  123. data/app/controllers/thredded/topic_previews_controller.rb +15 -0
  124. data/app/controllers/thredded/topics_controller.rb +205 -0
  125. data/app/forms/thredded/edit_topic_form.rb +51 -0
  126. data/app/forms/thredded/post_form.rb +54 -0
  127. data/app/forms/thredded/private_post_form.rb +50 -0
  128. data/app/forms/thredded/private_topic_form.rb +160 -0
  129. data/app/forms/thredded/topic_form.rb +95 -0
  130. data/app/forms/thredded/user_preferences_form.rb +110 -0
  131. data/app/helpers/thredded/application_helper.rb +129 -0
  132. data/app/helpers/thredded/nav_helper.rb +42 -0
  133. data/app/helpers/thredded/render_helper.rb +15 -0
  134. data/app/helpers/thredded/urls_helper.rb +134 -0
  135. data/app/jobs/thredded/activity_updater_job.rb +21 -0
  136. data/app/jobs/thredded/auto_follow_and_notify_job.rb +14 -0
  137. data/app/jobs/thredded/notify_private_topic_users_job.rb +12 -0
  138. data/app/mailer_previews/thredded/base_mailer_preview.rb +118 -0
  139. data/app/mailer_previews/thredded/post_mailer_preview.rb +13 -0
  140. data/app/mailer_previews/thredded/private_topic_mailer_preview.rb +13 -0
  141. data/app/mailers/thredded/base_mailer.rb +18 -0
  142. data/app/mailers/thredded/post_mailer.rb +21 -0
  143. data/app/mailers/thredded/private_topic_mailer.rb +21 -0
  144. data/app/models/concerns/thredded/content_moderation_state.rb +67 -0
  145. data/app/models/concerns/thredded/friendly_id_reserved_words_and_pagination.rb +17 -0
  146. data/app/models/concerns/thredded/moderation_state.rb +14 -0
  147. data/app/models/concerns/thredded/notifier_preference.rb +19 -0
  148. data/app/models/concerns/thredded/post_common.rb +76 -0
  149. data/app/models/concerns/thredded/search_parser.rb +41 -0
  150. data/app/models/concerns/thredded/topic_common.rb +94 -0
  151. data/app/models/concerns/thredded/topics_search.rb +67 -0
  152. data/app/models/concerns/thredded/user_topic_read_state_common.rb +42 -0
  153. data/app/models/thredded/category.rb +18 -0
  154. data/app/models/thredded/messageboard.rb +120 -0
  155. data/app/models/thredded/messageboard_group.rb +19 -0
  156. data/app/models/thredded/messageboard_notifications_for_followed_topics.rb +30 -0
  157. data/app/models/thredded/messageboard_user.rb +14 -0
  158. data/app/models/thredded/notifications_for_followed_topics.rb +25 -0
  159. data/app/models/thredded/notifications_for_private_topics.rb +22 -0
  160. data/app/models/thredded/null_user.rb +51 -0
  161. data/app/models/thredded/null_user_topic_read_state.rb +17 -0
  162. data/app/models/thredded/post.rb +77 -0
  163. data/app/models/thredded/post_moderation_record.rb +56 -0
  164. data/app/models/thredded/private_post.rb +63 -0
  165. data/app/models/thredded/private_topic.rb +89 -0
  166. data/app/models/thredded/private_user.rb +8 -0
  167. data/app/models/thredded/stats.rb +25 -0
  168. data/app/models/thredded/topic.rb +179 -0
  169. data/app/models/thredded/topic_category.rb +8 -0
  170. data/app/models/thredded/user_detail.rb +27 -0
  171. data/app/models/thredded/user_extender.rb +75 -0
  172. data/app/models/thredded/user_messageboard_preference.rb +45 -0
  173. data/app/models/thredded/user_permissions/admin/if_admin_column_true.rb +14 -0
  174. data/app/models/thredded/user_permissions/admin/none.rb +14 -0
  175. data/app/models/thredded/user_permissions/message/readers_of_writeable_boards.rb +15 -0
  176. data/app/models/thredded/user_permissions/moderate/if_moderator_column_true.rb +16 -0
  177. data/app/models/thredded/user_permissions/moderate/none.rb +16 -0
  178. data/app/models/thredded/user_permissions/read/all.rb +27 -0
  179. data/app/models/thredded/user_permissions/write/all.rb +16 -0
  180. data/app/models/thredded/user_permissions/write/none.rb +16 -0
  181. data/app/models/thredded/user_post_notification.rb +29 -0
  182. data/app/models/thredded/user_preference.rb +26 -0
  183. data/app/models/thredded/user_private_topic_read_state.rb +13 -0
  184. data/app/models/thredded/user_topic_follow.rb +34 -0
  185. data/app/models/thredded/user_topic_read_state.rb +13 -0
  186. data/app/notifiers/thredded/email_notifier.rb +25 -0
  187. data/app/policies/thredded/messageboard_group_policy.rb +16 -0
  188. data/app/policies/thredded/messageboard_policy.rb +49 -0
  189. data/app/policies/thredded/post_policy.rb +64 -0
  190. data/app/policies/thredded/private_post_policy.rb +38 -0
  191. data/app/policies/thredded/private_topic_policy.rb +24 -0
  192. data/app/policies/thredded/topic_policy.rb +49 -0
  193. data/app/view_hooks/thredded/all_view_hooks.rb +125 -0
  194. data/app/view_models/thredded/base_topic_view.rb +43 -0
  195. data/app/view_models/thredded/messageboard_group_view.rb +27 -0
  196. data/app/view_models/thredded/post_view.rb +89 -0
  197. data/app/view_models/thredded/posts_page_view.rb +27 -0
  198. data/app/view_models/thredded/private_topic_view.rb +20 -0
  199. data/app/view_models/thredded/private_topics_page_view.rb +31 -0
  200. data/app/view_models/thredded/topic_email_view.rb +18 -0
  201. data/app/view_models/thredded/topic_posts_page_view.rb +17 -0
  202. data/app/view_models/thredded/topic_view.rb +68 -0
  203. data/app/view_models/thredded/topics_page_view.rb +38 -0
  204. data/app/views/layouts/thredded/application.html.erb +18 -0
  205. data/app/views/thredded/categories/_category.html.erb +1 -0
  206. data/app/views/thredded/error_pages/forbidden.html.erb +6 -0
  207. data/app/views/thredded/error_pages/not_found.html.erb +6 -0
  208. data/app/views/thredded/kaminari/_first_page.html.erb +11 -0
  209. data/app/views/thredded/kaminari/_gap.html.erb +8 -0
  210. data/app/views/thredded/kaminari/_last_page.html.erb +11 -0
  211. data/app/views/thredded/kaminari/_next_page.html.erb +11 -0
  212. data/app/views/thredded/kaminari/_page.html.erb +12 -0
  213. data/app/views/thredded/kaminari/_paginator.html.erb +23 -0
  214. data/app/views/thredded/kaminari/_prev_page.html.erb +11 -0
  215. data/app/views/thredded/messageboard_groups/new.html.erb +28 -0
  216. data/app/views/thredded/messageboards/_form.html.erb +30 -0
  217. data/app/views/thredded/messageboards/_messageboard.html.erb +20 -0
  218. data/app/views/thredded/messageboards/_messageboard_meta.html.erb +13 -0
  219. data/app/views/thredded/messageboards/edit.html.erb +15 -0
  220. data/app/views/thredded/messageboards/index.html.erb +34 -0
  221. data/app/views/thredded/messageboards/new.html.erb +15 -0
  222. data/app/views/thredded/moderation/_nav.html.erb +19 -0
  223. data/app/views/thredded/moderation/_post.html.erb +19 -0
  224. data/app/views/thredded/moderation/_post_moderation_actions.html.erb +12 -0
  225. data/app/views/thredded/moderation/_post_moderation_record.html.erb +46 -0
  226. data/app/views/thredded/moderation/_user_moderation_state.html.erb +3 -0
  227. data/app/views/thredded/moderation/_user_post.html.erb +12 -0
  228. data/app/views/thredded/moderation/_users_search_form.html.erb +13 -0
  229. data/app/views/thredded/moderation/activity.html.erb +20 -0
  230. data/app/views/thredded/moderation/history.html.erb +13 -0
  231. data/app/views/thredded/moderation/pending.html.erb +24 -0
  232. data/app/views/thredded/moderation/user.html.erb +54 -0
  233. data/app/views/thredded/moderation/users.html.erb +41 -0
  234. data/app/views/thredded/post_mailer/post_notification.html.erb +26 -0
  235. data/app/views/thredded/post_mailer/post_notification.text.erb +14 -0
  236. data/app/views/thredded/post_previews/preview.html.erb +1 -0
  237. data/app/views/thredded/post_previews/update.html.erb +1 -0
  238. data/app/views/thredded/posts/_content.html.erb +1 -0
  239. data/app/views/thredded/posts/_form.html.erb +5 -0
  240. data/app/views/thredded/posts/_post.html.erb +13 -0
  241. data/app/views/thredded/posts/_user.html.erb +3 -0
  242. data/app/views/thredded/posts/edit.html.erb +16 -0
  243. data/app/views/thredded/posts/new.html.erb +15 -0
  244. data/app/views/thredded/posts_common/_actions.html.erb +28 -0
  245. data/app/views/thredded/posts_common/_content.html.erb +3 -0
  246. data/app/views/thredded/posts_common/_form.html.erb +22 -0
  247. data/app/views/thredded/posts_common/_header.html.erb +8 -0
  248. data/app/views/thredded/posts_common/_header_with_topic.html.erb +15 -0
  249. data/app/views/thredded/posts_common/_header_with_user_and_topic.html.erb +18 -0
  250. data/app/views/thredded/posts_common/actions/_delete.html.erb +4 -0
  251. data/app/views/thredded/posts_common/actions/_edit.html.erb +3 -0
  252. data/app/views/thredded/posts_common/actions/_mark_as_unread.html.erb +2 -0
  253. data/app/views/thredded/posts_common/actions/_quote.html.erb +4 -0
  254. data/app/views/thredded/posts_common/form/_after_content.html.erb +8 -0
  255. data/app/views/thredded/posts_common/form/_before_content.html.erb +8 -0
  256. data/app/views/thredded/posts_common/form/_content.html.erb +7 -0
  257. data/app/views/thredded/posts_common/form/_content_field.html.erb +8 -0
  258. data/app/views/thredded/posts_common/form/_preview_area.html.erb +16 -0
  259. data/app/views/thredded/preferences/_form.html.erb +95 -0
  260. data/app/views/thredded/preferences/_messageboards_nav.html.erb +8 -0
  261. data/app/views/thredded/preferences/_messageboards_nav_item.html.erb +2 -0
  262. data/app/views/thredded/preferences/edit.html.erb +20 -0
  263. data/app/views/thredded/private_post_previews/preview.html.erb +1 -0
  264. data/app/views/thredded/private_post_previews/update.html.erb +1 -0
  265. data/app/views/thredded/private_posts/_content.html.erb +1 -0
  266. data/app/views/thredded/private_posts/_form.html.erb +6 -0
  267. data/app/views/thredded/private_posts/_private_post.html.erb +6 -0
  268. data/app/views/thredded/private_posts/edit.html.erb +16 -0
  269. data/app/views/thredded/private_posts/new.html.erb +11 -0
  270. data/app/views/thredded/private_topic_mailer/message_notification.html.erb +26 -0
  271. data/app/views/thredded/private_topic_mailer/message_notification.text.erb +15 -0
  272. data/app/views/thredded/private_topic_previews/preview.html.erb +1 -0
  273. data/app/views/thredded/private_topics/_breadcrumbs.html.erb +4 -0
  274. data/app/views/thredded/private_topics/_form.html.erb +39 -0
  275. data/app/views/thredded/private_topics/_header.html.erb +17 -0
  276. data/app/views/thredded/private_topics/_no_private_topics.html.erb +6 -0
  277. data/app/views/thredded/private_topics/_private_topic.html.erb +23 -0
  278. data/app/views/thredded/private_topics/edit.html.erb +35 -0
  279. data/app/views/thredded/private_topics/header/_participant.html.erb +1 -0
  280. data/app/views/thredded/private_topics/index.html.erb +32 -0
  281. data/app/views/thredded/private_topics/new.html.erb +11 -0
  282. data/app/views/thredded/private_topics/private_topic/_participant.html.erb +1 -0
  283. data/app/views/thredded/private_topics/show.html.erb +28 -0
  284. data/app/views/thredded/search/_form.html.erb +13 -0
  285. data/app/views/thredded/shared/_breadcrumbs.html.erb +6 -0
  286. data/app/views/thredded/shared/_content_moderation_blocked_state.html.erb +8 -0
  287. data/app/views/thredded/shared/_currently_online.html.erb +6 -0
  288. data/app/views/thredded/shared/_flash_messages.html.erb +7 -0
  289. data/app/views/thredded/shared/_header.html.erb +3 -0
  290. data/app/views/thredded/shared/_nav.html.erb +19 -0
  291. data/app/views/thredded/shared/_page.html.erb +15 -0
  292. data/app/views/thredded/shared/currently_online/_header.html.erb +5 -0
  293. data/app/views/thredded/shared/currently_online/_user_list.html.erb +3 -0
  294. data/app/views/thredded/shared/currently_online/_user_list_item.html.erb +6 -0
  295. data/app/views/thredded/shared/nav/_moderation.html.erb +14 -0
  296. data/app/views/thredded/shared/nav/_notification_preferences.html.erb +8 -0
  297. data/app/views/thredded/shared/nav/_private_topics.html.erb +12 -0
  298. data/app/views/thredded/shared/nav/_standalone.html.erb +12 -0
  299. data/app/views/thredded/shared/nav/_standalone_profile.html.erb +3 -0
  300. data/app/views/thredded/shared/preview.html.erb +10 -0
  301. data/app/views/thredded/theme_previews/_section_title.html.erb +3 -0
  302. data/app/views/thredded/theme_previews/show.html.erb +99 -0
  303. data/app/views/thredded/topic_previews/preview.html.erb +1 -0
  304. data/app/views/thredded/topics/_followers.html.erb +12 -0
  305. data/app/views/thredded/topics/_form.html.erb +32 -0
  306. data/app/views/thredded/topics/_header.html.erb +32 -0
  307. data/app/views/thredded/topics/_sticky_topics_divider.html.erb +1 -0
  308. data/app/views/thredded/topics/_topic.html.erb +47 -0
  309. data/app/views/thredded/topics/_topic_form_admin_options.html.erb +12 -0
  310. data/app/views/thredded/topics/edit.html.erb +50 -0
  311. data/app/views/thredded/topics/index.html.erb +35 -0
  312. data/app/views/thredded/topics/new.html.erb +12 -0
  313. data/app/views/thredded/topics/search.html.erb +39 -0
  314. data/app/views/thredded/topics/show.html.erb +46 -0
  315. data/app/views/thredded/users/_link.html.erb +14 -0
  316. data/app/views/thredded/users/_post.html.erb +6 -0
  317. data/app/views/thredded/users/_posts.html.erb +7 -0
  318. data/bin/rails +6 -0
  319. data/config/i18n-tasks.yml +16 -0
  320. data/config/locales/de.yml +257 -0
  321. data/config/locales/en.yml +253 -0
  322. data/config/locales/es.yml +257 -0
  323. data/config/locales/fr.yml +255 -0
  324. data/config/locales/it.yml +257 -0
  325. data/config/locales/pl.yml +257 -0
  326. data/config/locales/pt-BR.yml +258 -0
  327. data/config/locales/ru.yml +255 -0
  328. data/config/locales/zh-CN.yml +246 -0
  329. data/config/routes.rb +87 -0
  330. data/db/migrate/20160329231848_create_thredded.rb +257 -0
  331. data/db/seeds.rb +4 -0
  332. data/db/upgrade_migrations/20160611094616_upgrade_v0_5_to_v0_6.rb +28 -0
  333. data/db/upgrade_migrations/20160723012349_upgrade_v0_6_to_v0_7.rb +46 -0
  334. data/db/upgrade_migrations/20161019150201_upgrade_v0_7_to_v0_8.rb +34 -0
  335. data/db/upgrade_migrations/20161113161801_upgrade_v0_8_to_v0_9.rb +60 -0
  336. data/db/upgrade_migrations/20170125033319_upgrade_v0_9_to_v0_10.rb +36 -0
  337. data/db/upgrade_migrations/20170312131417_upgrade_thredded_v0_10_to_v0_11.rb +23 -0
  338. data/db/upgrade_migrations/20170420163138_upgrade_thredded_v0_11_to_v0_12.rb +28 -0
  339. data/db/upgrade_migrations/20170811090735_upgrade_thredded_v0_13_to_v_014.rb +21 -0
  340. data/lib/generators/thredded/install/USAGE +8 -0
  341. data/lib/generators/thredded/install/install_generator.rb +20 -0
  342. data/lib/generators/thredded/install/templates/initializer.rb +160 -0
  343. data/lib/tasks/thredded_tasks.rake +14 -0
  344. data/lib/thredded.rb +190 -0
  345. data/lib/thredded/base_migration.rb +14 -0
  346. data/lib/thredded/base_notifier.rb +28 -0
  347. data/lib/thredded/collection_to_strings_with_cache_renderer.rb +86 -0
  348. data/lib/thredded/content_formatter.rb +129 -0
  349. data/lib/thredded/database_seeder.rb +290 -0
  350. data/lib/thredded/db_tools.rb +103 -0
  351. data/lib/thredded/email_transformer.rb +22 -0
  352. data/lib/thredded/email_transformer/base.rb +47 -0
  353. data/lib/thredded/email_transformer/onebox.rb +21 -0
  354. data/lib/thredded/engine.rb +28 -0
  355. data/lib/thredded/errors.rb +68 -0
  356. data/lib/thredded/formatting_demo_content.rb +30 -0
  357. data/lib/thredded/html_pipeline/at_mention_filter.rb +78 -0
  358. data/lib/thredded/html_pipeline/autolink_filter.rb +15 -0
  359. data/lib/thredded/html_pipeline/kramdown_filter.rb +39 -0
  360. data/lib/thredded/html_pipeline/onebox_filter.rb +143 -0
  361. data/lib/thredded/html_pipeline/wrap_iframes_filter.rb +13 -0
  362. data/lib/thredded/version.rb +5 -0
  363. data/lib/thredded/view_hooks/config.rb +37 -0
  364. data/lib/thredded/view_hooks/renderer.rb +30 -0
  365. data/vendor/assets/javascripts/autosize.min.js +6 -0
  366. data/vendor/assets/javascripts/textcomplete.min.js +2 -0
  367. metadata +1035 -0
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Thredded
4
+ class BaseMigration < (Thredded.rails_gte_51? ? ActiveRecord::Migration[5.1] : ActiveRecord::Migration)
5
+ def user_id_type
6
+ Thredded.user_class.columns.find { |c| c.name == Thredded.user_class.primary_key }.sql_type
7
+ end
8
+
9
+ def column_type(table, column_name)
10
+ column_name = column_name.to_s
11
+ columns(table).find { |c| c.name == column_name }.sql_type
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Thredded
4
+ class BaseNotifier
5
+ def self.validate_notifier(notifier)
6
+ unless notifier.respond_to?(:key) && /^[a-z_]+$/.match(notifier.key)
7
+ fail "Notifier problem: #{notifier.class.name} must respond to #key with a snake_case string"
8
+ end
9
+ %i[human_name new_post new_private_post].each do |m|
10
+ unless notifier.respond_to?(m)
11
+ fail "#{notifier.class.name} must respond to ##{m}"
12
+ end
13
+ end
14
+ end
15
+
16
+ class NotificationsDefault
17
+ def initialize(enabled)
18
+ @enabled = enabled
19
+ end
20
+
21
+ attr_reader :enabled
22
+
23
+ def enabled?
24
+ enabled
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'action_view/renderer/abstract_renderer'
4
+
5
+ module Thredded
6
+ class CollectionToStringsWithCacheRenderer < ActionView::AbstractRenderer
7
+ # The default number of threads to use for rendering.
8
+ mattr_accessor :render_threads
9
+ self.render_threads = 50
10
+
11
+ # @param view_context
12
+ # @param [Array<T>] collection
13
+ # @param [String] partial
14
+ # @param [ActiveSupport::Duration] expires_in
15
+ # @param [Integer] render_threads the number of threads to use for rendering. This is useful even on MRI ruby
16
+ # for IO-bound operations.
17
+ # @param [Hash] locals
18
+ # @return Array<[T, String]>
19
+ def render_collection_to_strings_with_cache( # rubocop:disable Metrics/ParameterLists
20
+ view_context, collection:, partial:, expires_in:, render_threads: self.class.render_threads, locals: {}, **opts
21
+ )
22
+ template = @lookup_context.find_template(partial, [], true, locals, {})
23
+ collection = collection.to_a
24
+ instrument(:collection, count: collection.size) do |instrumentation_payload|
25
+ return [] if collection.blank?
26
+ keyed_collection = collection.each_with_object({}) do |item, hash|
27
+ key = ActiveSupport::Cache.expand_cache_key(
28
+ view_context.cache_fragment_name(item, virtual_path: template.virtual_path), :views
29
+ )
30
+ # #read_multi & #write may require key mutability, Dalli 2.6.0.
31
+ hash[key.frozen? ? key.dup : key] = item
32
+ end
33
+ cache = collection_cache
34
+ cached_partials = cache.read_multi(*keyed_collection.keys)
35
+ instrumentation_payload[:cache_hits] = cached_partials.size if instrumentation_payload
36
+
37
+ collection_to_render = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values
38
+ rendered_partials = render_partials(
39
+ view_context,
40
+ collection: collection_to_render, render_threads: render_threads,
41
+ partial: partial, locals: locals, **opts
42
+ ).each
43
+
44
+ keyed_collection.map do |cache_key, item|
45
+ [item, cached_partials[cache_key] || rendered_partials.next.tap do |rendered|
46
+ cache.write(cache_key, rendered, expires_in: expires_in)
47
+ end]
48
+ end
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def collection_cache
55
+ if ActionView::PartialRenderer.respond_to?(:collection_cache)
56
+ # Rails 5.0+
57
+ ActionView::PartialRenderer.collection_cache
58
+ else
59
+ # Rails 4.2.x
60
+ Rails.application.config.action_controller.cache_store
61
+ end
62
+ end
63
+
64
+ # @return [Array<String>]
65
+ def render_partials(view_context, collection:, render_threads:, **opts)
66
+ return [] if collection.empty?
67
+ num_threads = [render_threads, collection.size].min
68
+ if num_threads == 1
69
+ render_partials_serial(view_context, collection, opts)
70
+ else
71
+ collection.each_slice(collection.size / num_threads).map do |slice|
72
+ Thread.start { render_partials_serial(view_context.dup, slice, opts) }
73
+ end.flat_map(&:value)
74
+ end
75
+ end
76
+
77
+ # @param [Array<Object>] collection
78
+ # @param [Hash] opts
79
+ # @param view_context
80
+ # @return [Array<String>]
81
+ def render_partials_serial(view_context, collection, opts)
82
+ partial_renderer = ActionView::PartialRenderer.new(@lookup_context)
83
+ collection.map { |object| partial_renderer.render(view_context, opts.merge(object: object), nil) }
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Thredded
4
+ # Generates HTML from content source.
5
+ class ContentFormatter
6
+ # Sanitization whitelist options.
7
+ mattr_accessor :whitelist
8
+
9
+ self.whitelist = HTML::Pipeline::SanitizationFilter::WHITELIST.deep_merge(
10
+ elements: HTML::Pipeline::SanitizationFilter::WHITELIST[:elements] + %w[abbr iframe span figure figcaption],
11
+ transformers: HTML::Pipeline::SanitizationFilter::WHITELIST[:transformers] + [
12
+ lambda do |env|
13
+ next unless env[:node_name] == 'a'
14
+ a_tag = env[:node]
15
+ a_tag['href'] ||= '#'
16
+ if a_tag['href'] =~ %r{^(?:[a-z]+:)?//}
17
+ a_tag['target'] = '_blank'
18
+ a_tag['rel'] = 'nofollow noopener'
19
+ end
20
+ end
21
+ ],
22
+ attributes: {
23
+ 'a' => %w[href rel],
24
+ 'abbr' => %w[title],
25
+ 'span' => %w[class],
26
+ 'div' => %w[class],
27
+ :all => HTML::Pipeline::SanitizationFilter::WHITELIST[:attributes][:all] +
28
+ %w[aria-label aria-labelledby aria-hidden],
29
+ }
30
+ )
31
+
32
+ # Filters that run before processing the markup.
33
+ # input: markup, output: markup.
34
+ mattr_accessor :before_markup_filters
35
+ self.before_markup_filters = [
36
+ ]
37
+
38
+ # Markup filters, such as BBCode, Markdown, Autolink, etc.
39
+ # input: markup, output: html.
40
+ mattr_accessor :markup_filters
41
+ self.markup_filters = [
42
+ Thredded::HtmlPipeline::KramdownFilter,
43
+ ]
44
+
45
+ # Filters that run after processing the markup.
46
+ # input: html, output: html.
47
+ mattr_accessor :after_markup_filters
48
+ self.after_markup_filters = [
49
+ # AutolinkFilter is required because Kramdown does not autolink by default.
50
+ # https://github.com/gettalong/kramdown/issues/306
51
+ Thredded::HtmlPipeline::AutolinkFilter,
52
+ HTML::Pipeline::EmojiFilter,
53
+ Thredded::HtmlPipeline::AtMentionFilter,
54
+ ]
55
+
56
+ # Filters that sanitize the resulting HTML.
57
+ # input: html, output: sanitized html.
58
+ mattr_accessor :sanitization_filters
59
+ self.sanitization_filters = [
60
+ HTML::Pipeline::SanitizationFilter,
61
+ ]
62
+
63
+ # Filters that run after sanitization
64
+ # input: sanitized html, output: html
65
+ mattr_accessor :after_sanitization_filters
66
+ self.after_sanitization_filters = [
67
+ Thredded::HtmlPipeline::OneboxFilter,
68
+ Thredded::HtmlPipeline::WrapIframesFilter,
69
+ ]
70
+
71
+ # All the HTML::Pipeline filters, read-only.
72
+ def self.pipeline_filters
73
+ filters = [
74
+ *before_markup_filters,
75
+ *markup_filters,
76
+ *after_markup_filters,
77
+ *sanitization_filters,
78
+ *after_sanitization_filters
79
+ ]
80
+ # Changing the result in-place has no effect on the ContentFormatter output,
81
+ # and is most likely the result of a programmer error.
82
+ # Freeze the array so that in-place changes raise an error.
83
+ filters.freeze
84
+ end
85
+
86
+ # @param view_context [Object] the context of the rendering view.
87
+ # @param pipeline_options [Hash]
88
+ def initialize(view_context, pipeline_options = {})
89
+ @view_context = view_context
90
+ @pipeline_options = pipeline_options
91
+ end
92
+
93
+ # @param content [String]
94
+ # @return [String] formatted and sanitized html-safe content.
95
+ def format_content(content)
96
+ pipeline = HTML::Pipeline.new(content_pipeline_filters, content_pipeline_options.deep_merge(@pipeline_options))
97
+ result = pipeline.call(content, view_context: @view_context)
98
+ # rubocop:disable Rails/OutputSafety
99
+ result[:output].to_s.html_safe
100
+ # rubocop:enable Rails/OutputSafety
101
+ end
102
+
103
+ # @param content [String]
104
+ # @return [String] a quote containing the formatted content
105
+ def self.quote_content(content)
106
+ result = String.new(content)
107
+ result.gsub!(/^(?!$)/, '> ')
108
+ result.gsub!(/^$/, '>')
109
+ result << "\n" unless result.end_with?("\n")
110
+ result << "\n"
111
+ result
112
+ end
113
+
114
+ protected
115
+
116
+ # @return [Array<HTML::Pipeline::Filter]>]
117
+ def content_pipeline_filters
118
+ ContentFormatter.pipeline_filters
119
+ end
120
+
121
+ # @return [Hash] options for HTML::Pipeline.new
122
+ def content_pipeline_options
123
+ {
124
+ asset_root: Rails.application.config.action_controller.asset_host || '',
125
+ whitelist: ContentFormatter.whitelist
126
+ }
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'factory_bot_rails'
4
+ require_relative '../../spec/support/features/fake_content'
5
+
6
+ # rubocop:disable HandleExceptions
7
+ begin
8
+ if FactoryBot.factories.instance_variable_get(:@items).none?
9
+ require_relative '../../spec/factories'
10
+ end
11
+ rescue NameError
12
+ end
13
+ # rubocop:enable HandleExceptions
14
+ module Thredded
15
+ class DatabaseSeeder
16
+ SKIP_CALLBACKS = [
17
+ [Thredded::Post, :commit, :after, :auto_follow_and_notify],
18
+ [Thredded::PrivatePost, :commit, :after, :notify_users],
19
+ ].freeze
20
+
21
+ def self.run(users: 200, topics: 55, posts: (1..60))
22
+ STDERR.puts 'Seeding the database...'
23
+ # Disable callbacks to avoid creating notifications and performing unnecessary updates
24
+ SKIP_CALLBACKS.each { |(klass, *args)| klass.skip_callback(*args) }
25
+ s = new
26
+ Messageboard.transaction do
27
+ s.seed(users: users, topics: topics, posts: posts)
28
+ s.log 'Running after_commit callbacks'
29
+ end
30
+ ensure
31
+ # Re-enable callbacks
32
+ SKIP_CALLBACKS.each { |(klass, *args)| klass.set_callback(*args) }
33
+ end
34
+
35
+ def seed(users: 200, topics: 55, posts: (1..60))
36
+ users(count: users)
37
+ first_messageboard
38
+ topics(count: topics)
39
+ private_topics(count: topics)
40
+ posts(count: posts)
41
+ private_posts(count: posts)
42
+ create_additional_messageboards
43
+ follow_some_topics
44
+ read_some_topics
45
+ end
46
+
47
+ def log(message)
48
+ STDERR.puts "- #{message}"
49
+ end
50
+
51
+ def first_user
52
+ @first_user ||= FirstUser.new(self).find_or_create
53
+ end
54
+
55
+ def users(count: 1)
56
+ @users ||= Users.new(self).find_or_create(count: count)
57
+ end
58
+
59
+ def first_messageboard
60
+ @first_messageboard ||= FirstMessageboard.new(self).find_or_create
61
+ end
62
+
63
+ def create_additional_messageboards
64
+ meta_group_id = MessageboardGroup.create!(name: 'Meta').id
65
+ additional_messageboards = [
66
+ ['Off-Topic', "Talk about whatever here, it's all good."],
67
+ ['Help, Bugs, and Suggestions',
68
+ 'Need help using the forum? Want to report a bug or make a suggestion? This is the place.', meta_group_id],
69
+ ['Praise', 'Want to tell us how great we are? This is the place.', meta_group_id]
70
+ ]
71
+ log "Creating #{additional_messageboards.length} additional messageboards..."
72
+ additional_messageboards.each do |(name, description, group_id)|
73
+ messageboard = Messageboard.create!(name: name, description: description, messageboard_group_id: group_id)
74
+ FactoryBot.create_list(:topic, 1 + rand(3), messageboard: messageboard, with_posts: 1)
75
+ end
76
+ end
77
+
78
+ def topics(count: 1)
79
+ @topics ||= Topics.new(self).find_or_create(count: count)
80
+ end
81
+
82
+ def private_topics(count: 1)
83
+ @private_topics ||= PrivateTopics.new(self).find_or_create(count: count)
84
+ end
85
+
86
+ def posts(count: (1..1))
87
+ @posts ||= Posts.new(self).find_or_create(count: count)
88
+ end
89
+
90
+ def private_posts(count: (1..1))
91
+ @private_posts ||= PrivatePosts.new(self).find_or_create(count: count)
92
+ end
93
+
94
+ def follow_some_topics(count: (5..10), count_users: (1..5))
95
+ log 'Following some topics...'
96
+ posts.each do |post|
97
+ Thredded::UserTopicFollow.create_unless_exists(post.user_id, post.postable_id, :posted) if post.user_id
98
+ end
99
+ follow_some_topics_by_user(first_user, count: count)
100
+ users.sample(count_users.min + rand(count_users.max - count_users.min + 2)).each do |user|
101
+ follow_some_topics_by_user(user, count: count)
102
+ end
103
+ end
104
+
105
+ def follow_some_topics_by_user(user, count: (1..10))
106
+ topics.sample(count.min + rand(count.max - count.min + 2)).each do |topic|
107
+ Thredded::UserTopicFollow.create_unless_exists(user.id, topic.id)
108
+ end
109
+ end
110
+
111
+ def read_some_topics(count: (5..10), count_users: (1..5))
112
+ log 'Reading some topics...'
113
+ topics.each do |topic|
114
+ read_topic(topic, topic.last_user_id) if topic.last_user_id
115
+ end
116
+ read_some_topics_by_user(first_user, count: count)
117
+ @users.sample(count_users.min + rand(count_users.max - count_users.min + 2)).each do |user|
118
+ read_some_topics_by_user(user, count: count)
119
+ end
120
+ end
121
+
122
+ def read_some_topics_by_user(user, count: (1..10))
123
+ topics.sample(count.min + rand(count.max - count.min + 2)).each do |topic|
124
+ read_topic(topic, user.id)
125
+ end
126
+ end
127
+
128
+ def read_topic(topic, user_id)
129
+ Thredded::UserTopicReadState.find_or_initialize_by(user_id: user_id, postable_id: topic.id)
130
+ .update!(read_at: topic.updated_at, page: 1)
131
+ end
132
+
133
+ class BaseSeedData
134
+ attr_reader :seeder
135
+
136
+ def initialize(seed_database = DatabaseSeeder.new)
137
+ @seeder = seed_database
138
+ end
139
+
140
+ # Utility method
141
+ def self.create(*args)
142
+ new.create(*args)
143
+ end
144
+
145
+ delegate :log, to: :seeder
146
+
147
+ def find_or_create(*args)
148
+ return @stored if @stored
149
+ @stored = (find || create(*args))
150
+ end
151
+
152
+ protected
153
+
154
+ def model_class
155
+ self.class::MODEL_CLASS
156
+ end
157
+
158
+ # @abstract
159
+ def create(*_args)
160
+ fail 'Unimplemented'
161
+ end
162
+
163
+ # @abstract
164
+ def find
165
+ fail 'Unimplemented'
166
+ end
167
+ end
168
+
169
+ class FirstSeedData < BaseSeedData
170
+ def find
171
+ model_class.first
172
+ end
173
+ end
174
+
175
+ class CollectionSeedData < BaseSeedData
176
+ def find
177
+ return nil unless model_class.exists?
178
+ model_class.all.to_a
179
+ end
180
+ end
181
+
182
+ class FirstUser < FirstSeedData
183
+ MODEL_CLASS = User
184
+
185
+ def create
186
+ log 'Creating first user...'
187
+ FactoryBot.create(:user, :approved, :admin, name: 'Joe', email: 'joe@example.com')
188
+ end
189
+ end
190
+
191
+ # Thredded::DatabaseSeeder::Users.create(count:200)
192
+ class Users < CollectionSeedData
193
+ MODEL_CLASS = User
194
+
195
+ def create(count: 1)
196
+ log "Creating #{count} users..."
197
+ approved_users_count = (count * 0.97).round
198
+ [seeder.first_user] +
199
+ FactoryBot.create_list(:user, approved_users_count, :approved) +
200
+ FactoryBot.create_list(:user, count - approved_users_count)
201
+ end
202
+ end
203
+
204
+ class FirstMessageboard < FirstSeedData
205
+ MODEL_CLASS = Messageboard
206
+
207
+ def create
208
+ log 'Creating a messageboard...'
209
+ @first_messageboard = FactoryBot.create(
210
+ :messageboard,
211
+ name: 'Main Board',
212
+ slug: 'main-board',
213
+ description: 'A board is not a board without some posts'
214
+ )
215
+ end
216
+ end
217
+
218
+ class Topics < CollectionSeedData
219
+ MODEL_CLASS = Topic
220
+
221
+ def create(count: 1, messageboard: seeder.first_messageboard)
222
+ log "Creating #{count} topics in #{messageboard.name}..."
223
+ FactoryBot.create_list(
224
+ :topic, count,
225
+ messageboard: messageboard,
226
+ user: seeder.users.sample,
227
+ last_user: seeder.users.sample
228
+ )
229
+ end
230
+ end
231
+
232
+ class PrivateTopics < CollectionSeedData
233
+ MODEL_CLASS = PrivateTopic
234
+
235
+ def create(count: 1)
236
+ Array.new(count) do
237
+ FactoryBot.create(
238
+ :private_topic,
239
+ user: seeder.users[1..-1].sample,
240
+ last_user: seeder.users.sample,
241
+ users: [seeder.first_user, *seeder.users.sample(1 + rand(3))]
242
+ )
243
+ end
244
+ end
245
+ end
246
+
247
+ class Posts < CollectionSeedData
248
+ MODEL_CLASS = Post
249
+
250
+ def create(count: (1..1))
251
+ log "Creating #{count} additional posts in each topic..."
252
+ seeder.topics.flat_map do |topic|
253
+ last_post_at = random_duration(0..72.hours).ago
254
+ posts_count = (count.min + rand(count.max + 1))
255
+ posts = range_of_dates_in_order(up_to: last_post_at, count: posts_count).map.with_index do |written_at, i|
256
+ author = i.zero? ? topic.user : seeder.users.sample
257
+ FactoryBot.create(:post, postable: topic, messageboard: seeder.first_messageboard,
258
+ user: author, created_at: written_at, updated_at: written_at)
259
+ end
260
+ topic.update!(last_user_id: posts.last.user.id, updated_at: last_post_at, last_post_at: last_post_at)
261
+ posts
262
+ end
263
+ end
264
+
265
+ def range_of_dates_in_order(up_to: Time.zone.now, count: 1)
266
+ written = up_to
267
+ Array.new(count - 1) { written -= random_duration(10.minutes..6.hours) }.reverse + [up_to]
268
+ end
269
+
270
+ def random_duration(range)
271
+ (range.min.to_i + rand(range.max.to_i)).seconds
272
+ end
273
+ end
274
+
275
+ class PrivatePosts < CollectionSeedData
276
+ MODEL_CLASS = PrivatePost
277
+
278
+ def create(count: (1..1))
279
+ log "Creating #{count} additional posts in each private topic..."
280
+ seeder.private_topics.flat_map do |topic|
281
+ (count.min + rand(count.max + 1)).times do |i|
282
+ author = i.zero? ? topic.user : topic.users.sample
283
+ FactoryBot.create(:private_post, postable: topic, user: author)
284
+ end
285
+ end
286
+ end
287
+ end
288
+ end
289
+ end
290
+ # rubocop:enable Metrics/ClassLength