agentbox-app 0.1.13 → 0.1.16

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 (278) hide show
  1. package/README.md +24 -9
  2. package/bin/agentbox.mjs +23 -1
  3. package/briefs/message-rules.md +36 -8
  4. package/briefs/worker.md +46 -23
  5. package/main/account-discovery.mjs +122 -30
  6. package/main/account-tooling.mjs +29 -1
  7. package/main/account-usage.mjs +46 -0
  8. package/main/agent-sessions.mjs +282 -268
  9. package/main/agents.mjs +179 -466
  10. package/main/analytics.mjs +12 -2
  11. package/main/answer-modes.mjs +103 -37
  12. package/main/approval-prompt-core.mjs +197 -0
  13. package/main/approval-prompt-server.mjs +4 -138
  14. package/main/approvals.mjs +101 -297
  15. package/main/artifact-path.mjs +47 -18
  16. package/main/claude-input.mjs +48 -4
  17. package/main/claude-plan.mjs +48 -0
  18. package/main/claude-session-open.mjs +28 -0
  19. package/main/claude-usage-process.mjs +40 -0
  20. package/main/claude-usage.mjs +18 -40
  21. package/main/cloud/account.mjs +179 -0
  22. package/main/cloud/aws.mjs +127 -0
  23. package/main/cloud/browser-return.mjs +58 -0
  24. package/main/cloud/clients.mjs +15 -0
  25. package/main/cloud/codex-cloud.mjs +53 -0
  26. package/main/cloud/config.mjs +107 -0
  27. package/main/cloud/docs.mjs +219 -0
  28. package/main/cloud/exec.mjs +27 -0
  29. package/main/cloud/host.mjs +165 -0
  30. package/main/cloud/kit.mjs +80 -0
  31. package/main/cloud/logins.mjs +543 -0
  32. package/main/cloud/managed.mjs +115 -0
  33. package/main/cloud/marker.mjs +104 -0
  34. package/main/cloud/org.mjs +53 -0
  35. package/main/cloud/reuse.mjs +208 -0
  36. package/main/cloud/run-cli.mjs +177 -0
  37. package/main/cloud/run.mjs +1934 -0
  38. package/main/cloud/usage.mjs +76 -0
  39. package/main/cloud-permission.mjs +51 -0
  40. package/main/codex-account.mjs +78 -0
  41. package/main/codex-app-server.mjs +276 -409
  42. package/main/codex-approvals.mjs +148 -433
  43. package/main/codex-models.mjs +20 -2
  44. package/main/codex-session.mjs +212 -507
  45. package/main/codex-threads-worker.mjs +12 -0
  46. package/main/codex-threads.mjs +71 -3
  47. package/main/codex-usage-file.mjs +5 -3
  48. package/main/codex-usage.mjs +56 -7
  49. package/main/codex-watch.mjs +17 -3
  50. package/main/codex.mjs +95 -363
  51. package/main/config.mjs +11 -1
  52. package/main/corner-tag.mjs +340 -19
  53. package/main/dev-window.mjs +1 -1
  54. package/main/engine-bin.mjs +232 -282
  55. package/main/engine-setup.mjs +21 -9
  56. package/main/feedback.mjs +3 -25
  57. package/main/folder-pictures.mjs +2 -2
  58. package/main/freeze-recorder-thread.mjs +86 -0
  59. package/main/freeze-recorder.mjs +146 -0
  60. package/main/fresh-user.mjs +20 -9
  61. package/main/git-change-offthread.mjs +17 -3
  62. package/main/git-change-thread.mjs +5 -2
  63. package/main/harnesses/claude.mjs +32 -11
  64. package/main/harnesses/codex.mjs +9 -2
  65. package/main/importable-threads.mjs +64 -6
  66. package/main/ipc.mjs +362 -111
  67. package/main/leftovers.mjs +117 -25
  68. package/main/liquid-glass.mjs +26 -0
  69. package/main/live-replies.mjs +10 -4
  70. package/main/main-went-red.mjs +330 -0
  71. package/main/main.mjs +199 -84
  72. package/main/memory-gate-history.mjs +36 -10
  73. package/main/memory-gate-server.mjs +62 -0
  74. package/main/memory-gate.mjs +14 -1
  75. package/main/menu.mjs +4 -2
  76. package/main/message-priority.mjs +2 -2
  77. package/main/mic.mjs +38 -0
  78. package/main/native/liquid-glass/build/liquid-glass.node +0 -0
  79. package/main/new-chats-watch.mjs +110 -0
  80. package/main/new-user-window.mjs +25 -0
  81. package/main/notify.mjs +24 -8
  82. package/main/phone-alerts.mjs +117 -0
  83. package/main/phone-inbox.mjs +625 -0
  84. package/main/phone-link.mjs +29 -9
  85. package/main/phone-uploads.mjs +476 -0
  86. package/main/picture-shape.mjs +114 -0
  87. package/main/quit-sequence.mjs +143 -0
  88. package/main/refresh-agent-models.mjs +15 -2
  89. package/main/relay-client.mjs +69 -1
  90. package/main/repeats.mjs +10 -4
  91. package/main/row-label.mjs +12 -41
  92. package/main/run-change.mjs +13 -0
  93. package/main/run-ledger.mjs +135 -0
  94. package/main/save-attachments.mjs +41 -0
  95. package/main/serve.mjs +45 -3
  96. package/main/settings.mjs +220 -44
  97. package/main/shell-path.mjs +185 -37
  98. package/main/ship-conflict-brief.mjs +119 -0
  99. package/main/ship-queue.mjs +115 -19
  100. package/main/source-updater.mjs +18 -10
  101. package/main/stage-attachment.mjs +68 -0
  102. package/main/stall-watch.mjs +50 -0
  103. package/main/store/home.mjs +87 -232
  104. package/main/store/paths.mjs +11 -0
  105. package/main/store/teammates-files.mjs +99 -0
  106. package/main/store/work-items.mjs +231 -13
  107. package/main/store.mjs +226 -52
  108. package/main/supervisor.mjs +3103 -341
  109. package/main/task-folders.mjs +430 -30
  110. package/main/task-terminals.mjs +110 -34
  111. package/main/team/index.mjs +114 -10
  112. package/main/team/memory-cloud.mjs +183 -1
  113. package/main/team/room-hours.mjs +48 -0
  114. package/main/team/room-setup.mjs +31 -0
  115. package/main/team/rooms-memory.mjs +454 -0
  116. package/main/team/rooms-shape.mjs +42 -0
  117. package/main/team/rooms-supabase.mjs +324 -0
  118. package/main/team/rooms.mjs +575 -0
  119. package/main/team/session.mjs +96 -26
  120. package/main/team/supabase-backend.mjs +180 -0
  121. package/main/team/sync.mjs +109 -1
  122. package/main/teammates.mjs +580 -0
  123. package/main/terminal-host.mjs +143 -0
  124. package/main/terminal-hosts.mjs +261 -0
  125. package/main/transcript-search.mjs +80 -0
  126. package/main/webview-guard.mjs +45 -0
  127. package/main/worktree-include.mjs +90 -219
  128. package/mcp/core/documents.mjs +3 -1
  129. package/mcp/core/repeats.mjs +88 -0
  130. package/mcp/core/teammates.mjs +222 -0
  131. package/mcp/core/work.mjs +76 -4
  132. package/mcp/stdio.mjs +24 -11
  133. package/mcp/tools.mjs +116 -8
  134. package/package.json +21 -5
  135. package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
  136. package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
  137. package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
  138. package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
  139. package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
  140. package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
  141. package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
  142. package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
  143. package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
  144. package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
  145. package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
  146. package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
  147. package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
  148. package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
  149. package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
  150. package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
  151. package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
  152. package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
  153. package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
  154. package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
  155. package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
  156. package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
  157. package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
  158. package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
  159. package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
  160. package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
  161. package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
  162. package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
  163. package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
  164. package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
  165. package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
  166. package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
  167. package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
  168. package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
  169. package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
  170. package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
  171. package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
  172. package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
  173. package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
  174. package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
  175. package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
  176. package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
  177. package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
  178. package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
  179. package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
  180. package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
  181. package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
  182. package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
  183. package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
  184. package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
  185. package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
  186. package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
  187. package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
  188. package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
  189. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
  190. package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
  191. package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
  192. package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
  193. package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
  194. package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
  195. package/renderer/dist/corner-tag.html +3 -3
  196. package/renderer/dist/index.html +3 -3
  197. package/scripts/after-install.mjs +30 -8
  198. package/scripts/cloud-shim.pl +57 -0
  199. package/shared/account-names.d.mts +6 -0
  200. package/shared/account-names.mjs +15 -0
  201. package/shared/agent-hours.mjs +136 -0
  202. package/shared/agent-import.mjs +60 -34
  203. package/shared/agents.mjs +126 -522
  204. package/shared/analytics-events.mjs +12 -0
  205. package/shared/approval-card.mjs +36 -0
  206. package/shared/bridge-map.mjs +27 -1
  207. package/shared/claude-code-shape.mjs +6 -0
  208. package/shared/claude-usage.d.mts +1 -0
  209. package/shared/claude-usage.mjs +42 -1
  210. package/shared/cloud-hand-back.d.mts +7 -0
  211. package/shared/cloud-hand-back.mjs +20 -0
  212. package/shared/cloud-kit.d.mts +219 -0
  213. package/shared/cloud-kit.mjs +341 -0
  214. package/shared/cloud-login-name.mjs +4 -0
  215. package/shared/cloud-start.mjs +17 -0
  216. package/shared/cloud-versions.mjs +23 -0
  217. package/shared/codex-import.mjs +2 -2
  218. package/shared/codex-modes.d.mts +22 -9
  219. package/shared/codex-modes.mjs +67 -11
  220. package/shared/codex-usage.mjs +108 -2
  221. package/shared/corner-tag.d.mts +23 -1
  222. package/shared/corner-tag.mjs +284 -29
  223. package/shared/dead-run-trace.mjs +19 -2
  224. package/shared/engines.mjs +70 -309
  225. package/shared/feedback.d.mts +7 -0
  226. package/shared/feedback.mjs +25 -0
  227. package/shared/first-run-practice.mjs +17 -24
  228. package/shared/harness-definitions.mjs +8 -0
  229. package/shared/instruction-defaults.json +2 -2
  230. package/shared/key-caps.d.mts +14 -0
  231. package/shared/key-caps.mjs +68 -0
  232. package/shared/message-edits.d.mts +7 -0
  233. package/shared/message-edits.mjs +35 -0
  234. package/shared/move-thread.d.mts +24 -0
  235. package/shared/move-thread.mjs +112 -0
  236. package/shared/named-threads.mjs +72 -0
  237. package/shared/not-shipped.d.mts +22 -0
  238. package/shared/not-shipped.mjs +89 -0
  239. package/shared/notify-rules.d.mts +6 -0
  240. package/shared/notify-rules.mjs +36 -8
  241. package/shared/phone-files.mjs +28 -0
  242. package/shared/phone-settings.mjs +118 -0
  243. package/shared/phone-snapshot.d.mts +16 -0
  244. package/shared/phone-snapshot.mjs +348 -0
  245. package/shared/picture-formats.d.mts +1 -0
  246. package/shared/picture-formats.mjs +13 -0
  247. package/shared/rank.d.mts +2 -1
  248. package/shared/rank.mjs +32 -19
  249. package/shared/repeats.d.mts +8 -0
  250. package/shared/repeats.mjs +33 -2
  251. package/shared/room-cards.mjs +167 -0
  252. package/shared/room-links.d.mts +4 -0
  253. package/shared/room-links.mjs +21 -0
  254. package/shared/runs-on.d.mts +3 -3
  255. package/shared/runs-on.mjs +8 -5
  256. package/shared/shipped-note.d.mts +4 -0
  257. package/shared/shipped-note.mjs +6 -0
  258. package/shared/shortcuts.mjs +14 -6
  259. package/shared/spawn-trouble.d.mts +4 -0
  260. package/shared/spawn-trouble.mjs +443 -40
  261. package/shared/team-rules.mjs +9 -2
  262. package/shared/teammates.d.mts +19 -0
  263. package/shared/teammates.mjs +756 -0
  264. package/shared/terminal-focus.d.mts +1 -0
  265. package/shared/terminal-focus.mjs +13 -0
  266. package/shared/thread-cards.mjs +4 -1
  267. package/shared/usage.d.mts +9 -0
  268. package/shared/usage.mjs +49 -3
  269. package/shared/websites.d.mts +15 -0
  270. package/shared/websites.mjs +61 -0
  271. package/shared/work-items.mjs +127 -30
  272. package/shared/work-lines.mjs +92 -244
  273. package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
  274. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
  275. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
  276. package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
  277. package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
  278. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
