collavre 0.24.0 → 0.25.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 (440) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +0 -1
  3. data/app/assets/stylesheets/collavre/comments_popup.css +165 -99
  4. data/app/assets/stylesheets/collavre/creatives.css +346 -37
  5. data/app/assets/stylesheets/collavre/desktop_setup.css +246 -0
  6. data/app/assets/stylesheets/collavre/gnb.css +64 -3
  7. data/app/assets/stylesheets/collavre/landing.css +108 -3
  8. data/app/assets/stylesheets/collavre/mention_menu.css +32 -0
  9. data/app/assets/stylesheets/collavre/popup.css +14 -0
  10. data/app/assets/stylesheets/collavre/print.css +0 -1
  11. data/app/assets/stylesheets/collavre/search_popup.css +1 -1
  12. data/app/assets/stylesheets/collavre/secret_fields.css +3 -0
  13. data/app/assets/stylesheets/collavre/workspace.css +307 -0
  14. data/app/channels/collavre/agent_channel.rb +14 -21
  15. data/app/channels/collavre/comments_presence_channel.rb +181 -21
  16. data/app/channels/collavre/inbox_badge_channel.rb +7 -1
  17. data/app/components/collavre/inbox/badge_component.rb +6 -13
  18. data/app/components/collavre/progress_filter_component.html.erb +2 -2
  19. data/app/components/collavre/progress_filter_component.rb +20 -0
  20. data/app/controllers/collavre/admin/settings_controller.rb +4 -1
  21. data/app/controllers/collavre/agent_connections_controller.rb +165 -0
  22. data/app/controllers/collavre/agent_gateways_controller.rb +132 -0
  23. data/app/controllers/collavre/agent_provisioning_controller.rb +79 -0
  24. data/app/controllers/collavre/api/v1/agents_controller.rb +45 -97
  25. data/app/controllers/collavre/application_controller.rb +13 -2
  26. data/app/controllers/collavre/comment_read_pointers_controller.rb +69 -40
  27. data/app/controllers/collavre/comments_controller.rb +45 -131
  28. data/app/controllers/collavre/concerns/shareable.rb +9 -3
  29. data/app/controllers/collavre/creatives_controller.rb +144 -11
  30. data/app/controllers/collavre/desktop_gateway_registrations_controller.rb +173 -0
  31. data/app/controllers/collavre/desktop_setup_controller.rb +119 -0
  32. data/app/controllers/collavre/desktop_sidecar_health_controller.rb +35 -0
  33. data/app/controllers/collavre/features_controller.rb +57 -0
  34. data/app/controllers/collavre/invites_controller.rb +12 -1
  35. data/app/controllers/collavre/llm_models_controller.rb +8 -0
  36. data/app/controllers/collavre/notices_controller.rb +20 -0
  37. data/app/controllers/collavre/tasks_controller.rb +40 -6
  38. data/app/controllers/collavre/topics_controller.rb +130 -53
  39. data/app/controllers/collavre/user_creative_preferences_controller.rb +214 -30
  40. data/app/controllers/concerns/collavre/comments/batch_operations.rb +15 -3
  41. data/app/controllers/concerns/collavre/comments/conversion.rb +1 -1
  42. data/app/controllers/concerns/collavre/users_controller/ai_user_management.rb +118 -8
  43. data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +1 -15
  44. data/app/controllers/concerns/collavre/users_controller/registration.rb +2 -2
  45. data/app/errors/collavre/turn_deadline_error.rb +20 -0
  46. data/app/helpers/collavre/application_helper.rb +38 -0
  47. data/app/helpers/collavre/creatives_helper.rb +56 -22
  48. data/app/helpers/collavre/navigation_helper.rb +12 -0
  49. data/app/javascript/collavre.js +1 -2
  50. data/app/javascript/components/InlineLexicalEditor.jsx +30 -21
  51. data/app/javascript/components/__tests__/creative_tree_row_parent_id.test.js +32 -2
  52. data/app/javascript/components/__tests__/creative_tree_row_progress_toggle.test.js +185 -0
  53. data/app/javascript/components/creative_tree_row.js +62 -22
  54. data/app/javascript/controllers/__tests__/agent_connection_controller.test.js +195 -0
  55. data/app/javascript/controllers/__tests__/desktop_proxy_setup_controller.test.js +96 -0
  56. data/app/javascript/controllers/__tests__/index.test.js +42 -0
  57. data/app/javascript/controllers/__tests__/last_visited_creative_controller.test.js +103 -0
  58. data/app/javascript/controllers/__tests__/link_creative_controller.test.js +253 -2
  59. data/app/javascript/controllers/__tests__/progress_filter_controller.test.js +134 -0
  60. data/app/javascript/controllers/__tests__/search_popup_controller.test.js +310 -0
  61. data/app/javascript/controllers/__tests__/topic_list_controller.test.js +111 -0
  62. data/app/javascript/controllers/__tests__/workspace_tree_controller.test.js +1011 -0
  63. data/app/javascript/controllers/agent_connection_controller.js +357 -0
  64. data/app/javascript/controllers/agent_vendor_controller.js +23 -0
  65. data/app/javascript/controllers/comment_controller.js +3 -1
  66. data/app/javascript/controllers/comments/__tests__/feature_cards_controller.test.js +223 -0
  67. data/app/javascript/controllers/comments/__tests__/form_controller_default_placeholder.test.js +83 -0
  68. data/app/javascript/controllers/comments/__tests__/form_controller_draft.test.js +2069 -0
  69. data/app/javascript/controllers/comments/__tests__/form_controller_ime_send.test.js +324 -0
  70. data/app/javascript/controllers/comments/__tests__/form_controller_review.test.js +28 -2
  71. data/app/javascript/controllers/comments/__tests__/form_controller_search_submit.test.js +106 -0
  72. data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft.test.js +134 -0
  73. data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft_creative_switch.test.js +166 -0
  74. data/app/javascript/controllers/comments/__tests__/form_controller_stashed_draft_failure.test.js +183 -0
  75. data/app/javascript/controllers/comments/__tests__/list_controller_deep_link_topic.test.js +467 -0
  76. data/app/javascript/controllers/comments/__tests__/list_controller_docked.test.js +115 -0
  77. data/app/javascript/controllers/comments/__tests__/list_controller_prev_message.test.js +245 -0
  78. data/app/javascript/controllers/comments/__tests__/list_controller_read_pointer.test.js +276 -0
  79. data/app/javascript/controllers/comments/__tests__/list_controller_stream_archived.test.js +221 -0
  80. data/app/javascript/controllers/comments/__tests__/list_controller_stream_search.test.js +104 -0
  81. data/app/javascript/controllers/comments/__tests__/placeholder_controller.test.js +79 -0
  82. data/app/javascript/controllers/comments/__tests__/popup_controller.test.js +754 -1
  83. data/app/javascript/controllers/comments/__tests__/presence_controller.test.js +717 -15
  84. data/app/javascript/controllers/comments/__tests__/prev_message_navigator.test.js +129 -0
  85. data/app/javascript/controllers/comments/__tests__/topics_controller_action_errors.test.js +184 -0
  86. data/app/javascript/controllers/comments/__tests__/topics_controller_archived.test.js +879 -0
  87. data/app/javascript/controllers/comments/__tests__/topics_controller_creation_container.test.js +154 -0
  88. data/app/javascript/controllers/comments/__tests__/topics_controller_creation_scroll.test.js +92 -0
  89. data/app/javascript/controllers/comments/__tests__/topics_controller_delete.test.js +130 -0
  90. data/app/javascript/controllers/comments/__tests__/topics_controller_docked.test.js +32 -0
  91. data/app/javascript/controllers/comments/__tests__/topics_controller_last_topic_echo.test.js +1938 -0
  92. data/app/javascript/controllers/comments/__tests__/topics_controller_primary_agent.test.js +256 -0
  93. data/app/javascript/controllers/comments/__tests__/topics_controller_scroll.test.js +182 -0
  94. data/app/javascript/controllers/comments/__tests__/topics_controller_selection_race.test.js +981 -0
  95. data/app/javascript/controllers/comments/__tests__/topics_controller_topic_list.test.js +313 -0
  96. data/app/javascript/controllers/comments/feature_cards_controller.js +83 -0
  97. data/app/javascript/controllers/comments/form_controller.js +795 -14
  98. data/app/javascript/controllers/comments/list_controller.js +338 -52
  99. data/app/javascript/controllers/comments/placeholder_controller.js +30 -0
  100. data/app/javascript/controllers/comments/popup_controller.js +425 -83
  101. data/app/javascript/controllers/comments/presence_controller.js +317 -55
  102. data/app/javascript/controllers/comments/prev_message_navigator.js +94 -0
  103. data/app/javascript/controllers/comments/topics_controller.js +1772 -100
  104. data/app/javascript/controllers/common_popup_controller.js +2 -2
  105. data/app/javascript/controllers/creatives/__tests__/select_mode_controller.test.js +139 -0
  106. data/app/javascript/controllers/creatives/__tests__/tree_controller.test.js +605 -0
  107. data/app/javascript/controllers/creatives/__tests__/write_access_request_controller.test.js +125 -0
  108. data/app/javascript/controllers/creatives/select_mode_controller.js +7 -2
  109. data/app/javascript/controllers/creatives/tree_controller.js +140 -31
  110. data/app/javascript/controllers/creatives/write_access_request_controller.js +58 -0
  111. data/app/javascript/controllers/desktop_proxy_setup_controller.js +51 -0
  112. data/app/javascript/controllers/gateway_check_controller.js +36 -0
  113. data/app/javascript/controllers/index.js +25 -1
  114. data/app/javascript/controllers/last_visited_creative_controller.js +58 -0
  115. data/app/javascript/controllers/link_creative_controller.js +89 -18
  116. data/app/javascript/controllers/progress_filter_controller.js +12 -22
  117. data/app/javascript/controllers/search_popup_controller.js +64 -46
  118. data/app/javascript/controllers/share_user_search_controller.js +2 -2
  119. data/app/javascript/controllers/topic_list_controller.js +44 -7
  120. data/app/javascript/controllers/workspace_tree_controller.js +662 -0
  121. data/app/javascript/creatives/__tests__/tree_renderer.test.js +80 -0
  122. data/app/javascript/creatives/drag_drop/__tests__/drop_completion_empty_state.test.js +98 -0
  123. data/app/javascript/creatives/drag_drop/__tests__/topic_move_released_agent.test.js +125 -0
  124. data/app/javascript/creatives/drag_drop/event_handlers.js +12 -0
  125. data/app/javascript/creatives/tree_renderer.js +65 -22
  126. data/app/javascript/lib/__tests__/chat_drafts.test.js +1458 -0
  127. data/app/javascript/lib/__tests__/common_popup_bounds.test.js +8 -3
  128. data/app/javascript/lib/__tests__/common_popup_placement.test.js +374 -0
  129. data/app/javascript/lib/__tests__/common_popup_reshow.test.js +131 -0
  130. data/app/javascript/lib/__tests__/creative_link_prefetch.test.js +46 -0
  131. data/app/javascript/lib/__tests__/last_visited_creative.test.js +87 -0
  132. data/app/javascript/lib/__tests__/skill_html_entities.test.js +23 -0
  133. data/app/javascript/lib/__tests__/turbo_stream_actions.test.js +190 -0
  134. data/app/javascript/lib/api/__tests__/creatives.test.js +68 -0
  135. data/app/javascript/lib/api/__tests__/csrf_fetch.test.js +27 -0
  136. data/app/javascript/lib/api/__tests__/topics_save_last_topic.test.js +112 -0
  137. data/app/javascript/lib/api/creatives.js +32 -13
  138. data/app/javascript/lib/api/csrf_fetch.js +6 -4
  139. data/app/javascript/lib/api/topics.js +63 -5
  140. data/app/javascript/lib/chat_drafts.js +786 -0
  141. data/app/javascript/lib/common_popup.js +164 -25
  142. data/app/javascript/lib/creative_link_prefetch.js +27 -0
  143. data/app/javascript/lib/last_visited_creative.js +97 -0
  144. data/app/javascript/lib/lexical/__tests__/creative_link_node.test.js +162 -0
  145. data/app/javascript/lib/lexical/__tests__/creative_link_trigger.test.js +133 -0
  146. data/app/javascript/lib/lexical/__tests__/link_toolbar.test.js +62 -0
  147. data/app/javascript/lib/lexical/creative_link_node.js +101 -0
  148. data/app/javascript/lib/lexical/creative_link_trigger.js +98 -0
  149. data/app/javascript/lib/lexical/link_toolbar.js +23 -0
  150. data/app/javascript/lib/turbo_stream_actions.js +23 -16
  151. data/app/javascript/lib/utils/__tests__/filter_navigation.test.js +210 -0
  152. data/app/javascript/lib/utils/__tests__/markdown.test.js +44 -0
  153. data/app/javascript/lib/utils/filter_navigation.js +100 -0
  154. data/app/javascript/modules/__tests__/command_menu_creative_picker.test.js +62 -0
  155. data/app/javascript/modules/__tests__/command_menu_typeahead.test.js +267 -0
  156. data/app/javascript/modules/__tests__/creative_link_picker.test.js +85 -0
  157. data/app/javascript/modules/__tests__/creative_row_editor_archive.test.js +391 -0
  158. data/app/javascript/modules/__tests__/creative_row_editor_delegated_clicks.test.js +158 -0
  159. data/app/javascript/modules/__tests__/creative_row_editor_empty_state_recovery.test.js +519 -0
  160. data/app/javascript/modules/__tests__/creative_row_editor_empty_state_removal.test.js +346 -0
  161. data/app/javascript/modules/__tests__/creative_row_editor_session.test.js +144 -0
  162. data/app/javascript/modules/__tests__/creative_tree_dom.test.js +30 -0
  163. data/app/javascript/modules/__tests__/creative_tree_empty_state.test.js +186 -0
  164. data/app/javascript/modules/__tests__/dom_listener_registry.test.js +67 -0
  165. data/app/javascript/modules/__tests__/support/inline_editor_dom.js +159 -0
  166. data/app/javascript/modules/command_args_form.js +0 -12
  167. data/app/javascript/modules/command_menu.js +80 -38
  168. data/app/javascript/modules/creative_link_picker.js +56 -0
  169. data/app/javascript/modules/creative_row_editor.js +446 -243
  170. data/app/javascript/modules/creative_row_editor_delegated_clicks.js +87 -0
  171. data/app/javascript/modules/creative_tree_dom.js +4 -0
  172. data/app/javascript/modules/creative_tree_empty_state.js +80 -0
  173. data/app/javascript/modules/dom_listener_registry.js +23 -0
  174. data/app/javascript/modules/mention_menu.js +2 -3
  175. data/app/javascript/utils/__tests__/caret_anchor.test.js +75 -0
  176. data/app/javascript/utils/__tests__/chevron_icons.test.js +15 -0
  177. data/app/javascript/utils/caret_position.js +10 -0
  178. data/app/javascript/utils/chevron_icons.js +7 -0
  179. data/app/jobs/collavre/ai_agent_job.rb +262 -72
  180. data/app/jobs/collavre/cancel_offline_delegated_tasks_job.rb +11 -36
  181. data/app/jobs/collavre/comment_badges_broadcast_job.rb +28 -0
  182. data/app/jobs/collavre/comment_link_preview_job.rb +33 -0
  183. data/app/jobs/collavre/comment_notification_job.rb +14 -0
  184. data/app/jobs/collavre/comment_push_delivery_sweep_job.rb +17 -0
  185. data/app/jobs/collavre/compress_job.rb +33 -22
  186. data/app/jobs/collavre/creative_broadcast_job.rb +161 -48
  187. data/app/jobs/collavre/cron_action_job.rb +36 -18
  188. data/app/jobs/collavre/drop_trigger_job.rb +23 -9
  189. data/app/jobs/collavre/merge_comments_job.rb +33 -16
  190. data/app/jobs/collavre/push_notification_job.rb +62 -18
  191. data/app/jobs/collavre/restore_dropped_dispatches_job.rb +17 -0
  192. data/app/jobs/collavre/stuck_detector_job.rb +1 -1
  193. data/app/jobs/collavre/touch_creative_subtree_job.rb +14 -0
  194. data/app/models/collavre/agent_gateway.rb +200 -0
  195. data/app/models/collavre/agent_subscription.rb +4 -4
  196. data/app/models/collavre/agent_workspace.rb +210 -0
  197. data/app/models/collavre/comment/broadcastable.rb +22 -52
  198. data/app/models/collavre/comment/notifiable.rb +154 -77
  199. data/app/models/collavre/comment/topic_membership.rb +41 -0
  200. data/app/models/collavre/comment.rb +336 -31
  201. data/app/models/collavre/comment_notification_delivery.rb +61 -0
  202. data/app/models/collavre/comment_presence_store/cache_batches.rb +64 -0
  203. data/app/models/collavre/comment_presence_store.rb +272 -12
  204. data/app/models/collavre/comment_read_pointer.rb +9 -15
  205. data/app/models/collavre/creative/describable.rb +8 -2
  206. data/app/models/collavre/creative/permissible.rb +11 -0
  207. data/app/models/collavre/creative.rb +29 -4
  208. data/app/models/collavre/creative_share.rb +109 -4
  209. data/app/models/collavre/current.rb +1 -0
  210. data/app/models/collavre/llm_model.rb +62 -0
  211. data/app/models/collavre/system_setting.rb +12 -1
  212. data/app/models/collavre/task.rb +121 -2
  213. data/app/models/collavre/topic.rb +51 -0
  214. data/app/models/collavre/user.rb +101 -41
  215. data/app/models/collavre/user_creative_preference.rb +3 -1
  216. data/app/services/collavre/agent_provisioning/archive.rb +108 -0
  217. data/app/services/collavre/ai_agent/a2a_dispatcher.rb +16 -3
  218. data/app/services/collavre/ai_agent/agent_lifecycle_manager.rb +63 -6
  219. data/app/services/collavre/ai_agent/approval_handler.rb +12 -4
  220. data/app/services/collavre/ai_agent/claude_channel_adapter.rb +10 -5
  221. data/app/services/collavre/ai_agent/merged_trigger_comments.rb +164 -0
  222. data/app/services/collavre/ai_agent/message_builder.rb +103 -16
  223. data/app/services/collavre/ai_agent/session_provisioner.rb +3 -7
  224. data/app/services/collavre/ai_agent/task_claim_service.rb +29 -15
  225. data/app/services/collavre/ai_agent/task_reply_service.rb +67 -0
  226. data/app/services/collavre/ai_agent_service.rb +108 -20
  227. data/app/services/collavre/ai_client.rb +211 -21
  228. data/app/services/collavre/cli_proxy/client.rb +147 -0
  229. data/app/services/collavre/cli_proxy/endpoint_policy.rb +81 -0
  230. data/app/services/collavre/cli_proxy/identity.rb +47 -0
  231. data/app/services/collavre/cli_proxy/safe_net_http_adapter.rb +19 -0
  232. data/app/services/collavre/command_menu_service.rb +0 -10
  233. data/app/services/collavre/comment_move_service.rb +96 -7
  234. data/app/services/collavre/comment_snapshot_restore_service.rb +11 -5
  235. data/app/services/collavre/comments/action_executor.rb +48 -20
  236. data/app/services/collavre/comments/command_processor.rb +0 -1
  237. data/app/services/collavre/comments/list_response.rb +133 -0
  238. data/app/services/collavre/comments/read_pointer_writer.rb +210 -0
  239. data/app/services/collavre/comments/read_receipt_broadcaster.rb +157 -0
  240. data/app/services/collavre/comments/read_receipt_index.rb +152 -0
  241. data/app/services/collavre/comments/topic_command.rb +72 -1
  242. data/app/services/collavre/comments/topic_mutation.rb +20 -0
  243. data/app/services/collavre/creatives/breadcrumb_resolver.rb +1 -1
  244. data/app/services/collavre/creatives/comment_badge_index.rb +299 -27
  245. data/app/services/collavre/creatives/reorderer.rb +15 -3
  246. data/app/services/collavre/creatives/tree_builder.rb +1 -1
  247. data/app/services/collavre/creatives/tree_formatter.rb +5 -6
  248. data/app/services/collavre/creatives/workspace_path_resolver.rb +50 -0
  249. data/app/services/collavre/creatives/workspace_tree_builder.rb +92 -0
  250. data/app/services/collavre/crons/recurring_topic_tasks.rb +23 -0
  251. data/app/services/collavre/home_page_path.rb +55 -0
  252. data/app/services/collavre/html_text.rb +64 -0
  253. data/app/services/collavre/http_client.rb +11 -2
  254. data/app/services/collavre/inbox_reply_service.rb +1 -1
  255. data/app/services/collavre/markdown_converter.rb +13 -1
  256. data/app/services/collavre/orchestration/agent_orchestrator.rb +776 -81
  257. data/app/services/collavre/orchestration/arbiter.rb +48 -5
  258. data/app/services/collavre/orchestration/deferred_trigger_scope.rb +53 -0
  259. data/app/services/collavre/orchestration/delivery_record.rb +792 -0
  260. data/app/services/collavre/orchestration/loop_breaker.rb +10 -4
  261. data/app/services/collavre/orchestration/matcher.rb +132 -17
  262. data/app/services/collavre/orchestration/policy_resolver.rb +58 -10
  263. data/app/services/collavre/orchestration/scheduler.rb +10 -4
  264. data/app/services/collavre/orchestration/scheduling_hooks.rb +10 -0
  265. data/app/services/collavre/orchestration/selection.rb +43 -0
  266. data/app/services/collavre/orchestration/stuck_detector.rb +25 -112
  267. data/app/services/collavre/orchestration/task_coalescer.rb +355 -0
  268. data/app/services/collavre/orchestration/topic_slot.rb +73 -0
  269. data/app/services/collavre/system_events/context_builder.rb +60 -11
  270. data/app/services/collavre/system_events/dispatcher.rb +58 -12
  271. data/app/services/collavre/system_events/envelope.rb +73 -0
  272. data/app/services/collavre/system_events/vocabulary.rb +45 -0
  273. data/app/services/collavre/tools/creative_retrieval_service.rb +1 -1
  274. data/app/services/collavre/tools/cron_create_service.rb +32 -18
  275. data/app/services/collavre/tools/id_list.rb +38 -0
  276. data/app/services/collavre/tools/topic_authorizer.rb +61 -7
  277. data/app/services/collavre/tools/topic_branch_service.rb +85 -0
  278. data/app/services/collavre/tools/topic_create_service.rb +117 -0
  279. data/app/services/collavre/tools/topic_list_service.rb +101 -0
  280. data/app/services/collavre/tools/topic_message_create_service.rb +192 -0
  281. data/app/services/collavre/tools/topic_messages_service.rb +346 -0
  282. data/app/services/collavre/tools/topic_move_service.rb +108 -0
  283. data/app/services/collavre/tools/topic_selection.rb +59 -0
  284. data/app/services/collavre/tools/topic_update_service.rb +165 -0
  285. data/app/services/collavre/topic_branch_service.rb +39 -10
  286. data/app/services/collavre/topics/agent_resolver.rb +87 -0
  287. data/app/services/collavre/topics/char_budget.rb +70 -0
  288. data/app/services/collavre/topics/last_topic_preference_payload.rb +18 -0
  289. data/app/services/collavre/topics/message_page.rb +380 -0
  290. data/app/services/collavre/topics/message_page_markdown.rb +102 -0
  291. data/app/services/collavre/topics/message_scope.rb +61 -0
  292. data/app/services/collavre/topics/message_stats.rb +92 -0
  293. data/app/services/collavre/topics/move_blocker.rb +47 -0
  294. data/app/services/collavre/topics/next_name.rb +35 -0
  295. data/app/services/collavre/topics/pin_rejection.rb +25 -0
  296. data/app/services/collavre/topics/read_pointer_relocator.rb +71 -0
  297. data/app/services/collavre/topics/reserved_name.rb +18 -0
  298. data/app/services/collavre/topics/serializer.rb +92 -0
  299. data/app/services/collavre/topics/topic_destroy.rb +30 -0
  300. data/app/services/collavre/topics/topic_move.rb +80 -0
  301. data/app/services/collavre/topics/topic_move_effects.rb +35 -0
  302. data/app/services/collavre/topics/topic_mutation.rb +27 -0
  303. data/app/views/collavre/admin/integrations/_setting_row.html.erb +5 -8
  304. data/app/views/collavre/admin/settings/_system_tab.html.erb +7 -0
  305. data/app/views/collavre/agent_connections/show.html.erb +77 -0
  306. data/app/views/collavre/agent_gateways/_form.html.erb +71 -0
  307. data/app/views/collavre/agent_gateways/edit.html.erb +2 -0
  308. data/app/views/collavre/agent_gateways/index.html.erb +54 -0
  309. data/app/views/collavre/agent_gateways/new.html.erb +2 -0
  310. data/app/views/collavre/comments/_comments_popup.html.erb +32 -17
  311. data/app/views/collavre/comments/_empty_state.html.erb +33 -0
  312. data/app/views/collavre/comments/_empty_state_minimal.html.erb +7 -0
  313. data/app/views/collavre/comments/_feature_card.html.erb +35 -0
  314. data/app/views/collavre/comments/_list.html.erb +3 -2
  315. data/app/views/collavre/comments/_no_search_results.html.erb +3 -0
  316. data/app/views/collavre/creatives/_add_button.html.erb +17 -19
  317. data/app/views/collavre/creatives/_empty_state.html.erb +109 -0
  318. data/app/views/collavre/creatives/_inline_edit_form.html.erb +5 -5
  319. data/app/views/collavre/creatives/_tree_loading_placeholder.html.erb +16 -0
  320. data/app/views/collavre/creatives/_workspace_tree.html.erb +33 -0
  321. data/app/views/collavre/creatives/index.html.erb +79 -18
  322. data/app/views/collavre/desktop_setup/show.html.erb +114 -0
  323. data/app/views/collavre/features/index.html.erb +42 -0
  324. data/app/views/collavre/features/show.html.erb +52 -0
  325. data/app/views/collavre/invitation_mailer/invite.html.erb +1 -1
  326. data/app/views/collavre/invitation_mailer/invite.text.erb +1 -1
  327. data/app/views/collavre/invites/show.html.erb +14 -1
  328. data/app/views/collavre/landing/show.html.erb +5 -1
  329. data/app/views/collavre/shared/_link_creative_modal.html.erb +4 -1
  330. data/app/views/collavre/shared/navigation/_help_button.html.erb +3 -1
  331. data/app/views/collavre/shared/navigation/_panels.html.erb +1 -8
  332. data/app/views/collavre/shared/navigation/_search_form.html.erb +13 -2
  333. data/app/views/collavre/user_themes/index.html.erb +1 -1
  334. data/app/views/collavre/users/_llm_fields.html.erb +65 -0
  335. data/app/views/collavre/users/_org_chart_node.html.erb +1 -1
  336. data/app/views/collavre/users/edit_ai.html.erb +33 -31
  337. data/app/views/collavre/users/new.html.erb +4 -2
  338. data/app/views/collavre/users/new_ai.html.erb +14 -32
  339. data/app/views/collavre/users/show.html.erb +13 -1
  340. data/app/views/layouts/collavre/landing.html.erb +9 -4
  341. data/config/initializers/ruby_llm_openai_reasoning_content.rb +77 -0
  342. data/config/locales/admin.en.yml +3 -1
  343. data/config/locales/admin.ko.yml +3 -1
  344. data/config/locales/agent_gateway.en.yml +217 -0
  345. data/config/locales/agent_gateway.ko.yml +217 -0
  346. data/config/locales/ai_agent.en.yml +0 -1
  347. data/config/locales/ai_agent.ko.yml +0 -1
  348. data/config/locales/comments.en.yml +72 -30
  349. data/config/locales/comments.ko.yml +72 -30
  350. data/config/locales/creatives.en.yml +32 -1
  351. data/config/locales/creatives.ko.yml +32 -1
  352. data/config/locales/features.en.yml +140 -0
  353. data/config/locales/features.ko.yml +140 -0
  354. data/config/locales/invites.en.yml +3 -0
  355. data/config/locales/invites.ko.yml +3 -0
  356. data/config/locales/landing.en.yml +2 -1
  357. data/config/locales/landing.ko.yml +2 -1
  358. data/config/locales/tools.en.yml +20 -0
  359. data/config/locales/tools.ko.yml +20 -0
  360. data/config/locales/users.en.yml +15 -14
  361. data/config/locales/users.ko.yml +14 -14
  362. data/config/routes.rb +45 -19
  363. data/db/migrate/20250911084338_backfill_creative_in_comment_inbox_items.rb +9 -1
  364. data/db/migrate/20260724000000_add_creative_id_index_to_comments.rb +34 -0
  365. data/db/migrate/20260724000001_add_creative_private_id_index_to_comments.rb +50 -0
  366. data/db/migrate/20260724090000_add_async_comment_notification_state.rb +64 -0
  367. data/db/migrate/20260727000000_add_waiting_notice_scope_to_comments.rb +30 -0
  368. data/db/migrate/20260727010000_add_waiting_notice_scope_to_tasks.rb +26 -0
  369. data/db/migrate/20260805000000_add_creative_workspace_enabled_to_users.rb +5 -0
  370. data/db/migrate/20260807050000_add_dismissed_notices_to_users.rb +5 -0
  371. data/db/migrate/20260809000000_change_creative_workspace_enabled_default_to_true.rb +18 -0
  372. data/db/migrate/20260809000001_create_llm_models.rb +64 -0
  373. data/db/migrate/20260809000002_create_agent_gateways_and_workspaces.rb +35 -0
  374. data/db/migrate/20260809000003_hash_agent_workspace_callback_tokens.rb +44 -0
  375. data/db/migrate/20260809000004_encrypt_agent_workspace_manifest_tokens.rb +59 -0
  376. data/db/migrate/20260810000000_split_agent_workspace_identity_axes.rb +58 -0
  377. data/db/migrate/20260810000001_seed_codex_custom_proxy_llm_models.rb +28 -0
  378. data/db/migrate/20260810010000_add_last_visited_creative_to_users.rb +8 -0
  379. data/db/migrate/20260810020000_add_last_visited_creative_at_to_users.rb +5 -0
  380. data/db/migrate/20260810030000_add_last_visited_creative_navigation_order_to_users.rb +6 -0
  381. data/db/migrate/20260810060000_add_last_visited_creative_issued_sequence_to_users.rb +5 -0
  382. data/db/migrate/20260810100000_add_desktop_managed_to_agent_gateways.rb +40 -0
  383. data/db/migrate/20260810100001_add_desktop_preset_adapter_to_users.rb +38 -0
  384. data/db/migrate/20260811000000_make_agent_gateway_completion_key_nullable.rb +7 -0
  385. data/db/migrate/20260812000000_remove_workflow_columns_from_tasks.rb +101 -0
  386. data/db/migrate/20260812000001_remove_android_devices.rb +24 -0
  387. data/db/migrate/20260812010000_drop_inbox_items.rb +22 -0
  388. data/db/migrate/20260813000000_clear_claude_channel_presence_routing_expressions.rb +16 -0
  389. data/db/migrate/20260813000001_add_last_topic_revision_to_user_creative_preferences.rb +5 -0
  390. data/db/migrate/20260813000002_add_last_topic_save_order_to_user_creative_preferences.rb +6 -0
  391. data/db/migrate/20260813000003_add_last_topic_save_sequences_to_user_creative_preferences.rb +5 -0
  392. data/db/migrate/20260813000004_add_last_topic_save_fences_to_user_creative_preferences.rb +6 -0
  393. data/db/migrate/20260814000000_add_topic_to_comment_read_pointers.rb +79 -0
  394. data/db/migrate/20260814000001_add_creative_topic_private_id_index_to_comments.rb +37 -0
  395. data/db/migrate/20260816000000_add_watermark_index_to_comment_read_pointers.rb +27 -0
  396. data/db/migrate/20260816000001_add_private_recipient_topic_index_to_comments.rb +48 -0
  397. data/db/migrate/20260820000000_add_topic_assigned_at_to_comments.rb +13 -0
  398. data/db/seeds.rb +1 -51
  399. data/lib/collavre/engine.rb +108 -1
  400. data/lib/collavre/feature_card_registry.rb +168 -0
  401. data/lib/collavre/hashed_access_token_lookup.rb +22 -0
  402. data/lib/collavre/sensitive_request_silencer.rb +23 -0
  403. data/lib/collavre/user_extensions.rb +0 -2
  404. data/lib/collavre/version.rb +1 -1
  405. data/lib/collavre.rb +1 -0
  406. data/skills/collavre/SKILL.md +165 -0
  407. data/skills/collavre/package.json +8 -0
  408. data/skills/collavre/references/tool-reference.md +347 -0
  409. data/skills/collavre/scripts/collavre +634 -0
  410. data/skills/collavre/scripts/html_entities.js +24 -0
  411. metadata +228 -30
  412. data/app/controllers/collavre/api/v1/mobile/agent_events_controller.rb +0 -224
  413. data/app/controllers/collavre/api/v1/mobile/base_controller.rb +0 -95
  414. data/app/controllers/collavre/api/v1/mobile/devices_controller.rb +0 -31
  415. data/app/controllers/collavre/api/v1/mobile/voice_commands_controller.rb +0 -25
  416. data/app/controllers/collavre/inbox_items_controller.rb +0 -70
  417. data/app/controllers/collavre/typo_corrections_controller.rb +0 -39
  418. data/app/javascript/lib/__tests__/typo_correction.test.js +0 -192
  419. data/app/javascript/lib/typo_correction.js +0 -146
  420. data/app/javascript/modules/__tests__/typo_corrector.test.js +0 -365
  421. data/app/javascript/modules/creative_guide.js +0 -67
  422. data/app/javascript/modules/typo_corrector.js +0 -534
  423. data/app/jobs/collavre/inbox_summary_job.rb +0 -24
  424. data/app/mailers/collavre/inbox_mailer.rb +0 -19
  425. data/app/models/collavre/inbox_item.rb +0 -89
  426. data/app/services/collavre/comments/concerns/workflow_support.rb +0 -115
  427. data/app/services/collavre/comments/work_command.rb +0 -161
  428. data/app/services/collavre/comments/workflow_executor.rb +0 -276
  429. data/app/services/collavre/gemini_parent_recommender.rb +0 -77
  430. data/app/services/collavre/mobile/event_summarizer.rb +0 -40
  431. data/app/services/collavre/typo_corrector.rb +0 -188
  432. data/app/views/collavre/inbox_items/_item.html.erb +0 -18
  433. data/app/views/collavre/inbox_items/_items.html.erb +0 -3
  434. data/app/views/collavre/inbox_items/_list.html.erb +0 -7
  435. data/app/views/collavre/inbox_items/index.html.erb +0 -1
  436. data/app/views/collavre/inbox_mailer/daily_summary.html.erb +0 -11
  437. data/app/views/collavre/inbox_mailer/daily_summary.text.erb +0 -8
  438. data/app/views/collavre/users/typo_correction.html.erb +0 -50
  439. data/config/locales/mobile.en.yml +0 -16
  440. data/config/locales/mobile.ko.yml +0 -16
