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,3008 @@
1
+ (() => {
2
+ const ROOT_SELECTOR =
3
+ "[data-rails-den-chat-root]"
4
+
5
+ const PAYLOAD_SELECTOR =
6
+ "[data-rails-den-chat-payload]"
7
+
8
+ const MODE_SHELL_SELECTOR =
9
+ "[data-rails-den-chat-mode-shell]"
10
+
11
+ const SPATIAL_ROOM_EVENT =
12
+ "rails-den:spatial-room-event"
13
+
14
+ const SPATIAL_ROOM_CHANGED_EVENT =
15
+ "rails-den:spatial-room-changed"
16
+
17
+ const CHAT_CHANNEL_CHANGE_EVENT =
18
+ "rails-den:chat-channel-change"
19
+
20
+ const CHAT_MESSAGE_ACCEPTED_EVENT =
21
+ "rails-den:chat-message-accepted"
22
+
23
+ const CHAT_DECK_STATE_EVENT =
24
+ "rails-den:chat-deck-state-change"
25
+
26
+ const REACTION_TYPES_META =
27
+ "rails-den-chat-reaction-types-url"
28
+
29
+ const REACTION_SEARCH_DEBOUNCE =
30
+ 180
31
+
32
+ const roots =
33
+ new Map()
34
+
35
+ function reactAvailable() {
36
+ return (
37
+ window.React &&
38
+ window.ReactDOM &&
39
+ typeof window.ReactDOM.createRoot ===
40
+ "function"
41
+ )
42
+ }
43
+
44
+ function payloadFor(root) {
45
+ const element =
46
+ root.querySelector(
47
+ PAYLOAD_SELECTOR
48
+ )
49
+
50
+ if (!element) {
51
+ return null
52
+ }
53
+
54
+ try {
55
+ return JSON.parse(
56
+ element.textContent
57
+ )
58
+ } catch (_error) {
59
+ return null
60
+ }
61
+ }
62
+
63
+ function csrfToken() {
64
+ return document
65
+ .querySelector(
66
+ 'meta[name="csrf-token"]'
67
+ )
68
+ ?.content
69
+ }
70
+
71
+ function reactionTypesUrl() {
72
+ return document
73
+ .querySelector(
74
+ `meta[name="${REACTION_TYPES_META}"]`
75
+ )
76
+ ?.content
77
+ }
78
+
79
+ function presenceSessionId() {
80
+ if (
81
+ !window.RailsDenPresence ||
82
+ typeof window.RailsDenPresence
83
+ .sessionId !== "function"
84
+ ) {
85
+ return null
86
+ }
87
+
88
+ return window.RailsDenPresence
89
+ .sessionId()
90
+ }
91
+
92
+ function formatTime(value) {
93
+ const date =
94
+ new Date(value)
95
+
96
+ if (
97
+ Number.isNaN(
98
+ date.getTime()
99
+ )
100
+ ) {
101
+ return ""
102
+ }
103
+
104
+ return date.toLocaleTimeString(
105
+ [],
106
+ {
107
+ hour: "numeric",
108
+ minute: "2-digit"
109
+ }
110
+ )
111
+ }
112
+
113
+ function modeShellFor(root) {
114
+ return root.closest(
115
+ MODE_SHELL_SELECTOR
116
+ )
117
+ }
118
+
119
+ function threeDimensionalMode(shell) {
120
+ return (
121
+ shell?.dataset
122
+ ?.railsDenChatActiveMode ===
123
+ "3d"
124
+ )
125
+ }
126
+
127
+ function deckOpen(shell) {
128
+ return (
129
+ shell?.dataset
130
+ ?.railsDenChatDeckOpen ===
131
+ "true"
132
+ )
133
+ }
134
+
135
+ function editableTarget(target) {
136
+ if (!(target instanceof Element)) {
137
+ return false
138
+ }
139
+
140
+ return Boolean(
141
+ target.closest(
142
+ "input, textarea, select, button, [contenteditable='true'], [contenteditable='']"
143
+ )
144
+ )
145
+ }
146
+
147
+ function dispatchDeckState(
148
+ shell,
149
+ open,
150
+ restorePointerLock = false
151
+ ) {
152
+ if (!shell) {
153
+ return
154
+ }
155
+
156
+ shell.dataset
157
+ .railsDenChatDeckOpen =
158
+ open
159
+ ? "true"
160
+ : "false"
161
+
162
+ document.dispatchEvent(
163
+ new CustomEvent(
164
+ CHAT_DECK_STATE_EVENT,
165
+ {
166
+ detail: {
167
+ shell,
168
+ open,
169
+ restorePointerLock
170
+ }
171
+ }
172
+ )
173
+ )
174
+ }
175
+
176
+ function dispatchAcceptedMessage(
177
+ slug,
178
+ message
179
+ ) {
180
+ if (
181
+ !slug ||
182
+ !message
183
+ ) {
184
+ return
185
+ }
186
+
187
+ window.dispatchEvent(
188
+ new CustomEvent(
189
+ CHAT_MESSAGE_ACCEPTED_EVENT,
190
+ {
191
+ detail: {
192
+ slug,
193
+ message
194
+ }
195
+ }
196
+ )
197
+ )
198
+ }
199
+
200
+ function parseChannelResponse(html) {
201
+ const parsed =
202
+ new DOMParser()
203
+ .parseFromString(
204
+ html,
205
+ "text/html"
206
+ )
207
+
208
+ const payloadElement =
209
+ parsed.querySelector(
210
+ `${ROOT_SELECTOR} ${PAYLOAD_SELECTOR}`
211
+ ) ||
212
+ parsed.querySelector(
213
+ PAYLOAD_SELECTOR
214
+ )
215
+
216
+ if (!payloadElement) {
217
+ return null
218
+ }
219
+
220
+ try {
221
+ return JSON.parse(
222
+ payloadElement.textContent
223
+ )
224
+ } catch (_error) {
225
+ return null
226
+ }
227
+ }
228
+
229
+ function messageIds(messages) {
230
+ return new Set(
231
+ (messages || [])
232
+ .filter(
233
+ (message) =>
234
+ message?.id != null
235
+ )
236
+ .map(
237
+ (message) =>
238
+ String(message.id)
239
+ )
240
+ )
241
+ }
242
+
243
+ function messageAfter(candidate, marker) {
244
+ if (!candidate || !marker) {
245
+ return Boolean(candidate)
246
+ }
247
+
248
+ const candidateTime =
249
+ new Date(candidate.created_at)
250
+ .getTime()
251
+
252
+ const markerTime =
253
+ new Date(marker.created_at)
254
+ .getTime()
255
+
256
+ return candidateTime > markerTime ||
257
+ (
258
+ candidateTime === markerTime &&
259
+ Number(candidate.id) > Number(marker.id)
260
+ )
261
+ }
262
+
263
+ function orderedMessages(messages) {
264
+ return [...(messages || [])].sort(
265
+ (left, right) => {
266
+ if (messageAfter(left, right)) {
267
+ return 1
268
+ }
269
+
270
+ if (messageAfter(right, left)) {
271
+ return -1
272
+ }
273
+
274
+ return 0
275
+ }
276
+ )
277
+ }
278
+
279
+ function newestMessage(messages) {
280
+ return (messages || []).reduce(
281
+ (newest, candidate) =>
282
+ !newest || messageAfter(candidate, newest)
283
+ ? candidate
284
+ : newest,
285
+ null
286
+ )
287
+ }
288
+
289
+ function channelList(configuration) {
290
+ return Array.isArray(
291
+ configuration?.channels
292
+ )
293
+ ? configuration.channels
294
+ : []
295
+ }
296
+
297
+ function emptyPresence() {
298
+ return {
299
+ visible: false,
300
+ available: true,
301
+ channels: {}
302
+ }
303
+ }
304
+
305
+ function initialSpatialMembers(configuration) {
306
+ const snapshot =
307
+ configuration
308
+ ?.spatial
309
+ ?.room
310
+ ?.snapshot
311
+
312
+ return Array.isArray(snapshot)
313
+ ? snapshot
314
+ : []
315
+ }
316
+
317
+ function atriumConfiguration(configuration) {
318
+ return {
319
+ channel: null,
320
+
321
+ channels:
322
+ channelList(
323
+ configuration
324
+ ),
325
+
326
+ messages: [],
327
+ can_post: false,
328
+ post_url: null,
329
+ presence_url: null,
330
+
331
+ presence:
332
+ configuration?.presence ||
333
+ emptyPresence(),
334
+
335
+ subscription_token: null,
336
+
337
+ spatial:
338
+ configuration?.spatial ||
339
+ null
340
+ }
341
+ }
342
+
343
+ function ChatApp({
344
+ initialConfiguration,
345
+ hostRoot
346
+ }) {
347
+ const React =
348
+ window.React
349
+
350
+ const h =
351
+ React.createElement
352
+
353
+ const {
354
+ useCallback,
355
+ useEffect,
356
+ useRef,
357
+ useState
358
+ } = React
359
+
360
+ const [
361
+ configuration,
362
+ setConfiguration
363
+ ] =
364
+ useState(
365
+ initialConfiguration
366
+ )
367
+
368
+ const [
369
+ messages,
370
+ setMessages
371
+ ] =
372
+ useState(
373
+ initialConfiguration.messages || []
374
+ )
375
+
376
+ const [unreadCounts, setUnreadCounts] =
377
+ useState(
378
+ Object.fromEntries(
379
+ channelList(initialConfiguration).map(
380
+ (candidate) => [candidate.slug, Number(candidate.unread_count) || 0]
381
+ )
382
+ )
383
+ )
384
+
385
+ const [firstUnreadMessageId, setFirstUnreadMessageId] =
386
+ useState(
387
+ initialConfiguration.first_unread_message_id || null
388
+ )
389
+
390
+ const [pinnedMessages, setPinnedMessages] =
391
+ useState(
392
+ initialConfiguration.pinned_messages || []
393
+ )
394
+
395
+ const [
396
+ body,
397
+ setBody
398
+ ] =
399
+ useState("")
400
+
401
+ const [
402
+ sending,
403
+ setSending
404
+ ] =
405
+ useState(false)
406
+
407
+ const [
408
+ switching,
409
+ setSwitching
410
+ ] =
411
+ useState(false)
412
+
413
+ const [
414
+ error,
415
+ setError
416
+ ] =
417
+ useState("")
418
+
419
+ const [
420
+ connectionState,
421
+ setConnectionState
422
+ ] =
423
+ useState(
424
+ initialConfiguration.channel
425
+ ? "Connecting…"
426
+ : "Atrium"
427
+ )
428
+
429
+ const [
430
+ presence,
431
+ setPresence
432
+ ] =
433
+ useState(
434
+ initialConfiguration.presence ||
435
+ emptyPresence()
436
+ )
437
+
438
+ const [
439
+ spatialMembers,
440
+ setSpatialMembers
441
+ ] =
442
+ useState(
443
+ initialSpatialMembers(
444
+ initialConfiguration
445
+ )
446
+ )
447
+
448
+ const [
449
+ reactionPickerOpen,
450
+ setReactionPickerOpen
451
+ ] =
452
+ useState(false)
453
+
454
+ const [
455
+ reactionQuery,
456
+ setReactionQuery
457
+ ] =
458
+ useState("")
459
+
460
+ const [
461
+ reactionTypes,
462
+ setReactionTypes
463
+ ] =
464
+ useState([])
465
+
466
+ const [
467
+ reactionLoading,
468
+ setReactionLoading
469
+ ] =
470
+ useState(false)
471
+
472
+ const [
473
+ reactionError,
474
+ setReactionError
475
+ ] =
476
+ useState("")
477
+
478
+ const messagesRef =
479
+ useRef(null)
480
+
481
+ const displayedMessagesRef =
482
+ useRef(initialConfiguration.messages || [])
483
+
484
+ const acknowledgementTimerRef =
485
+ useRef(null)
486
+
487
+ const pendingAcknowledgementRef =
488
+ useRef(null)
489
+
490
+ const acknowledgementInFlightRef =
491
+ useRef(false)
492
+
493
+ const textareaRef =
494
+ useRef(null)
495
+
496
+ const reactionSearchRef =
497
+ useRef(null)
498
+
499
+ const composerSelectionRef =
500
+ useRef({
501
+ start: 0,
502
+ end: 0
503
+ })
504
+
505
+ const configurationRef =
506
+ useRef(
507
+ initialConfiguration
508
+ )
509
+
510
+ const acceptedMessageIdsRef =
511
+ useRef(
512
+ messageIds(
513
+ initialConfiguration.messages
514
+ )
515
+ )
516
+
517
+ const requestRef =
518
+ useRef(null)
519
+
520
+ const requestGenerationRef =
521
+ useRef(0)
522
+
523
+ const shell =
524
+ modeShellFor(
525
+ hostRoot
526
+ )
527
+
528
+ const activelyReading =
529
+ useCallback(
530
+ () => {
531
+ const mode = shell?.dataset?.railsDenChatActiveMode
532
+
533
+ return document.visibilityState === "visible" &&
534
+ (
535
+ mode === "classic" ||
536
+ (mode === "3d" && deckOpen(shell))
537
+ )
538
+ },
539
+ [shell]
540
+ )
541
+
542
+ const sendPendingAcknowledgement =
543
+ useCallback(
544
+ async () => {
545
+ acknowledgementTimerRef.current = null
546
+ const active = configurationRef.current
547
+ const message = pendingAcknowledgementRef.current
548
+
549
+ if (!activelyReading() || !active?.read_state_url || !message || acknowledgementInFlightRef.current) {
550
+ return
551
+ }
552
+
553
+ pendingAcknowledgementRef.current = null
554
+ acknowledgementInFlightRef.current = true
555
+
556
+ try {
557
+ const response = await fetch(
558
+ active.read_state_url,
559
+ {
560
+ method: "PATCH",
561
+ credentials: "same-origin",
562
+ headers: {
563
+ Accept: "application/json",
564
+ "Content-Type": "application/json",
565
+ "X-CSRF-Token": csrfToken()
566
+ },
567
+ body: JSON.stringify({last_read_message_id: message.id})
568
+ }
569
+ )
570
+
571
+ if (!response.ok) {
572
+ throw new Error("Read acknowledgement failed.")
573
+ }
574
+
575
+ const result = await response.json()
576
+ const slug = configurationRef.current?.channel?.slug
577
+
578
+ if (slug && result.channel_slug === slug) {
579
+ setUnreadCounts(
580
+ (current) => ({
581
+ ...current,
582
+ [slug]: Number(result.unread_count) || 0
583
+ })
584
+ )
585
+ }
586
+ } catch (_error) {
587
+ pendingAcknowledgementRef.current = message
588
+ } finally {
589
+ acknowledgementInFlightRef.current = false
590
+
591
+ if (pendingAcknowledgementRef.current && activelyReading()) {
592
+ acknowledgementTimerRef.current = window.setTimeout(
593
+ sendPendingAcknowledgement,
594
+ 1000
595
+ )
596
+ }
597
+ }
598
+ },
599
+ [activelyReading]
600
+ )
601
+
602
+ const scheduleAcknowledgement =
603
+ useCallback(
604
+ (message) => {
605
+ if (!message?.id || !activelyReading()) {
606
+ return
607
+ }
608
+
609
+ const pending =
610
+ pendingAcknowledgementRef.current
611
+
612
+ if (
613
+ !pending ||
614
+ messageAfter(message, pending)
615
+ ) {
616
+ pendingAcknowledgementRef.current = message
617
+ }
618
+
619
+ if (!acknowledgementTimerRef.current) {
620
+ acknowledgementTimerRef.current = window.setTimeout(
621
+ sendPendingAcknowledgement,
622
+ 1000
623
+ )
624
+ }
625
+ },
626
+ [activelyReading, sendPendingAcknowledgement]
627
+ )
628
+
629
+ const acknowledgeNewestDisplayed =
630
+ useCallback(
631
+ () => {
632
+ const displayed = displayedMessagesRef.current
633
+ scheduleAcknowledgement(
634
+ newestMessage(displayed)
635
+ )
636
+ },
637
+ [scheduleAcknowledgement]
638
+ )
639
+
640
+ const focusComposer =
641
+ useCallback(
642
+ () => {
643
+ textareaRef.current
644
+ ?.focus()
645
+ },
646
+ []
647
+ )
648
+
649
+ const rememberComposerSelection =
650
+ useCallback(
651
+ () => {
652
+ const textarea =
653
+ textareaRef.current
654
+
655
+ if (!textarea) {
656
+ return
657
+ }
658
+
659
+ composerSelectionRef.current = {
660
+ start:
661
+ textarea.selectionStart ??
662
+ textarea.value.length,
663
+ end:
664
+ textarea.selectionEnd ??
665
+ textarea.value.length
666
+ }
667
+ },
668
+ []
669
+ )
670
+
671
+ const insertReactionEmoji =
672
+ useCallback(
673
+ (emoji) => {
674
+ const textarea =
675
+ textareaRef.current
676
+
677
+ if (
678
+ !textarea ||
679
+ !emoji
680
+ ) {
681
+ return
682
+ }
683
+
684
+ const remembered =
685
+ composerSelectionRef.current
686
+
687
+ const start =
688
+ Math.min(
689
+ remembered.start,
690
+ body.length
691
+ )
692
+
693
+ const finish =
694
+ Math.min(
695
+ Math.max(
696
+ remembered.end,
697
+ start
698
+ ),
699
+ body.length
700
+ )
701
+
702
+ const nextBody =
703
+ `${body.slice(0, start)}${emoji}${body.slice(finish)}`
704
+
705
+ const nextCaret =
706
+ start + emoji.length
707
+
708
+ composerSelectionRef.current = {
709
+ start: nextCaret,
710
+ end: nextCaret
711
+ }
712
+
713
+ setBody(
714
+ nextBody
715
+ )
716
+
717
+ window.requestAnimationFrame(
718
+ () => {
719
+ const current =
720
+ textareaRef.current
721
+
722
+ if (!current) {
723
+ return
724
+ }
725
+
726
+ current.focus()
727
+ current.setSelectionRange(
728
+ nextCaret,
729
+ nextCaret
730
+ )
731
+ }
732
+ )
733
+ },
734
+ [body]
735
+ )
736
+
737
+ const focusComposerAfterPointerRelease =
738
+ useCallback(
739
+ () => {
740
+ if (
741
+ !document.pointerLockElement
742
+ ) {
743
+ window.requestAnimationFrame(
744
+ focusComposer
745
+ )
746
+
747
+ return
748
+ }
749
+
750
+ let finished =
751
+ false
752
+
753
+ const finish =
754
+ () => {
755
+ if (finished) {
756
+ return
757
+ }
758
+
759
+ finished =
760
+ true
761
+
762
+ document.removeEventListener(
763
+ "pointerlockchange",
764
+ handlePointerLockChange
765
+ )
766
+
767
+ window.requestAnimationFrame(
768
+ focusComposer
769
+ )
770
+ }
771
+
772
+ const handlePointerLockChange =
773
+ () => {
774
+ if (
775
+ !document.pointerLockElement
776
+ ) {
777
+ finish()
778
+ }
779
+ }
780
+
781
+ document.addEventListener(
782
+ "pointerlockchange",
783
+ handlePointerLockChange
784
+ )
785
+
786
+ window.setTimeout(
787
+ finish,
788
+ 250
789
+ )
790
+ },
791
+ [focusComposer]
792
+ )
793
+
794
+ const openDeck =
795
+ useCallback(
796
+ () => {
797
+ if (
798
+ !shell ||
799
+ !threeDimensionalMode(
800
+ shell
801
+ )
802
+ ) {
803
+ return false
804
+ }
805
+
806
+ dispatchDeckState(
807
+ shell,
808
+ true,
809
+ false
810
+ )
811
+
812
+ focusComposerAfterPointerRelease()
813
+
814
+ return true
815
+ },
816
+ [
817
+ focusComposerAfterPointerRelease,
818
+ shell
819
+ ]
820
+ )
821
+
822
+ const closeDeck =
823
+ useCallback(
824
+ (
825
+ restorePointerLock = true
826
+ ) => {
827
+ if (!shell) {
828
+ return false
829
+ }
830
+
831
+ textareaRef.current
832
+ ?.blur()
833
+
834
+ setReactionPickerOpen(
835
+ false
836
+ )
837
+
838
+ setReactionQuery("")
839
+ setReactionError("")
840
+
841
+ dispatchDeckState(
842
+ shell,
843
+ false,
844
+ restorePointerLock
845
+ )
846
+
847
+ return true
848
+ },
849
+ [shell]
850
+ )
851
+
852
+ const acceptMessage =
853
+ useCallback(
854
+ (message) => {
855
+ if (
856
+ !message ||
857
+ message.id == null
858
+ ) {
859
+ return false
860
+ }
861
+
862
+ const key =
863
+ String(
864
+ message.id
865
+ )
866
+
867
+ if (
868
+ acceptedMessageIdsRef
869
+ .current
870
+ .has(key)
871
+ ) {
872
+ return false
873
+ }
874
+
875
+ acceptedMessageIdsRef
876
+ .current
877
+ .add(key)
878
+
879
+ setMessages(
880
+ (current) => {
881
+ const next =
882
+ orderedMessages(
883
+ [...current, message]
884
+ )
885
+
886
+ displayedMessagesRef.current = next
887
+ return next
888
+ }
889
+ )
890
+
891
+ const slug = configurationRef.current?.channel?.slug
892
+
893
+ if (activelyReading()) {
894
+ scheduleAcknowledgement(message)
895
+ } else if (slug) {
896
+ setUnreadCounts(
897
+ (current) => ({
898
+ ...current,
899
+ [slug]: (current[slug] || 0) + 1
900
+ })
901
+ )
902
+ setFirstUnreadMessageId(
903
+ (current) => {
904
+ if (!current) {
905
+ return message.id
906
+ }
907
+
908
+ const boundary =
909
+ displayedMessagesRef.current.find(
910
+ (candidate) =>
911
+ String(candidate.id) === String(current)
912
+ )
913
+
914
+ return boundary && messageAfter(boundary, message)
915
+ ? message.id
916
+ : current
917
+ }
918
+ )
919
+ }
920
+
921
+ dispatchAcceptedMessage(
922
+ configurationRef
923
+ .current
924
+ ?.channel
925
+ ?.slug,
926
+ message
927
+ )
928
+
929
+ return true
930
+ },
931
+ [activelyReading, scheduleAcknowledgement]
932
+ )
933
+
934
+ const applyPinnedMessage =
935
+ useCallback(
936
+ (message, pinned) => {
937
+ if (!message?.id) {
938
+ return
939
+ }
940
+
941
+ const canonical = {...message, pinned}
942
+
943
+ setMessages(
944
+ (current) => {
945
+ const next = current.map(
946
+ (candidate) =>
947
+ String(candidate.id) === String(message.id)
948
+ ? canonical
949
+ : candidate
950
+ )
951
+ displayedMessagesRef.current = next
952
+ return next
953
+ }
954
+ )
955
+
956
+ setPinnedMessages(
957
+ (current) => {
958
+ const without = current.filter(
959
+ (candidate) => String(candidate.id) !== String(message.id)
960
+ )
961
+
962
+ if (!pinned) {
963
+ return without
964
+ }
965
+
966
+ return orderedMessages(
967
+ [...without, canonical]
968
+ )
969
+ }
970
+ )
971
+ },
972
+ []
973
+ )
974
+
975
+ const refreshPresence =
976
+ useCallback(
977
+ async () => {
978
+ const active =
979
+ configurationRef.current
980
+
981
+ if (
982
+ !active?.presence_url
983
+ ) {
984
+ return
985
+ }
986
+
987
+ try {
988
+ const response =
989
+ await fetch(
990
+ active.presence_url,
991
+ {
992
+ method: "GET",
993
+ credentials:
994
+ "same-origin",
995
+ headers: {
996
+ Accept:
997
+ "application/json"
998
+ }
999
+ }
1000
+ )
1001
+
1002
+ if (!response.ok) {
1003
+ return
1004
+ }
1005
+
1006
+ const contentType =
1007
+ response.headers.get(
1008
+ "content-type"
1009
+ ) || ""
1010
+
1011
+ if (
1012
+ !contentType.includes(
1013
+ "application/json"
1014
+ )
1015
+ ) {
1016
+ return
1017
+ }
1018
+
1019
+ setPresence(
1020
+ await response.json()
1021
+ )
1022
+ } catch (_error) {
1023
+ return
1024
+ }
1025
+ },
1026
+ []
1027
+ )
1028
+
1029
+ const installConfiguration =
1030
+ useCallback(
1031
+ (next) => {
1032
+ configurationRef.current =
1033
+ next
1034
+
1035
+ acceptedMessageIdsRef.current =
1036
+ messageIds(
1037
+ next.messages
1038
+ )
1039
+
1040
+ setConfiguration(
1041
+ next
1042
+ )
1043
+
1044
+ setMessages(
1045
+ next.messages || []
1046
+ )
1047
+ displayedMessagesRef.current = next.messages || []
1048
+
1049
+ setUnreadCounts(
1050
+ Object.fromEntries(
1051
+ channelList(next).map(
1052
+ (candidate) => [candidate.slug, Number(candidate.unread_count) || 0]
1053
+ )
1054
+ )
1055
+ )
1056
+ setFirstUnreadMessageId(
1057
+ next.first_unread_message_id || null
1058
+ )
1059
+ setPinnedMessages(
1060
+ next.pinned_messages || []
1061
+ )
1062
+
1063
+ setPresence(
1064
+ next.presence ||
1065
+ emptyPresence()
1066
+ )
1067
+
1068
+ setSpatialMembers(
1069
+ initialSpatialMembers(
1070
+ next
1071
+ )
1072
+ )
1073
+
1074
+ setConnectionState(
1075
+ next.channel
1076
+ ? "Connecting…"
1077
+ : "Atrium"
1078
+ )
1079
+
1080
+ setBody("")
1081
+ setError("")
1082
+ setReactionPickerOpen(
1083
+ false
1084
+ )
1085
+ setReactionQuery("")
1086
+ setReactionTypes([])
1087
+ setReactionError("")
1088
+ composerSelectionRef.current = {
1089
+ start: 0,
1090
+ end: 0
1091
+ }
1092
+ },
1093
+ []
1094
+ )
1095
+
1096
+ const loadChannel =
1097
+ useCallback(
1098
+ async (
1099
+ candidate,
1100
+ {
1101
+ transitionWorld = false
1102
+ } = {}
1103
+ ) => {
1104
+ if (
1105
+ !candidate?.slug ||
1106
+ !candidate?.url
1107
+ ) {
1108
+ return false
1109
+ }
1110
+
1111
+ const current =
1112
+ configurationRef.current
1113
+
1114
+ if (
1115
+ current?.channel?.slug ===
1116
+ candidate.slug
1117
+ ) {
1118
+ if (transitionWorld) {
1119
+ document.dispatchEvent(
1120
+ new CustomEvent(
1121
+ CHAT_CHANNEL_CHANGE_EVENT,
1122
+ {
1123
+ detail: {
1124
+ shell,
1125
+ slug:
1126
+ candidate.slug
1127
+ }
1128
+ }
1129
+ )
1130
+ )
1131
+ }
1132
+
1133
+ focusComposer()
1134
+
1135
+ return true
1136
+ }
1137
+
1138
+ requestRef.current
1139
+ ?.abort()
1140
+
1141
+ const controller =
1142
+ new AbortController()
1143
+
1144
+ requestRef.current =
1145
+ controller
1146
+
1147
+ const generation =
1148
+ ++requestGenerationRef
1149
+ .current
1150
+
1151
+ setSwitching(true)
1152
+ setError("")
1153
+
1154
+ try {
1155
+ const response =
1156
+ await fetch(
1157
+ candidate.url,
1158
+ {
1159
+ method: "GET",
1160
+ credentials:
1161
+ "same-origin",
1162
+ headers: {
1163
+ Accept:
1164
+ "text/html"
1165
+ },
1166
+ signal:
1167
+ controller.signal
1168
+ }
1169
+ )
1170
+
1171
+ if (!response.ok) {
1172
+ throw new Error(
1173
+ "Channel request failed."
1174
+ )
1175
+ }
1176
+
1177
+ const next =
1178
+ parseChannelResponse(
1179
+ await response.text()
1180
+ )
1181
+
1182
+ if (
1183
+ generation !==
1184
+ requestGenerationRef.current ||
1185
+ !next?.channel?.slug ||
1186
+ next.channel.slug !==
1187
+ candidate.slug
1188
+ ) {
1189
+ if (
1190
+ generation ===
1191
+ requestGenerationRef.current
1192
+ ) {
1193
+ throw new Error(
1194
+ "Channel response was not usable."
1195
+ )
1196
+ }
1197
+
1198
+ return false
1199
+ }
1200
+
1201
+ installConfiguration(
1202
+ next
1203
+ )
1204
+
1205
+ if (transitionWorld) {
1206
+ document.dispatchEvent(
1207
+ new CustomEvent(
1208
+ CHAT_CHANNEL_CHANGE_EVENT,
1209
+ {
1210
+ detail: {
1211
+ shell,
1212
+ slug:
1213
+ candidate.slug
1214
+ }
1215
+ }
1216
+ )
1217
+ )
1218
+ }
1219
+
1220
+ window.requestAnimationFrame(
1221
+ focusComposer
1222
+ )
1223
+
1224
+ return true
1225
+ } catch (requestError) {
1226
+ if (
1227
+ requestError?.name ===
1228
+ "AbortError"
1229
+ ) {
1230
+ return false
1231
+ }
1232
+
1233
+ setError(
1234
+ "Unable to open that chat room."
1235
+ )
1236
+
1237
+ return false
1238
+ } finally {
1239
+ if (
1240
+ generation ===
1241
+ requestGenerationRef.current
1242
+ ) {
1243
+ setSwitching(false)
1244
+ }
1245
+ }
1246
+ },
1247
+ [
1248
+ focusComposer,
1249
+ installConfiguration,
1250
+ shell
1251
+ ]
1252
+ )
1253
+
1254
+ const returnToAtrium =
1255
+ useCallback(
1256
+ () => {
1257
+ requestRef.current
1258
+ ?.abort()
1259
+
1260
+ requestGenerationRef.current +=
1261
+ 1
1262
+
1263
+ installConfiguration(
1264
+ atriumConfiguration(
1265
+ configurationRef.current
1266
+ )
1267
+ )
1268
+
1269
+ setSpatialMembers([])
1270
+ setSwitching(false)
1271
+ },
1272
+ [installConfiguration]
1273
+ )
1274
+
1275
+ useEffect(
1276
+ () => {
1277
+ configurationRef.current =
1278
+ configuration
1279
+ },
1280
+ [configuration]
1281
+ )
1282
+
1283
+ useEffect(
1284
+ () => {
1285
+ if (
1286
+ !reactionPickerOpen
1287
+ ) {
1288
+ return
1289
+ }
1290
+
1291
+ const url =
1292
+ reactionTypesUrl()
1293
+
1294
+ if (!url) {
1295
+ setReactionTypes([])
1296
+ setReactionError(
1297
+ "Reactions are unavailable."
1298
+ )
1299
+
1300
+ return
1301
+ }
1302
+
1303
+ const controller =
1304
+ new AbortController()
1305
+
1306
+ const timer =
1307
+ window.setTimeout(
1308
+ async () => {
1309
+ setReactionLoading(true)
1310
+ setReactionError("")
1311
+
1312
+ try {
1313
+ const requestUrl =
1314
+ new URL(
1315
+ url,
1316
+ window.location.href
1317
+ )
1318
+
1319
+ if (
1320
+ reactionQuery.trim()
1321
+ ) {
1322
+ requestUrl.searchParams.set(
1323
+ "q",
1324
+ reactionQuery
1325
+ )
1326
+ }
1327
+
1328
+ const response =
1329
+ await fetch(
1330
+ requestUrl.toString(),
1331
+ {
1332
+ method: "GET",
1333
+ credentials:
1334
+ "same-origin",
1335
+ headers: {
1336
+ Accept:
1337
+ "application/json"
1338
+ },
1339
+ signal:
1340
+ controller.signal
1341
+ }
1342
+ )
1343
+
1344
+ if (!response.ok) {
1345
+ throw new Error(
1346
+ "Reaction search failed."
1347
+ )
1348
+ }
1349
+
1350
+ const payload =
1351
+ await response.json()
1352
+
1353
+ setReactionTypes(
1354
+ Array.isArray(
1355
+ payload.reaction_types
1356
+ )
1357
+ ? payload.reaction_types
1358
+ : []
1359
+ )
1360
+ } catch (requestError) {
1361
+ if (
1362
+ requestError?.name ===
1363
+ "AbortError"
1364
+ ) {
1365
+ return
1366
+ }
1367
+
1368
+ setReactionTypes([])
1369
+ setReactionError(
1370
+ "Could not load reactions."
1371
+ )
1372
+ } finally {
1373
+ if (
1374
+ !controller.signal.aborted
1375
+ ) {
1376
+ setReactionLoading(false)
1377
+ }
1378
+ }
1379
+ },
1380
+ REACTION_SEARCH_DEBOUNCE
1381
+ )
1382
+
1383
+ return () => {
1384
+ window.clearTimeout(
1385
+ timer
1386
+ )
1387
+
1388
+ controller.abort()
1389
+ }
1390
+ },
1391
+ [
1392
+ reactionPickerOpen,
1393
+ reactionQuery
1394
+ ]
1395
+ )
1396
+
1397
+ useEffect(
1398
+ () => {
1399
+ if (
1400
+ reactionPickerOpen
1401
+ ) {
1402
+ window.requestAnimationFrame(
1403
+ () => {
1404
+ reactionSearchRef.current
1405
+ ?.focus()
1406
+ }
1407
+ )
1408
+ }
1409
+ },
1410
+ [reactionPickerOpen]
1411
+ )
1412
+
1413
+ useEffect(
1414
+ () => {
1415
+ function handleReadingOpportunity(event) {
1416
+ if (event?.detail?.shell && event.detail.shell !== shell) {
1417
+ return
1418
+ }
1419
+
1420
+ if (activelyReading()) {
1421
+ acknowledgeNewestDisplayed()
1422
+ }
1423
+ }
1424
+
1425
+ function handleModeClick(event) {
1426
+ if (event.target.closest('[data-rails-den-chat-mode="classic"]')) {
1427
+ window.requestAnimationFrame(handleReadingOpportunity)
1428
+ }
1429
+ }
1430
+
1431
+ document.addEventListener("visibilitychange", handleReadingOpportunity)
1432
+ document.addEventListener(CHAT_DECK_STATE_EVENT, handleReadingOpportunity)
1433
+ document.addEventListener("click", handleModeClick)
1434
+ handleReadingOpportunity()
1435
+
1436
+ return () => {
1437
+ document.removeEventListener("visibilitychange", handleReadingOpportunity)
1438
+ document.removeEventListener(CHAT_DECK_STATE_EVENT, handleReadingOpportunity)
1439
+ document.removeEventListener("click", handleModeClick)
1440
+ }
1441
+ },
1442
+ [acknowledgeNewestDisplayed, activelyReading, configuration.channel?.slug, shell]
1443
+ )
1444
+
1445
+ useEffect(
1446
+ () => {
1447
+ const container =
1448
+ messagesRef.current
1449
+
1450
+ if (!container) {
1451
+ return
1452
+ }
1453
+
1454
+ container.scrollTop =
1455
+ container.scrollHeight
1456
+ },
1457
+ [
1458
+ messages.length,
1459
+ configuration.channel?.slug
1460
+ ]
1461
+ )
1462
+
1463
+ useEffect(
1464
+ () => {
1465
+ const slug =
1466
+ configuration.channel?.slug
1467
+
1468
+ const token =
1469
+ configuration.subscription_token
1470
+
1471
+ if (
1472
+ !slug ||
1473
+ !token
1474
+ ) {
1475
+ setConnectionState(
1476
+ configuration.channel
1477
+ ? "Offline"
1478
+ : "Atrium"
1479
+ )
1480
+
1481
+ return
1482
+ }
1483
+
1484
+ const cable =
1485
+ window.RailsDenCable &&
1486
+ window.RailsDenCable.consumer()
1487
+
1488
+ if (!cable) {
1489
+ setConnectionState(
1490
+ "Offline"
1491
+ )
1492
+
1493
+ return
1494
+ }
1495
+
1496
+ setConnectionState(
1497
+ "Connecting…"
1498
+ )
1499
+
1500
+ let disposed =
1501
+ false
1502
+
1503
+ const subscription =
1504
+ cable.subscriptions.create(
1505
+ {
1506
+ channel:
1507
+ "RailsDen::ChatStreamChannel",
1508
+ token,
1509
+ slug,
1510
+ session_id:
1511
+ presenceSessionId()
1512
+ },
1513
+ {
1514
+ connected() {
1515
+ if (disposed) {
1516
+ return
1517
+ }
1518
+
1519
+ setConnectionState(
1520
+ "Live"
1521
+ )
1522
+
1523
+ refreshPresence()
1524
+ },
1525
+
1526
+ disconnected() {
1527
+ if (disposed) {
1528
+ return
1529
+ }
1530
+
1531
+ setConnectionState(
1532
+ "Reconnecting…"
1533
+ )
1534
+ },
1535
+
1536
+ rejected() {
1537
+ if (disposed) {
1538
+ return
1539
+ }
1540
+
1541
+ setConnectionState(
1542
+ "Unavailable"
1543
+ )
1544
+ },
1545
+
1546
+ received(data) {
1547
+ if (
1548
+ disposed ||
1549
+ !data ||
1550
+ configurationRef
1551
+ .current
1552
+ ?.channel
1553
+ ?.slug !== slug
1554
+ ) {
1555
+ return
1556
+ }
1557
+
1558
+ if (
1559
+ data.type ===
1560
+ "chat_message_created"
1561
+ ) {
1562
+ acceptMessage(
1563
+ data.message
1564
+ )
1565
+
1566
+ return
1567
+ }
1568
+
1569
+ if (
1570
+ data.type ===
1571
+ "presence_changed"
1572
+ ) {
1573
+ refreshPresence()
1574
+
1575
+ return
1576
+ }
1577
+
1578
+ if (
1579
+ data.type === "chat_message_pinned" ||
1580
+ data.type === "chat_message_unpinned"
1581
+ ) {
1582
+ applyPinnedMessage(
1583
+ data.message,
1584
+ data.type === "chat_message_pinned"
1585
+ )
1586
+
1587
+ return
1588
+ }
1589
+
1590
+ if (
1591
+ data.type ===
1592
+ "chat_membership_removed"
1593
+ ) {
1594
+ window.location.reload()
1595
+ }
1596
+ }
1597
+ }
1598
+ )
1599
+
1600
+ return () => {
1601
+ disposed =
1602
+ true
1603
+
1604
+ subscription.unsubscribe()
1605
+ }
1606
+ },
1607
+ [
1608
+ acceptMessage,
1609
+ applyPinnedMessage,
1610
+ configuration.channel?.slug,
1611
+ configuration.subscription_token,
1612
+ refreshPresence
1613
+ ]
1614
+ )
1615
+
1616
+ useEffect(
1617
+ () => {
1618
+ function handleSpatialRoom(
1619
+ event
1620
+ ) {
1621
+ const activeSlug =
1622
+ configurationRef
1623
+ .current
1624
+ ?.channel
1625
+ ?.slug
1626
+
1627
+ if (
1628
+ !activeSlug ||
1629
+ event.detail?.slug !==
1630
+ activeSlug ||
1631
+ !Array.isArray(
1632
+ event.detail.members
1633
+ )
1634
+ ) {
1635
+ return
1636
+ }
1637
+
1638
+ setSpatialMembers(
1639
+ event.detail.members
1640
+ )
1641
+ }
1642
+
1643
+ window.addEventListener(
1644
+ SPATIAL_ROOM_EVENT,
1645
+ handleSpatialRoom
1646
+ )
1647
+
1648
+ return () => {
1649
+ window.removeEventListener(
1650
+ SPATIAL_ROOM_EVENT,
1651
+ handleSpatialRoom
1652
+ )
1653
+ }
1654
+ },
1655
+ []
1656
+ )
1657
+
1658
+ useEffect(
1659
+ () => {
1660
+ function handleSpatialRoomChanged(
1661
+ event
1662
+ ) {
1663
+ if (
1664
+ event.detail?.shell &&
1665
+ event.detail.shell !==
1666
+ shell
1667
+ ) {
1668
+ return
1669
+ }
1670
+
1671
+ if (
1672
+ !threeDimensionalMode(
1673
+ shell
1674
+ )
1675
+ ) {
1676
+ return
1677
+ }
1678
+
1679
+ const kind =
1680
+ event.detail?.kind
1681
+
1682
+ const slug =
1683
+ event.detail?.slug
1684
+
1685
+ if (
1686
+ kind === "atrium" ||
1687
+ slug === "atrium"
1688
+ ) {
1689
+ returnToAtrium()
1690
+
1691
+ return
1692
+ }
1693
+
1694
+ if (
1695
+ kind !== "channel" ||
1696
+ !slug
1697
+ ) {
1698
+ return
1699
+ }
1700
+
1701
+ if (
1702
+ configurationRef
1703
+ .current
1704
+ ?.channel
1705
+ ?.slug === slug
1706
+ ) {
1707
+ return
1708
+ }
1709
+
1710
+ const candidate =
1711
+ channelList(
1712
+ configurationRef.current
1713
+ ).find(
1714
+ (channel) =>
1715
+ channel.slug ===
1716
+ slug
1717
+ )
1718
+
1719
+ if (!candidate) {
1720
+ setError(
1721
+ "This spatial room is not available in Chat."
1722
+ )
1723
+
1724
+ return
1725
+ }
1726
+
1727
+ loadChannel(
1728
+ candidate,
1729
+ {
1730
+ transitionWorld:
1731
+ false
1732
+ }
1733
+ )
1734
+ }
1735
+
1736
+ document.addEventListener(
1737
+ SPATIAL_ROOM_CHANGED_EVENT,
1738
+ handleSpatialRoomChanged
1739
+ )
1740
+
1741
+ return () => {
1742
+ document.removeEventListener(
1743
+ SPATIAL_ROOM_CHANGED_EVENT,
1744
+ handleSpatialRoomChanged
1745
+ )
1746
+ }
1747
+ },
1748
+ [
1749
+ loadChannel,
1750
+ returnToAtrium,
1751
+ shell
1752
+ ]
1753
+ )
1754
+
1755
+ useEffect(
1756
+ () => {
1757
+ if (!shell) {
1758
+ return
1759
+ }
1760
+
1761
+ shell.dataset
1762
+ .railsDenChatDeckOpen =
1763
+ "false"
1764
+
1765
+ const launcher =
1766
+ document.createElement(
1767
+ "button"
1768
+ )
1769
+
1770
+ launcher.type =
1771
+ "button"
1772
+
1773
+ launcher.className =
1774
+ "rails-den-chat-launcher"
1775
+
1776
+ launcher.textContent =
1777
+ "Enter · Chat"
1778
+
1779
+ launcher.setAttribute(
1780
+ "aria-label",
1781
+ "Open chat"
1782
+ )
1783
+
1784
+ launcher.addEventListener(
1785
+ "click",
1786
+ openDeck
1787
+ )
1788
+
1789
+ shell.append(
1790
+ launcher
1791
+ )
1792
+
1793
+ function handleDocumentKeyDown(
1794
+ event
1795
+ ) {
1796
+ if (
1797
+ !threeDimensionalMode(
1798
+ shell
1799
+ ) ||
1800
+ event.repeat
1801
+ ) {
1802
+ return
1803
+ }
1804
+
1805
+ if (
1806
+ event.key === "Escape" &&
1807
+ deckOpen(shell)
1808
+ ) {
1809
+ event.preventDefault()
1810
+ event.stopPropagation()
1811
+
1812
+ closeDeck(
1813
+ true
1814
+ )
1815
+
1816
+ return
1817
+ }
1818
+
1819
+ if (
1820
+ event.key !== "Enter" ||
1821
+ deckOpen(shell) ||
1822
+ editableTarget(
1823
+ event.target
1824
+ )
1825
+ ) {
1826
+ return
1827
+ }
1828
+
1829
+ event.preventDefault()
1830
+ event.stopPropagation()
1831
+
1832
+ openDeck()
1833
+ }
1834
+
1835
+ document.addEventListener(
1836
+ "keydown",
1837
+ handleDocumentKeyDown,
1838
+ true
1839
+ )
1840
+
1841
+ return () => {
1842
+ launcher.removeEventListener(
1843
+ "click",
1844
+ openDeck
1845
+ )
1846
+
1847
+ launcher.remove()
1848
+
1849
+ document.removeEventListener(
1850
+ "keydown",
1851
+ handleDocumentKeyDown,
1852
+ true
1853
+ )
1854
+ }
1855
+ },
1856
+ [
1857
+ closeDeck,
1858
+ openDeck,
1859
+ shell
1860
+ ]
1861
+ )
1862
+
1863
+ useEffect(
1864
+ () => {
1865
+ return () => {
1866
+ requestRef.current
1867
+ ?.abort()
1868
+
1869
+ if (acknowledgementTimerRef.current) {
1870
+ window.clearTimeout(
1871
+ acknowledgementTimerRef.current
1872
+ )
1873
+ }
1874
+ }
1875
+ },
1876
+ []
1877
+ )
1878
+
1879
+ async function changePin(message, pinned) {
1880
+ const template = configurationRef.current?.message_pin_url_template
1881
+
1882
+ if (!template || !message?.id || !configurationRef.current?.can_manage_channel) {
1883
+ return
1884
+ }
1885
+
1886
+ setError("")
1887
+
1888
+ try {
1889
+ const response = await fetch(
1890
+ template.replace("__MESSAGE_ID__", encodeURIComponent(message.id)),
1891
+ {
1892
+ method: pinned ? "POST" : "DELETE",
1893
+ credentials: "same-origin",
1894
+ headers: {
1895
+ Accept: "application/json",
1896
+ "Content-Type": "application/json",
1897
+ "X-CSRF-Token": csrfToken()
1898
+ }
1899
+ }
1900
+ )
1901
+
1902
+ if (!response.ok) {
1903
+ throw new Error("Pin request failed.")
1904
+ }
1905
+
1906
+ const result = await response.json()
1907
+ applyPinnedMessage(result.message, Boolean(result.message?.pinned))
1908
+ } catch (_error) {
1909
+ setError("The pinned-message state could not be changed. Please try again.")
1910
+ }
1911
+ }
1912
+
1913
+ async function sendMessage() {
1914
+ const value =
1915
+ body.trim()
1916
+
1917
+ const active =
1918
+ configurationRef.current
1919
+
1920
+ if (
1921
+ !active?.channel?.slug ||
1922
+ !active.post_url ||
1923
+ !value ||
1924
+ sending ||
1925
+ switching ||
1926
+ !active.can_post
1927
+ ) {
1928
+ return
1929
+ }
1930
+
1931
+ const postSlug =
1932
+ active.channel.slug
1933
+
1934
+ setSending(true)
1935
+ setError("")
1936
+
1937
+ const token =
1938
+ csrfToken()
1939
+
1940
+ const headers = {
1941
+ Accept:
1942
+ "application/json",
1943
+ "Content-Type":
1944
+ "application/x-www-form-urlencoded;charset=UTF-8"
1945
+ }
1946
+
1947
+ if (token) {
1948
+ headers["X-CSRF-Token"] =
1949
+ token
1950
+ }
1951
+
1952
+ try {
1953
+ const response =
1954
+ await fetch(
1955
+ active.post_url,
1956
+ {
1957
+ method: "POST",
1958
+ credentials:
1959
+ "same-origin",
1960
+ headers,
1961
+ body:
1962
+ new URLSearchParams({
1963
+ "chat_message[body]":
1964
+ value
1965
+ })
1966
+ }
1967
+ )
1968
+
1969
+ const payload =
1970
+ await response.json()
1971
+
1972
+ if (!response.ok) {
1973
+ const errors =
1974
+ Array.isArray(
1975
+ payload.errors
1976
+ )
1977
+ ? payload.errors
1978
+ : []
1979
+
1980
+ setError(
1981
+ errors.join(", ") ||
1982
+ "Unable to send message."
1983
+ )
1984
+
1985
+ return
1986
+ }
1987
+
1988
+ if (
1989
+ configurationRef
1990
+ .current
1991
+ ?.channel
1992
+ ?.slug !== postSlug
1993
+ ) {
1994
+ return
1995
+ }
1996
+
1997
+ acceptMessage(
1998
+ payload.message
1999
+ )
2000
+
2001
+ setBody("")
2002
+ setReactionPickerOpen(
2003
+ false
2004
+ )
2005
+ setReactionQuery("")
2006
+ setReactionError("")
2007
+ composerSelectionRef.current = {
2008
+ start: 0,
2009
+ end: 0
2010
+ }
2011
+
2012
+ window.requestAnimationFrame(
2013
+ focusComposer
2014
+ )
2015
+ } catch (_error) {
2016
+ setError(
2017
+ "Unable to send message."
2018
+ )
2019
+ } finally {
2020
+ setSending(false)
2021
+ }
2022
+ }
2023
+
2024
+ function handleSubmit(event) {
2025
+ event.preventDefault()
2026
+
2027
+ sendMessage()
2028
+ }
2029
+
2030
+ function handleComposerKeyDown(
2031
+ event
2032
+ ) {
2033
+ if (
2034
+ event.key === "Enter" &&
2035
+ !event.shiftKey
2036
+ ) {
2037
+ event.preventDefault()
2038
+
2039
+ sendMessage()
2040
+ }
2041
+ }
2042
+
2043
+ function handleChannelSelection(
2044
+ event
2045
+ ) {
2046
+ const slug =
2047
+ event.target.value
2048
+
2049
+ if (!slug) {
2050
+ return
2051
+ }
2052
+
2053
+ const candidate =
2054
+ channelList(
2055
+ configurationRef.current
2056
+ ).find(
2057
+ (channel) =>
2058
+ channel.slug === slug
2059
+ )
2060
+
2061
+ if (!candidate) {
2062
+ return
2063
+ }
2064
+
2065
+ loadChannel(
2066
+ candidate,
2067
+ {
2068
+ transitionWorld:
2069
+ true
2070
+ }
2071
+ )
2072
+ }
2073
+
2074
+ function handleClassicChannelClick(
2075
+ event,
2076
+ candidate
2077
+ ) {
2078
+ if (
2079
+ !threeDimensionalMode(
2080
+ shell
2081
+ )
2082
+ ) {
2083
+ return
2084
+ }
2085
+
2086
+ event.preventDefault()
2087
+
2088
+ loadChannel(
2089
+ candidate,
2090
+ {
2091
+ transitionWorld:
2092
+ true
2093
+ }
2094
+ )
2095
+ }
2096
+
2097
+ const channel =
2098
+ configuration.channel
2099
+
2100
+ const channels =
2101
+ channelList(
2102
+ configuration
2103
+ )
2104
+
2105
+ const roomPresence =
2106
+ channel
2107
+ ? presence.channels?.[
2108
+ channel.slug
2109
+ ] || {
2110
+ count: 0,
2111
+ members: []
2112
+ }
2113
+ : {
2114
+ count: 0,
2115
+ members: []
2116
+ }
2117
+
2118
+ const spatialMemberNames =
2119
+ spatialMembers
2120
+ .map(
2121
+ (member) =>
2122
+ member?.display_name
2123
+ ?.toString()
2124
+ .trim()
2125
+ )
2126
+ .filter(Boolean)
2127
+
2128
+ return h(
2129
+ "div",
2130
+ {
2131
+ className:
2132
+ "rails-den-chat-workspace"
2133
+ },
2134
+
2135
+ h(
2136
+ "aside",
2137
+ {
2138
+ className:
2139
+ "rails-den-chat-sidebar"
2140
+ },
2141
+
2142
+ h(
2143
+ "div",
2144
+ {
2145
+ className:
2146
+ "rails-den-chat-sidebar-title"
2147
+ },
2148
+ "Channels"
2149
+ ),
2150
+
2151
+ h(
2152
+ "nav",
2153
+ {
2154
+ className:
2155
+ "rails-den-chat-channel-list",
2156
+ "aria-label":
2157
+ "Chat channels"
2158
+ },
2159
+
2160
+ ...channels.map(
2161
+ (candidate) => {
2162
+ const candidatePresence =
2163
+ presence.channels?.[
2164
+ candidate.slug
2165
+ ]
2166
+
2167
+ return h(
2168
+ "a",
2169
+ {
2170
+ key:
2171
+ candidate.slug,
2172
+ href:
2173
+ candidate.url,
2174
+ onClick:
2175
+ (event) =>
2176
+ handleClassicChannelClick(
2177
+ event,
2178
+ candidate
2179
+ ),
2180
+ className:
2181
+ candidate.slug ===
2182
+ channel?.slug
2183
+ ? "rails-den-chat-channel-link is-active"
2184
+ : "rails-den-chat-channel-link"
2185
+ },
2186
+
2187
+ h(
2188
+ "span",
2189
+ null,
2190
+ "# ",
2191
+ candidate.title
2192
+ ),
2193
+
2194
+ h(
2195
+ "span",
2196
+ {
2197
+ className:
2198
+ "rails-den-chat-channel-meta"
2199
+ },
2200
+
2201
+ candidate.visibility ===
2202
+ "private" &&
2203
+ h(
2204
+ "small",
2205
+ null,
2206
+ "Private"
2207
+ ),
2208
+
2209
+ (unreadCounts[candidate.slug] || 0) > 0 &&
2210
+ h(
2211
+ "span",
2212
+ {
2213
+ className: "rails-den-chat-unread-badge"
2214
+ },
2215
+ `${unreadCounts[candidate.slug]} unread`
2216
+ ),
2217
+
2218
+ presence.visible &&
2219
+ presence.available &&
2220
+ h(
2221
+ "span",
2222
+ {
2223
+ className:
2224
+ "rails-den-chat-room-count"
2225
+ },
2226
+ String(
2227
+ candidatePresence?.count ||
2228
+ 0
2229
+ )
2230
+ )
2231
+ )
2232
+ )
2233
+ }
2234
+ )
2235
+ )
2236
+ ),
2237
+
2238
+ h(
2239
+ "section",
2240
+ {
2241
+ className:
2242
+ "rails-den-chat-main"
2243
+ },
2244
+
2245
+ h(
2246
+ "header",
2247
+ {
2248
+ className:
2249
+ "rails-den-chat-header"
2250
+ },
2251
+
2252
+ h(
2253
+ "div",
2254
+ {
2255
+ className:
2256
+ "rails-den-chat-header-copy"
2257
+ },
2258
+
2259
+ h(
2260
+ "span",
2261
+ {
2262
+ className:
2263
+ "rails-den-chat-hud-kicker"
2264
+ },
2265
+ channel
2266
+ ? "Spatial room"
2267
+ : "Atrium"
2268
+ ),
2269
+
2270
+ h(
2271
+ "h1",
2272
+ null,
2273
+ channel
2274
+ ? `# ${channel.title}`
2275
+ : "Chat"
2276
+ ),
2277
+
2278
+ h(
2279
+ "p",
2280
+ null,
2281
+ channel
2282
+ ? channel.description ||
2283
+ "Live room conversation"
2284
+ : "Enter a portal or choose a room to start chatting."
2285
+ )
2286
+ ),
2287
+
2288
+ h(
2289
+ "div",
2290
+ {
2291
+ className:
2292
+ "rails-den-chat-header-actions"
2293
+ },
2294
+
2295
+ channel &&
2296
+ h(
2297
+ "span",
2298
+ {
2299
+ className:
2300
+ "rails-den-chat-spatial-count"
2301
+ },
2302
+ spatialMembers.length === 1
2303
+ ? "1 here"
2304
+ : `${spatialMembers.length} here`
2305
+ ),
2306
+
2307
+ channel &&
2308
+ h(
2309
+ "span",
2310
+ {
2311
+ className:
2312
+ connectionState ===
2313
+ "Live"
2314
+ ? "rails-den-chat-connection is-live"
2315
+ : "rails-den-chat-connection"
2316
+ },
2317
+ switching
2318
+ ? "Switching…"
2319
+ : connectionState
2320
+ ),
2321
+
2322
+ h(
2323
+ "button",
2324
+ {
2325
+ type:
2326
+ "button",
2327
+ className:
2328
+ "rails-den-chat-hud-close",
2329
+ title:
2330
+ "Close chat",
2331
+ "aria-label":
2332
+ "Close chat",
2333
+ onClick:
2334
+ () =>
2335
+ closeDeck(
2336
+ true
2337
+ )
2338
+ },
2339
+ "×"
2340
+ )
2341
+ )
2342
+ ),
2343
+
2344
+ h(
2345
+ "div",
2346
+ {
2347
+ className:
2348
+ "rails-den-chat-hud-room-picker"
2349
+ },
2350
+
2351
+ h(
2352
+ "label",
2353
+ {
2354
+ htmlFor:
2355
+ "rails-den-chat-hud-room-select"
2356
+ },
2357
+ "Room"
2358
+ ),
2359
+
2360
+ h(
2361
+ "select",
2362
+ {
2363
+ id:
2364
+ "rails-den-chat-hud-room-select",
2365
+ value:
2366
+ channel?.slug || "",
2367
+ disabled:
2368
+ switching,
2369
+ onChange:
2370
+ handleChannelSelection
2371
+ },
2372
+
2373
+ h(
2374
+ "option",
2375
+ {
2376
+ value: ""
2377
+ },
2378
+ channel
2379
+ ? "Choose another room…"
2380
+ : "Choose a room…"
2381
+ ),
2382
+
2383
+ ...channels.map(
2384
+ (candidate) =>
2385
+ h(
2386
+ "option",
2387
+ {
2388
+ key:
2389
+ candidate.slug,
2390
+ value:
2391
+ candidate.slug
2392
+ },
2393
+ `${candidate.visibility === "private" ? "◇" : "#"} ${candidate.title}${(unreadCounts[candidate.slug] || 0) > 0 ? ` · ${unreadCounts[candidate.slug]} unread` : ""}`
2394
+ )
2395
+ )
2396
+ )
2397
+ ),
2398
+
2399
+ channel &&
2400
+ spatialMemberNames.length > 0 &&
2401
+ h(
2402
+ "div",
2403
+ {
2404
+ className:
2405
+ "rails-den-chat-spatial-members"
2406
+ },
2407
+ spatialMemberNames.join(
2408
+ " · "
2409
+ )
2410
+ ),
2411
+
2412
+ channel &&
2413
+ presence.visible &&
2414
+ h(
2415
+ "div",
2416
+ {
2417
+ className:
2418
+ "rails-den-chat-room-presence"
2419
+ },
2420
+
2421
+ !presence.available
2422
+ ? h(
2423
+ "span",
2424
+ {
2425
+ className:
2426
+ "rails-den-chat-presence-unavailable"
2427
+ },
2428
+ "Room presence unavailable"
2429
+ )
2430
+ : h(
2431
+ React.Fragment,
2432
+ null,
2433
+
2434
+ h(
2435
+ "strong",
2436
+ null,
2437
+ roomPresence.count === 1
2438
+ ? "1 person here"
2439
+ : `${roomPresence.count} people here`
2440
+ ),
2441
+
2442
+ roomPresence.members
2443
+ .length > 0 &&
2444
+ h(
2445
+ "div",
2446
+ {
2447
+ className:
2448
+ "rails-den-chat-room-people"
2449
+ },
2450
+
2451
+ ...roomPresence.members.map(
2452
+ (member, index) =>
2453
+ h(
2454
+ "span",
2455
+ {
2456
+ key:
2457
+ member.mention_name ||
2458
+ `${member.display_name}-${index}`,
2459
+ className:
2460
+ "rails-den-chat-room-person"
2461
+ },
2462
+ member.display_name
2463
+ )
2464
+ )
2465
+ )
2466
+ )
2467
+ ),
2468
+
2469
+ channel &&
2470
+ pinnedMessages.length > 0 &&
2471
+ h(
2472
+ "section",
2473
+ {
2474
+ className: "rails-den-chat-pinned-panel",
2475
+ "aria-label": "Pinned messages"
2476
+ },
2477
+ h("h2", null, "Pinned messages"),
2478
+ h(
2479
+ "ul",
2480
+ null,
2481
+ ...pinnedMessages.map(
2482
+ (message) =>
2483
+ h(
2484
+ "li",
2485
+ {key: message.id},
2486
+ h(
2487
+ "div",
2488
+ null,
2489
+ h("strong", null, message.author_name),
2490
+ h("span", {className: "rails-den-chat-pinned-preview"}, message.body)
2491
+ ),
2492
+ configuration.can_manage_channel &&
2493
+ h(
2494
+ "button",
2495
+ {
2496
+ type: "button",
2497
+ onClick: () => changePin(message, false)
2498
+ },
2499
+ "Unpin"
2500
+ )
2501
+ )
2502
+ )
2503
+ )
2504
+ ),
2505
+
2506
+ h(
2507
+ "div",
2508
+ {
2509
+ className:
2510
+ "rails-den-chat-message-list",
2511
+ ref:
2512
+ messagesRef
2513
+ },
2514
+
2515
+ !channel
2516
+ ? h(
2517
+ "div",
2518
+ {
2519
+ className:
2520
+ "rails-den-chat-hud-atrium-empty"
2521
+ },
2522
+
2523
+ h(
2524
+ "strong",
2525
+ null,
2526
+ "You're in the Atrium."
2527
+ ),
2528
+
2529
+ h(
2530
+ "span",
2531
+ null,
2532
+ "Walk through a portal or choose a room above."
2533
+ )
2534
+ )
2535
+ : messages.length === 0
2536
+ ? h(
2537
+ "div",
2538
+ {
2539
+ className:
2540
+ "rails-den-chat-empty"
2541
+ },
2542
+ "No messages yet. Start the conversation."
2543
+ )
2544
+ : messages.map(
2545
+ (message) =>
2546
+ h(
2547
+ React.Fragment,
2548
+ {
2549
+ key: message.id
2550
+ },
2551
+ String(message.id) === String(firstUnreadMessageId) &&
2552
+ h(
2553
+ "div",
2554
+ {
2555
+ className: "rails-den-chat-unread-divider",
2556
+ role: "separator"
2557
+ },
2558
+ "Unread messages"
2559
+ ),
2560
+ h(
2561
+ "article",
2562
+ {
2563
+ id: `chat-message-${message.id}`,
2564
+ className: message.pinned
2565
+ ? "rails-den-chat-message is-pinned"
2566
+ : "rails-den-chat-message"
2567
+ },
2568
+ h(
2569
+ "div",
2570
+ {className: "rails-den-chat-message-meta"},
2571
+ h("strong", null, message.author_name),
2572
+ message.pinned &&
2573
+ h("span", {className: "rails-den-chat-pin-indicator"}, "Pinned"),
2574
+ h(
2575
+ "time",
2576
+ {dateTime: message.created_at},
2577
+ formatTime(message.created_at)
2578
+ ),
2579
+ configuration.can_manage_channel &&
2580
+ h(
2581
+ "button",
2582
+ {
2583
+ type: "button",
2584
+ className: "rails-den-chat-pin-control",
2585
+ onClick: () => changePin(message, !message.pinned)
2586
+ },
2587
+ message.pinned ? "Unpin" : "Pin"
2588
+ )
2589
+ ),
2590
+ h("div", {className: "rails-den-chat-message-body"}, message.body)
2591
+ )
2592
+ )
2593
+ )
2594
+ ),
2595
+
2596
+ error &&
2597
+ h(
2598
+ "div",
2599
+ {
2600
+ className:
2601
+ "rails-den-chat-error",
2602
+ role:
2603
+ "alert"
2604
+ },
2605
+ error
2606
+ ),
2607
+
2608
+ channel &&
2609
+ configuration.can_post
2610
+ ? h(
2611
+ "form",
2612
+ {
2613
+ className:
2614
+ "rails-den-chat-composer",
2615
+ onSubmit:
2616
+ handleSubmit
2617
+ },
2618
+
2619
+ h(
2620
+ "textarea",
2621
+ {
2622
+ ref:
2623
+ textareaRef,
2624
+ value:
2625
+ body,
2626
+ maxLength:
2627
+ 50000,
2628
+ rows:
2629
+ 3,
2630
+ required:
2631
+ true,
2632
+ disabled:
2633
+ switching,
2634
+ placeholder:
2635
+ `Message #${channel.title}`,
2636
+ onChange:
2637
+ (event) => {
2638
+ setBody(
2639
+ event.target.value
2640
+ )
2641
+
2642
+ composerSelectionRef.current = {
2643
+ start:
2644
+ event.target.selectionStart ??
2645
+ event.target.value.length,
2646
+ end:
2647
+ event.target.selectionEnd ??
2648
+ event.target.value.length
2649
+ }
2650
+ },
2651
+ onClick:
2652
+ rememberComposerSelection,
2653
+ onKeyUp:
2654
+ rememberComposerSelection,
2655
+ onSelect:
2656
+ rememberComposerSelection,
2657
+ onKeyDown:
2658
+ handleComposerKeyDown
2659
+ }
2660
+ ),
2661
+
2662
+ h(
2663
+ "div",
2664
+ {
2665
+ className:
2666
+ "rails-den-chat-composer-footer"
2667
+ },
2668
+
2669
+ h(
2670
+ "div",
2671
+ {
2672
+ className:
2673
+ "rails-den-chat-composer-tools"
2674
+ },
2675
+
2676
+ h(
2677
+ "div",
2678
+ {
2679
+ className:
2680
+ "rails-den-reaction-picker rails-den-chat-reaction-picker"
2681
+ },
2682
+
2683
+ h(
2684
+ "button",
2685
+ {
2686
+ type:
2687
+ "button",
2688
+ className:
2689
+ "rails-den-add-reaction",
2690
+ "aria-expanded":
2691
+ reactionPickerOpen,
2692
+ onMouseDown:
2693
+ () =>
2694
+ rememberComposerSelection(),
2695
+ onClick:
2696
+ () => {
2697
+ if (
2698
+ reactionPickerOpen
2699
+ ) {
2700
+ setReactionPickerOpen(
2701
+ false
2702
+ )
2703
+ setReactionQuery("")
2704
+ setReactionError("")
2705
+ } else {
2706
+ setReactionLoading(
2707
+ true
2708
+ )
2709
+ setReactionPickerOpen(
2710
+ true
2711
+ )
2712
+ }
2713
+ }
2714
+ },
2715
+ "Emoji"
2716
+ ),
2717
+
2718
+ reactionPickerOpen &&
2719
+ h(
2720
+ "div",
2721
+ {
2722
+ className:
2723
+ "rails-den-reaction-picker-menu",
2724
+ "aria-label":
2725
+ "Choose a reaction emoji"
2726
+ },
2727
+
2728
+ h(
2729
+ "div",
2730
+ {
2731
+ className:
2732
+ "rails-den-reaction-search"
2733
+ },
2734
+
2735
+ h(
2736
+ "input",
2737
+ {
2738
+ ref:
2739
+ reactionSearchRef,
2740
+ type:
2741
+ "search",
2742
+ value:
2743
+ reactionQuery,
2744
+ placeholder:
2745
+ "Search emoji",
2746
+ autoComplete:
2747
+ "off",
2748
+ "aria-label":
2749
+ "Search reactions",
2750
+ onChange:
2751
+ (event) =>
2752
+ setReactionQuery(
2753
+ event.target.value
2754
+ ),
2755
+ onKeyDown:
2756
+ (event) => {
2757
+ if (
2758
+ event.key ===
2759
+ "Enter"
2760
+ ) {
2761
+ event.preventDefault()
2762
+ }
2763
+
2764
+ if (
2765
+ event.key ===
2766
+ "Escape"
2767
+ ) {
2768
+ event.preventDefault()
2769
+ setReactionPickerOpen(
2770
+ false
2771
+ )
2772
+ setReactionQuery("")
2773
+ setReactionError("")
2774
+ focusComposer()
2775
+ }
2776
+ }
2777
+ }
2778
+ )
2779
+ ),
2780
+
2781
+ h(
2782
+ "div",
2783
+ {
2784
+ className:
2785
+ "rails-den-reaction-options",
2786
+ role:
2787
+ "group",
2788
+ "aria-label":
2789
+ "Available reactions"
2790
+ },
2791
+
2792
+ reactionLoading
2793
+ ? h(
2794
+ "p",
2795
+ {
2796
+ className:
2797
+ "rails-den-reaction-no-results"
2798
+ },
2799
+ "Loading reactions…"
2800
+ )
2801
+ : reactionError
2802
+ ? h(
2803
+ "p",
2804
+ {
2805
+ className:
2806
+ "rails-den-reaction-error"
2807
+ },
2808
+ reactionError
2809
+ )
2810
+ : reactionTypes.length ===
2811
+ 0
2812
+ ? h(
2813
+ "p",
2814
+ {
2815
+ className:
2816
+ "rails-den-reaction-no-results"
2817
+ },
2818
+ "No matching reactions found."
2819
+ )
2820
+ : reactionTypes.map(
2821
+ (reactionType) =>
2822
+ h(
2823
+ "button",
2824
+ {
2825
+ key:
2826
+ reactionType.id,
2827
+ type:
2828
+ "button",
2829
+ className:
2830
+ "rails-den-reaction-option",
2831
+ title:
2832
+ reactionType.name,
2833
+ onClick:
2834
+ () =>
2835
+ insertReactionEmoji(
2836
+ reactionType.emoji
2837
+ )
2838
+ },
2839
+
2840
+ h(
2841
+ "span",
2842
+ {
2843
+ "aria-hidden":
2844
+ "true"
2845
+ },
2846
+ reactionType.emoji
2847
+ ),
2848
+
2849
+ h(
2850
+ "span",
2851
+ null,
2852
+ reactionType.name
2853
+ )
2854
+ )
2855
+ )
2856
+ )
2857
+ )
2858
+ ),
2859
+
2860
+ h(
2861
+ "span",
2862
+ null,
2863
+ "Enter sends · Shift+Enter adds a line · Esc moves"
2864
+ )
2865
+ ),
2866
+
2867
+ h(
2868
+ "button",
2869
+ {
2870
+ type:
2871
+ "submit",
2872
+ disabled:
2873
+ sending ||
2874
+ switching ||
2875
+ body.trim() === ""
2876
+ },
2877
+ sending
2878
+ ? "Sending…"
2879
+ : "Send"
2880
+ )
2881
+ )
2882
+ )
2883
+ : channel
2884
+ ? h(
2885
+ "div",
2886
+ {
2887
+ className:
2888
+ "rails-den-chat-cannot-post"
2889
+ },
2890
+ "You cannot send messages in this channel."
2891
+ )
2892
+ : null
2893
+ )
2894
+ )
2895
+ }
2896
+
2897
+ function initializeChat(root) {
2898
+ if (
2899
+ root.dataset
2900
+ .railsDenChatReady ===
2901
+ "true"
2902
+ ) {
2903
+ return
2904
+ }
2905
+
2906
+ if (!reactAvailable()) {
2907
+ return
2908
+ }
2909
+
2910
+ const configuration =
2911
+ payloadFor(root)
2912
+
2913
+ if (!configuration) {
2914
+ return
2915
+ }
2916
+
2917
+ const originalHtml =
2918
+ root.innerHTML
2919
+
2920
+ root.dataset
2921
+ .railsDenChatReady =
2922
+ "true"
2923
+
2924
+ const reactRoot =
2925
+ window.ReactDOM.createRoot(
2926
+ root
2927
+ )
2928
+
2929
+ roots.set(
2930
+ root,
2931
+ {
2932
+ root:
2933
+ reactRoot,
2934
+ originalHtml
2935
+ }
2936
+ )
2937
+
2938
+ reactRoot.render(
2939
+ window.React.createElement(
2940
+ ChatApp,
2941
+ {
2942
+ initialConfiguration:
2943
+ configuration,
2944
+ hostRoot:
2945
+ root
2946
+ }
2947
+ )
2948
+ )
2949
+ }
2950
+
2951
+ function initializeChats() {
2952
+ document
2953
+ .querySelectorAll(
2954
+ ROOT_SELECTOR
2955
+ )
2956
+ .forEach(
2957
+ initializeChat
2958
+ )
2959
+ }
2960
+
2961
+ function restoreChatsForTurboCache() {
2962
+ roots.forEach(
2963
+ (
2964
+ {
2965
+ root,
2966
+ originalHtml
2967
+ },
2968
+ element
2969
+ ) => {
2970
+ root.unmount()
2971
+
2972
+ element.innerHTML =
2973
+ originalHtml
2974
+
2975
+ delete element.dataset
2976
+ .railsDenChatReady
2977
+ }
2978
+ )
2979
+
2980
+ document
2981
+ .querySelectorAll(
2982
+ MODE_SHELL_SELECTOR
2983
+ )
2984
+ .forEach(
2985
+ (shell) => {
2986
+ delete shell.dataset
2987
+ .railsDenChatDeckOpen
2988
+ }
2989
+ )
2990
+
2991
+ roots.clear()
2992
+ }
2993
+
2994
+ document.addEventListener(
2995
+ "DOMContentLoaded",
2996
+ initializeChats
2997
+ )
2998
+
2999
+ document.addEventListener(
3000
+ "turbo:load",
3001
+ initializeChats
3002
+ )
3003
+
3004
+ document.addEventListener(
3005
+ "turbo:before-cache",
3006
+ restoreChatsForTurboCache
3007
+ )
3008
+ })()