@@ -0,0 +1,454 @@
1
+ // ROOMS IN MEMORY (w-3d0fa23ffb), with the rules of
2
+ // cloud/supabase/migrations/20261021000000_rooms.sql, so the app's rooms can
3
+ // be tested with several people and no network. tests/rooms-backends-agree
4
+ // runs one scenario against this and against a real database.
5
+ import crypto from 'node:crypto';
6
+ import { PROFILE_FIELDS, PRESENCE_FIELDS, pick, roomOut, messageOut, reactionOut, ROOM_FILE_NAME, ROOM_FILE_MAX_DATA, ROOM_FILE_MAX_BYTES, fileExists } from './rooms-shape.mjs';
7
+
8
+ export const GENERAL_ROOM = '9e9e9e9e-0000-4000-8000-000000000001';
9
+ const MIN = 60_000;
10
+ const HOUR = 60 * MIN;
11
+ const DAY = 24 * HOUR;
12
+ const ATTACHMENT = /\]\(attachments\/([A-Za-z0-9][A-Za-z0-9_.-]{0,159})\)/g;
13
+ // 1 to 4000 characters once trimmed, counted as Postgres's char_length counts:
14
+ // an emoji is one character, not the two halves JavaScript's length sees.
15
+ const fitsAMessage = (text) => { const n = [...String(text).trim()].length; return n >= 1 && n <= 4000; };
16
+
17
+ const PROFILE_DEFAULTS = { display_name: '', avatar_url: null, headline: null, bio: null, show_working: 'team', show_agents: true, show_level: true, show_status: true, show_activity: true, invisible_until: null, status_text: null, status_until: null };
18
+ const PRESENCE_DEFAULTS = { gone: false, agents_running: 0, working_on: null, working_private: false, working_at: null, hours_total: 0, hours_week: 0, week_start: null, hours_today: 0, day: null, streak_days: 0, active_days: [], hours_by_month: [], activity: [] };
19
+
20
+ function state(cloud) {
21
+ cloud.rooms ??= new Map();
22
+ cloud.roomMembers ??= [];
23
+ cloud.roomLinks ??= [];
24
+ cloud.roomInvites ??= [];
25
+ cloud.roomProfiles ??= new Map();
26
+ cloud.roomPresence ??= new Map();
27
+ cloud.roomMessages ??= [];
28
+ cloud.roomReads ??= new Map();
29
+ cloud.roomReactions ??= new Map(); // `${messageId}:${personId}:${emoji}` -> reactionOut row
30
+ cloud.roomFiles ??= new Map(); // `${roomId}:${name}` -> { roomId, name, byPerson, data, createdAt }
31
+ cloud.roomActs ??= []; // { personId, kind, at }: edits and reactions, for their limits
32
+ if (!cloud.rooms.has(GENERAL_ROOM)) cloud.rooms.set(GENERAL_ROOM, { id: GENERAL_ROOM, name: 'Coworking', kind: 'room', teamId: null, listed: true, createdBy: null, createdAt: 0 });
33
+ // Every team has its room, named after it (the team_room_kept trigger).
34
+ for (const t of cloud.teams.values()) {
35
+ const room = [...cloud.rooms.values()].find((r) => r.teamId === t.id);
36
+ if (!room) cloud.rooms.set(crypto.randomUUID(), { id: null, name: String(t.name).slice(0, 40), kind: 'room', teamId: t.id, listed: false, createdBy: t.createdBy ?? null, createdAt: cloud.now() });
37
+ else room.name = String(t.name).slice(0, 40);
38
+ }
39
+ for (const [id, r] of cloud.rooms) r.id ??= id;
40
+ return cloud;
41
+ }
42
+
43
+ export function roomsMemory(cloud, personId, { confirmedEmail, refuse }) {
44
+ const now = () => cloud.now();
45
+ const iso = (ms) => new Date(ms).toISOString();
46
+ const emit = (e) => cloud.events.emit('rooms', e);
47
+
48
+ const roomsOf = (p) => {
49
+ const s = state(cloud);
50
+ const teams = new Set(s.members.filter((m) => m.personId === p).map((m) => m.teamId));
51
+ const out = new Set(s.roomMembers.filter((m) => m.personId === p).map((m) => m.roomId));
52
+ for (const r of s.rooms.values()) if (r.teamId && teams.has(r.teamId)) out.add(r.id);
53
+ return out;
54
+ };
55
+ const inRoom = (r) => roomsOf(personId).has(r);
56
+ const sharesRoom = (p) => { const mine = roomsOf(personId); return [...roomsOf(p)].some((r) => mine.has(r)); };
57
+ const peopleOf = (r) => {
58
+ const s = state(cloud);
59
+ const room = s.rooms.get(r);
60
+ const out = new Set(s.roomMembers.filter((m) => m.roomId === r).map((m) => m.personId));
61
+ if (room?.teamId) for (const m of s.members) if (m.teamId === room.teamId) out.add(m.personId);
62
+ return out;
63
+ };
64
+ const ownsRoom = (r) => {
65
+ const s = state(cloud);
66
+ if (s.roomMembers.some((m) => m.roomId === r && m.personId === personId && m.role === 'owner')) return true;
67
+ const room = s.rooms.get(r);
68
+ return !!room?.teamId && s.members.some((m) => m.teamId === room.teamId && m.personId === personId && m.role === 'owner');
69
+ };
70
+ const visible = (p) => { const until = Date.parse(state(cloud).roomProfiles.get(p)?.invisible_until ?? ''); return !(until > now()); };
71
+ const limit = (what, used, n) => { if (used >= n) refuse(`too many ${what} for now`); };
72
+ const act = (what, n, per) => {
73
+ const s = state(cloud);
74
+ limit(what, s.roomActs.filter((a) => a.personId === personId && a.kind === what && a.at > now() - per).length, n);
75
+ s.roomActs = s.roomActs.filter((a) => !(a.personId === personId && a.at < now() - HOUR));
76
+ s.roomActs.push({ personId, kind: what, at: now() });
77
+ };
78
+ const copy = (m) => ({ ...m, mentions: [...m.mentions] });
79
+ // A message I may change: in a room I am in, and mine.
80
+ const myMessage = (id, what) => {
81
+ const m = state(cloud).roomMessages.find((x) => x.id === id);
82
+ if (!m || !inRoom(m.roomId)) refuse(`${what} a message in a room you are not in`);
83
+ if (m.by !== personId) refuse(`${what} somebody else's message`);
84
+ return m;
85
+ };
86
+
87
+ const join = (r) => {
88
+ const s = state(cloud);
89
+ if (inRoom(r)) return r;
90
+ limit('joins', s.roomMembers.filter((m) => m.personId === personId && m.joinedAt > now() - HOUR).length, 30);
91
+ if (peopleOf(r).size >= 500) refuse('join a full room');
92
+ s.roomMembers.push({ roomId: r, personId, role: 'member', joinedAt: now() });
93
+ emit({ kind: 'membership', roomId: r });
94
+ return r;
95
+ };
96
+
97
+ const dropMember = (r, p) => {
98
+ const s = state(cloud);
99
+ const row = s.roomMembers.find((m) => m.roomId === r && m.personId === p);
100
+ if (!row) return;
101
+ s.roomMembers = s.roomMembers.filter((m) => m !== row);
102
+ const room = s.rooms.get(r);
103
+ if (room && !room.teamId) {
104
+ const left = s.roomMembers.filter((m) => m.roomId === r);
105
+ if (!left.length) { if (!room.listed) deleteRoom(r); }
106
+ else if (row.role === 'owner' && !left.some((m) => m.role === 'owner')) {
107
+ left.sort((a, b) => a.joinedAt - b.joinedAt || a.personId.localeCompare(b.personId))[0].role = 'owner';
108
+ }
109
+ }
110
+ emit({ kind: 'membership', roomId: r });
111
+ };
112
+
113
+ const deleteRoom = (r) => {
114
+ const s = state(cloud);
115
+ s.rooms.delete(r);
116
+ s.roomMembers = s.roomMembers.filter((m) => m.roomId !== r);
117
+ s.roomLinks = s.roomLinks.filter((l) => l.roomId !== r);
118
+ s.roomInvites = s.roomInvites.filter((i) => i.roomId !== r);
119
+ s.roomMessages = s.roomMessages.filter((m) => m.roomId !== r);
120
+ for (const [k, x] of s.roomReactions) if (x.roomId === r) s.roomReactions.delete(k);
121
+ for (const [k, f] of s.roomFiles) if (f.roomId === r) s.roomFiles.delete(k);
122
+ };
123
+
124
+ const room = (r) => state(cloud).rooms.get(r);
125
+ const liveLink = (r) => state(cloud).roomLinks.find((l) => l.roomId === r && l.createdBy === personId && !l.revokedAt);
126
+ const newLink = (r) => { const l = { token: crypto.randomUUID().replaceAll('-', ''), roomId: r, createdBy: personId, createdAt: now(), revokedAt: null }; state(cloud).roomLinks.push(l); return l.token; };
127
+ const faces = (ids, n) => ids.map((p) => state(cloud).roomProfiles.get(p)).filter(Boolean)
128
+ .sort((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at)).slice(0, n)
129
+ .map((p) => ({ name: p.display_name, avatar: p.avatar_url }));
130
+ const freshAgents = (ids) => [...state(cloud).roomPresence.values()]
131
+ .filter((p) => ids.includes(p.person_id) && !p.gone && Date.parse(p.updated_at) > now() - 15 * MIN);
132
+
133
+ return {
134
+ async myRooms() {
135
+ const s = state(cloud);
136
+ return [...roomsOf(personId)].map((id) => s.rooms.get(id)).filter(Boolean)
137
+ .sort((a, b) => (a.teamId ? 0 : 1) - (b.teamId ? 0 : 1) || a.createdAt - b.createdAt)
138
+ .map((r) => roomOut({ id: r.id, name: r.name, kind: r.kind, team_id: r.teamId, listed: r.listed, invite_only: !!r.inviteOnly, role: ownsRoom(r.id) ? 'owner' : 'member', members: peopleOf(r.id).size, created_at: iso(r.createdAt) }));
139
+ },
140
+
141
+ async roomPeople(r) {
142
+ return inRoom(r) ? [...peopleOf(r)] : [];
143
+ },
144
+
145
+ async roomNames(r) {
146
+ if (!inRoom(r)) return [];
147
+ const s = state(cloud);
148
+ return [...peopleOf(r)].map((p) => {
149
+ const card = s.roomProfiles.get(p);
150
+ const person = cloud.people?.get(p);
151
+ return { person_id: p, name: card?.display_name || person?.name || '', avatar_url: card?.avatar_url ?? person?.avatarUrl ?? null, has_card: !!card };
152
+ });
153
+ },
154
+
155
+ async roomProfiles(ids) {
156
+ const s = state(cloud);
157
+ return (ids ?? []).map((p) => s.roomProfiles.get(p))
158
+ .filter((row) => row && (row.person_id === personId || (sharesRoom(row.person_id) && visible(row.person_id))))
159
+ .map((row) => structuredClone(row));
160
+ },
161
+
162
+ async roomPresence(ids) {
163
+ const want = new Set(ids ?? []);
164
+ return [...state(cloud).roomPresence.values()]
165
+ .filter((row) => want.has(row.person_id))
166
+ .filter((row) => row.person_id === personId || (!row.gone && sharesRoom(row.person_id) && visible(row.person_id)))
167
+ .map((row) => structuredClone(row));
168
+ },
169
+
170
+ async myRoomProfile() {
171
+ const row = state(cloud).roomProfiles.get(personId);
172
+ return row ? structuredClone(row) : null;
173
+ },
174
+
175
+ async saveRoomProfile(fields) {
176
+ const s = state(cloud);
177
+ const row = { ...PROFILE_DEFAULTS, ...(s.roomProfiles.get(personId) ?? {}), ...pick(fields, PROFILE_FIELDS), person_id: personId, updated_at: iso(now()) };
178
+ if (String(row.display_name).length > 40) refuse('a name that long');
179
+ if (!['none', 'team', 'everyone'].includes(row.show_working)) refuse('that choice of who sees your work');
180
+ s.roomProfiles.set(personId, row);
181
+ emit({ kind: 'profile' });
182
+ return structuredClone(row);
183
+ },
184
+
185
+ async putPresence(device, fields) {
186
+ if (!/^[0-9a-f]{12}$/.test(String(device))) refuse('presence for no computer');
187
+ const s = state(cloud);
188
+ const key = `${personId}:${device}`;
189
+ const row = { ...PRESENCE_DEFAULTS, ...(s.roomPresence.get(key) ?? {}), ...pick(fields, PRESENCE_FIELDS), person_id: personId, device, updated_at: iso(now()) };
190
+ if (row.working_on != null && String(row.working_on).length > 64) refuse('a thread name that long');
191
+ if ((row.active_days ?? []).length > 60) refuse('that many days');
192
+ if (!Array.isArray(row.activity) || row.activity.length > 36) refuse('that much activity');
193
+ s.roomPresence.set(key, row);
194
+ emit({ kind: 'presence' });
195
+ },
196
+
197
+ async roomMessages(r, { before = null, limit: n = 200 } = {}) {
198
+ if (!inRoom(r)) return [];
199
+ return state(cloud).roomMessages
200
+ .filter((m) => m.roomId === r && (before == null || m.at < before))
201
+ .slice(-n).map(copy);
202
+ },
203
+
204
+ async roomMentions({ limit: n = 50 } = {}) {
205
+ return state(cloud).roomMessages.filter((m) => !m.deleted && m.mentions.includes(personId) && inRoom(m.roomId))
206
+ .slice(-n).reverse().map(copy);
207
+ },
208
+
209
+ async sendRoomMessage(r, { body, inReplyTo = null, mentions = [] }) {
210
+ const s = state(cloud);
211
+ if (!inRoom(r)) refuse('write in a room you are not in');
212
+ const text = String(body ?? '');
213
+ if (!fitsAMessage(text)) refuse('a message that empty or that long');
214
+ limit('messages', s.roomMessages.filter((m) => m.by === personId && m.at > now() - MIN).length, 20);
215
+ if (inReplyTo && !s.roomMessages.some((m) => m.id === inReplyTo && m.roomId === r)) refuse('a reply to a message in another room');
216
+ const people = peopleOf(r);
217
+ const msg = messageOut({ id: crypto.randomUUID(), room_id: r, by_person: personId, body: text, in_reply_to: inReplyTo, mentions: [...new Set(mentions)].filter((p) => people.has(p)), created_at: iso(now()) });
218
+ s.roomMessages.push(msg);
219
+ emit({ kind: 'message', message: { ...msg }, roomId: r });
220
+ return { ...msg, mentions: [...msg.mentions] };
221
+ },
222
+
223
+ async editRoomMessage(id, { body, mentions = [] } = {}) {
224
+ const m = myMessage(id, 'edit');
225
+ if (m.deleted) refuse('edit a deleted message');
226
+ const text = String(body ?? '');
227
+ if (!fitsAMessage(text)) refuse('a message that empty or that long');
228
+ act('edits', 20, MIN);
229
+ const people = peopleOf(m.roomId);
230
+ Object.assign(m, { body: text, editedAt: now(), mentions: [...new Set(mentions ?? [])].filter((p) => people.has(p)) });
231
+ emit({ kind: 'message', message: copy(m), roomId: m.roomId });
232
+ return copy(m);
233
+ },
234
+
235
+ async deleteRoomMessage(id) {
236
+ const s = state(cloud);
237
+ const m = myMessage(id, 'delete');
238
+ if (m.deleted) return;
239
+ const named = new Set([...m.body.matchAll(ATTACHMENT)].map((x) => x[1]));
240
+ Object.assign(m, { body: '', mentions: [], deleted: true });
241
+ for (const [k, x] of s.roomReactions) if (x.messageId === id) s.roomReactions.delete(k);
242
+ for (const [k, f] of s.roomFiles) if (f.roomId === m.roomId && f.byPerson === personId && named.has(f.name)) s.roomFiles.delete(k);
243
+ emit({ kind: 'message', message: copy(m), roomId: m.roomId });
244
+ },
245
+
246
+ async roomReactions(r) {
247
+ if (!inRoom(r)) return [];
248
+ return [...state(cloud).roomReactions.values()].filter((x) => x.roomId === r)
249
+ .sort((a, b) => a.at - b.at).slice(-1000).map((x) => ({ ...x }));
250
+ },
251
+
252
+ async reactRoomMessage(id, emoji, on = true) {
253
+ const s = state(cloud);
254
+ const m = s.roomMessages.find((x) => x.id === id);
255
+ if (!m || !inRoom(m.roomId)) refuse('react in a room you are not in');
256
+ if (m.deleted) refuse('react to a deleted message');
257
+ const e = String(emoji ?? '');
258
+ const n = [...e].length;
259
+ if (n < 1 || n > 16 || !/[^ ]/.test(e)) refuse('a reaction that is not one emoji');
260
+ act('reactions', 60, MIN);
261
+ const key = `${id}:${personId}:${e}`;
262
+ if (!on && !s.roomReactions.has(key)) return;
263
+ const row = reactionOut({ message_id: id, room_id: m.roomId, person_id: personId, emoji: e, is_on: !!on, updated_at: iso(now()) });
264
+ s.roomReactions.set(key, row);
265
+ emit({ kind: 'reaction', reaction: { ...row } });
266
+ },
267
+
268
+ async putRoomFile(r, name, data) {
269
+ const s = state(cloud);
270
+ const n = String(name ?? '');
271
+ const d = String(data ?? '');
272
+ limit('files', [...s.roomFiles.values()].filter((f) => f.byPerson === personId && f.createdAt > now() - 10 * MIN).length, 30);
273
+ if (!inRoom(r)) refuse('send a file to a room you are not in');
274
+ if (!ROOM_FILE_NAME.test(n)) refuse('a file name that is not a plain name');
275
+ if (d.length > ROOM_FILE_MAX_DATA || Buffer.from(d, 'base64').length > ROOM_FILE_MAX_BYTES) refuse('a file that big');
276
+ if (s.roomFiles.has(`${r}:${n}`)) throw fileExists(n);
277
+ s.roomFiles.set(`${r}:${n}`, { roomId: r, name: n, byPerson: personId, data: d, createdAt: now() });
278
+ return n;
279
+ },
280
+
281
+ async roomFile(r, name) {
282
+ if (!inRoom(r)) return null;
283
+ return state(cloud).roomFiles.get(`${r}:${name}`)?.data ?? null;
284
+ },
285
+
286
+ async roomReads() {
287
+ return [...state(cloud).roomReads].filter(([k]) => k.endsWith(`:${personId}`)).map(([k, readAt]) => ({ roomId: k.split(':')[0], readAt }));
288
+ },
289
+
290
+ async markRoomRead(r, at) {
291
+ if (!inRoom(r)) refuse('read a room you are not in');
292
+ const key = `${r}:${personId}`;
293
+ const s = state(cloud);
294
+ s.roomReads.set(key, Math.max(s.roomReads.get(key) ?? 0, Math.min(at, now())));
295
+ },
296
+
297
+ // Unlike markRoomRead, this may move the read point back.
298
+ async markRoomUnread(r, at) {
299
+ if (!inRoom(r)) refuse('mark unread a room you are not in');
300
+ if (at == null || !Number.isFinite(Number(at))) refuse('mark a room unread from no message');
301
+ state(cloud).roomReads.set(`${r}:${personId}`, Math.min(Number(at), now()));
302
+ },
303
+
304
+ async roomLink(r) {
305
+ if (!inRoom(r)) refuse('make a link to a room you are not in');
306
+ if (room(r)?.kind === 'group') refuse('a link to a group');
307
+ if (room(r)?.inviteOnly) refuse('a link to a room that takes only people its members invite');
308
+ return liveLink(r)?.token ?? newLink(r);
309
+ },
310
+
311
+ async revokeRoomLink(r) {
312
+ if (!inRoom(r)) refuse('turn off a link to a room you are not in');
313
+ for (const l of state(cloud).roomLinks) if (l.roomId === r && !l.revokedAt && (l.createdBy === personId || ownsRoom(r))) l.revokedAt = now();
314
+ return newLink(r);
315
+ },
316
+
317
+ async joinRoomByLink(token) {
318
+ const l = state(cloud).roomLinks.find((x) => x.token === token && !x.revokedAt);
319
+ if (!l) refuse('an invite link that does not work any more');
320
+ if (room(l.roomId)?.kind === 'group') refuse('join a group by link');
321
+ if (room(l.roomId)?.inviteOnly) refuse('join by link a room that takes only people its members invite');
322
+ return join(l.roomId);
323
+ },
324
+
325
+ async joinListedRoom(r) {
326
+ if (!room(r)?.listed) refuse('join a room not open to everyone');
327
+ return join(r);
328
+ },
329
+
330
+ async listedRooms() {
331
+ return [...state(cloud).rooms.values()].filter((r) => r.listed).map((r) => {
332
+ const ids = [...peopleOf(r.id)];
333
+ const members = new Set(state(cloud).roomMembers.filter((m) => m.roomId === r.id).map((m) => m.personId));
334
+ return {
335
+ id: r.id, name: r.name, members: ids.length,
336
+ hereNow: new Set(freshAgents([...members]).filter((p) => visible(p.person_id)).map((p) => p.person_id)).size,
337
+ joined: inRoom(r.id), faces: faces([...members].filter(visible), 4), createdAt: r.createdAt ?? 0,
338
+ };
339
+ }).sort((a, b) => b.members - a.members || a.createdAt - b.createdAt);
340
+ },
341
+
342
+ async roomPreview(token) {
343
+ const l = state(cloud).roomLinks.find((x) => x.token === token && !x.revokedAt);
344
+ const r = l && room(l.roomId);
345
+ if (!r || r.inviteOnly) return null;
346
+ const ids = [...peopleOf(r.id)];
347
+ const shown = ids.filter(visible);
348
+ const by = ids.includes(l.createdBy) ? state(cloud).roomProfiles.get(l.createdBy) : null;
349
+ return {
350
+ name: r.name, members: ids.length, agentsRunning: freshAgents(shown).reduce((n, p) => n + p.agents_running, 0), faces: faces(shown, 8),
351
+ invitedBy: by?.display_name ? { name: by.display_name, avatar: by.avatar_url ?? null } : null,
352
+ };
353
+ },
354
+
355
+ async createRoom({ name, kind = 'room', listed = false, people = [], inviteOnly = false }) {
356
+ const s = state(cloud);
357
+ const clean = String(name ?? '').trim();
358
+ if (clean.length < 1 || clean.length > 40) refuse('a room name that empty or that long');
359
+ if (!['room', 'group'].includes(kind)) refuse(`a ${kind}`);
360
+ if (listed && kind !== 'room') refuse('list a group');
361
+ if (listed && inviteOnly) refuse('list a room that takes only people its members invite');
362
+ if (listed && !s.members.some((m) => m.personId === personId)) refuse('list a room without a team');
363
+ if ((people ?? []).length > 11) refuse('a group of more than twelve');
364
+ limit('rooms', [...s.rooms.values()].filter((r) => r.createdBy === personId && r.createdAt > now() - DAY).length, 10);
365
+ for (const p of people ?? []) if (p !== personId && !sharesRoom(p)) refuse('add somebody you share no room with');
366
+ const id = crypto.randomUUID();
367
+ s.rooms.set(id, { id, name: clean, kind, teamId: null, listed: !!listed, inviteOnly: !!inviteOnly, createdBy: personId, createdAt: now() });
368
+ s.roomMembers.push({ roomId: id, personId, role: 'owner', joinedAt: now() });
369
+ for (const p of new Set(people ?? [])) if (p !== personId) s.roomMembers.push({ roomId: id, personId: p, role: 'member', joinedAt: now() });
370
+ emit({ kind: 'membership', roomId: id });
371
+ return id;
372
+ },
373
+
374
+ async renameRoom(r, name) {
375
+ if (!ownsRoom(r) || room(r)?.teamId) refuse('rename a room you do not own');
376
+ const clean = String(name ?? '').trim();
377
+ if (clean.length < 1 || clean.length > 40) refuse('a room name that empty or that long');
378
+ room(r).name = clean;
379
+ emit({ kind: 'membership', roomId: r });
380
+ },
381
+
382
+ async leaveRoom(r) {
383
+ dropMember(r, personId);
384
+ },
385
+
386
+ async removeRoomMember(r, p) {
387
+ if (!ownsRoom(r)) refuse('remove somebody from a room you do not own');
388
+ dropMember(r, p);
389
+ },
390
+
391
+ async inviteToRoom(r, email) {
392
+ if (!inRoom(r)) refuse('invite to a room you are not in');
393
+ if (room(r)?.kind === 'group') refuse('invite to a group by email');
394
+ const clean = String(email).trim().toLowerCase();
395
+ if (!clean.includes('@')) refuse('an invite to no email');
396
+ const s = state(cloud);
397
+ if (s.roomInvites.some((i) => i.roomId === r && i.email === clean)) return;
398
+ limit('invites', s.roomInvites.filter((i) => i.invitedBy === personId && i.createdAt > now() - DAY).length, 50);
399
+ s.roomInvites.push({ roomId: r, email: clean, invitedBy: personId, createdAt: now(), acceptedAt: null });
400
+ },
401
+
402
+ async roomInvitesSent(r) {
403
+ if (!inRoom(r)) return [];
404
+ return state(cloud).roomInvites.filter((i) => i.roomId === r && i.invitedBy === personId)
405
+ .sort((a, b) => b.createdAt - a.createdAt).slice(0, 50)
406
+ .map((i) => ({ email: i.email, at: i.createdAt, joined: !!i.acceptedAt }));
407
+ },
408
+
409
+ async pendingRoomInvites() {
410
+ const email = confirmedEmail();
411
+ if (!email) return [];
412
+ const s = state(cloud);
413
+ return s.roomInvites.filter((i) => i.email === email && !i.acceptedAt && !inRoom(i.roomId))
414
+ .sort((a, b) => a.createdAt - b.createdAt)
415
+ .map((i) => ({ roomId: i.roomId, roomName: room(i.roomId)?.name ?? '', invitedBy: i.invitedBy, invitedByName: s.roomProfiles.get(i.invitedBy)?.display_name || null }));
416
+ },
417
+
418
+ async acceptRoomInvite(r) {
419
+ const email = confirmedEmail();
420
+ if (!email) refuse('join a room without a confirmed email');
421
+ const s = state(cloud);
422
+ const invite = s.roomInvites.find((i) => i.roomId === r && i.email === email && !i.acceptedAt);
423
+ if (!invite) refuse('join a room that did not invite you');
424
+ invite.acceptedAt = now();
425
+ return join(r);
426
+ },
427
+
428
+ async agentboxRanks(ids) {
429
+ const totals = new Map();
430
+ for (const p of state(cloud).roomPresence.values()) {
431
+ if (p.gone || !visible(p.person_id)) continue;
432
+ totals.set(p.person_id, (totals.get(p.person_id) ?? 0) + Number(p.hours_total));
433
+ }
434
+ const out = {};
435
+ for (const p of ids ?? []) {
436
+ if (p !== personId && !(sharesRoom(p) && visible(p))) continue;
437
+ const h = totals.get(p) ?? 0;
438
+ out[p] = 1 + [...totals.values()].filter((x) => x > h).length;
439
+ }
440
+ return out;
441
+ },
442
+
443
+ subscribeRooms(onEvent) {
444
+ const hear = (e) => {
445
+ if (e.kind === 'message') { if (inRoom(e.message.roomId)) onEvent({ kind: 'message', message: copy(e.message) }); return; }
446
+ if (e.kind === 'reaction') { if (inRoom(e.reaction.roomId)) onEvent({ kind: 'reaction', reaction: { ...e.reaction } }); return; }
447
+ onEvent({ kind: e.kind });
448
+ };
449
+ cloud.events.on('rooms', hear);
450
+ queueMicrotask(() => onEvent({ kind: 'subscribed' }));
451
+ return () => cloud.events.off('rooms', hear);
452
+ },
453
+ };
454
+ }
@@ -0,0 +1,42 @@
1
+ // The columns both room backends read and write, so they hand the app the
2
+ // same shapes.
3
+ export const PROFILE_FIELDS = ['display_name', 'avatar_url', 'headline', 'bio', 'show_working', 'show_agents', 'show_level', 'show_status', 'show_activity', 'invisible_until', 'status_text', 'status_until'];
4
+ export const PRESENCE_FIELDS = ['gone', 'agents_running', 'working_on', 'working_private', 'working_at', 'hours_total', 'hours_week', 'week_start', 'hours_today', 'day', 'streak_days', 'active_days', 'hours_by_month', 'activity'];
5
+ // What a cloud from before 20261022000100_rooms_show_everyone.sql has.
6
+ export const OLD_PRESENCE_FIELDS = PRESENCE_FIELDS.filter((f) => f !== 'activity');
7
+ export const PROFILE_COLS = ['person_id', ...PROFILE_FIELDS, 'updated_at'].join(',');
8
+ export const PRESENCE_COLS = ['person_id', 'device', ...PRESENCE_FIELDS, 'updated_at'].join(',');
9
+ export const OLD_PRESENCE_COLS = ['person_id', 'device', ...OLD_PRESENCE_FIELDS, 'updated_at'].join(',');
10
+
11
+ export const pick = (o, keys) => Object.fromEntries(keys.filter((k) => o && k in o).map((k) => [k, o[k]]));
12
+
13
+ export const roomOut = (r) => ({
14
+ id: r.id, name: r.name, kind: r.kind, teamId: r.team_id ?? null, listed: !!r.listed, inviteOnly: !!r.invite_only,
15
+ role: r.role, members: Number(r.members) || 0, createdAt: Date.parse(r.created_at) || 0,
16
+ });
17
+
18
+ export const MSG_COLS = 'id,room_id,by_person,body,in_reply_to,mentions,created_at,edited_at,deleted_at';
19
+ export const REACTION_COLS = 'message_id,room_id,person_id,emoji,is_on,updated_at';
20
+
21
+ export const messageOut = (r) => ({
22
+ id: r.id, roomId: r.room_id, by: r.by_person ?? null, body: r.deleted_at ? '' : r.body, inReplyTo: r.in_reply_to ?? null,
23
+ mentions: r.deleted_at ? [] : r.mentions ?? [], at: Date.parse(r.created_at),
24
+ editedAt: r.edited_at ? Date.parse(r.edited_at) : null, deleted: !!r.deleted_at,
25
+ });
26
+
27
+ export const reactionOut = (r) => ({
28
+ messageId: r.message_id, roomId: r.room_id, by: r.person_id, emoji: r.emoji, on: !!r.is_on, at: Date.parse(r.updated_at),
29
+ });
30
+
31
+ // The checks room_files makes (20261022000000_room_messages_do_more.sql).
32
+ export const ROOM_FILE_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,159}$/;
33
+ // 10 MiB of file, and that much as base64 with its padding.
34
+ export const ROOM_FILE_MAX_BYTES = 10 * 1024 * 1024;
35
+ export const ROOM_FILE_MAX_DATA = 13_981_016;
36
+ // A name already in the room: the file is there, which is what was wanted.
37
+ export const FILE_EXISTS = 'room-file-exists';
38
+ export function fileExists(name) {
39
+ const err = new Error(`${name} is already in the room`);
40
+ err.code = FILE_EXISTS;
41
+ return err;
42
+ }