@@ -18,6 +18,18 @@ module Collavre
18
18
  end
19
19
 
20
20
  module ClassMethods
21
+ # The one enqueue seam for badge recomputation. Every write path that
22
+ # invalidates a badge goes through here rather than calling
23
+ # #broadcast_badges directly, so "badges are recounted off the request"
24
+ # holds for all of them and can't drift back one caller at a time.
25
+ # #broadcast_badges itself stays synchronous: the job — and the tests
26
+ # that pin the arithmetic — need a place that just computes.
27
+ def broadcast_badges_later(creative)
28
+ return unless creative
29
+
30
+ CommentBadgesBroadcastJob.perform_later(creative.id)
31
+ end
32
+
21
33
  def broadcast_badges(creative)
22
34
  origin = creative.effective_origin
23
35
  users = [ origin.user ].compact + origin.all_shared_users(:feedback).map(&:user)
@@ -25,74 +37,32 @@ module Collavre
25
37
  users.uniq!
26
38
  return if users.empty?
27
39
 
28
- user_ids = users.map(&:id)
29
-
30
- pointers = CommentReadPointer.where(user_id: user_ids, creative: origin).index_by(&:user_id)
31
- present_user_ids = CommentPresenceStore.list(origin.id)
32
-
33
- public_count = origin.comments.public_only.count
34
- private_counts = origin.comments
35
- .where(private: true, user_id: user_ids)
36
- .group(:user_id)
37
- .count
38
-
39
- last_read_ids = pointers.transform_values { |p| p.last_read_comment_id || 0 }
40
-
41
- unread_public_by_threshold = {}
42
- # Include 0 for users without a read pointer (never opened chat)
43
- all_thresholds = (last_read_ids.values + [ 0 ]).uniq
44
- all_thresholds.each do |threshold|
45
- unread_public_by_threshold[threshold] = origin.comments
46
- .where(private: false)
47
- .where("comments.id > ?", threshold)
48
- .count
49
- end
50
-
51
- unread_private_counts = {}
52
- user_ids.each do |uid|
53
- threshold = last_read_ids[uid] || 0
54
- unread_private_counts[uid] = origin.comments
55
- .where(private: true, user_id: uid)
56
- .where("comments.id > ?", threshold)
57
- .count
58
- end
40
+ badges_by_user_id = Creatives::CommentBadgeIndex.for_users(origin: origin, users: users)
59
41
 
