pi-roundtable-web 0.25.0 → 0.26.1

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 (243) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +165 -12
  3. package/dist/account-zk4efb87.css +102 -0
  4. package/dist/chunk-account-ghq105t4.js +1 -0
  5. package/dist/chunk-action-menu-me0g9qsc.js +1 -0
  6. package/dist/{chunk-agent-0pm9p3nn.js → chunk-agent-rdv7ekrt.js} +1 -1
  7. package/dist/chunk-arranged-rooms-81rqhx9x.js +1 -0
  8. package/dist/chunk-character-creator-bd426yjw.js +1 -0
  9. package/dist/chunk-codeeditor-ajhgd33t.js +2 -0
  10. package/dist/chunk-confirm-p3mts8mx.js +1 -0
  11. package/dist/{chunk-connectors-anhghy3k.js → chunk-connectors-bkv3xn83.js} +1 -1
  12. package/dist/chunk-conversations-0e0vbxwr.js +1 -0
  13. package/dist/{chunk-drag-q4fx9b22.js → chunk-drag-peyb8t0k.js} +1 -1
  14. package/dist/{chunk-entry-he1embgp.js → chunk-entry-mv7q8br0.js} +1 -1
  15. package/dist/chunk-folder-list-era3tksh.js +1 -0
  16. package/dist/chunk-folds-hvws0bd5.js +1 -0
  17. package/dist/chunk-gate-hc7dd0fx.js +1 -0
  18. package/dist/chunk-group-scene-dkbpmbh5.js +1 -0
  19. package/dist/chunk-group-table-fqmxgdfw.js +1 -0
  20. package/dist/chunk-host-icons-4xde9ntk.js +1 -0
  21. package/dist/chunk-host-picture-014yzk20.js +1 -0
  22. package/dist/chunk-host-state-t56e5sdy.js +1 -0
  23. package/dist/chunk-host-ui-p1zy9nmp.js +1 -0
  24. package/dist/chunk-index-0fmmvgee.js +2 -0
  25. package/dist/chunk-index-0q15ysrb.js +1 -0
  26. package/dist/chunk-index-0xh24y25.js +2 -0
  27. package/dist/chunk-index-2yebd9tb.js +426 -0
  28. package/dist/chunk-index-4t0r3t3b.js +2 -0
  29. package/dist/chunk-index-5yzfrf7b.js +138 -0
  30. package/dist/{chunk-index-d5abvq7v.js → chunk-index-6twb7ccd.js} +2 -2
  31. package/dist/chunk-index-73gh7kgr.js +920 -0
  32. package/dist/chunk-index-7xffskfa.js +8 -0
  33. package/dist/chunk-index-aqw7cy5j.js +1 -0
  34. package/dist/chunk-index-b6ca02e8.js +2 -0
  35. package/dist/chunk-index-b8nvhcx5.js +2 -0
  36. package/dist/{chunk-index-bvfqjh77.js → chunk-index-cvbdf2ca.js} +2 -2
  37. package/dist/chunk-index-czdketze.js +2 -0
  38. package/dist/{chunk-index-9jqcskb4.js → chunk-index-dt9775ka.js} +5 -5
  39. package/dist/chunk-index-eg55mqqe.js +1 -0
  40. package/dist/chunk-index-gr6e0hse.js +2 -0
  41. package/dist/chunk-index-gyary7np.js +13 -0
  42. package/dist/chunk-index-j214tczc.js +1 -0
  43. package/dist/chunk-index-jd6632ef.js +2 -0
  44. package/dist/chunk-index-jw7dtshr.js +2 -0
  45. package/dist/{chunk-index-krtk6nn2.js → chunk-index-k7t14rwa.js} +2 -2
  46. package/dist/chunk-index-kwcx62wb.js +4 -0
  47. package/dist/chunk-index-m7fvw92f.js +2 -0
  48. package/dist/chunk-index-mmg788a1.js +2 -0
  49. package/dist/chunk-index-mq0dacwr.js +2 -0
  50. package/dist/chunk-index-p53ak25v.js +2 -0
  51. package/dist/{chunk-index-27qca4mr.js → chunk-index-pk1yxkv4.js} +2 -2
  52. package/dist/{chunk-index-9b1kt7qw.css → chunk-index-rbcz9jd4.css} +1 -1
  53. package/dist/{chunk-index-s681nny9.js → chunk-index-rmxbdmn2.js} +2 -2
  54. package/dist/chunk-index-rnxj3z8k.js +2 -0
  55. package/dist/chunk-index-snzzfhk3.js +2 -0
  56. package/dist/chunk-index-v3bmr0nw.js +2 -0
  57. package/dist/chunk-index-vcyzwtk2.js +2 -0
  58. package/dist/{chunk-index-zfd00hwb.js → chunk-index-w2j3njb6.js} +2 -2
  59. package/dist/chunk-index-w31c4yjv.js +2 -0
  60. package/dist/chunk-index-wrm8w165.js +2 -0
  61. package/dist/chunk-index-xh0mg9cw.js +2 -0
  62. package/dist/chunk-index-ybtvvx85.js +2 -0
  63. package/dist/chunk-index-yzsgsvnp.js +2 -0
  64. package/dist/{chunk-index-18gxy79n.js → chunk-index-z9x4cnzf.js} +2 -2
  65. package/dist/chunk-index-zrvg23jd.js +2 -0
  66. package/dist/chunk-index-zynq0b0w.js +50 -0
  67. package/dist/chunk-lang-css-88v3hp3d.js +1 -0
  68. package/dist/{chunk-lang-dockerfile-d5mzxs60.js → chunk-lang-dockerfile-k728ewsf.js} +1 -1
  69. package/dist/{chunk-lang-go-830fmhba.js → chunk-lang-go-wjwgay4v.js} +3 -3
  70. package/dist/chunk-lang-html-07d47es4.js +1 -0
  71. package/dist/chunk-lang-javascript-fmhhgzwd.js +1 -0
  72. package/dist/{chunk-lang-json-5eaarz7j.js → chunk-lang-json-7gd89mvs.js} +1 -1
  73. package/dist/chunk-lang-markdown-b6qbe8bt.js +6 -0
  74. package/dist/{chunk-lang-python-fjw84w10.js → chunk-lang-python-3v56y31b.js} +2 -2
  75. package/dist/{chunk-lang-rust-5rbp2zdk.js → chunk-lang-rust-kh0z74d1.js} +1 -1
  76. package/dist/{chunk-lang-shell-vzagpym3.js → chunk-lang-shell-9zv0zvbx.js} +1 -1
  77. package/dist/{chunk-lang-sql-0c2w9c2y.js → chunk-lang-sql-yw6bf3t7.js} +2 -2
  78. package/dist/{chunk-lang-toml-yr9fv6eb.js → chunk-lang-toml-tg1r96nq.js} +1 -1
  79. package/dist/chunk-lang-typescript-etjjvr2g.js +1 -0
  80. package/dist/{chunk-lang-xml-eze08p42.js → chunk-lang-xml-ev1mndd4.js} +1 -1
  81. package/dist/{chunk-lang-yaml-c4b6ft3y.js → chunk-lang-yaml-fz5qpb3r.js} +1 -1
  82. package/dist/chunk-layout-actions-48ecgtkg.js +1 -0
  83. package/dist/{chunk-menu-items-jrwxycr1.js → chunk-menu-items-1epy3h0t.js} +2 -2
  84. package/dist/chunk-mode-toggle-fxmfxsa4.js +1 -0
  85. package/dist/chunk-note-row-kd0fy7ee.js +75 -0
  86. package/dist/chunk-notes-yb8f057c.js +1 -0
  87. package/dist/chunk-notice-bell-wr4f1rph.js +1 -0
  88. package/dist/chunk-notices-x07da5bb.js +1 -0
  89. package/dist/chunk-onboarding-csrkbh9z.js +1 -0
  90. package/dist/chunk-overview-avc2d30t.js +1 -0
  91. package/dist/chunk-palette-9nhp95bz.js +1 -0
  92. package/dist/chunk-placement-items-q694dpgk.js +1 -0
  93. package/dist/chunk-profile-6qemf3m7.js +1 -0
  94. package/dist/chunk-prompt-history-7enzp2p3.js +1 -0
  95. package/dist/chunk-prompts-nv6mrn0q.js +1 -0
  96. package/dist/chunk-room-confirms-zz7n61y7.js +1 -0
  97. package/dist/chunk-room-menu-rr6nzap1.js +1 -0
  98. package/dist/chunk-rooms-menu-button-zbtwj47d.js +1 -0
  99. package/dist/chunk-schedules-rw57nr0m.js +1 -0
  100. package/dist/{chunk-seat-menu-jv4s7hmj.js → chunk-seat-menu-ys5v9abz.js} +1 -1
  101. package/dist/chunk-sheet-hosts-jhvaacst.js +1 -0
  102. package/dist/chunk-skill-editor-sz8an8qr.js +8 -0
  103. package/dist/chunk-skills-tnr1h16h.js +1 -0
  104. package/dist/chunk-switcher-list-17enhrse.js +1 -0
  105. package/dist/chunk-tab-dot-r6brdbmw.js +1 -0
  106. package/dist/chunk-table-r12cja6p.js +1 -0
  107. package/dist/chunk-table-seats-g3t392zz.js +1 -0
  108. package/dist/{chunk-table2d-4sje1fr0.js → chunk-table2d-p3ezsdcj.js} +1 -1
  109. package/dist/{chunk-table3d-h64skazp.js → chunk-table3d-4x2n2b00.js} +23 -23
  110. package/dist/chunk-thread-actions-qc6ezkha.js +1 -0
  111. package/dist/chunk-transcript-n2s6dher.js +1 -0
  112. package/dist/chunk-where-f3scdydc.js +1 -0
  113. package/dist/chunk-workspace-xs023bxm.js +1 -0
  114. package/dist/chunk-zh-tw-efvmk3fd.js +1 -0
  115. package/dist/index.html +1 -1
  116. package/dist/sw.js +1 -1
  117. package/package.json +5 -4
  118. package/src/api-types.ts +356 -4
  119. package/src/app/app.ts +201 -32
  120. package/src/app/desk.ts +47 -22
  121. package/src/app/files.ts +73 -7
  122. package/src/app/presence.ts +2 -2
  123. package/src/app/protocol.ts +63 -1
  124. package/src/app/room-log.ts +153 -14
  125. package/src/app/rooms.ts +39 -0
  126. package/src/app/routes.ts +168 -28
  127. package/src/app/socket.ts +349 -81
  128. package/src/app/sockets.ts +108 -20
  129. package/src/app/surface.ts +72 -36
  130. package/src/app/threads.ts +31 -2
  131. package/src/app/uploads.ts +33 -7
  132. package/src/auth/authenticator.ts +77 -8
  133. package/src/auth/login-page.ts +4 -2
  134. package/src/auth/next.ts +52 -0
  135. package/src/auth/oidc-pages.ts +62 -0
  136. package/src/auth/oidc.ts +649 -0
  137. package/src/auth/sessions.ts +193 -0
  138. package/src/authz.ts +173 -0
  139. package/src/console-api.ts +122 -25
  140. package/src/console-server.ts +102 -12
  141. package/src/gate.ts +90 -0
  142. package/src/host/access.ts +161 -0
  143. package/src/host/cards.ts +112 -0
  144. package/src/host/context.ts +188 -0
  145. package/src/host/forms.ts +240 -0
  146. package/src/host/frames.ts +349 -0
  147. package/src/host/host-app.ts +178 -0
  148. package/src/host/host-context.ts +49 -0
  149. package/src/host/host-routes.ts +396 -0
  150. package/src/host/pages.ts +266 -0
  151. package/src/host/reads.ts +155 -0
  152. package/src/host/retention.ts +41 -0
  153. package/src/host/room-views.ts +137 -0
  154. package/src/host/rooms-service.ts +429 -0
  155. package/src/host/stored-json.ts +13 -0
  156. package/src/host/views.ts +106 -0
  157. package/src/i18n/zh-tw.ts +139 -0
  158. package/src/index.ts +15 -1
  159. package/src/notices/api.ts +43 -0
  160. package/src/notices/mount.ts +292 -0
  161. package/src/notices/service.ts +294 -0
  162. package/src/notices/store.ts +298 -0
  163. package/src/notices/urls.ts +38 -0
  164. package/src/onboarding/api.ts +32 -0
  165. package/src/onboarding/service.ts +149 -0
  166. package/src/onboarding/steps.ts +75 -0
  167. package/src/onboarding/waivers.ts +40 -0
  168. package/src/options.ts +534 -34
  169. package/src/page-presentation.ts +47 -2
  170. package/src/profile/api.ts +175 -0
  171. package/src/profile/links.ts +162 -0
  172. package/src/push/api.ts +189 -52
  173. package/src/push/dispatch.ts +156 -0
  174. package/src/push/drain.ts +139 -0
  175. package/src/push/outbox.ts +218 -0
  176. package/src/push/prefs.ts +118 -0
  177. package/src/push/sender.ts +7 -2
  178. package/src/push/triggers.ts +56 -5
  179. package/src/push/vapid.ts +19 -0
  180. package/src/push/web-push-transport.ts +106 -0
  181. package/src/push-sender/index.ts +14 -0
  182. package/src/push-sender/run.ts +118 -0
  183. package/src/push-sender/session.ts +54 -0
  184. package/src/pwa.ts +32 -4
  185. package/src/visitors.ts +94 -6
  186. package/src/web-app.ts +718 -0
  187. package/src/web-plugin.ts +279 -26
  188. package/dist/chunk-action-menu-qe839g4b.js +0 -1
  189. package/dist/chunk-arranged-rooms-enpfswmg.js +0 -1
  190. package/dist/chunk-character-creator-6yf29xwx.js +0 -1
  191. package/dist/chunk-codeeditor-gdbdg7rp.js +0 -2
  192. package/dist/chunk-confirm-xrw7d732.js +0 -1
  193. package/dist/chunk-conversations-ry228aes.js +0 -1
  194. package/dist/chunk-folder-list-5cp8stna.js +0 -1
  195. package/dist/chunk-folds-hff104tk.js +0 -1
  196. package/dist/chunk-group-scene-h228jdat.js +0 -1
  197. package/dist/chunk-index-0p5h2kdz.js +0 -2
  198. package/dist/chunk-index-0pt08yd9.js +0 -1
  199. package/dist/chunk-index-12bw9yj8.js +0 -2
  200. package/dist/chunk-index-2ad17eh4.js +0 -2
  201. package/dist/chunk-index-31wh5twj.js +0 -52
  202. package/dist/chunk-index-4dy94pbz.js +0 -2
  203. package/dist/chunk-index-4ecjtdy8.js +0 -13
  204. package/dist/chunk-index-4v1b7ks9.js +0 -2
  205. package/dist/chunk-index-5d8yjj4m.js +0 -2
  206. package/dist/chunk-index-89ajmxh7.js +0 -2
  207. package/dist/chunk-index-b9ny4623.js +0 -2
  208. package/dist/chunk-index-e1dr6g10.js +0 -2
  209. package/dist/chunk-index-gpkzhq1g.js +0 -2
  210. package/dist/chunk-index-hk3xyy20.js +0 -2
  211. package/dist/chunk-index-jm23vn03.js +0 -4
  212. package/dist/chunk-index-k7gh0jvz.js +0 -2
  213. package/dist/chunk-index-ky42ev0b.js +0 -2
  214. package/dist/chunk-index-pa2yyqh0.js +0 -2
  215. package/dist/chunk-index-pcvt668q.js +0 -2
  216. package/dist/chunk-index-pqw27eye.js +0 -1
  217. package/dist/chunk-index-wmbmmrd4.js +0 -302
  218. package/dist/chunk-index-ymbjrsp6.js +0 -2
  219. package/dist/chunk-lang-css-t85r8rz0.js +0 -1
  220. package/dist/chunk-lang-html-n6jparge.js +0 -1
  221. package/dist/chunk-lang-javascript-xj00cp97.js +0 -1
  222. package/dist/chunk-lang-markdown-zxx8eke8.js +0 -6
  223. package/dist/chunk-lang-typescript-8f6yywt3.js +0 -1
  224. package/dist/chunk-layout-actions-bv0chj41.js +0 -1
  225. package/dist/chunk-notes-9b7pgqfe.js +0 -1
  226. package/dist/chunk-overview-8yw71esz.js +0 -1
  227. package/dist/chunk-palette-frms7axe.js +0 -1
  228. package/dist/chunk-placement-items-fvfy9m0y.js +0 -1
  229. package/dist/chunk-prompt-history-yrqehads.js +0 -1
  230. package/dist/chunk-prompts-g6609tqx.js +0 -1
  231. package/dist/chunk-room-confirms-0a2y0925.js +0 -1
  232. package/dist/chunk-room-menu-g11q97br.js +0 -1
  233. package/dist/chunk-rooms-menu-button-h9qge48n.js +0 -1
  234. package/dist/chunk-schedules-ezmacrbj.js +0 -1
  235. package/dist/chunk-sheet-hosts-b0azyfhb.js +0 -1
  236. package/dist/chunk-skill-editor-h6k6whwg.js +0 -8
  237. package/dist/chunk-skills-gqcfhq56.js +0 -1
  238. package/dist/chunk-switcher-list-mcpamqnj.js +0 -1
  239. package/dist/chunk-table-s8cde1xz.js +0 -1
  240. package/dist/chunk-table-seats-wqkjn8bc.js +0 -1
  241. package/dist/chunk-transcript-c59c6mpm.js +0 -1
  242. package/dist/chunk-workspace-0cyspkk4.js +0 -1
  243. package/dist/chunk-zh-tw-nc93jm10.js +0 -1
