rails_den 2.0.0

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 (528) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +652 -0
  3. data/README.md +74 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/javascript/rails_den/attachments.js +733 -0
  6. data/app/assets/javascript/rails_den/bookmarks.js +58 -0
  7. data/app/assets/javascript/rails_den/cable.js +18 -0
  8. data/app/assets/javascript/rails_den/chat.js +3008 -0
  9. data/app/assets/javascript/rails_den/chat_3d_management.js +1230 -0
  10. data/app/assets/javascript/rails_den/composer.js +1785 -0
  11. data/app/assets/javascript/rails_den/content.js +1337 -0
  12. data/app/assets/javascript/rails_den/emoji_catalog.js +136 -0
  13. data/app/assets/javascript/rails_den/licenses/react-LICENSE.txt +21 -0
  14. data/app/assets/javascript/rails_den/licenses/react-dom-LICENSE.txt +21 -0
  15. data/app/assets/javascript/rails_den/licenses/scheduler-LICENSE.txt +21 -0
  16. data/app/assets/javascript/rails_den/licenses/three-LICENSE.txt +21 -0
  17. data/app/assets/javascript/rails_den/members.js +187 -0
  18. data/app/assets/javascript/rails_den/notifications.js +211 -0
  19. data/app/assets/javascript/rails_den/presence.js +757 -0
  20. data/app/assets/javascript/rails_den/private_messages.js +92 -0
  21. data/app/assets/javascript/rails_den/react_runtime.js +43 -0
  22. data/app/assets/javascript/rails_den/reactions.js +139 -0
  23. data/app/assets/javascript/rails_den/spatial.js +4338 -0
  24. data/app/assets/stylesheets/rails_den/advertising.css +91 -0
  25. data/app/assets/stylesheets/rails_den/application.css +1724 -0
  26. data/app/assets/stylesheets/rails_den/attachments.css +255 -0
  27. data/app/assets/stylesheets/rails_den/board_presence.css +38 -0
  28. data/app/assets/stylesheets/rails_den/bookmarks.css +36 -0
  29. data/app/assets/stylesheets/rails_den/chat.css +1555 -0
  30. data/app/assets/stylesheets/rails_den/chat_3d.css +1283 -0
  31. data/app/assets/stylesheets/rails_den/chat_3d_management.css +294 -0
  32. data/app/assets/stylesheets/rails_den/chat_reactions.css +79 -0
  33. data/app/assets/stylesheets/rails_den/community_presence.css +51 -0
  34. data/app/assets/stylesheets/rails_den/composer.css +358 -0
  35. data/app/assets/stylesheets/rails_den/emoji_catalog.css +33 -0
  36. data/app/assets/stylesheets/rails_den/member_profiles.css +662 -0
  37. data/app/assets/stylesheets/rails_den/moderation.css +561 -0
  38. data/app/assets/stylesheets/rails_den/notifications.css +222 -0
  39. data/app/assets/stylesheets/rails_den/onebox.css +126 -0
  40. data/app/assets/stylesheets/rails_den/post_links.css +37 -0
  41. data/app/assets/stylesheets/rails_den/post_presence.css +47 -0
  42. data/app/assets/stylesheets/rails_den/presence.css +96 -0
  43. data/app/assets/stylesheets/rails_den/presence_directory.css +217 -0
  44. data/app/assets/stylesheets/rails_den/presence_live.css +15 -0
  45. data/app/assets/stylesheets/rails_den/private_messages.css +41 -0
  46. data/app/assets/stylesheets/rails_den/reactions.css +53 -0
  47. data/app/assets/stylesheets/rails_den/search.css +100 -0
  48. data/app/assets/stylesheets/rails_den/tags.css +136 -0
  49. data/app/channels/rails_den/chat_spatial_channel.rb +203 -0
  50. data/app/channels/rails_den/chat_spatial_occupancy_channel.rb +69 -0
  51. data/app/channels/rails_den/chat_stream_channel.rb +108 -0
  52. data/app/channels/rails_den/notifications_channel.rb +30 -0
  53. data/app/channels/rails_den/presence_channel.rb +205 -0
  54. data/app/controllers/admin/rails_den/administrators_controller.rb +26 -0
  55. data/app/controllers/admin/rails_den/advertising_management_controller.rb +337 -0
  56. data/app/controllers/admin/rails_den/application_controller.rb +116 -0
  57. data/app/controllers/admin/rails_den/badges_controller.rb +106 -0
  58. data/app/controllers/admin/rails_den/board_access_controller.rb +143 -0
  59. data/app/controllers/admin/rails_den/boards_controller.rb +89 -0
  60. data/app/controllers/admin/rails_den/categories_controller.rb +89 -0
  61. data/app/controllers/admin/rails_den/member_badge_awards_controller.rb +92 -0
  62. data/app/controllers/admin/rails_den/members_controller.rb +495 -0
  63. data/app/controllers/admin/rails_den/plugins_controller.rb +25 -0
  64. data/app/controllers/admin/rails_den/profile_fields_controller.rb +104 -0
  65. data/app/controllers/admin/rails_den/reaction_types_controller.rb +127 -0
  66. data/app/controllers/admin/rails_den/report_reasons_controller.rb +15 -0
  67. data/app/controllers/admin/rails_den/roles_controller.rb +190 -0
  68. data/app/controllers/admin/rails_den/tags_controller.rb +11 -0
  69. data/app/controllers/admin/rails_den/themes_controller.rb +60 -0
  70. data/app/controllers/admin/rails_den/trust_levels_controller.rb +97 -0
  71. data/app/controllers/rails_den/advertising_controller.rb +14 -0
  72. data/app/controllers/rails_den/application_controller.rb +218 -0
  73. data/app/controllers/rails_den/attachments_controller.rb +128 -0
  74. data/app/controllers/rails_den/board_follows_controller.rb +119 -0
  75. data/app/controllers/rails_den/board_read_states_controller.rb +36 -0
  76. data/app/controllers/rails_den/boards_controller.rb +103 -0
  77. data/app/controllers/rails_den/bookmarks_controller.rb +91 -0
  78. data/app/controllers/rails_den/chat_channel_memberships_controller.rb +193 -0
  79. data/app/controllers/rails_den/chat_channel_read_states_controller.rb +46 -0
  80. data/app/controllers/rails_den/chat_channels_controller.rb +556 -0
  81. data/app/controllers/rails_den/chat_message_pins_controller.rb +78 -0
  82. data/app/controllers/rails_den/chat_messages_controller.rb +166 -0
  83. data/app/controllers/rails_den/chat_reaction_types_controller.rb +24 -0
  84. data/app/controllers/rails_den/community_read_states_controller.rb +12 -0
  85. data/app/controllers/rails_den/home_controller.rb +35 -0
  86. data/app/controllers/rails_den/member_profiles_controller.rb +286 -0
  87. data/app/controllers/rails_den/member_reports_controller.rb +22 -0
  88. data/app/controllers/rails_den/members_controller.rb +42 -0
  89. data/app/controllers/rails_den/mention_candidates_controller.rb +14 -0
  90. data/app/controllers/rails_den/moderation/audit_events_controller.rb +14 -0
  91. data/app/controllers/rails_den/moderation/member_histories_controller.rb +37 -0
  92. data/app/controllers/rails_den/moderation/member_notes_controller.rb +26 -0
  93. data/app/controllers/rails_den/moderation/report_notes_controller.rb +27 -0
  94. data/app/controllers/rails_den/moderation/reports_controller.rb +97 -0
  95. data/app/controllers/rails_den/moderation/restrictions_controller.rb +71 -0
  96. data/app/controllers/rails_den/moderation/topics_controller.rb +61 -0
  97. data/app/controllers/rails_den/moderation/warnings_controller.rb +50 -0
  98. data/app/controllers/rails_den/notification_preferences_controller.rb +116 -0
  99. data/app/controllers/rails_den/notifications_controller.rb +67 -0
  100. data/app/controllers/rails_den/post_bookmarks_controller.rb +29 -0
  101. data/app/controllers/rails_den/post_drafts_controller.rb +125 -0
  102. data/app/controllers/rails_den/post_reports_controller.rb +31 -0
  103. data/app/controllers/rails_den/posts_controller.rb +318 -0
  104. data/app/controllers/rails_den/presence_controller.rb +12 -0
  105. data/app/controllers/rails_den/presence_snapshots_controller.rb +83 -0
  106. data/app/controllers/rails_den/previews_controller.rb +10 -0
  107. data/app/controllers/rails_den/private_conversations_controller.rb +120 -0
  108. data/app/controllers/rails_den/private_messages_controller.rb +45 -0
  109. data/app/controllers/rails_den/reactions_controller.rb +124 -0
  110. data/app/controllers/rails_den/reports_controller.rb +63 -0
  111. data/app/controllers/rails_den/search_controller.rb +64 -0
  112. data/app/controllers/rails_den/tags_controller.rb +70 -0
  113. data/app/controllers/rails_den/topic_bookmarks_controller.rb +35 -0
  114. data/app/controllers/rails_den/topic_follows_controller.rb +117 -0
  115. data/app/controllers/rails_den/topic_read_positions_controller.rb +40 -0
  116. data/app/controllers/rails_den/topic_reports_controller.rb +18 -0
  117. data/app/controllers/rails_den/topics_controller.rb +583 -0
  118. data/app/dashboards/rails_den/administrator_dashboard.rb +51 -0
  119. data/app/dashboards/rails_den/board_dashboard.rb +57 -0
  120. data/app/dashboards/rails_den/category_dashboard.rb +50 -0
  121. data/app/dashboards/rails_den/profile_field_dashboard.rb +53 -0
  122. data/app/dashboards/rails_den/reaction_type_dashboard.rb +26 -0
  123. data/app/dashboards/rails_den/report_reason_dashboard.rb +57 -0
  124. data/app/dashboards/rails_den/tag_dashboard.rb +59 -0
  125. data/app/helpers/rails_den/application_helper.rb +39 -0
  126. data/app/helpers/rails_den/boards_helper.rb +4 -0
  127. data/app/helpers/rails_den/moderation/reports_helper.rb +42 -0
  128. data/app/helpers/rails_den/notifications_helper.rb +4 -0
  129. data/app/helpers/rails_den/posts_helper.rb +4 -0
  130. data/app/helpers/rails_den/topics_helper.rb +4 -0
  131. data/app/jobs/rails_den/application_job.rb +4 -0
  132. data/app/jobs/rails_den/expire_user_restriction_job.rb +9 -0
  133. data/app/jobs/rails_den/member_profile_stats_refresh_job.rb +48 -0
  134. data/app/jobs/rails_den/reconcile_user_restrictions_job.rb +9 -0
  135. data/app/jobs/rails_den/resolve_onebox_entry_job.rb +74 -0
  136. data/app/mailers/rails_den/application_mailer.rb +6 -0
  137. data/app/models/rails_den/administrator.rb +18 -0
  138. data/app/models/rails_den/advertising_configuration.rb +14 -0
  139. data/app/models/rails_den/application_record.rb +5 -0
  140. data/app/models/rails_den/badge.rb +142 -0
  141. data/app/models/rails_den/badge_award.rb +87 -0
  142. data/app/models/rails_den/board.rb +129 -0
  143. data/app/models/rails_den/board_follow.rb +18 -0
  144. data/app/models/rails_den/board_read_state.rb +60 -0
  145. data/app/models/rails_den/board_role_assignment.rb +45 -0
  146. data/app/models/rails_den/board_role_permission.rb +31 -0
  147. data/app/models/rails_den/board_role_requirement.rb +16 -0
  148. data/app/models/rails_den/board_sponsorship.rb +21 -0
  149. data/app/models/rails_den/bookmark.rb +31 -0
  150. data/app/models/rails_den/category.rb +81 -0
  151. data/app/models/rails_den/category_role_requirement.rb +16 -0
  152. data/app/models/rails_den/category_sponsorship.rb +21 -0
  153. data/app/models/rails_den/chat_channel.rb +44 -0
  154. data/app/models/rails_den/chat_channel_membership.rb +21 -0
  155. data/app/models/rails_den/chat_channel_read_state.rb +100 -0
  156. data/app/models/rails_den/chat_message.rb +37 -0
  157. data/app/models/rails_den/community_read_state.rb +52 -0
  158. data/app/models/rails_den/community_sponsorship.rb +15 -0
  159. data/app/models/rails_den/member_profile.rb +113 -0
  160. data/app/models/rails_den/mention.rb +20 -0
  161. data/app/models/rails_den/moderation_event.rb +16 -0
  162. data/app/models/rails_den/moderation_note.rb +11 -0
  163. data/app/models/rails_den/notification.rb +33 -0
  164. data/app/models/rails_den/notification_event.rb +40 -0
  165. data/app/models/rails_den/notification_mute.rb +9 -0
  166. data/app/models/rails_den/onebox_entry.rb +214 -0
  167. data/app/models/rails_den/post.rb +396 -0
  168. data/app/models/rails_den/post_draft.rb +63 -0
  169. data/app/models/rails_den/post_revision.rb +10 -0
  170. data/app/models/rails_den/private_conversation.rb +25 -0
  171. data/app/models/rails_den/private_conversation_participant.rb +16 -0
  172. data/app/models/rails_den/private_message.rb +35 -0
  173. data/app/models/rails_den/private_message_recipient.rb +44 -0
  174. data/app/models/rails_den/profile_field.rb +150 -0
  175. data/app/models/rails_den/profile_field_value.rb +136 -0
  176. data/app/models/rails_den/reaction.rb +56 -0
  177. data/app/models/rails_den/reaction_type.rb +23 -0
  178. data/app/models/rails_den/reader_visit.rb +61 -0
  179. data/app/models/rails_den/report.rb +127 -0
  180. data/app/models/rails_den/report_reason.rb +52 -0
  181. data/app/models/rails_den/reputation_event.rb +92 -0
  182. data/app/models/rails_den/role.rb +87 -0
  183. data/app/models/rails_den/role_assignment.rb +18 -0
  184. data/app/models/rails_den/role_permission.rb +41 -0
  185. data/app/models/rails_den/sluggable.rb +102 -0
  186. data/app/models/rails_den/sponsor.rb +20 -0
  187. data/app/models/rails_den/sponsorship.rb +87 -0
  188. data/app/models/rails_den/tag.rb +61 -0
  189. data/app/models/rails_den/theme_selection.rb +4 -0
  190. data/app/models/rails_den/topic.rb +307 -0
  191. data/app/models/rails_den/topic_follow.rb +18 -0
  192. data/app/models/rails_den/topic_read_state.rb +76 -0
  193. data/app/models/rails_den/topic_role_requirement.rb +16 -0
  194. data/app/models/rails_den/topic_tag.rb +16 -0
  195. data/app/models/rails_den/trust_level.rb +62 -0
  196. data/app/models/rails_den/trust_level_achievement.rb +21 -0
  197. data/app/models/rails_den/user_restriction.rb +54 -0
  198. data/app/models/rails_den/user_warning.rb +10 -0
  199. data/app/presenters/rails_den/member_profile_presenter.rb +285 -0
  200. data/app/queries/rails_den/topic_new_post_counts.rb +153 -0
  201. data/app/queries/rails_den/topic_unread_counts.rb +137 -0
  202. data/app/services/rails_den/board_following.rb +91 -0
  203. data/app/services/rails_den/bookmark_list.rb +109 -0
  204. data/app/services/rails_den/bookmark_state.rb +17 -0
  205. data/app/services/rails_den/built_in_role_provisioner.rb +98 -0
  206. data/app/services/rails_den/chat/broadcaster.rb +81 -0
  207. data/app/services/rails_den/chat/message_payload.rb +26 -0
  208. data/app/services/rails_den/chat/presence_snapshot.rb +269 -0
  209. data/app/services/rails_den/chat/spatial_broadcaster.rb +135 -0
  210. data/app/services/rails_den/chat/spatial_occupancy_snapshot.rb +35 -0
  211. data/app/services/rails_den/chat/spatial_registry.rb +682 -0
  212. data/app/services/rails_den/chat/spatial_room.rb +81 -0
  213. data/app/services/rails_den/chat/spatial_snapshot.rb +261 -0
  214. data/app/services/rails_den/chat/spatial_update.rb +243 -0
  215. data/app/services/rails_den/chat/subscription_token.rb +28 -0
  216. data/app/services/rails_den/chat/unread_counts.rb +79 -0
  217. data/app/services/rails_den/chat/whisper_delivery.rb +287 -0
  218. data/app/services/rails_den/chat_channel_access.rb +97 -0
  219. data/app/services/rails_den/default_report_reason_provisioner.rb +30 -0
  220. data/app/services/rails_den/emoji_catalog.rb +101 -0
  221. data/app/services/rails_den/gamification/activity_recorder.rb +69 -0
  222. data/app/services/rails_den/gamification/badge_evaluator.rb +55 -0
  223. data/app/services/rails_den/gamification/current_trust_level.rb +21 -0
  224. data/app/services/rails_den/gamification/metrics.rb +93 -0
  225. data/app/services/rails_den/gamification/reputation_ledger.rb +261 -0
  226. data/app/services/rails_den/gamification/trust_level_evaluator.rb +78 -0
  227. data/app/services/rails_den/global_moderation_policy.rb +32 -0
  228. data/app/services/rails_den/member_profile_bio_renderer.rb +28 -0
  229. data/app/services/rails_den/mentions/autocomplete.rb +69 -0
  230. data/app/services/rails_den/mentions/candidate.rb +19 -0
  231. data/app/services/rails_den/mentions/parser.rb +185 -0
  232. data/app/services/rails_den/mentions/reconciler.rb +46 -0
  233. data/app/services/rails_den/mentions/renderer.rb +165 -0
  234. data/app/services/rails_den/moderation_access.rb +42 -0
  235. data/app/services/rails_den/moderation_audit.rb +38 -0
  236. data/app/services/rails_den/moderation_event_scope.rb +12 -0
  237. data/app/services/rails_den/moderation_notification_delivery.rb +23 -0
  238. data/app/services/rails_den/notification_events/emitter.rb +226 -0
  239. data/app/services/rails_den/notification_preferences.rb +169 -0
  240. data/app/services/rails_den/notifications/broadcaster.rb +72 -0
  241. data/app/services/rails_den/notifications/materializer.rb +284 -0
  242. data/app/services/rails_den/notifications/subscription_token.rb +28 -0
  243. data/app/services/rails_den/permission_resolver.rb +252 -0
  244. data/app/services/rails_den/presence/board_summary.rb +131 -0
  245. data/app/services/rails_den/presence/broadcaster.rb +193 -0
  246. data/app/services/rails_den/presence/chat_room_registry.rb +263 -0
  247. data/app/services/rails_den/presence/community_summary.rb +166 -0
  248. data/app/services/rails_den/presence/directory.rb +499 -0
  249. data/app/services/rails_den/presence/last_seen.rb +23 -0
  250. data/app/services/rails_den/presence/page_map.rb +202 -0
  251. data/app/services/rails_den/presence/registry.rb +805 -0
  252. data/app/services/rails_den/presence/subscription_token.rb +28 -0
  253. data/app/services/rails_den/presence/visibility.rb +95 -0
  254. data/app/services/rails_den/private_messages/mailbox.rb +108 -0
  255. data/app/services/rails_den/private_messages/read_marker.rb +25 -0
  256. data/app/services/rails_den/private_messages/sender.rb +113 -0
  257. data/app/services/rails_den/private_messages/unread_count.rb +22 -0
  258. data/app/services/rails_den/reaction_summary.rb +54 -0
  259. data/app/services/rails_den/reaction_type_importer.rb +91 -0
  260. data/app/services/rails_den/reaction_type_search.rb +34 -0
  261. data/app/services/rails_den/readable_community_content.rb +156 -0
  262. data/app/services/rails_den/report_assignment.rb +31 -0
  263. data/app/services/rails_den/report_creator.rb +96 -0
  264. data/app/services/rails_den/report_delivery.rb +39 -0
  265. data/app/services/rails_den/report_disposition.rb +25 -0
  266. data/app/services/rails_den/report_recipients.rb +89 -0
  267. data/app/services/rails_den/report_review_policy.rb +66 -0
  268. data/app/services/rails_den/report_review_scope.rb +49 -0
  269. data/app/services/rails_den/restriction_resolver.rb +28 -0
  270. data/app/services/rails_den/role_resolver.rb +66 -0
  271. data/app/services/rails_den/search/adapters/basic.rb +35 -0
  272. data/app/services/rails_den/search/adapters/postgresql.rb +73 -0
  273. data/app/services/rails_den/search/adapters.rb +21 -0
  274. data/app/services/rails_den/search/query.rb +358 -0
  275. data/app/services/rails_den/topic_following.rb +91 -0
  276. data/app/services/rails_den/topic_merger.rb +27 -0
  277. data/app/services/rails_den/topic_moderation.rb +57 -0
  278. data/app/services/rails_den/topic_splitter.rb +36 -0
  279. data/app/services/rails_den/user_restriction_manager.rb +84 -0
  280. data/app/services/rails_den/user_warning_issuer.rb +19 -0
  281. data/app/views/admin/rails_den/advertising_management/_assignment_form.html.erb +340 -0
  282. data/app/views/admin/rails_den/advertising_management/_errors.html.erb +10 -0
  283. data/app/views/admin/rails_den/advertising_management/show.html.erb +324 -0
  284. data/app/views/admin/rails_den/badges/_form.html.erb +191 -0
  285. data/app/views/admin/rails_den/badges/edit.html.erb +14 -0
  286. data/app/views/admin/rails_den/badges/index.html.erb +98 -0
  287. data/app/views/admin/rails_den/badges/new.html.erb +14 -0
  288. data/app/views/admin/rails_den/board_access/show.html.erb +51 -0
  289. data/app/views/admin/rails_den/boards/_form.html.erb +5 -0
  290. data/app/views/admin/rails_den/categories/_form.html.erb +4 -0
  291. data/app/views/admin/rails_den/members/index.html.erb +121 -0
  292. data/app/views/admin/rails_den/members/show.html.erb +398 -0
  293. data/app/views/admin/rails_den/plugins/index.html.erb +49 -0
  294. data/app/views/admin/rails_den/profile_fields/_form.html.erb +169 -0
  295. data/app/views/admin/rails_den/reaction_types/_catalog_search.html.erb +22 -0
  296. data/app/views/admin/rails_den/reaction_types/_errors.html.erb +3 -0
  297. data/app/views/admin/rails_den/reaction_types/index.html.erb +66 -0
  298. data/app/views/admin/rails_den/reaction_types/new.html.erb +23 -0
  299. data/app/views/admin/rails_den/reaction_types/selected.html.erb +15 -0
  300. data/app/views/admin/rails_den/roles/_form.html.erb +64 -0
  301. data/app/views/admin/rails_den/roles/edit.html.erb +18 -0
  302. data/app/views/admin/rails_den/roles/index.html.erb +49 -0
  303. data/app/views/admin/rails_den/roles/new.html.erb +6 -0
  304. data/app/views/admin/rails_den/shared/_access_form.html.erb +59 -0
  305. data/app/views/admin/rails_den/themes/index.html.erb +129 -0
  306. data/app/views/admin/rails_den/trust_levels/_form.html.erb +80 -0
  307. data/app/views/admin/rails_den/trust_levels/edit.html.erb +14 -0
  308. data/app/views/admin/rails_den/trust_levels/index.html.erb +86 -0
  309. data/app/views/admin/rails_den/trust_levels/new.html.erb +14 -0
  310. data/app/views/administrate/application/_index_header.html.erb +68 -0
  311. data/app/views/administrate/application/new.html.erb +42 -0
  312. data/app/views/administrate/application/show.html.erb +89 -0
  313. data/app/views/layouts/administrate/application/_navigation.html.erb +61 -0
  314. data/app/views/layouts/administrate/application.html.erb +40 -0
  315. data/app/views/layouts/rails_den/_navbar.html.erb +293 -0
  316. data/app/views/layouts/rails_den/application.html.erb +198 -0
  317. data/app/views/rails_den/advertising/show.html.erb +13 -0
  318. data/app/views/rails_den/boards/_follow_button.html.erb +45 -0
  319. data/app/views/rails_den/boards/show.html.erb +150 -0
  320. data/app/views/rails_den/bookmarks/_post_control.html.erb +12 -0
  321. data/app/views/rails_den/bookmarks/_topic_control.html.erb +12 -0
  322. data/app/views/rails_den/bookmarks/index.html.erb +78 -0
  323. data/app/views/rails_den/chat_channels/_form.html.erb +174 -0
  324. data/app/views/rails_den/chat_channels/edit.html.erb +51 -0
  325. data/app/views/rails_den/chat_channels/index.html.erb +402 -0
  326. data/app/views/rails_den/chat_channels/new.html.erb +31 -0
  327. data/app/views/rails_den/chat_channels/show.html.erb +476 -0
  328. data/app/views/rails_den/home/index.html.erb +142 -0
  329. data/app/views/rails_den/member_profiles/_avatar.html.erb +18 -0
  330. data/app/views/rails_den/member_profiles/_custom_fields.html.erb +39 -0
  331. data/app/views/rails_den/member_profiles/_followed_boards.html.erb +15 -0
  332. data/app/views/rails_den/member_profiles/_followed_topics.html.erb +15 -0
  333. data/app/views/rails_den/member_profiles/_header.html.erb +103 -0
  334. data/app/views/rails_den/member_profiles/_recent_posts.html.erb +19 -0
  335. data/app/views/rails_den/member_profiles/_recent_topics.html.erb +15 -0
  336. data/app/views/rails_den/member_profiles/_stats.html.erb +11 -0
  337. data/app/views/rails_den/member_profiles/edit.html.erb +265 -0
  338. data/app/views/rails_den/member_profiles/show.html.erb +97 -0
  339. data/app/views/rails_den/member_reports/new.html.erb +1 -0
  340. data/app/views/rails_den/moderation/audit_events/index.html.erb +11 -0
  341. data/app/views/rails_den/moderation/member_histories/show.html.erb +314 -0
  342. data/app/views/rails_den/moderation/reports/index.html.erb +47 -0
  343. data/app/views/rails_den/moderation/reports/show.html.erb +344 -0
  344. data/app/views/rails_den/moderation/restrictions/new.html.erb +14 -0
  345. data/app/views/rails_den/moderation/topics/show.html.erb +14 -0
  346. data/app/views/rails_den/moderation/warnings/new.html.erb +10 -0
  347. data/app/views/rails_den/notifications/_empty_state.html.erb +13 -0
  348. data/app/views/rails_den/notifications/index.html.erb +308 -0
  349. data/app/views/rails_den/passwords/edit.html.erb +55 -0
  350. data/app/views/rails_den/passwords/new.html.erb +49 -0
  351. data/app/views/rails_den/passwords_mailer/reset.html.erb +22 -0
  352. data/app/views/rails_den/passwords_mailer/reset.text.erb +14 -0
  353. data/app/views/rails_den/post_reports/new.html.erb +1 -0
  354. data/app/views/rails_den/posts/_attachments.html.erb +94 -0
  355. data/app/views/rails_den/posts/edit.html.erb +95 -0
  356. data/app/views/rails_den/posts/history.html.erb +123 -0
  357. data/app/views/rails_den/presence/_directory_body.html.erb +155 -0
  358. data/app/views/rails_den/presence/_summary.html.erb +26 -0
  359. data/app/views/rails_den/presence/index.html.erb +29 -0
  360. data/app/views/rails_den/private_conversations/_composer.html.erb +28 -0
  361. data/app/views/rails_den/private_conversations/_conversation_row.html.erb +19 -0
  362. data/app/views/rails_den/private_conversations/_mailbox_nav.html.erb +7 -0
  363. data/app/views/rails_den/private_conversations/_message.html.erb +4 -0
  364. data/app/views/rails_den/private_conversations/_pagination.html.erb +6 -0
  365. data/app/views/rails_den/private_conversations/index.html.erb +11 -0
  366. data/app/views/rails_den/private_conversations/new.html.erb +5 -0
  367. data/app/views/rails_den/private_conversations/sent.html.erb +11 -0
  368. data/app/views/rails_den/private_conversations/show.html.erb +23 -0
  369. data/app/views/rails_den/reactions/_picker_options.html.erb +14 -0
  370. data/app/views/rails_den/reactions/_post_reactions.html.erb +87 -0
  371. data/app/views/rails_den/reactions/_reactor_list.html.erb +16 -0
  372. data/app/views/rails_den/reactions/index.html.erb +34 -0
  373. data/app/views/rails_den/reactions/picker.html.erb +14 -0
  374. data/app/views/rails_den/registrations/edit.html.erb +95 -0
  375. data/app/views/rails_den/registrations/new.html.erb +77 -0
  376. data/app/views/rails_den/reports/_form.html.erb +54 -0
  377. data/app/views/rails_den/search/index.html.erb +235 -0
  378. data/app/views/rails_den/sessions/new.html.erb +64 -0
  379. data/app/views/rails_den/shared/_ad_network_placement.html.erb +5 -0
  380. data/app/views/rails_den/shared/_composer.html.erb +266 -0
  381. data/app/views/rails_den/shared/_sponsor_placement.html.erb +44 -0
  382. data/app/views/rails_den/tags/show.html.erb +91 -0
  383. data/app/views/rails_den/topic_reports/new.html.erb +1 -0
  384. data/app/views/rails_den/topics/_author_badges.html.erb +15 -0
  385. data/app/views/rails_den/topics/_follow_button.html.erb +45 -0
  386. data/app/views/rails_den/topics/_summary.html.erb +121 -0
  387. data/app/views/rails_den/topics/_tags.html.erb +47 -0
  388. data/app/views/rails_den/topics/new.html.erb +181 -0
  389. data/app/views/rails_den/topics/show.html.erb +430 -0
  390. data/config/locales/rails_den.en.yml +65 -0
  391. data/config/routes.rb +442 -0
  392. data/db/migrate/20260820181527_create_rails_den_administrators.rb +12 -0
  393. data/db/migrate/20260820205806_create_rails_den_categories.rb +16 -0
  394. data/db/migrate/20260820212200_create_rails_den_boards.rb +23 -0
  395. data/db/migrate/20260820234758_create_rails_den_topics.rb +24 -0
  396. data/db/migrate/20260821001937_create_rails_den_posts.rb +11 -0
  397. data/db/migrate/20260822004306_add_soft_deletion_to_rails_den_posts.rb +9 -0
  398. data/db/migrate/20260823183944_create_rails_den_onebox_entries.rb +65 -0
  399. data/db/migrate/20260824025007_add_reply_to_to_rails_den_posts.rb +8 -0
  400. data/db/migrate/20260824092300_create_rails_den_post_revisions.rb +22 -0
  401. data/db/migrate/20260824103358_create_rails_den_post_drafts.rb +36 -0
  402. data/db/migrate/20260824142522_create_rails_den_topic_read_states.rb +32 -0
  403. data/db/migrate/20260824214941_create_rails_den_reader_visits.rb +16 -0
  404. data/db/migrate/20260824225733_create_rails_den_board_read_states.rb +34 -0
  405. data/db/migrate/20260824232113_create_rails_den_community_read_states.rb +26 -0
  406. data/db/migrate/20260825121109_create_rails_den_topic_follows.rb +36 -0
  407. data/db/migrate/20260825232623_create_rails_den_board_follows.rb +36 -0
  408. data/db/migrate/20260826003155_create_rails_den_mentions.rb +27 -0
  409. data/db/migrate/20260826080109_create_rails_den_member_profiles.rb +32 -0
  410. data/db/migrate/20260826092936_add_quoted_post_to_rails_den_posts_and_post_drafts.rb +17 -0
  411. data/db/migrate/20260826101755_create_rails_den_notification_events.rb +19 -0
  412. data/db/migrate/20260826121710_create_rails_den_notifications.rb +28 -0
  413. data/db/migrate/20260826140846_create_rails_den_notification_mutes.rb +25 -0
  414. data/db/migrate/20260826175426_create_rails_den_tags.rb +31 -0
  415. data/db/migrate/20260826175428_create_rails_den_topic_tags.rb +26 -0
  416. data/db/migrate/20260827021133_create_rails_den_roles.rb +16 -0
  417. data/db/migrate/20260827021135_create_rails_den_role_assignments.rb +22 -0
  418. data/db/migrate/20260827031134_create_rails_den_role_permissions.rb +28 -0
  419. data/db/migrate/20260827031136_create_rails_den_board_role_permissions.rb +35 -0
  420. data/db/migrate/20260827033116_provision_rails_den_built_in_roles_and_permissions.rb +108 -0
  421. data/db/migrate/20260827040515_create_rails_den_board_role_assignments.rb +33 -0
  422. data/db/migrate/20260827041710_create_rails_den_board_role_requirements.rb +27 -0
  423. data/db/migrate/20260827063901_create_rails_den_category_role_requirements.rb +23 -0
  424. data/db/migrate/20260827063909_create_rails_den_topic_role_requirements.rb +23 -0
  425. data/db/migrate/20260827203747_create_rails_den_report_reasons.rb +19 -0
  426. data/db/migrate/20260827203752_create_rails_den_reports.rb +24 -0
  427. data/db/migrate/20260827203807_provision_rails_den_default_report_reasons.rb +32 -0
  428. data/db/migrate/20260828030352_create_rails_den_moderation_events.rb +26 -0
  429. data/db/migrate/20260828030354_create_rails_den_moderation_notes.rb +13 -0
  430. data/db/migrate/20260828030357_create_rails_den_user_warnings.rb +17 -0
  431. data/db/migrate/20260828030359_create_rails_den_user_restrictions.rb +30 -0
  432. data/db/migrate/20260828030404_add_moderation_fields_to_rails_den_reports.rb +55 -0
  433. data/db/migrate/20260828030407_add_moderation_fields_to_rails_den_topics.rb +14 -0
  434. data/db/migrate/20260828031955_add_opening_post_to_rails_den_topics.rb +25 -0
  435. data/db/migrate/20260828083552_create_rails_den_reaction_types.rb +15 -0
  436. data/db/migrate/20260828083554_create_rails_den_reactions.rb +20 -0
  437. data/db/migrate/20260828083556_provision_rails_den_default_reaction_type.rb +18 -0
  438. data/db/migrate/20260828151420_create_rails_den_bookmarks.rb +20 -0
  439. data/db/migrate/20260828165545_create_rails_den_profile_fields.rb +37 -0
  440. data/db/migrate/20260828165547_create_rails_den_profile_field_values.rb +31 -0
  441. data/db/migrate/20260828191644_add_presence_fields_to_rails_den_member_profiles.rb +22 -0
  442. data/db/migrate/20260829003137_create_rails_den_private_conversations.rb +13 -0
  443. data/db/migrate/20260829003139_create_rails_den_private_conversation_participants.rb +35 -0
  444. data/db/migrate/20260829003141_create_rails_den_private_messages.rb +19 -0
  445. data/db/migrate/20260829003143_create_rails_den_private_message_recipients.rb +26 -0
  446. data/db/migrate/20260829003435_provision_rails_den_message_capability_defaults.rb +39 -0
  447. data/db/migrate/20260829013557_create_rails_den_chat_channels.rb +17 -0
  448. data/db/migrate/20260829014413_create_rails_den_chat_channel_memberships.rb +26 -0
  449. data/db/migrate/20260829014523_create_rails_den_chat_messages.rb +27 -0
  450. data/db/migrate/20260830155021_create_rails_den_chat_channel_read_states.rb +37 -0
  451. data/db/migrate/20260830155023_add_pinned_to_rails_den_chat_messages.rb +16 -0
  452. data/db/migrate/20260830185149_create_rails_den_badges.rb +44 -0
  453. data/db/migrate/20260830185151_create_rails_den_badge_awards.rb +43 -0
  454. data/db/migrate/20260830190053_create_rails_den_reputation_events.rb +52 -0
  455. data/db/migrate/20260830190055_create_rails_den_trust_levels.rb +52 -0
  456. data/db/migrate/20260830190057_create_rails_den_trust_level_achievements.rb +39 -0
  457. data/db/migrate/20260830190059_add_reputation_points_to_rails_den_member_profiles.rb +17 -0
  458. data/db/migrate/20260831120000_create_rails_den_advertising_and_sponsorships.rb +55 -0
  459. data/db/migrate/20260831120001_add_description_to_rails_den_sponsors.rb +5 -0
  460. data/db/migrate/20260831120002_add_advertising_page_to_rails_den_advertising_configurations.rb +7 -0
  461. data/db/migrate/20260831120003_add_destination_url_to_rails_den_sponsors.rb +5 -0
  462. data/db/migrate/20260831120004_move_destination_url_to_rails_den_sponsorships.rb +138 -0
  463. data/db/migrate/20260831204539_create_rails_den_theme_selections.rb +9 -0
  464. data/exe/rails_den +5 -0
  465. data/lib/generators/rails_den/install/install_generator.rb +42 -0
  466. data/lib/generators/rails_den/install/templates/rails_den.rb +75 -0
  467. data/lib/generators/rails_den/install/views/views_generator.rb +56 -0
  468. data/lib/rails/commands/rails_den/import/discourse/discourse_command.rb +48 -0
  469. data/lib/rails_den/cli.rb +174 -0
  470. data/lib/rails_den/configuration.rb +173 -0
  471. data/lib/rails_den/engine.rb +10 -0
  472. data/lib/rails_den/extensions/descriptor.rb +138 -0
  473. data/lib/rails_den/extensions/registry.rb +65 -0
  474. data/lib/rails_den/extensions.rb +49 -0
  475. data/lib/rails_den/formatters/markdown.rb +30 -0
  476. data/lib/rails_den/formatters/plain_text.rb +18 -0
  477. data/lib/rails_den/formatting.rb +130 -0
  478. data/lib/rails_den/gamification.rb +77 -0
  479. data/lib/rails_den/import/discourse/backup.rb +169 -0
  480. data/lib/rails_den/import/discourse/finalizer.rb +617 -0
  481. data/lib/rails_den/import/discourse/importer.rb +266 -0
  482. data/lib/rails_den/import/discourse/source_database.rb +320 -0
  483. data/lib/rails_den/import/discourse/staff_roles.rb +173 -0
  484. data/lib/rails_den/import/discourse/transformer.rb +940 -0
  485. data/lib/rails_den/import/discourse.rb +9 -0
  486. data/lib/rails_den/licensing/client.rb +128 -0
  487. data/lib/rails_den/licensing/result.rb +57 -0
  488. data/lib/rails_den/licensing.rb +28 -0
  489. data/lib/rails_den/metadata.rb +343 -0
  490. data/lib/rails_den/onebox/address_guard.rb +108 -0
  491. data/lib/rails_den/onebox/body_renderer.rb +135 -0
  492. data/lib/rails_den/onebox/embed_policy.rb +179 -0
  493. data/lib/rails_den/onebox/errors.rb +24 -0
  494. data/lib/rails_den/onebox/extractor.rb +130 -0
  495. data/lib/rails_den/onebox/fetcher.rb +199 -0
  496. data/lib/rails_den/onebox/http_transport.rb +164 -0
  497. data/lib/rails_den/onebox/metadata.rb +343 -0
  498. data/lib/rails_den/onebox/oembed/client.rb +46 -0
  499. data/lib/rails_den/onebox/oembed/discovery.rb +296 -0
  500. data/lib/rails_den/onebox/oembed/response.rb +375 -0
  501. data/lib/rails_den/onebox/oembed.rb +18 -0
  502. data/lib/rails_den/onebox/providers/base.rb +23 -0
  503. data/lib/rails_den/onebox/providers/bluesky.rb +402 -0
  504. data/lib/rails_den/onebox/providers/generic.rb +310 -0
  505. data/lib/rails_den/onebox/providers/giphy.rb +281 -0
  506. data/lib/rails_den/onebox/providers/github.rb +300 -0
  507. data/lib/rails_den/onebox/providers/mastodon.rb +505 -0
  508. data/lib/rails_den/onebox/providers/soundcloud.rb +199 -0
  509. data/lib/rails_den/onebox/providers/spotify.rb +196 -0
  510. data/lib/rails_den/onebox/providers/tenor.rb +296 -0
  511. data/lib/rails_den/onebox/providers/tiktok.rb +292 -0
  512. data/lib/rails_den/onebox/providers/twitch.rb +414 -0
  513. data/lib/rails_den/onebox/providers/vimeo.rb +295 -0
  514. data/lib/rails_den/onebox/providers/youtube.rb +201 -0
  515. data/lib/rails_den/onebox/registry.rb +41 -0
  516. data/lib/rails_den/onebox/renderer.rb +403 -0
  517. data/lib/rails_den/onebox/result.rb +86 -0
  518. data/lib/rails_den/onebox/synchronizer.rb +161 -0
  519. data/lib/rails_den/onebox/url.rb +35 -0
  520. data/lib/rails_den/onebox.rb +176 -0
  521. data/lib/rails_den/presence.rb +162 -0
  522. data/lib/rails_den/standalone_template.rb +700 -0
  523. data/lib/rails_den/themes.rb +80 -0
  524. data/lib/rails_den/version.rb +3 -0
  525. data/lib/rails_den/view_installer.rb +129 -0
  526. data/lib/rails_den.rb +113 -0
  527. data/lib/tasks/rails_den_tasks.rake +4 -0
  528. metadata +735 -0