60
42
  users.each do |u|
61
- user_private_count = private_counts[u.id] || 0
62
- total_count = public_count + user_private_count
63
-
64
- threshold = last_read_ids[u.id] || 0
65
- unread_public = unread_public_by_threshold[threshold] || 0
66
- unread_private = unread_private_counts[u.id] || 0
67
- unread_count = unread_public + unread_private
68
-
69
- unread_count = 0 if present_user_ids.include?(u.id)
43
+ badge = badges_by_user_id.fetch(u.id)
70
44
 
71
45
  Turbo::StreamsChannel.broadcast_replace_to(
72
46
  [ u, origin, :comment_badge ],
73
47
  target: "comment-badge-#{origin.id}",
74
48
  partial: "inbox/badge_component/count",
75
49
  locals: {
76
- count: unread_count,
50
+ count: badge.unread_count,
77
51
  badge_id: "comment-badge-#{origin.id}",
78
- show_zero: total_count.positive?
52
+ show_zero: badge.visible_comments
79
53
  }
80
54
  )
81
55
 
82
56
  # Also update the global inbox badge when the creative is an inbox
83
- broadcast_inbox_badge(origin, u, count: unread_count) if origin.inbox?
57
+ broadcast_inbox_badge(origin, u, count: badge.unread_count) if origin.inbox?
84
58
  end