package/src/i18n/zh-tw.ts CHANGED
@@ -266,6 +266,9 @@ export const ZH_TW: Readonly<Record<string, string>> = {
266
266
  // 進度
267
267
  "{name} is working…": "{name}正在處理…",
268
268
  "Show the whole stream": "顯示完整串流",
269
+ "{count} earlier tool": "前面還有 {count} 個工具",
270
+ "{count} earlier tools": "前面還有 {count} 個工具",
271
+ "Show fewer tools": "收合工具",
269
272
 
270
273
  // 聊天室選單
271
274
  "A thread has no settings.": "討論串沒有設定。",
@@ -285,6 +288,7 @@ export const ZH_TW: Readonly<Record<string, string>> = {
285
288
  "Leave the table": "離席",
286
289
  "{name} left the table": "{name}已離席",
287
290
  "The table is empty": "圓桌空了",
291
+ "There are no rooms to seat yet.": "還沒有可以入座的聊天室。",
288
292
  "Every agent has left the table. Choose who sits down.":
289
293
  "所有夥伴都離席了。選擇誰要上桌。",
290
294
  "An agent who leaves the table is muted: its room stops pushing to you. A card that waits for you is always pushed.":
@@ -577,6 +581,8 @@ export const ZH_TW: Readonly<Record<string, string>> = {
577
581
  "{count} cards waiting": "{count} 張卡片等待中",
578
582
  "{count} finished thread": "{count} 個已結束的討論串",
579
583
  "{count} finished threads": "{count} 個已結束的討論串",
584
+ "Clear all": "清除全部",
585
+ "The thread is still open.": "這個討論串還在進行中。",
580
586
 
581
587
  // 排程
582
588
  "{days} at {time}": "每{days} {time}",
@@ -1021,4 +1027,137 @@ export const ZH_TW: Readonly<Record<string, string>> = {
1021
1027
  "baseHash 必須是讀取技能時的雜湊值。",
1022
1028
  "This skill changed while you were editing it.":
1023
1029
  "這個技能在你編輯時被更動了。",
1030
+ // 成員、登入與設定關卡
1031
+ Profile: "個人資料",
1032
+ Owner: "擁有者",
1033
+ Admin: "管理員",
1034
+ Member: "成員",
1035
+ "Finish setting up": "完成設定",
1036
+ "Finish setting up first.": "請先完成設定。",
1037
+ "These steps come first. The rest of the app opens when they are done.":
1038
+ "請先完成這些步驟,完成後才能使用其餘功能。",
1039
+ "Push is not available to you.": "你不能使用推播通知。",
1040
+ "You are signed out.": "你已登出。",
1041
+ "This account is not allowed to use this app.":
1042
+ "這個帳號不能使用此應用程式。",
1043
+ "Sign-in did not complete.": "登入沒有完成。",
1044
+ // 主機的聊天室、頁面、卡片與已讀
1045
+ "Host room": "主機聊天室",
1046
+ "No rooms yet.": "還沒有聊天室。",
1047
+ "No messages yet.": "還沒有訊息。",
1048
+ "Nothing here.": "這裡沒有內容。",
1049
+ "Show more": "顯示更多",
1050
+ "Search {name}": "搜尋{name}",
1051
+ "Search…": "搜尋…",
1052
+ "Needs attention": "需要處理",
1053
+ "This page is not available.": "這個頁面無法使用。",
1054
+ "You can read this room but not write in it.":
1055
+ "你可以閱讀這個聊天室,但不能在其中發言。",
1056
+ "@AI": "@AI",
1057
+ "The message starts with @AI, so the agent answers.":
1058
+ "訊息開頭是 @AI,所以 AI 會回覆。",
1059
+ "Write an internal note…": "寫一則內部備註…",
1060
+ "Ask AI…": "問 AI…",
1061
+ "Internal note": "內部備註",
1062
+ "Earlier cards ({count})": "先前的卡片({count})",
1063
+ Replaced: "已被取代",
1064
+ Image: "圖片",
1065
+ edited: "已編輯",
1066
+ "{count} file was removed": "已移除 {count} 個檔案",
1067
+ "{count} files were removed": "已移除 {count} 個檔案",
1068
+ "{count} unread message": "{count} 則未讀訊息",
1069
+ "{count} unread messages": "{count} 則未讀訊息",
1070
+ "New messages": "新訊息",
1071
+ "Seen by {count}": "已讀 {count}",
1072
+ "More settings": "更多設定",
1073
+ // 主機聊天室與頁面:伺服器的拒絕訊息
1074
+ "There is no such room.": "沒有這個聊天室。",
1075
+ "There is no such page.": "沒有這個頁面。",
1076
+ "input must be an object.": "input 必須是物件。",
1077
+ "input holds text or lists of text.": "input 只能放文字或文字清單。",
1078
+ "action must be text.": "action 必須是文字。",
1079
+ "card must be text.": "card 必須是文字。",
1080
+ "This is not yours to do.": "這個操作不是你可以做的。",
1081
+ "The page's query is too long.": "頁面的查詢參數太長。",
1082
+ "This action takes no form.": "這個操作沒有表單。",
1083
+ "The form has no such field.": "表單沒有這個欄位。",
1084
+ "This field is required.": "這個欄位必填。",
1085
+ "Choose from the list.": "請從清單中選擇。",
1086
+ "This field takes one value.": "這個欄位只能填一個值。",
1087
+ "This field is too long.": "這個欄位的內容太長。",
1088
+ "Give a web address.": "請填入網址。",
1089
+ "The action could not be done.": "無法執行這個操作。",
1090
+ // 通知、設定步驟與帳號連結
1091
+ Notices: "通知",
1092
+ "Notices, {count} waiting": "通知,{count} 則待處理",
1093
+ "Turn on notifications": "開啟通知",
1094
+ "Link your {name} account": "連結你的 {name} 帳號",
1095
+ "Prove which account is yours, so the app knows who you are there.":
1096
+ "證明哪個帳號是你的,讓應用程式知道你在那裡是誰。",
1097
+ "So that you hear when something needs you, even when the app is closed.":
1098
+ "這樣即使應用程式關著,有事找你時你也聽得到。",
1099
+ Urgent: "緊急",
1100
+ Handled: "已處理",
1101
+ "Nothing needs you.": "目前沒有需要你處理的事。",
1102
+ Linked: "已連結",
1103
+ Required: "必填",
1104
+ Recommended: "建議",
1105
+ "The server's configuration links this account.":
1106
+ "這個帳號由伺服器設定連結,無法在這裡移除。",
1107
+ Unlink: "解除連結",
1108
+ "Unlink {name}?": "要解除連結 {name} 嗎?",
1109
+ "The app will no longer know this account is yours. You can link it again later.":
1110
+ "應用程式將不再知道這個帳號是你的。之後可以再連結。",
1111
+ "Your code": "你的驗證碼",
1112
+ "The code works until {time}.": "驗證碼的有效時間到 {time}。",
1113
+ "A code is waiting to be used.": "有一組驗證碼等著被使用。",
1114
+ "{name} used the code. Confirm that this is you.":
1115
+ "{name} 使用了驗證碼。請確認那是你。",
1116
+ "I did it": "我完成了",
1117
+ "Link {name} first.": "請先連結 {name}。",
1118
+ Link: "連結",
1119
+ "In your quiet hours a normal notice waits in the bell instead of ringing your phone. An urgent one still rings.":
1120
+ "勿擾時段內,一般通知只會留在通知鈴裡,不會響手機;緊急通知照常響。",
1121
+ From: "從",
1122
+ To: "到",
1123
+ "Turn off quiet hours": "關閉勿擾時段",
1124
+ "A test notification is on its way.": "測試通知已送出。",
1125
+ "Send a test notification": "傳送測試通知",
1126
+ "Shown as": "顯示為",
1127
+ "The name and picture of this account are what others see in the app.":
1128
+ "別人在應用程式裡看到的,是這個帳號的名稱與頭像。",
1129
+ Automatic: "自動",
1130
+ "Your accounts": "你的帳號",
1131
+ "Put the app on your phone": "把應用程式放到手機上",
1132
+ "On iPhone and iPad, open this page in Safari, tap Share, then Add to Home Screen, and open the app from there.":
1133
+ "在 iPhone 與 iPad 上,用 Safari 開啟此頁,點「分享」,再點「加入主畫面」,之後從主畫面開啟。",
1134
+ "On a phone, add it to the home screen from the browser's menu so notifications reach you when it is closed.":
1135
+ "在手機上,請從瀏覽器選單加入主畫面,這樣應用程式關著時也收得到通知。",
1136
+ "Allow notifications": "允許通知",
1137
+ "This browser cannot get push notifications.": "這個瀏覽器無法接收推播通知。",
1138
+ "Allowed on this device.": "這台裝置已允許。",
1139
+ "Send a test": "傳送測試",
1140
+ "It arrived. Notifications work on this device.":
1141
+ "收到了。這台裝置的通知可以正常運作。",
1142
+ "Waiting for it…": "等待通知中…",
1143
+ "It has not arrived. Check that notifications are allowed for this app, then send another.":
1144
+ "還沒收到。請確認這個應用程式已允許通知,再傳送一次。",
1145
+ Skip: "略過",
1146
+ "{count} device(s) will get notifications.": "{count} 台裝置會收到通知。",
1147
+ Skipped: "已略過",
1148
+ "There is nothing left to set up.": "沒有需要再設定的項目。",
1149
+ "There is no such notice to handle.": "沒有這則可處理的通知。",
1150
+ "This account has no device that gets push yet. Turn it on first.":
1151
+ "這個帳號還沒有接收推播的裝置,請先開啟推播。",
1152
+ "A receipt needs a tag and received or opened.":
1153
+ "回報需要標籤,以及「已收到」或「已開啟」。",
1154
+ "There is no such notification.": "沒有這則通知。",
1155
+ "Send quiet: {start, end, timeZone} or null.":
1156
+ "請傳入 quiet:{start, end, timeZone},或 null。",
1157
+ "Send provider: the account's provider, or null for the default.":
1158
+ "請傳入 provider(帳號的提供者),或 null 使用預設。",
1159
+ "There is no picture.": "沒有頭像。",
1160
+ "Send request: the id the code came with.":
1161
+ "請傳入 request:驗證碼附帶的代號。",
1162
+ "That step cannot be waived.": "這個步驟不能略過。",
1024
1163
  };
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ export type {
12
12
  ConversationView,
13
13
  DashboardView,
14
14
  GroupView,
15
+ HostIconName,
15
16
  NoteInput,
16
17
  NoteKind,
17
18
  NoteView,
@@ -26,7 +27,7 @@ export type {
26
27
  TranscriptRole,
27
28
  TranscriptView,
28
29
  } from "./api-types.ts";
29
- export { PANES } from "./api-types.ts";
30
+ export { HOST_ICONS, PANES } from "./api-types.ts";
30
31
  export type { CloudflareAccessOptions } from "./cloudflare-access.ts";
31
32
  export {
32
33
  cloudflareAccess,
@@ -39,4 +40,17 @@ export type { WebAuthOptions, WebConsoleOptions } from "./options.ts";
39
40
  export { DEFAULT_RELAY_NOTE } from "./options.ts";
40
41
  export type { RequestVerifier, Verdict } from "./verifier.ts";
41
42
  export { admit, admitAs, refuse } from "./verifier.ts";
43
+ export type {
44
+ ActionResult,
45
+ HostCard,
46
+ HostPage,
47
+ HostRoomKind,
48
+ LinkProver,
49
+ Notices,
50
+ OnboardingStep,
51
+ OpenTarget,
52
+ Viewer,
53
+ WebApp,
54
+ } from "./web-app.ts";
55
+ export { WEB_APP } from "./web-app.ts";
42
56
  export { webConsole } from "./web-plugin.ts";
@@ -0,0 +1,43 @@
1
+ import { ConsoleHttpError as HttpError, json } from "../http.ts";
2
+ import type { FrameHandler, PluggedFrames, RouteArea } from "../web-app.ts";
3
+ import type { NoticeService } from "./service.ts";
4
+
5
+ /**
6
+ * `GET api/notices` and `POST api/notices/<tag>/ack`. Both are the signed-in person's own: the
7
+ * principal is the session's, and a notice not addressed to them answers 404 as if there were none.
8
+ */
9
+ export function noticesArea(notices: NoticeService): RouteArea {
10
+ return {
11
+ area: "notices",
12
+ async handle(request, parts, { visitor }) {
13
+ try {
14
+ const principalId = visitor.principal.id;
15
+ const [tag, action] = parts;
16
+ if (tag === undefined && request.method === "GET")
17
+ return json(await notices.listFor(principalId));
18
+ if (tag !== undefined && action === "ack" && parts.length === 2) {
19
+ if (request.method !== "POST")
20
+ throw new HttpError(405, "Method not allowed.");
21
+ if (!(await notices.ack(tag, principalId)))
22
+ throw new HttpError(404, "There is no such notice to handle.");
23
+ return json({ ok: true });
24
+ }
25
+ throw new HttpError(404, "There is no such API.");
26
+ } catch (error) {
27
+ if (error instanceof HttpError)
28
+ return json({ error: error.message }, error.status);
29
+ throw error;
30
+ }
31
+ },
32
+ };
33
+ }
34
+
35
+ /** The `ack` frame: the person handled a notice. A notice that is not theirs, or needs no answer, is refused. */
36
+ export function ackFrame(
37
+ notices: NoticeService,
38
+ ): FrameHandler<PluggedFrames["ack"]> {
39
+ return async ({ frame, viewer, refuse }) => {
40
+ if (!(await notices.ack(frame.tag, viewer.principalId)))
41
+ refuse("not_accepted");
42
+ };
43
+ }
@@ -0,0 +1,292 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { SQL } from "bun";
3
+ import type {
4
+ IdentityService,
5
+ Logger,
6
+ Migration,
7
+ PersonProfiles,
8
+ } from "pi-roundtable";
9
+ import type { AppSocket, Sockets } from "../app/sockets.ts";
10
+ import type { Gate } from "../gate.ts";
11
+ import { onboardingArea } from "../onboarding/api.ts";
12
+ import { OnboardingService, PUSH_STEP } from "../onboarding/service.ts";
13
+ import { builtInSteps } from "../onboarding/steps.ts";
14
+ import { OnboardingWaivers } from "../onboarding/waivers.ts";
15
+ import type { ResolvedOptions } from "../options.ts";
16
+ import { linksArea, peopleArea, profileArea } from "../profile/api.ts";
17
+ import { LinkService } from "../profile/links.ts";
18
+ import { DeviceRoutes, PushApi } from "../push/api.ts";
19
+ import { PushDispatch } from "../push/dispatch.ts";
20
+ import { OutboxSender, type PushTransport } from "../push/drain.ts";
21
+ import type { EndpointPolicy } from "../push/endpoint.ts";
22
+ import {
23
+ type OutboxKind,
24
+ PushOutbox,
25
+ type ReceiptEvent,
26
+ } from "../push/outbox.ts";
27
+ import { PushPrefsStore } from "../push/prefs.ts";
28
+ import type { PushSubscriptions } from "../push/subscriptions.ts";
29
+ import type { OutboxRoute } from "../push/triggers.ts";
30
+ import type { Vapid } from "../push/vapid.ts";
31
+ import { webPushTransport } from "../push/web-push-transport.ts";
32
+ import type { WebAppRegistry } from "../web-app.ts";
33
+ import { ackFrame, noticesArea } from "./api.ts";
34
+ import { NoticeService } from "./service.ts";
35
+ import { NoticeStore } from "./store.ts";
36
+
37
+ /** The tables of notices, push delivery, preferences and waivers. */
38
+ export const NOTICE_MIGRATIONS: readonly Migration[] = [
39
+ NoticeStore.migration,
40
+ PushOutbox.migration,
41
+ PushPrefsStore.migration,
42
+ OnboardingWaivers.migration,
43
+ ];
44
+
45
+ /** The words of a push the app sends itself when its row carries none (`push.content: "none"`). */
46
+ const WORDS: Record<OutboxKind, string> = {
47
+ card: "Card waiting",
48
+ reply: "New reply",
49
+ notice: "Notice",
50
+ test: "Test notification",
51
+ clear: "",
52
+ };
53
+
54
+ export interface NoticesMountOptions {
55
+ database: SQL;
56
+ logger: Logger;
57
+ registry: WebAppRegistry;
58
+ gate: Gate;
59
+ sockets: Pick<Sockets, "anyVisible" | "deliver">;
60
+ identity: IdentityService;
61
+ /** Present on every host that has the identity plugin; the profile page needs it. */
62
+ profiles?: PersonProfiles;
63
+ subscriptions: PushSubscriptions;
64
+ policy: EndpointPolicy;
65
+ mount: string;
66
+ routing: "hash" | "path";
67
+ /** The tiers, content, sender and transports the options resolved to. */
68
+ push: Pick<
69
+ ResolvedOptions["push"],
70
+ | "tiers"
71
+ | "content"
72
+ | "preview"
73
+ | "sender"
74
+ | "transports"
75
+ | "subject"
76
+ | "endpointHosts"
77
+ | "fetch"
78
+ >;
79
+ /** The operator's quiet window for a normal notice; absent = none. */
80
+ quiet?: (at: number) => boolean;
81
+ /** The app's own key, present with the internal sender. */
82
+ vapid?: Vapid;
83
+ onboarding: ResolvedOptions["onboarding"];
84
+ }
85
+
86
+ export interface NoticesMount {
87
+ notices: NoticeService;
88
+ pushApi: PushApi;
89
+ /** Present when the app does not sign: the built-in pushes are queued for the external sender. */
90
+ route?: OutboxRoute;
91
+ outbox: PushOutbox;
92
+ /** Sends a socket its notices and the badge once it is ready. */
93
+ opened(connection: AppSocket): Promise<void>;
94
+ start(): void;
95
+ stop(): Promise<void>;
96
+ }
97
+
98
+ /**
99
+ * Builds the notices, the push outbox and its in-process sender (internal mode), the onboarding
100
+ * service and the profile and link routes, and registers them with the web app. A host plugin
101
+ * reaches the first three through `WEB_APP` once the web service has started.
102
+ */
103
+ export function mountNotices(options: NoticesMountOptions): NoticesMount {
104
+ const { database, registry, sockets, identity, logger, push } = options;
105
+ const outbox = new PushOutbox(database);
106
+ const prefs = new PushPrefsStore(database);
107
+ const external = push.sender !== "internal";
108
+ const enabled = external || push.subject !== undefined;
109
+ const transports: PushTransport[] = [];
110
+ if (!external && options.vapid && push.subject)
111
+ transports.push(
112
+ webPushTransport({
113
+ vapid: options.vapid,
114
+ subject: push.subject,
115
+ endpointHosts: push.endpointHosts,
116
+ database,
117
+ ...(push.fetch ? { fetch: push.fetch } : {}),
118
+ logger,
119
+ }),
120
+ );
121
+ const sender = external
122
+ ? undefined
123
+ : new OutboxSender({
124
+ outbox,
125
+ transports,
126
+ render: (row) => ({
127
+ title: row.title ?? WORDS[row.kind],
128
+ ...(row.body ? { body: row.body } : {}),
129
+ }),
130
+ logger,
131
+ });
132
+ const dispatch = new PushDispatch({
133
+ outbox,
134
+ prefs,
135
+ identity,
136
+ tiers: push.tiers,
137
+ content: push.content,
138
+ preview: push.preview,
139
+ visible: (principalId) => sockets.anyVisible(principalId),
140
+ ...(options.quiet ? { quiet: options.quiet } : {}),
141
+ ...(sender
142
+ ? { kick: () => void sender.drain().catch(() => undefined) }
143
+ : {}),
144
+ enabled,
145
+ transports: push.transports,
146
+ logger,
147
+ });
148
+ const routing = { mount: options.mount, routing: options.routing };
149
+ const store = new NoticeStore(database);
150
+ const notices = new NoticeService({
151
+ store,
152
+ dispatch,
153
+ deliver: (frame, principal) => sockets.deliver(frame, { principal }),
154
+ routing,
155
+ logger,
156
+ });
157
+ const pushStatus = {
158
+ status: async (principalId: string) => {
159
+ const lastReceiptAt = await outbox.lastReceiptAt(principalId);
160
+ return {
161
+ devices: (await options.subscriptions.of(principalId)).length,
162
+ ...(lastReceiptAt ? { lastReceiptAt } : {}),
163
+ waived: await registry.waived(principalId, PUSH_STEP),
164
+ };
165
+ },
166
+ };
167
+ const waivers = new OnboardingWaivers(database);
168
+ registry.setWaivers(waivers);
169
+ const onboarding = new OnboardingService({
170
+ registry,
171
+ waivers,
172
+ gate: options.gate,
173
+ identity,
174
+ ...(options.profiles ? { profiles: options.profiles } : {}),
175
+ push: pushStatus,
176
+ logger,
177
+ });
178
+ const links = new LinkService({
179
+ registry,
180
+ steps: options.onboarding.links,
181
+ identity,
182
+ gate: options.gate,
183
+ logger,
184
+ });
185
+ registry.provide({ notices, onboarding, push: pushStatus });
186
+ for (const step of builtInSteps({
187
+ links: options.onboarding.links,
188
+ push: {
189
+ mode: options.onboarding.push,
190
+ waiver: options.onboarding.pushWaiver,
191
+ },
192
+ pushTiers: push.tiers,
193
+ prover: (provider) => registry.prover(provider),
194
+ identity,
195
+ outbox,
196
+ }))
197
+ registry.registerStep(step);
198
+ registry.onArea(noticesArea(notices));
199
+ registry.onArea(onboardingArea(onboarding));
200
+ registry.onArea(linksArea(links));
201
+ registry.onArea(
202
+ peopleArea({
203
+ profiles: options.profiles ?? { picture: async () => undefined },
204
+ gate: options.gate,
205
+ }),
206
+ );
207
+ if (options.profiles)
208
+ registry.onArea(
209
+ profileArea({
210
+ profiles: options.profiles,
211
+ identity,
212
+ sessionLabel: "Sign-in account",
213
+ }),
214
+ );
215
+ registry.onFrame("ack", ackFrame(notices));
216
+
217
+ const receipt = async (
218
+ principalId: string,
219
+ tag: string,
220
+ event: ReceiptEvent,
221
+ ): Promise<boolean> => {
222
+ const mine = tag.startsWith("test:")
223
+ ? await outbox.has(principalId, "test", tag)
224
+ : await notices.receipt(tag, principalId, event);
225
+ if (!mine) return false;
226
+ await outbox.receipt(principalId, tag, event);
227
+ // A receipt may finish the push step: the gate asks again.
228
+ onboarding.changed(principalId);
229
+ return true;
230
+ };
231
+ const pushApi = new PushApi({
232
+ vapid: {
233
+ publicKey:
234
+ push.sender === "internal"
235
+ ? (options.vapid?.publicKey ?? "")
236
+ : push.sender.external.publicKey,
237
+ },
238
+ devices: new DeviceRoutes({
239
+ policy: options.policy,
240
+ subscriptions: options.subscriptions,
241
+ enabled,
242
+ }),
243
+ tiers: push.tiers,
244
+ subscriptions:
245
+ push.sender === "internal" || !push.sender.external.subscribePath,
246
+ ...(enabled
247
+ ? {
248
+ test: async (principalId: string) => {
249
+ if ((await options.subscriptions.of(principalId)).length === 0)
250
+ return "no-device" as const;
251
+ const tag = `test:${randomUUID()}`;
252
+ await dispatch.test(
253
+ principalId,
254
+ tag,
255
+ `${options.mount}/`,
256
+ await notices.badge(principalId),
257
+ );
258
+ return { tag };
259
+ },
260
+ }
261
+ : {}),
262
+ receipt,
263
+ prefs,
264
+ });
265
+ const route: OutboxRoute | undefined = external
266
+ ? { outbox, content: push.content, badge: (id) => notices.badge(id) }
267
+ : undefined;
268
+ let sweeper: ReturnType<typeof setInterval> | undefined;
269
+ return {
270
+ notices,
271
+ pushApi,
272
+ ...(route ? { route } : {}),
273
+ outbox,
274
+ opened: async (connection) => {
275
+ for (const frame of await notices.frames(connection.principal.id))
276
+ sockets.deliver(frame, { connection });
277
+ },
278
+ start: () => {
279
+ sender?.start();
280
+ const sweep = () =>
281
+ Promise.all([store.sweep(), outbox.sweep()]).catch((error: unknown) =>
282
+ logger.warn({ err: error }, "old notices were not deleted"),
283
+ );
284
+ void sweep();
285
+ sweeper = setInterval(sweep, 3_600_000);
286
+ },
287
+ stop: async () => {
288
+ clearInterval(sweeper);
289
+ await sender?.stop();
290
+ },
291
+ };
292
+ }