@@ -0,0 +1,1785 @@
1
+ (() => {
2
+ const COMPOSER_SELECTOR =
3
+ "[data-rails-den-composer]"
4
+
5
+ const INPUT_SELECTOR =
6
+ "[data-rails-den-composer-input]"
7
+
8
+ const MENTION_NAME_PATTERN =
9
+ /[A-Za-z0-9_]/
10
+
11
+ const MENTION_BOUNDARY_PATTERN =
12
+ /[\s([{"']/
13
+
14
+ const MENTION_DEBOUNCE =
15
+ 175
16
+
17
+ const roots =
18
+ new Map()
19
+
20
+ const emojis = [
21
+ [ "😄", "smile" ],
22
+ [ "😂", "laughing" ],
23
+ [ "❤️", "heart" ],
24
+ [ "👍", "thumbs up" ],
25
+ [ "👀", "eyes" ],
26
+ [ "🔥", "fire" ],
27
+ [ "🎉", "party" ],
28
+ [ "🚀", "rocket" ],
29
+ [ "✅", "check" ],
30
+ [ "❌", "x" ],
31
+ [ "🤔", "thinking" ],
32
+ [ "🙌", "raised hands" ]
33
+ ]
34
+
35
+ const graphemeSegmenter =
36
+ typeof Intl !== "undefined" &&
37
+ typeof Intl.Segmenter === "function"
38
+ ? new Intl.Segmenter(
39
+ undefined,
40
+ {
41
+ granularity: "grapheme"
42
+ }
43
+ )
44
+ : null
45
+
46
+ function reactAvailable() {
47
+ return (
48
+ window.React &&
49
+ window.ReactDOM &&
50
+ typeof window.ReactDOM.createRoot ===
51
+ "function"
52
+ )
53
+ }
54
+
55
+ function composerConfiguration(composer) {
56
+ const input =
57
+ composer.querySelector(
58
+ INPUT_SELECTOR
59
+ )
60
+
61
+ if (!input) {
62
+ return null
63
+ }
64
+
65
+ return {
66
+ previewUrl:
67
+ composer.dataset.previewUrl,
68
+ mentionCandidatesUrl:
69
+ composer.dataset
70
+ .railsDenMentionCandidatesUrl,
71
+ id:
72
+ input.id,
73
+ name:
74
+ input.name,
75
+ value:
76
+ input.value,
77
+ rows:
78
+ input.rows || 8,
79
+ required:
80
+ input.required,
81
+ autofocus:
82
+ input.autofocus,
83
+ placeholder:
84
+ input.placeholder || ""
85
+ }
86
+ }
87
+
88
+ function wordCount(value) {
89
+ const normalized =
90
+ value.trim()
91
+
92
+ if (!normalized) {
93
+ return 0
94
+ }
95
+
96
+ return normalized
97
+ .split(/\s+/u)
98
+ .length
99
+ }
100
+
101
+ function characterCount(value) {
102
+ if (graphemeSegmenter) {
103
+ return Array.from(
104
+ graphemeSegmenter.segment(
105
+ value
106
+ )
107
+ ).length
108
+ }
109
+
110
+ return Array.from(
111
+ value
112
+ ).length
113
+ }
114
+
115
+ function markdownRangeExcluded(value, offset) {
116
+ const before = value.slice(0, offset)
117
+ const lineStart = before.lastIndexOf("\n") + 1
118
+ const line = value.slice(lineStart, offset)
119
+
120
+ if (/^ {0,3}>/.test(line)) {
121
+ return true
122
+ }
123
+
124
+ const lines = before.split("\n")
125
+ let fence = null
126
+
127
+ for (const candidateLine of lines) {
128
+ const marker = candidateLine.match(/^ {0,3}(`{3,}|~{3,})/)
129
+
130
+ if (!marker) {
131
+ continue
132
+ }
133
+
134
+ if (fence === marker[1][0]) {
135
+ fence = null
136
+ } else if (!fence) {
137
+ fence = marker[1][0]
138
+ }
139
+ }
140
+
141
+ if (fence) {
142
+ return true
143
+ }
144
+
145
+ const backticks = (line.match(/`/g) || []).length
146
+
147
+ if (backticks % 2 === 1) {
148
+ return true
149
+ }
150
+
151
+ const openingAngle = line.lastIndexOf("<")
152
+ const closingAngle = line.lastIndexOf(">")
153
+
154
+ if (openingAngle > closingAngle) {
155
+ return true
156
+ }
157
+
158
+ const openingDestination = line.lastIndexOf("](")
159
+ const closingDestination = line.lastIndexOf(")")
160
+
161
+ if (openingDestination > closingDestination) {
162
+ return true
163
+ }
164
+
165
+ const token = line.split(/\s/).pop() || ""
166
+
167
+ return /^(?:https?:\/\/|mailto:|www\.)/i.test(token)
168
+ }
169
+
170
+ function activeMentionQuery(value, start, finish) {
171
+ if (start !== finish) {
172
+ return null
173
+ }
174
+
175
+ let tokenStart = start
176
+
177
+ while (
178
+ tokenStart > 0 &&
179
+ MENTION_NAME_PATTERN.test(
180
+ value[tokenStart - 1]
181
+ )
182
+ ) {
183
+ tokenStart -= 1
184
+ }
185
+
186
+ if (
187
+ tokenStart === 0 ||
188
+ value[tokenStart - 1] !== "@"
189
+ ) {
190
+ return null
191
+ }
192
+
193
+ const atIndex = tokenStart - 1
194
+ const prefix = value.slice(tokenStart, start)
195
+
196
+ if (
197
+ prefix.length < 1 ||
198
+ prefix.length > 30 ||
199
+ !/^[A-Za-z0-9_]+$/.test(prefix)
200
+ ) {
201
+ return null
202
+ }
203
+
204
+ if (atIndex > 0) {
205
+ const previous = value[atIndex - 1]
206
+
207
+ if (!MENTION_BOUNDARY_PATTERN.test(previous)) {
208
+ return null
209
+ }
210
+
211
+ let slashes = 0
212
+ let index = atIndex - 1
213
+
214
+ while (index >= 0 && value[index] === "\\") {
215
+ slashes += 1
216
+ index -= 1
217
+ }
218
+
219
+ if (slashes % 2 === 1) {
220
+ return null
221
+ }
222
+ }
223
+
224
+ if (markdownRangeExcluded(value, atIndex)) {
225
+ return null
226
+ }
227
+
228
+ return {
229
+ start: atIndex,
230
+ finish: start,
231
+ prefix: prefix.toLowerCase()
232
+ }
233
+ }
234
+
235
+ function Composer({ configuration }) {
236
+ const React =
237
+ window.React
238
+
239
+ const h =
240
+ React.createElement
241
+
242
+ const {
243
+ useEffect,
244
+ useId,
245
+ useRef,
246
+ useState
247
+ } = React
248
+
249
+ const [
250
+ body,
251
+ setBody
252
+ ] =
253
+ useState(
254
+ configuration.value
255
+ )
256
+
257
+ const [
258
+ mode,
259
+ setMode
260
+ ] =
261
+ useState(
262
+ "write"
263
+ )
264
+
265
+ const [
266
+ status,
267
+ setStatus
268
+ ] =
269
+ useState(
270
+ "Markdown supported"
271
+ )
272
+
273
+ const [
274
+ previewHtml,
275
+ setPreviewHtml
276
+ ] =
277
+ useState(
278
+ ""
279
+ )
280
+
281
+ const [
282
+ previewFailed,
283
+ setPreviewFailed
284
+ ] =
285
+ useState(
286
+ false
287
+ )
288
+
289
+ const [
290
+ emojiOpen,
291
+ setEmojiOpen
292
+ ] =
293
+ useState(
294
+ false
295
+ )
296
+
297
+ const [
298
+ mentionQuery,
299
+ setMentionQuery
300
+ ] = useState(null)
301
+
302
+ const [
303
+ mentionCandidates,
304
+ setMentionCandidates
305
+ ] = useState([])
306
+
307
+ const [
308
+ mentionActiveIndex,
309
+ setMentionActiveIndex
310
+ ] = useState(0)
311
+
312
+ const [
313
+ mentionPosition,
314
+ setMentionPosition
315
+ ] = useState(null)
316
+
317
+ const [
318
+ mentionAnnouncement,
319
+ setMentionAnnouncement
320
+ ] = useState("")
321
+
322
+ const composerRef =
323
+ useRef(null)
324
+
325
+ const textareaRef =
326
+ useRef(null)
327
+
328
+ const previewRef =
329
+ useRef(null)
330
+
331
+ const previewTimerRef =
332
+ useRef(null)
333
+
334
+ const previewRequestRef =
335
+ useRef(null)
336
+
337
+ const mentionTimerRef =
338
+ useRef(null)
339
+
340
+ const mentionRequestRef =
341
+ useRef(null)
342
+
343
+ const mentionSequenceRef =
344
+ useRef(0)
345
+
346
+ const mentionListRef =
347
+ useRef(null)
348
+
349
+ const mentionListId =
350
+ `rails-den-mention-list-${useId().replace(/:/g, "")}`
351
+
352
+ const mentionOpen =
353
+ mentionQuery !== null &&
354
+ mentionCandidates.length > 0
355
+
356
+ const words =
357
+ wordCount(body)
358
+
359
+ const characters =
360
+ characterCount(body)
361
+
362
+ const statusText =
363
+ `${status} · ` +
364
+ `${words} ` +
365
+ `${words === 1 ? "word" : "words"} · ` +
366
+ `${characters} ` +
367
+ `${characters === 1 ? "character" : "characters"}`
368
+
369
+ function cancelPreview() {
370
+ if (previewTimerRef.current) {
371
+ window.clearTimeout(
372
+ previewTimerRef.current
373
+ )
374
+
375
+ previewTimerRef.current = null
376
+ }
377
+
378
+ if (previewRequestRef.current) {
379
+ previewRequestRef.current.abort()
380
+
381
+ previewRequestRef.current = null
382
+ }
383
+ }
384
+
385
+ function cancelMentionRequest() {
386
+ if (mentionTimerRef.current) {
387
+ window.clearTimeout(mentionTimerRef.current)
388
+ mentionTimerRef.current = null
389
+ }
390
+
391
+ if (mentionRequestRef.current) {
392
+ mentionRequestRef.current.abort()
393
+ mentionRequestRef.current = null
394
+ }
395
+ }
396
+
397
+ function closeMentions({announce = ""} = {}) {
398
+ cancelMentionRequest()
399
+ mentionSequenceRef.current += 1
400
+ setMentionQuery(null)
401
+ setMentionCandidates([])
402
+ setMentionActiveIndex(0)
403
+ setMentionPosition(null)
404
+
405
+ if (announce) {
406
+ setMentionAnnouncement(announce)
407
+ }
408
+ }
409
+
410
+ function positionMentionList() {
411
+ const input = textareaRef.current
412
+
413
+ if (!input || !mentionQuery) {
414
+ return
415
+ }
416
+
417
+ const pane = input.parentElement
418
+ const style = window.getComputedStyle(input)
419
+ const mirror = document.createElement("div")
420
+ const marker = document.createElement("span")
421
+ const copiedProperties = [
422
+ "boxSizing",
423
+ "width",
424
+ "height",
425
+ "paddingTop",
426
+ "paddingRight",
427
+ "paddingBottom",
428
+ "paddingLeft",
429
+ "borderTopWidth",
430
+ "borderRightWidth",
431
+ "borderBottomWidth",
432
+ "borderLeftWidth",
433
+ "fontFamily",
434
+ "fontSize",
435
+ "fontStyle",
436
+ "fontWeight",
437
+ "letterSpacing",
438
+ "lineHeight",
439
+ "textAlign",
440
+ "textTransform",
441
+ "wordSpacing",
442
+ "tabSize"
443
+ ]
444
+
445
+ mirror.className = "rails-den-mention-caret-mirror"
446
+ copiedProperties.forEach((property) => {
447
+ mirror.style[property] = style[property]
448
+ })
449
+ mirror.style.whiteSpace = "pre-wrap"
450
+ mirror.style.overflowWrap = "break-word"
451
+ mirror.style.top = `${-input.scrollTop}px`
452
+ mirror.style.left = `${-input.scrollLeft}px`
453
+ mirror.textContent = input.value.slice(0, mentionQuery.finish)
454
+ marker.textContent = "\u200b"
455
+ mirror.appendChild(marker)
456
+ pane.appendChild(mirror)
457
+
458
+ const markerRect = marker.getBoundingClientRect()
459
+ const paneRect = pane.getBoundingClientRect()
460
+ const lineHeight = parseFloat(style.lineHeight) || 24
461
+ const listWidth = Math.min(320, paneRect.width - 16)
462
+ const left = Math.max(
463
+ 8,
464
+ Math.min(markerRect.left - paneRect.left, paneRect.width - listWidth - 8)
465
+ )
466
+ const below = markerRect.bottom - paneRect.top + 6
467
+ const estimatedHeight = Math.min(mentionCandidates.length, 8) * 48 + 2
468
+ const above = markerRect.top - paneRect.top - estimatedHeight - 6
469
+ const top = below + estimatedHeight <= pane.clientHeight
470
+ ? below
471
+ : Math.max(8, above || lineHeight)
472
+
473
+ pane.removeChild(mirror)
474
+ setMentionPosition({left, top, width: listWidth})
475
+ }
476
+
477
+ async function fetchMentionCandidates(query, sequence) {
478
+ if (!configuration.mentionCandidatesUrl) {
479
+ closeMentions()
480
+ return
481
+ }
482
+
483
+ const controller = new AbortController()
484
+ mentionRequestRef.current = controller
485
+
486
+ try {
487
+ const url = new URL(configuration.mentionCandidatesUrl, window.location.origin)
488
+ url.searchParams.set("q", query.prefix)
489
+
490
+ const response = await fetch(url.toString(), {
491
+ method: "GET",
492
+ credentials: "same-origin",
493
+ headers: {"Accept": "application/json"},
494
+ signal: controller.signal
495
+ })
496
+ const contentType = response.headers.get("content-type") || ""
497
+
498
+ if (!response.ok || !contentType.includes("application/json")) {
499
+ closeMentions()
500
+ return
501
+ }
502
+
503
+ const payload = await response.json()
504
+
505
+ if (sequence !== mentionSequenceRef.current) {
506
+ return
507
+ }
508
+
509
+ const suggestions = Array.isArray(payload.suggestions)
510
+ ? payload.suggestions.slice(0, 10)
511
+ : []
512
+
513
+ setMentionCandidates(suggestions)
514
+ setMentionActiveIndex(0)
515
+ setMentionAnnouncement(
516
+ suggestions.length === 0
517
+ ? "No mention suggestions"
518
+ : `${suggestions.length} mention ${suggestions.length === 1 ? "suggestion" : "suggestions"} available`
519
+ )
520
+ } catch (error) {
521
+ if (error.name !== "AbortError") {
522
+ closeMentions()
523
+ }
524
+ } finally {
525
+ if (mentionRequestRef.current === controller) {
526
+ mentionRequestRef.current = null
527
+ }
528
+ }
529
+ }
530
+
531
+ function scheduleMentionCandidates(value, input) {
532
+ const query = activeMentionQuery(
533
+ value,
534
+ input.selectionStart ?? value.length,
535
+ input.selectionEnd ?? value.length
536
+ )
537
+
538
+ cancelMentionRequest()
539
+
540
+ if (!query || mode !== "write") {
541
+ closeMentions()
542
+ return
543
+ }
544
+
545
+ const sequence = mentionSequenceRef.current + 1
546
+ mentionSequenceRef.current = sequence
547
+ setMentionQuery(query)
548
+ setMentionCandidates([])
549
+ setMentionActiveIndex(0)
550
+ mentionTimerRef.current = window.setTimeout(() => {
551
+ mentionTimerRef.current = null
552
+ fetchMentionCandidates(query, sequence)
553
+ }, MENTION_DEBOUNCE)
554
+ }
555
+
556
+ function insertMention(candidate) {
557
+ const input = textareaRef.current
558
+
559
+ if (!input || !mentionQuery) {
560
+ return
561
+ }
562
+
563
+ const following = input.value[mentionQuery.finish]
564
+ const suffix = following && /\s|[.,!?;:)}\]]/.test(following)
565
+ ? ""
566
+ : " "
567
+ const replacement = `@${candidate.mention_name}${suffix}`
568
+
569
+ input.setSelectionRange(mentionQuery.start, mentionQuery.finish)
570
+
571
+ const inserted = document.execCommand(
572
+ "insertText",
573
+ false,
574
+ replacement
575
+ )
576
+
577
+ if (!inserted) {
578
+ input.setRangeText(
579
+ replacement,
580
+ mentionQuery.start,
581
+ mentionQuery.finish,
582
+ "end"
583
+ )
584
+ }
585
+ const nextBody = input.value
586
+ const cursor = input.selectionStart
587
+
588
+ if (typeof window.ReactDOM.flushSync === "function") {
589
+ window.ReactDOM.flushSync(() => setBody(nextBody))
590
+ } else {
591
+ setBody(nextBody)
592
+ }
593
+
594
+ closeMentions({announce: `Inserted @${candidate.mention_name}`})
595
+
596
+ window.requestAnimationFrame(() => {
597
+ const currentInput = textareaRef.current
598
+
599
+ if (!currentInput) {
600
+ return
601
+ }
602
+
603
+ currentInput.focus()
604
+ currentInput.setSelectionRange(cursor, cursor)
605
+ })
606
+ }
607
+
608
+ function handleMentionKeyDown(event) {
609
+ if (!mentionOpen) {
610
+ if (event.key === "Escape") {
611
+ closeMentions()
612
+ }
613
+ return
614
+ }
615
+
616
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
617
+ event.preventDefault()
618
+ const direction = event.key === "ArrowDown" ? 1 : -1
619
+ setMentionActiveIndex((current) =>
620
+ (current + direction + mentionCandidates.length) % mentionCandidates.length
621
+ )
622
+ } else if (event.key === "Enter") {
623
+ event.preventDefault()
624
+ insertMention(mentionCandidates[mentionActiveIndex])
625
+ } else if (event.key === "Escape") {
626
+ event.preventDefault()
627
+ closeMentions()
628
+ } else if (event.key === "Tab") {
629
+ closeMentions()
630
+ }
631
+ }
632
+
633
+ async function updatePreview(value) {
634
+ if (!configuration.previewUrl) {
635
+ setPreviewFailed(true)
636
+
637
+ setStatus(
638
+ "Preview unavailable"
639
+ )
640
+
641
+ return
642
+ }
643
+
644
+ if (previewRequestRef.current) {
645
+ previewRequestRef.current.abort()
646
+ }
647
+
648
+ const controller =
649
+ new AbortController()
650
+
651
+ previewRequestRef.current =
652
+ controller
653
+
654
+ setStatus(
655
+ "Rendering preview…"
656
+ )
657
+
658
+ try {
659
+ const csrfToken =
660
+ document.querySelector(
661
+ 'meta[name="csrf-token"]'
662
+ )?.content
663
+
664
+ const headers = {
665
+ "Accept":
666
+ "text/html",
667
+ "Content-Type":
668
+ "application/x-www-form-urlencoded;charset=UTF-8"
669
+ }
670
+
671
+ if (csrfToken) {
672
+ headers["X-CSRF-Token"] =
673
+ csrfToken
674
+ }
675
+
676
+ const response =
677
+ await fetch(
678
+ configuration.previewUrl,
679
+ {
680
+ method: "POST",
681
+ headers,
682
+ body:
683
+ new URLSearchParams({
684
+ body: value
685
+ }),
686
+ signal:
687
+ controller.signal
688
+ }
689
+ )
690
+
691
+ if (!response.ok) {
692
+ throw new Error(
693
+ `Preview failed with ${response.status}`
694
+ )
695
+ }
696
+
697
+ const html =
698
+ await response.text()
699
+
700
+ if (
701
+ previewRequestRef.current !==
702
+ controller
703
+ ) {
704
+ return
705
+ }
706
+
707
+ setPreviewHtml(html)
708
+ setPreviewFailed(false)
709
+
710
+ setStatus(
711
+ "Preview updated"
712
+ )
713
+ } catch (error) {
714
+ if (
715
+ error.name ===
716
+ "AbortError"
717
+ ) {
718
+ return
719
+ }
720
+
721
+ setPreviewHtml("")
722
+ setPreviewFailed(true)
723
+
724
+ setStatus(
725
+ "Preview unavailable"
726
+ )
727
+ } finally {
728
+ if (
729
+ previewRequestRef.current ===
730
+ controller
731
+ ) {
732
+ previewRequestRef.current =
733
+ null
734
+ }
735
+ }
736
+ }
737
+
738
+ function schedulePreview(
739
+ value,
740
+ delay = 300
741
+ ) {
742
+ if (previewTimerRef.current) {
743
+ window.clearTimeout(
744
+ previewTimerRef.current
745
+ )
746
+ }
747
+
748
+ if (previewRequestRef.current) {
749
+ previewRequestRef.current.abort()
750
+
751
+ previewRequestRef.current = null
752
+ }
753
+
754
+ previewTimerRef.current =
755
+ window.setTimeout(
756
+ () => {
757
+ previewTimerRef.current =
758
+ null
759
+
760
+ updatePreview(value)
761
+ },
762
+ delay
763
+ )
764
+ }
765
+
766
+ function selection() {
767
+ const input =
768
+ textareaRef.current
769
+
770
+ if (!input) {
771
+ return {
772
+ start: body.length,
773
+ finish: body.length,
774
+ text: ""
775
+ }
776
+ }
777
+
778
+ const start =
779
+ input.selectionStart ??
780
+ input.value.length
781
+
782
+ const finish =
783
+ input.selectionEnd ??
784
+ start
785
+
786
+ return {
787
+ start,
788
+ finish,
789
+ text:
790
+ input.value.slice(
791
+ start,
792
+ finish
793
+ )
794
+ }
795
+ }
796
+
797
+ function replaceSelection(
798
+ replacement,
799
+ selectedStart = null,
800
+ selectedEnd = null
801
+ ) {
802
+ const input =
803
+ textareaRef.current
804
+
805
+ if (!input) {
806
+ return
807
+ }
808
+
809
+ const current =
810
+ selection()
811
+
812
+ const nextBody =
813
+ input.value.slice(
814
+ 0,
815
+ current.start
816
+ ) +
817
+ replacement +
818
+ input.value.slice(
819
+ current.finish
820
+ )
821
+
822
+ setBody(nextBody)
823
+
824
+ if (mode === "preview") {
825
+ schedulePreview(
826
+ nextBody
827
+ )
828
+ }
829
+
830
+ window.requestAnimationFrame(
831
+ () => {
832
+ const currentInput =
833
+ textareaRef.current
834
+
835
+ if (!currentInput) {
836
+ return
837
+ }
838
+
839
+ if (
840
+ selectedStart !== null &&
841
+ selectedEnd !== null
842
+ ) {
843
+ currentInput.setSelectionRange(
844
+ current.start +
845
+ selectedStart,
846
+ current.start +
847
+ selectedEnd
848
+ )
849
+ } else {
850
+ const cursor =
851
+ current.start +
852
+ replacement.length
853
+
854
+ currentInput.setSelectionRange(
855
+ cursor,
856
+ cursor
857
+ )
858
+ }
859
+
860
+ currentInput.focus()
861
+ }
862
+ )
863
+ }
864
+
865
+ function wrapSelection(
866
+ prefix,
867
+ suffix,
868
+ placeholder
869
+ ) {
870
+ const current =
871
+ selection()
872
+
873
+ const content =
874
+ current.text ||
875
+ placeholder
876
+
877
+ const replacement =
878
+ `${prefix}${content}${suffix}`
879
+
880
+ replaceSelection(
881
+ replacement,
882
+ prefix.length,
883
+ prefix.length +
884
+ content.length
885
+ )
886
+ }
887
+
888
+ function insertLink() {
889
+ const current =
890
+ selection()
891
+
892
+ const label =
893
+ current.text ||
894
+ "link text"
895
+
896
+ const replacement =
897
+ `[${label}](https://example.com)`
898
+
899
+ const urlStart =
900
+ label.length + 3
901
+
902
+ const urlEnd =
903
+ replacement.length - 1
904
+
905
+ replaceSelection(
906
+ replacement,
907
+ urlStart,
908
+ urlEnd
909
+ )
910
+ }
911
+
912
+ function insertCodeBlock() {
913
+ const current =
914
+ selection()
915
+
916
+ const content =
917
+ current.text ||
918
+ "code"
919
+
920
+ const replacement =
921
+ `\`\`\`\n${content}\n\`\`\``
922
+
923
+ replaceSelection(
924
+ replacement,
925
+ 4,
926
+ 4 + content.length
927
+ )
928
+ }
929
+
930
+ function quoteSelection() {
931
+ const current =
932
+ selection()
933
+
934
+ const content =
935
+ current.text ||
936
+ "quoted text"
937
+
938
+ const quoted =
939
+ content
940
+ .split("\n")
941
+ .map(
942
+ (line) =>
943
+ `> ${line}`
944
+ )
945
+ .join("\n")
946
+
947
+ replaceSelection(
948
+ quoted
949
+ )
950
+ }
951
+
952
+ function applyAction(action) {
953
+ switch (action) {
954
+ case "bold":
955
+ wrapSelection(
956
+ "**",
957
+ "**",
958
+ "bold text"
959
+ )
960
+ break
961
+
962
+ case "italic":
963
+ wrapSelection(
964
+ "*",
965
+ "*",
966
+ "italic text"
967
+ )
968
+ break
969
+
970
+ case "strike":
971
+ wrapSelection(
972
+ "~~",
973
+ "~~",
974
+ "strikethrough text"
975
+ )
976
+ break
977
+
978
+ case "link":
979
+ insertLink()
980
+ break
981
+
982
+ case "code":
983
+ wrapSelection(
984
+ "`",
985
+ "`",
986
+ "code"
987
+ )
988
+ break
989
+
990
+ case "code-block":
991
+ insertCodeBlock()
992
+ break
993
+
994
+ case "quote":
995
+ quoteSelection()
996
+ break
997
+
998
+ case "spoiler":
999
+ wrapSelection(
1000
+ "||",
1001
+ "||",
1002
+ "spoiler text"
1003
+ )
1004
+ break
1005
+ }
1006
+ }
1007
+
1008
+ function showWrite() {
1009
+ cancelPreview()
1010
+
1011
+ setMode(
1012
+ "write"
1013
+ )
1014
+
1015
+ setEmojiOpen(
1016
+ false
1017
+ )
1018
+
1019
+ setStatus(
1020
+ "Markdown supported"
1021
+ )
1022
+
1023
+ window.requestAnimationFrame(
1024
+ () => {
1025
+ textareaRef.current?.focus()
1026
+ }
1027
+ )
1028
+ }
1029
+
1030
+ function showPreview() {
1031
+ closeMentions()
1032
+
1033
+ setMode(
1034
+ "preview"
1035
+ )
1036
+
1037
+ setEmojiOpen(
1038
+ false
1039
+ )
1040
+
1041
+ schedulePreview(
1042
+ body,
1043
+ 0
1044
+ )
1045
+ }
1046
+
1047
+ function handleBodyChange(event) {
1048
+ const nextBody =
1049
+ event.target.value
1050
+
1051
+ setBody(
1052
+ nextBody
1053
+ )
1054
+
1055
+ scheduleMentionCandidates(
1056
+ nextBody,
1057
+ event.target
1058
+ )
1059
+
1060
+ if (mode === "preview") {
1061
+ schedulePreview(
1062
+ nextBody
1063
+ )
1064
+ }
1065
+ }
1066
+
1067
+ function insertEmoji(emoji) {
1068
+ replaceSelection(
1069
+ emoji
1070
+ )
1071
+
1072
+ setEmojiOpen(
1073
+ false
1074
+ )
1075
+ }
1076
+
1077
+ useEffect(
1078
+ () => {
1079
+ function handleDocumentClick(event) {
1080
+ if (
1081
+ !emojiOpen ||
1082
+ !composerRef.current
1083
+ ) {
1084
+ return
1085
+ }
1086
+
1087
+ if (
1088
+ composerRef.current.contains(
1089
+ event.target
1090
+ )
1091
+ ) {
1092
+ return
1093
+ }
1094
+
1095
+ setEmojiOpen(
1096
+ false
1097
+ )
1098
+ }
1099
+
1100
+ function handleKeyDown(event) {
1101
+ if (
1102
+ event.key ===
1103
+ "Escape"
1104
+ ) {
1105
+ setEmojiOpen(
1106
+ false
1107
+ )
1108
+ }
1109
+ }
1110
+
1111
+ document.addEventListener(
1112
+ "click",
1113
+ handleDocumentClick
1114
+ )
1115
+
1116
+ document.addEventListener(
1117
+ "keydown",
1118
+ handleKeyDown
1119
+ )
1120
+
1121
+ return () => {
1122
+ document.removeEventListener(
1123
+ "click",
1124
+ handleDocumentClick
1125
+ )
1126
+
1127
+ document.removeEventListener(
1128
+ "keydown",
1129
+ handleKeyDown
1130
+ )
1131
+ }
1132
+ },
1133
+ [
1134
+ emojiOpen
1135
+ ]
1136
+ )
1137
+
1138
+ useEffect(
1139
+ () => {
1140
+ if (!mentionOpen) {
1141
+ return
1142
+ }
1143
+
1144
+ positionMentionList()
1145
+
1146
+ const activeOption = mentionListRef.current?.querySelector(
1147
+ `[data-rails-den-mention-option="${mentionActiveIndex}"]`
1148
+ )
1149
+
1150
+ activeOption?.scrollIntoView({block: "nearest"})
1151
+ },
1152
+ [mentionOpen, mentionActiveIndex, mentionCandidates]
1153
+ )
1154
+
1155
+ useEffect(
1156
+ () => {
1157
+ const input = textareaRef.current
1158
+ const composer = input?.closest(COMPOSER_SELECTOR)
1159
+ const form = input?.form
1160
+
1161
+ function handleOutsidePointer(event) {
1162
+ if (composer && !composer.contains(event.target)) {
1163
+ closeMentions()
1164
+ }
1165
+ }
1166
+
1167
+ function handleSelectionActivity() {
1168
+ if (!input || document.activeElement !== input) {
1169
+ return
1170
+ }
1171
+
1172
+ scheduleMentionCandidates(input.value, input)
1173
+ }
1174
+
1175
+ function handlePositionChange() {
1176
+ if (mentionOpen) {
1177
+ positionMentionList()
1178
+ }
1179
+ }
1180
+
1181
+ function handleSubmit() {
1182
+ if (input && typeof window.ReactDOM.flushSync === "function") {
1183
+ window.ReactDOM.flushSync(() => setBody(input.value))
1184
+ }
1185
+
1186
+ closeMentions()
1187
+ }
1188
+
1189
+ document.addEventListener("pointerdown", handleOutsidePointer)
1190
+ input?.addEventListener("click", handleSelectionActivity)
1191
+ input?.addEventListener("select", handleSelectionActivity)
1192
+ input?.addEventListener("scroll", handlePositionChange)
1193
+ window.addEventListener("resize", handlePositionChange)
1194
+ window.visualViewport?.addEventListener("resize", handlePositionChange)
1195
+ window.addEventListener("scroll", handlePositionChange, true)
1196
+ form?.addEventListener("submit", handleSubmit)
1197
+
1198
+ return () => {
1199
+ document.removeEventListener("pointerdown", handleOutsidePointer)
1200
+ input?.removeEventListener("click", handleSelectionActivity)
1201
+ input?.removeEventListener("select", handleSelectionActivity)
1202
+ input?.removeEventListener("scroll", handlePositionChange)
1203
+ window.removeEventListener("resize", handlePositionChange)
1204
+ window.visualViewport?.removeEventListener("resize", handlePositionChange)
1205
+ window.removeEventListener("scroll", handlePositionChange, true)
1206
+ form?.removeEventListener("submit", handleSubmit)
1207
+ }
1208
+ },
1209
+ [mentionOpen, mentionQuery]
1210
+ )
1211
+
1212
+ useEffect(
1213
+ () => {
1214
+ if (
1215
+ mode !== "preview" ||
1216
+ previewFailed ||
1217
+ !previewRef.current
1218
+ ) {
1219
+ return
1220
+ }
1221
+
1222
+ document.dispatchEvent(
1223
+ new CustomEvent(
1224
+ "rails-den:content-updated",
1225
+ {
1226
+ detail: {
1227
+ root:
1228
+ previewRef.current
1229
+ }
1230
+ }
1231
+ )
1232
+ )
1233
+ },
1234
+ [
1235
+ previewHtml,
1236
+ previewFailed,
1237
+ mode
1238
+ ]
1239
+ )
1240
+
1241
+ useEffect(
1242
+ () => {
1243
+ return () => {
1244
+ cancelPreview()
1245
+ cancelMentionRequest()
1246
+ }
1247
+ },
1248
+ []
1249
+ )
1250
+
1251
+ function toolButton(
1252
+ action,
1253
+ title,
1254
+ content
1255
+ ) {
1256
+ return h(
1257
+ "button",
1258
+ {
1259
+ type: "button",
1260
+ className:
1261
+ "rails-den-composer-tool",
1262
+ title,
1263
+ onClick: () =>
1264
+ applyAction(action)
1265
+ },
1266
+ content
1267
+ )
1268
+ }
1269
+
1270
+ const previewContentProps = {
1271
+ className:
1272
+ "rails-den-composer-preview-content",
1273
+ ref:
1274
+ previewRef
1275
+ }
1276
+
1277
+ if (!previewFailed) {
1278
+ previewContentProps
1279
+ .dangerouslySetInnerHTML = {
1280
+ __html:
1281
+ previewHtml
1282
+ }
1283
+ }
1284
+
1285
+ return h(
1286
+ React.Fragment,
1287
+ null,
1288
+
1289
+ h(
1290
+ "div",
1291
+ {
1292
+ className:
1293
+ "rails-den-composer-tabs"
1294
+ },
1295
+
1296
+ h(
1297
+ "button",
1298
+ {
1299
+ type: "button",
1300
+ className:
1301
+ mode === "write"
1302
+ ? "rails-den-composer-tab is-active"
1303
+ : "rails-den-composer-tab",
1304
+ "aria-pressed":
1305
+ mode === "write"
1306
+ ? "true"
1307
+ : "false",
1308
+ onClick:
1309
+ showWrite
1310
+ },
1311
+ "Write"
1312
+ ),
1313
+
1314
+ h(
1315
+ "button",
1316
+ {
1317
+ type: "button",
1318
+ className:
1319
+ mode === "preview"
1320
+ ? "rails-den-composer-tab is-active"
1321
+ : "rails-den-composer-tab",
1322
+ "aria-pressed":
1323
+ mode === "preview"
1324
+ ? "true"
1325
+ : "false",
1326
+ onClick:
1327
+ showPreview
1328
+ },
1329
+ "Preview"
1330
+ )
1331
+ ),
1332
+
1333
+ h(
1334
+ "div",
1335
+ {
1336
+ ref:
1337
+ composerRef,
1338
+ className:
1339
+ "rails-den-composer-toolbar",
1340
+ role:
1341
+ "toolbar",
1342
+ "aria-label":
1343
+ "Formatting"
1344
+ },
1345
+
1346
+ toolButton(
1347
+ "bold",
1348
+ "Bold",
1349
+ h(
1350
+ "strong",
1351
+ null,
1352
+ "B"
1353
+ )
1354
+ ),
1355
+
1356
+ toolButton(
1357
+ "italic",
1358
+ "Italic",
1359
+ h(
1360
+ "em",
1361
+ null,
1362
+ "I"
1363
+ )
1364
+ ),
1365
+
1366
+ toolButton(
1367
+ "strike",
1368
+ "Strikethrough",
1369
+ h(
1370
+ "s",
1371
+ null,
1372
+ "S"
1373
+ )
1374
+ ),
1375
+
1376
+ h(
1377
+ "span",
1378
+ {
1379
+ className:
1380
+ "rails-den-composer-toolbar-divider"
1381
+ }
1382
+ ),
1383
+
1384
+ toolButton(
1385
+ "link",
1386
+ "Link",
1387
+ "Link"
1388
+ ),
1389
+
1390
+ toolButton(
1391
+ "code",
1392
+ "Inline code",
1393
+ "Code"
1394
+ ),
1395
+
1396
+ toolButton(
1397
+ "code-block",
1398
+ "Code block",
1399
+ "Code block"
1400
+ ),
1401
+
1402
+ toolButton(
1403
+ "quote",
1404
+ "Quote",
1405
+ "Quote"
1406
+ ),
1407
+
1408
+ toolButton(
1409
+ "spoiler",
1410
+ "Spoiler",
1411
+ "Spoiler"
1412
+ ),
1413
+
1414
+ h(
1415
+ "span",
1416
+ {
1417
+ className:
1418
+ "rails-den-composer-toolbar-divider"
1419
+ }
1420
+ ),
1421
+
1422
+ h(
1423
+ "div",
1424
+ {
1425
+ className:
1426
+ "rails-den-composer-emoji-container"
1427
+ },
1428
+
1429
+ h(
1430
+ "button",
1431
+ {
1432
+ type: "button",
1433
+ className:
1434
+ "rails-den-composer-tool rails-den-composer-emoji-toggle",
1435
+ "aria-expanded":
1436
+ emojiOpen
1437
+ ? "true"
1438
+ : "false",
1439
+ title:
1440
+ "Emoji",
1441
+ onClick:
1442
+ () => {
1443
+ setEmojiOpen(
1444
+ (open) =>
1445
+ !open
1446
+ )
1447
+ }
1448
+ },
1449
+ "😀"
1450
+ ),
1451
+
1452
+ h(
1453
+ "div",
1454
+ {
1455
+ className:
1456
+ "rails-den-composer-emoji-picker",
1457
+ hidden:
1458
+ !emojiOpen
1459
+ },
1460
+
1461
+ ...emojis.map(
1462
+ ([ emoji, name ]) =>
1463
+ h(
1464
+ "button",
1465
+ {
1466
+ key:
1467
+ `${emoji}-${name}`,
1468
+ type:
1469
+ "button",
1470
+ className:
1471
+ "rails-den-composer-emoji",
1472
+ "aria-label":
1473
+ name,
1474
+ title:
1475
+ name,
1476
+ onClick:
1477
+ () =>
1478
+ insertEmoji(
1479
+ emoji
1480
+ )
1481
+ },
1482
+ emoji
1483
+ )
1484
+ )
1485
+ )
1486
+ )
1487
+ ),
1488
+
1489
+ h(
1490
+ "div",
1491
+ {
1492
+ className:
1493
+ "rails-den-composer-pane",
1494
+ hidden:
1495
+ mode !== "write"
1496
+ },
1497
+
1498
+ h(
1499
+ "textarea",
1500
+ {
1501
+ ref:
1502
+ textareaRef,
1503
+ id:
1504
+ configuration.id,
1505
+ name:
1506
+ configuration.name,
1507
+ rows:
1508
+ configuration.rows,
1509
+ required:
1510
+ configuration.required,
1511
+ autoFocus:
1512
+ configuration.autofocus,
1513
+ placeholder:
1514
+ configuration.placeholder,
1515
+ value:
1516
+ body,
1517
+ role:
1518
+ "combobox",
1519
+ "aria-autocomplete":
1520
+ "list",
1521
+ "aria-expanded":
1522
+ mentionOpen
1523
+ ? "true"
1524
+ : "false",
1525
+ "aria-controls":
1526
+ mentionListId,
1527
+ "aria-activedescendant":
1528
+ mentionOpen
1529
+ ? `${mentionListId}-option-${mentionActiveIndex}`
1530
+ : undefined,
1531
+ "data-rails-den-composer-input":
1532
+ "true",
1533
+ onChange:
1534
+ handleBodyChange,
1535
+ onKeyDown:
1536
+ handleMentionKeyDown
1537
+ }
1538
+ ),
1539
+
1540
+ mentionOpen &&
1541
+ h(
1542
+ "div",
1543
+ {
1544
+ ref:
1545
+ mentionListRef,
1546
+ id:
1547
+ mentionListId,
1548
+ role:
1549
+ "listbox",
1550
+ className:
1551
+ "rails-den-mention-suggestions",
1552
+ style:
1553
+ mentionPosition || {
1554
+ left: 8,
1555
+ top: 8
1556
+ }
1557
+ },
1558
+
1559
+ ...mentionCandidates.map(
1560
+ (candidate, index) => {
1561
+ const displayName = candidate.display_name || ""
1562
+ const showDisplayName =
1563
+ displayName.toLowerCase() !== candidate.mention_name.toLowerCase()
1564
+
1565
+ return h(
1566
+ "div",
1567
+ {
1568
+ key:
1569
+ candidate.mention_name,
1570
+ id:
1571
+ `${mentionListId}-option-${index}`,
1572
+ role:
1573
+ "option",
1574
+ "aria-selected":
1575
+ index === mentionActiveIndex
1576
+ ? "true"
1577
+ : "false",
1578
+ "data-rails-den-mention-option":
1579
+ index,
1580
+ className:
1581
+ index === mentionActiveIndex
1582
+ ? "rails-den-mention-suggestion is-active"
1583
+ : "rails-den-mention-suggestion",
1584
+ onPointerDown:
1585
+ (event) => {
1586
+ event.preventDefault()
1587
+ insertMention(candidate)
1588
+ },
1589
+ onMouseEnter:
1590
+ () => setMentionActiveIndex(index)
1591
+ },
1592
+ h(
1593
+ "span",
1594
+ {
1595
+ className:
1596
+ "rails-den-mention-suggestion-name"
1597
+ },
1598
+ `@${candidate.mention_name}`
1599
+ ),
1600
+ showDisplayName &&
1601
+ h(
1602
+ "span",
1603
+ {
1604
+ className:
1605
+ "rails-den-mention-suggestion-display"
1606
+ },
1607
+ displayName
1608
+ )
1609
+ )
1610
+ }
1611
+ )
1612
+ )
1613
+ ),
1614
+
1615
+ h(
1616
+ "div",
1617
+ {
1618
+ className:
1619
+ "rails-den-composer-pane rails-den-composer-preview",
1620
+ hidden:
1621
+ mode !== "preview"
1622
+ },
1623
+
1624
+ previewFailed
1625
+ ? h(
1626
+ "div",
1627
+ previewContentProps,
1628
+ "Unable to render preview."
1629
+ )
1630
+ : h(
1631
+ "div",
1632
+ previewContentProps
1633
+ )
1634
+ ),
1635
+
1636
+ h(
1637
+ "div",
1638
+ {
1639
+ className:
1640
+ "rails-den-composer-status",
1641
+ "aria-live":
1642
+ "polite"
1643
+ },
1644
+ statusText
1645
+ ),
1646
+
1647
+ h(
1648
+ "div",
1649
+ {
1650
+ className:
1651
+ "rails-den-visually-hidden",
1652
+ "aria-live":
1653
+ "polite",
1654
+ "aria-atomic":
1655
+ "true"
1656
+ },
1657
+ mentionAnnouncement
1658
+ )
1659
+ )
1660
+ }
1661
+
1662
+ function initializeComposer(composer) {
1663
+ if (
1664
+ composer.dataset
1665
+ .railsDenComposerReady ===
1666
+ "true"
1667
+ ) {
1668
+ return
1669
+ }
1670
+
1671
+ if (!reactAvailable()) {
1672
+ return
1673
+ }
1674
+
1675
+ const configuration =
1676
+ composerConfiguration(
1677
+ composer
1678
+ )
1679
+
1680
+ if (!configuration) {
1681
+ return
1682
+ }
1683
+
1684
+ const originalHtml =
1685
+ composer.innerHTML
1686
+
1687
+ composer.dataset
1688
+ .railsDenComposerReady =
1689
+ "true"
1690
+
1691
+ const root =
1692
+ window.ReactDOM.createRoot(
1693
+ composer
1694
+ )
1695
+
1696
+ roots.set(
1697
+ composer,
1698
+ {
1699
+ root,
1700
+ originalHtml
1701
+ }
1702
+ )
1703
+
1704
+ root.render(
1705
+ window.React.createElement(
1706
+ Composer,
1707
+ {
1708
+ configuration
1709
+ }
1710
+ )
1711
+ )
1712
+ }
1713
+
1714
+ function initializeComposers() {
1715
+ document
1716
+ .querySelectorAll(
1717
+ COMPOSER_SELECTOR
1718
+ )
1719
+ .forEach(
1720
+ initializeComposer
1721
+ )
1722
+ }
1723
+
1724
+ function restoreComposersForTurboCache() {
1725
+ roots.forEach(
1726
+ (
1727
+ {
1728
+ root,
1729
+ originalHtml
1730
+ },
1731
+ composer
1732
+ ) => {
1733
+ const currentInput =
1734
+ composer.querySelector(
1735
+ INPUT_SELECTOR
1736
+ )
1737
+
1738
+ const currentValue =
1739
+ currentInput?.value
1740
+
1741
+ root.unmount()
1742
+
1743
+ composer.innerHTML =
1744
+ originalHtml
1745
+
1746
+ delete composer.dataset
1747
+ .railsDenComposerReady
1748
+
1749
+ if (
1750
+ currentValue !== undefined
1751
+ ) {
1752
+ const restoredInput =
1753
+ composer.querySelector(
1754
+ INPUT_SELECTOR
1755
+ )
1756
+
1757
+ if (restoredInput) {
1758
+ restoredInput.value =
1759
+ currentValue
1760
+
1761
+ restoredInput.defaultValue =
1762
+ currentValue
1763
+ }
1764
+ }
1765
+ }
1766
+ )
1767
+
1768
+ roots.clear()
1769
+ }
1770
+
1771
+ document.addEventListener(
1772
+ "DOMContentLoaded",
1773
+ initializeComposers
1774
+ )
1775
+
1776
+ document.addEventListener(
1777
+ "turbo:load",
1778
+ initializeComposers
1779
+ )
1780
+
1781
+ document.addEventListener(
1782
+ "turbo:before-cache",
1783
+ restoreComposersForTurboCache
1784
+ )
1785
+ })()