85
59
  end
86
60
 
87
61
  def broadcast_badge(creative, user)
88
62
  origin = creative.effective_origin
89
- visible_comments = origin.comments.visible_to(user)
90
- comments_count = visible_comments.count
91
- pointer = CommentReadPointer.find_by(user: user, creative: origin)
92
- last_read_id = pointer&.last_read_comment_id
93
- unread_scope = last_read_id ? visible_comments.where("comments.id > ?", last_read_id) : visible_comments
94
- unread_count = unread_scope.count
95
- unread_count = 0 if CommentPresenceStore.list(origin.id).include?(user.id)
63
+ badge_index = Creatives::CommentBadgeIndex.new(user: user)
64
+ badge_index.index([ origin ])
65
+ unread_count = badge_index.unread_count_for(origin)
96
66
  Turbo::StreamsChannel.broadcast_replace_to(
97
67
  [ user, origin, :comment_badge ],
98
68
  target: "comment-badge-#{origin.id}",
@@ -100,7 +70,7 @@ module Collavre
100
70
  locals: {
101
71
  count: unread_count,
102
72
  badge_id: "comment-badge-#{origin.id}",
103
- show_zero: comments_count.positive?
73
+ show_zero: badge_index.visible_comments?(origin)
104
74
  }
105
75
  )
106
76
 
@@ -187,7 +157,7 @@ module Collavre
187
157
 
188
158
  def broadcast_badges
189
159
  return unless creative
190
- self.class.broadcast_badges(creative)
160
+ self.class.broadcast_badges_later(creative)
191
161
  end
192
162
  end
193
163
  end
@@ -3,17 +3,21 @@ module Collavre
3
3
  module Notifiable
4
4
  extend ActiveSupport::Concern
5
5
 
6
+ NOTIFICATION_RELEVANT_ATTRIBUTES = %w[
7
+ action approver_id content creative_id private topic_id user_id
8
+ ].freeze
9
+
6
10
  included do
7
- after_create_commit :notify_write_users, :notify_mentions, :notify_approver
11
+ before_update :advance_notification_revision, if: :notification_relevant_change?
12
+ after_create_commit :enqueue_create_notifications, if: :notification_eligible_on_create?
8
13
  end
9
14
 
10
- def mentioned_users
15
+ def mentioned_users(notification_content = content)
11
16
  return Collavre.user_class.none unless user
12
- emails = mentioned_emails - [ user.email.downcase ]
13
- names = mentioned_names - [ user.name.downcase ]
14
17
 
15
- origin = creative.effective_origin
16
- mentionable_users = Collavre.user_class.mentionable_for(origin)
18
+ emails = mentioned_emails(notification_content) - [ user.email.downcase ]
19
+ names = mentioned_names(notification_content) - [ user.name.downcase ]
20
+ mentionable_users = Collavre.user_class.mentionable_for(creative.effective_origin)
17
21
 
18
22
  scope = Collavre.user_class.none
19
23
  scope = scope.or(mentionable_users.where(email: emails)) if emails.any?
@@ -21,54 +25,128 @@ module Collavre
21
25
  scope
22
26
  end
23
27
 
24
- # Notify users about a completed AI streaming message.
25
- # Called from ResponseFinalizer after placeholder is updated with final content.
26
- # Sends both write-user and mention notifications that were skipped at placeholder creation.
27
- # Errors are rescued to avoid breaking the finalization flow.
28
+ def notification_event
29
+ {
30
+ "creative_id" => creative_id,
31
+ "content" => content,
32
+ "regular_notification" => regular_notification_eligible?,
33
+ "approval_notification" => approval_notification_eligible?,
34
+ "revision" => notification_revision
35
+ }
36
+ end
37
+
38
+ # Notify users about a completed AI streaming message without holding up
39
+ # response finalization. Placeholder notifications are skipped on create.
28
40
  def notify_ai_completion
29
41
  return unless user&.ai_user?
30
- return if private?
42
+ return if private? || suppress_inbox_notification?
31
43
 
32
- notify_ai_write_users
33
- notify_ai_mentions
44
+ CommentNotificationJob.perform_later(id, "ai_completion", notification_event)
34
45
  rescue StandardError => e
35
46
  Rails.logger.error("[notify_ai_completion] Failed for comment #{id}: #{e.message}")
36
47
  end
37
48
 
49
+ def deliver_notifications(kind, event)
50
+ event = event.stringify_keys
51
+ return unless event["creative_id"] == creative_id
52
+ return unless event["revision"] == notification_revision
53
+
54
+ notification_content = event.fetch("content", content)
55
+
56
+ case kind.to_s
57
+ when "created"
58
+ if event["regular_notification"]
59
+ notify_write_users("created", notification_content)
60
+ notify_mentions("created", notification_content)
61
+ end
62
+ notify_approver("created") if event["approval_notification"]
63
+ when "ai_completion"
64
+ return unless user&.ai_user?
65
+ return if private? || suppress_inbox_notification?
66
+
67
+ notify_write_users("ai_completion", notification_content)
68
+ notify_mentions("ai_completion", notification_content)
69
+ else
70
+ raise ArgumentError, "Unknown comment notification kind: #{kind}"
71
+ end
72
+ end
73
+
38
74
  private
39
75
 
40
- def create_inbox_comment(owner, key, params = {})
76
+ def notification_relevant_change?
77
+ return false if skip_notification_revision
78
+
79
+ NOTIFICATION_RELEVANT_ATTRIBUTES.any? { |attribute| will_save_change_to_attribute?(attribute) }
80
+ end
81
+
82
+ def advance_notification_revision
83
+ self.notification_revision += 1
84
+ end
85
+
86
+ def notification_eligible_on_create?
87
+ regular_notification_eligible? || approval_notification_eligible?
88
+ end
89
+
90
+ def regular_notification_eligible?
91
+ !private? && !streaming_placeholder? && !suppress_inbox_notification?
92
+ end
93
+
94
+ def approval_notification_eligible?
95
+ approver.present? && approval_action? && !creative&.inbox?
96
+ end
97
+
98
+ def enqueue_create_notifications
99
+ CommentNotificationJob.perform_later(id, "created", notification_event)
100
+ end
101
+
102
+ def create_inbox_comment(owner, key, params = {}, delivery_key: nil)
41
103
  inbox_creative = Creative.inbox_for(owner)
42
104
  return unless inbox_creative
43
105
 
44
106
  system_topic = inbox_creative.system_topic(fallback_user: owner)
45
107
  msg = I18n.t(key, **params.symbolize_keys, locale: owner.locale || "en")
46
-
47
- comment = Comment.create!(
108
+ attributes = {
48
109
  creative: inbox_creative,
49
110
  topic: system_topic,
50
111
  content: msg,
51
112
  user: nil,
52
113
  skip_default_user: true,
53
114
  quoted_comment: self
54
- )
115
+ }
116
+
117
+ if delivery_key
118
+ comment = nil
119
+ delivery = nil
120
+
121
+ Comment.transaction do
122
+ comment = Comment.create_or_find_by!(notification_key: delivery_key) do |candidate|
123
+ candidate.assign_attributes(attributes)
124
+ end
125
+ delivery = CommentNotificationDelivery.create_or_find_by!(delivery_key: delivery_key) do |candidate|
126
+ candidate.assign_attributes(
127
+ inbox_comment_id: comment.id,
128
+ recipient_id: owner.id,
129
+ message: msg,
130
+ link: inbox_comment_link
131
+ )
132
+ end
133
+ end
55
134
 
56
- Comment.broadcast_inbox_badge(inbox_creative, owner)
57
- PushNotificationJob.perform_later(owner.id, message: msg, link: inbox_comment_link)
58
- comment
135
+ delivery.enqueue_push!
136
+ comment
137
+ else
138
+ comment = Comment.create!(attributes)
139
+ PushNotificationJob.perform_later(owner.id, message: msg, link: inbox_comment_link)
140
+ comment
141
+ end
59
142
  rescue StandardError => e
60
143
  Rails.logger.error("[Notifiable] Failed to create inbox comment for user #{owner.id}: #{e.message}")
61
- nil
62
- end
63
-
64
- # Keep legacy method for backward compatibility during transition
65
- def create_inbox_item(owner, key, params = {})
66
- create_inbox_comment(owner, key, params)
144
+ raise
67
145
  end
68
146
 
69
147
  def creative_markdown_link(target_creative = creative)
70
- path = Collavre::Engine.routes.url_helpers.creative_path(target_creative, open_comments: true)
71
- "[#{target_creative.creative_snippet}](#{path})"
148
+ path = Collavre::Engine.routes.url_helpers.creative_path(target_creative, comment_id: id)
149
+ "[#{target_creative.creative_snippet_markdown}](#{path})"
72
150
  end
73
151
 
74
152
  def inbox_comment_link
@@ -83,25 +161,29 @@ module Collavre
83
161
  user&.ai_user? && content == STREAMING_PLACEHOLDER_CONTENT
84
162
  end
85
163
 
86
- def mentioned_emails
87
- return [] unless content
88
- content.scan(/@([\w.\-+]+@[a-zA-Z0-9\-.]+\.[a-zA-Z]{2,})/)
89
- .flatten
90
- .map(&:downcase)
91
- .uniq
164
+ def mentioned_emails(notification_content = content)
165
+ return [] unless notification_content
166
+
167
+ notification_content
168
+ .scan(/@([\w.\-+]+@[a-zA-Z0-9\-.]+\.[a-zA-Z]{2,})/)
169
+ .flatten
170
+ .map(&:downcase)
171
+ .uniq
92
172
  end
93
173
 
94
- def mentioned_names
95
- return [] unless content
96
- content.scan(/@([^:]+):/)
97
- .flatten
98
- .map(&:downcase)
99
- .uniq
174
+ def mentioned_names(notification_content = content)
175
+ return [] unless notification_content
176
+
177
+ notification_content
178
+ .scan(/@([^:]+):/)
179
+ .flatten
180
+ .map(&:downcase)
181
+ .uniq
100
182
  end
101
183
 
102
184
  # Build the list of write-access recipients minus the comment author,
103
185
  # mentioned users, and users currently present on the creative.
104
- def notification_recipients
186
+ def notification_recipients(notification_content = content)
105
187
  base_creative = creative.effective_origin
106
188
  present_ids = CommentPresenceStore.list(base_creative.id)
107
189
 
@@ -110,57 +192,43 @@ module Collavre
110
192
  recipients.compact!
111
193
  recipients.uniq!
112
194
  recipients.delete(user)
113
- recipients -= mentioned_users.to_a
114
- recipients.reject! { |u| present_ids.include?(u.id) }
195
+ recipients -= mentioned_users(notification_content).to_a
196
+ recipients.reject! { |recipient| present_ids.include?(recipient.id) }
115
197
  recipients
116
198
  end
117
199
 
118
- def notify_write_users
119
- return if private? || !user
120
- return if streaming_placeholder?
121
- return if suppress_inbox_notification? # only the System topic is the alarm stream
122
- notification_recipients.each do |recipient|
200
+ def notify_write_users(kind, notification_content)
201
+ return unless user
202
+
203
+ notification_recipients(notification_content).each do |recipient|
123
204
  create_inbox_comment(
124
205
  recipient,
125
206
  "inbox.comment_added",
126
- { user: user.display_name, comment: content, creative: creative_markdown_link }
207
+ {
208
+ user: user.display_name,
209
+ comment: notification_content,
210
+ creative: creative_markdown_link
211
+ },
212
+ delivery_key: notification_delivery_key(kind, "write", recipient)
127
213
  )
128
214
  end
129
215
  end
130
216
 
131
- def notify_mentions
132
- return if private?
133
- return if streaming_placeholder?
134
- return if suppress_inbox_notification? # only the System topic is the alarm stream
135
- notify_mentioned_users
136
- end
137
-
138
- def notify_mentioned_users
139
- mentioned_users.each do |mentioned|
217
+ def notify_mentions(kind, notification_content)
218
+ mentioned_users(notification_content).each do |mentioned|
140
219
  create_inbox_comment(
141
220
  mentioned,
142
221
  "inbox.user_mentioned",
143
- { user: user.display_name, comment: content, creative: creative_markdown_link }
222
+ {
223
+ user: user.display_name,
224
+ comment: notification_content,
225
+ creative: creative_markdown_link
226
+ },
227
+ delivery_key: notification_delivery_key(kind, "mention", mentioned)
144
228
  )
145
229
  end
146
230
  end
147
231
 
148
- def notify_ai_write_users
149
- return if suppress_inbox_notification? # only the System topic is the alarm stream
150
- notification_recipients.each do |recipient|
151
- create_inbox_comment(
152
- recipient,
153
- "inbox.comment_added",
154
- { user: user.display_name, comment: content, creative: creative_markdown_link }
155
- )
156
- end
157
- end
158
-
159
- def notify_ai_mentions
160
- return if suppress_inbox_notification? # only the System topic is the alarm stream
161
- notify_mentioned_users
162
- end
163
-
164
232
  # #1301 made every inbox topic EXCEPT System dispatch like a normal topic;
165
233
  # the alarm stream follows suit. Suppress notifications ONLY for the System
166
234
  # topic itself (its system-authored notices must not cascade into more
@@ -171,7 +239,11 @@ module Collavre
171
239
  creative&.inbox? && inbox_system_topic?
172
240
  end
173
241
 
174
- def notify_approver
242
+ def notification_delivery_key(kind, recipient_type, recipient)
243
+ "comment:#{id}:#{notification_revision}:#{kind}:#{recipient_type}:recipient:#{recipient.id}"
244
+ end
245
+
246
+ def notify_approver(kind)
175
247
  return unless approver.present? && approval_action?
176
248
  return if approver == user
177
249
  return if creative&.inbox? # Don't notify about inbox comments
@@ -179,7 +251,12 @@ module Collavre
179
251
  create_inbox_comment(
180
252
  approver,
181
253
  "inbox.approval_requested",
182
- { user: user&.display_name, tool_name: parsed_action_tool_name, creative: creative_markdown_link }
254
+ {
255
+ user: user&.display_name,
256
+ tool_name: parsed_action_tool_name,
257
+ creative: creative_markdown_link
258
+ },
259
+ delivery_key: notification_delivery_key(kind, "approver", approver)
183
260
  )
184
261
  end
185
262
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Collavre
4
+ class Comment
5
+ module TopicMembership
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ before_validation :assign_main_topic, on: :create
10
+ before_validation :stamp_topic_assignment
11
+ before_create :lock_matching_topic
12
+ end
13
+
14
+ private
15
+
16
+ def assign_main_topic
17
+ return if topic_id.present?
18
+ return unless creative
19
+
20
+ fallback = user || Collavre.current_user || creative.user
21
+ self.topic = creative.main_topic(fallback_user: fallback)
22
+ end
23
+
24
+ # Paging snapshots topic membership separately from comment ids. Content
25
+ # edits leave this timestamp alone; topic changes advance it.
26
+ def stamp_topic_assignment
27
+ return unless will_save_change_to_topic_id? || topic_assigned_at.nil?
28
+
29
+ self.topic_assigned_at = Time.current
30
+ end
31
+
32
+ def lock_matching_topic
33
+ locked_topic = Topic.where(id: topic_id).lock.first
34
+ return if locked_topic&.creative_id == creative_id
35
+
36
+ errors.add(:topic, I18n.t("collavre.comments.invalid_topic"))
37
+ throw :abort
38
+ end
39
+ end
40
+ end
41
+ end