@awiki/dsh-plugin 0.3.7 → 0.3.10

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 (213) hide show
  1. package/README.md +109 -71
  2. package/README.zh.md +89 -61
  3. package/THIRD_PARTY_NOTICES.md +1 -1
  4. package/cordis.patch.yml +6 -5
  5. package/lib/builtin-tenants.json +22 -0
  6. package/lib/builtin-tenants.sha256 +1 -0
  7. package/lib/client.js +7973 -2264
  8. package/lib/client.js.map +1 -1
  9. package/lib/index.js +3481 -1145
  10. package/lib/model-proxy-contract.js +11 -3
  11. package/lib/provider.js +73 -17
  12. package/lib/{sdk-adapter--trNlr-T.mjs → sdk-adapter-CYcJH7sM.mjs} +374 -101
  13. package/lib/typert.host.js +1595 -168
  14. package/lib/typert.remote-client.d.ts +43 -7
  15. package/lib/typert.remote-client.d.ts.map +1 -1
  16. package/lib/typert.remote-client.js +1595 -168
  17. package/lib/types/builtin-tenant-config.d.ts +19 -0
  18. package/lib/types/builtin-tenant-config.d.ts.map +1 -0
  19. package/lib/types/builtin-tenant-config.js +57 -0
  20. package/lib/types/builtin-tenant-config.js.map +1 -0
  21. package/lib/types/client/AwikiDeviceJoinReminder.d.ts +15 -0
  22. package/lib/types/client/AwikiDeviceJoinReminder.d.ts.map +1 -0
  23. package/lib/types/client/AwikiDeviceJoinReminder.js +74 -0
  24. package/lib/types/client/AwikiDeviceJoinReminder.js.map +1 -0
  25. package/lib/types/client/AwikiDevices.d.ts +33 -0
  26. package/lib/types/client/AwikiDevices.d.ts.map +1 -0
  27. package/lib/types/client/AwikiDevices.js +141 -0
  28. package/lib/types/client/AwikiDevices.js.map +1 -0
  29. package/lib/types/client/AwikiGroupDetails.d.ts.map +1 -1
  30. package/lib/types/client/AwikiGroupDetails.js +16 -13
  31. package/lib/types/client/AwikiGroupDetails.js.map +1 -1
  32. package/lib/types/client/AwikiIdentityAccess.d.ts +15 -8
  33. package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -1
  34. package/lib/types/client/AwikiIdentityAccess.js +203 -39
  35. package/lib/types/client/AwikiIdentityAccess.js.map +1 -1
  36. package/lib/types/client/AwikiIntegrationSettings.d.ts +21 -0
  37. package/lib/types/client/AwikiIntegrationSettings.d.ts.map +1 -0
  38. package/lib/types/client/AwikiIntegrationSettings.js +105 -0
  39. package/lib/types/client/AwikiIntegrationSettings.js.map +1 -0
  40. package/lib/types/client/AwikiMail.d.ts +1 -1
  41. package/lib/types/client/AwikiMail.d.ts.map +1 -1
  42. package/lib/types/client/AwikiMail.js +17 -137
  43. package/lib/types/client/AwikiMail.js.map +1 -1
  44. package/lib/types/client/AwikiOverlay.d.ts +0 -1
  45. package/lib/types/client/AwikiOverlay.d.ts.map +1 -1
  46. package/lib/types/client/AwikiOverlay.js +26 -28
  47. package/lib/types/client/AwikiOverlay.js.map +1 -1
  48. package/lib/types/client/AwikiProfileCard.d.ts.map +1 -1
  49. package/lib/types/client/AwikiProfileCard.js +17 -18
  50. package/lib/types/client/AwikiProfileCard.js.map +1 -1
  51. package/lib/types/client/AwikiRecoveryForm.d.ts +5 -1
  52. package/lib/types/client/AwikiRecoveryForm.d.ts.map +1 -1
  53. package/lib/types/client/AwikiRecoveryForm.js +73 -30
  54. package/lib/types/client/AwikiRecoveryForm.js.map +1 -1
  55. package/lib/types/client/AwikiSettingsSection.d.ts +28 -13
  56. package/lib/types/client/AwikiSettingsSection.d.ts.map +1 -1
  57. package/lib/types/client/AwikiSettingsSection.js +93 -61
  58. package/lib/types/client/AwikiSettingsSection.js.map +1 -1
  59. package/lib/types/client/AwikiUpdates.d.ts +10 -0
  60. package/lib/types/client/AwikiUpdates.d.ts.map +1 -0
  61. package/lib/types/client/AwikiUpdates.js +57 -0
  62. package/lib/types/client/AwikiUpdates.js.map +1 -0
  63. package/lib/types/client/awiki-client-bridge.d.ts +1 -1
  64. package/lib/types/client/awiki-client-bridge.d.ts.map +1 -1
  65. package/lib/types/client/awiki-client-bridge.js +4 -1
  66. package/lib/types/client/awiki-client-bridge.js.map +1 -1
  67. package/lib/types/client/controller.d.ts +84 -44
  68. package/lib/types/client/controller.d.ts.map +1 -1
  69. package/lib/types/client/controller.js +515 -309
  70. package/lib/types/client/controller.js.map +1 -1
  71. package/lib/types/client/drafts.d.ts +31 -0
  72. package/lib/types/client/drafts.d.ts.map +1 -0
  73. package/lib/types/client/drafts.js +94 -0
  74. package/lib/types/client/drafts.js.map +1 -0
  75. package/lib/types/client/image-cache.d.ts +2 -2
  76. package/lib/types/client/image-cache.d.ts.map +1 -1
  77. package/lib/types/client/image-cache.js +15 -2
  78. package/lib/types/client/image-cache.js.map +1 -1
  79. package/lib/types/client/index.d.ts.map +1 -1
  80. package/lib/types/client/index.js +63 -19
  81. package/lib/types/client/index.js.map +1 -1
  82. package/lib/types/client/integration-operation.d.ts +9 -0
  83. package/lib/types/client/integration-operation.d.ts.map +1 -0
  84. package/lib/types/client/integration-operation.js +54 -0
  85. package/lib/types/client/integration-operation.js.map +1 -0
  86. package/lib/types/client/mail-list-cache.d.ts +2 -0
  87. package/lib/types/client/mail-list-cache.d.ts.map +1 -1
  88. package/lib/types/client/mail-list-cache.js +18 -0
  89. package/lib/types/client/mail-list-cache.js.map +1 -1
  90. package/lib/types/client/otp-cooldown.d.ts +6 -0
  91. package/lib/types/client/otp-cooldown.d.ts.map +1 -0
  92. package/lib/types/client/otp-cooldown.js +18 -0
  93. package/lib/types/client/otp-cooldown.js.map +1 -0
  94. package/lib/types/client/settings-controller.d.ts +34 -0
  95. package/lib/types/client/settings-controller.d.ts.map +1 -1
  96. package/lib/types/client/settings-controller.js +182 -2
  97. package/lib/types/client/settings-controller.js.map +1 -1
  98. package/lib/types/client/settings-locales.d.ts +1 -1
  99. package/lib/types/client/settings-locales.d.ts.map +1 -1
  100. package/lib/types/client/settings-locales.js +192 -14
  101. package/lib/types/client/settings-locales.js.map +1 -1
  102. package/lib/types/client/slots.d.ts +20 -10
  103. package/lib/types/client/slots.d.ts.map +1 -1
  104. package/lib/types/client/slots.js +0 -1
  105. package/lib/types/client/slots.js.map +1 -1
  106. package/lib/types/conversation-preferences.d.ts.map +1 -1
  107. package/lib/types/conversation-preferences.js +0 -17
  108. package/lib/types/conversation-preferences.js.map +1 -1
  109. package/lib/types/desktop-distribution.d.ts +25 -0
  110. package/lib/types/desktop-distribution.d.ts.map +1 -0
  111. package/lib/types/desktop-distribution.js +57 -0
  112. package/lib/types/desktop-distribution.js.map +1 -0
  113. package/lib/types/domain.d.ts +1 -1
  114. package/lib/types/domain.d.ts.map +1 -1
  115. package/lib/types/domain.js +2 -1
  116. package/lib/types/domain.js.map +1 -1
  117. package/lib/types/index.d.ts +164 -41
  118. package/lib/types/index.d.ts.map +1 -1
  119. package/lib/types/index.js +1562 -529
  120. package/lib/types/index.js.map +1 -1
  121. package/lib/types/integration-client.d.ts +17 -0
  122. package/lib/types/integration-client.d.ts.map +1 -0
  123. package/lib/types/integration-client.js +233 -0
  124. package/lib/types/integration-client.js.map +1 -0
  125. package/lib/types/legacy-sent-mail-cache.d.ts +4 -0
  126. package/lib/types/legacy-sent-mail-cache.d.ts.map +1 -0
  127. package/lib/types/legacy-sent-mail-cache.js +29 -0
  128. package/lib/types/legacy-sent-mail-cache.js.map +1 -0
  129. package/lib/types/listener-state.d.ts +5 -3
  130. package/lib/types/listener-state.d.ts.map +1 -1
  131. package/lib/types/listener-state.js +28 -13
  132. package/lib/types/listener-state.js.map +1 -1
  133. package/lib/types/listener.d.ts +9 -32
  134. package/lib/types/listener.d.ts.map +1 -1
  135. package/lib/types/listener.js +11 -127
  136. package/lib/types/listener.js.map +1 -1
  137. package/lib/types/mail-list-client.d.ts +12 -0
  138. package/lib/types/mail-list-client.d.ts.map +1 -0
  139. package/lib/types/mail-list-client.js +254 -0
  140. package/lib/types/mail-list-client.js.map +1 -0
  141. package/lib/types/mail-recovery-observability.d.ts +8 -0
  142. package/lib/types/mail-recovery-observability.d.ts.map +1 -0
  143. package/lib/types/mail-recovery-observability.js +119 -0
  144. package/lib/types/mail-recovery-observability.js.map +1 -0
  145. package/lib/types/mail.d.ts +2 -28
  146. package/lib/types/mail.d.ts.map +1 -1
  147. package/lib/types/mail.js +2 -79
  148. package/lib/types/mail.js.map +1 -1
  149. package/lib/types/model-proxy-contract.d.ts.map +1 -1
  150. package/lib/types/model-proxy-contract.js +7 -0
  151. package/lib/types/model-proxy-contract.js.map +1 -1
  152. package/lib/types/package-version.generated.d.ts +4 -0
  153. package/lib/types/package-version.generated.d.ts.map +1 -0
  154. package/lib/types/package-version.generated.js +4 -0
  155. package/lib/types/package-version.generated.js.map +1 -0
  156. package/lib/types/profile-state.d.ts +2 -13
  157. package/lib/types/profile-state.d.ts.map +1 -1
  158. package/lib/types/profile-state.js +3 -20
  159. package/lib/types/profile-state.js.map +1 -1
  160. package/lib/types/provider-api.d.ts +134 -43
  161. package/lib/types/provider-api.d.ts.map +1 -1
  162. package/lib/types/provider.d.ts +8 -2
  163. package/lib/types/provider.d.ts.map +1 -1
  164. package/lib/types/provider.js +76 -16
  165. package/lib/types/provider.js.map +1 -1
  166. package/lib/types/realtime-supervisor.d.ts +60 -0
  167. package/lib/types/realtime-supervisor.d.ts.map +1 -0
  168. package/lib/types/realtime-supervisor.js +255 -0
  169. package/lib/types/realtime-supervisor.js.map +1 -0
  170. package/lib/types/sdk-adapter.d.ts +62 -11
  171. package/lib/types/sdk-adapter.d.ts.map +1 -1
  172. package/lib/types/sdk-adapter.js +285 -123
  173. package/lib/types/sdk-adapter.js.map +1 -1
  174. package/lib/types/settings-rpc-contract.d.ts +58 -0
  175. package/lib/types/settings-rpc-contract.d.ts.map +1 -1
  176. package/lib/types/settings-rpc-contract.js +103 -0
  177. package/lib/types/settings-rpc-contract.js.map +1 -1
  178. package/lib/types/settings-rpc.d.ts +14 -1
  179. package/lib/types/settings-rpc.d.ts.map +1 -1
  180. package/lib/types/settings-rpc.js +130 -1
  181. package/lib/types/settings-rpc.js.map +1 -1
  182. package/lib/types/tenant-registry.d.ts +74 -0
  183. package/lib/types/tenant-registry.d.ts.map +1 -0
  184. package/lib/types/tenant-registry.js +440 -0
  185. package/lib/types/tenant-registry.js.map +1 -0
  186. package/lib/types/tools.d.ts +1 -1
  187. package/lib/types/tools.d.ts.map +1 -1
  188. package/lib/types/tools.js +4 -20
  189. package/lib/types/tools.js.map +1 -1
  190. package/lib/types/types.d.ts +249 -66
  191. package/lib/types/types.d.ts.map +1 -1
  192. package/lib/types/types.js.map +1 -1
  193. package/lib/types/update-policy.d.ts +51 -0
  194. package/lib/types/update-policy.d.ts.map +1 -0
  195. package/lib/types/update-policy.js +379 -0
  196. package/lib/types/update-policy.js.map +1 -0
  197. package/lib/types/version.d.ts +4 -0
  198. package/lib/types/version.d.ts.map +1 -0
  199. package/lib/types/version.js +44 -0
  200. package/lib/types/version.js.map +1 -0
  201. package/package.json +26 -8
  202. package/lib/types/base64.d.ts +0 -4
  203. package/lib/types/base64.d.ts.map +0 -1
  204. package/lib/types/base64.js +0 -28
  205. package/lib/types/base64.js.map +0 -1
  206. package/lib/types/client/mail-attachment.d.ts +0 -40
  207. package/lib/types/client/mail-attachment.d.ts.map +0 -1
  208. package/lib/types/client/mail-attachment.js +0 -187
  209. package/lib/types/client/mail-attachment.js.map +0 -1
  210. package/lib/types/sent-mail-store.d.ts +0 -27
  211. package/lib/types/sent-mail-store.d.ts.map +0 -1
  212. package/lib/types/sent-mail-store.js +0 -348
  213. package/lib/types/sent-mail-store.js.map +0 -1
@@ -1,5 +1,6 @@
1
- /** React-free browser controller for the deployment's one AWiki identity. */
1
+ import { AwikiDraftStore } from "./drafts.js";
2
2
  import { IndexedDbAwikiBrowserImageCache, } from "./image-cache.js";
3
+ import { clearMailBrowserCache } from "./mail-list-cache.js";
3
4
  /** Turn a registration rejection into an actionable message without exposing remote response text. */
4
5
  function registrationFailureMessage(failure) {
5
6
  switch (failure.code) {
@@ -19,6 +20,8 @@ function registrationFailureMessage(failure) {
19
20
  return '注册请求过于频繁,请稍后重试。';
20
21
  case 'network':
21
22
  return '无法连接 AWiki 服务,请检查网络后重试。';
23
+ case 'identity-recovery-required':
24
+ return '这台设备保留了原 AWiki 身份,但本地登录状态已被清除。请先恢复该身份或完整清除这台设备上的 AWiki 身份数据,再重新注册。';
22
25
  case 'forbidden':
23
26
  return '当前 AWiki 服务未开放公开注册,或该手机号不在注册白名单。请使用已获准的手机号,或联系管理员开通注册权限。';
24
27
  case 'remote':
@@ -65,7 +68,7 @@ function recoveryPreparationFailureMessage(failure) {
65
68
  function recoveryContinuationFailureMessage(failure, phase) {
66
69
  const remoteCommitted = phase === 'remote_committed' || phase === 'identity_transition_pending';
67
70
  if (remoteCommitted && ['invalid-request', 'conflict', 'forbidden', 'remote'].includes(failure.code)) {
68
- return '身份已在服务端恢复,但本机切换尚未完成。请保留当前恢复操作,并继续完成本机切换;不要重新获取验证码或创建新身份。';
71
+ return '身份已在服务端恢复,本机切换尚未完成。可以重试本机切换,也可以返回入口稍后继续;请勿为同一账号重复发起恢复。';
69
72
  }
70
73
  switch (failure.code) {
71
74
  case 'network':
@@ -74,9 +77,12 @@ function recoveryContinuationFailureMessage(failure, phase) {
74
77
  : '恢复请求结果尚未确认。请保留当前恢复操作并重新检查状态,不要重复提交。';
75
78
  case 'invalid-request':
76
79
  case 'conflict':
77
- return '身份恢复暂未完成。请保留当前恢复操作并重新检查状态,不要重新获取验证码或创建新身份。';
80
+ return '身份恢复暂未完成。请重新检查状态,或返回入口稍后继续;请勿为同一账号重复发起恢复。';
78
81
  case 'rate-limited':
79
82
  return '恢复状态检查过于频繁,请稍后继续。';
83
+ case 'remote':
84
+ case 'forbidden':
85
+ return 'AWiki 服务暂时未能完成恢复。可以重新检查结果,或返回入口稍后继续。';
80
86
  default:
81
87
  return `${failure.code}:${failure.message}`;
82
88
  }
@@ -106,7 +112,6 @@ const INITIAL_VIEW = Object.freeze({
106
112
  groupAccess: null,
107
113
  groupMembers: Object.freeze([]),
108
114
  groupMembersHasMore: false,
109
- groupRecovery: null,
110
115
  messages: Object.freeze([]),
111
116
  historyHasMore: false,
112
117
  localPending: false,
@@ -114,9 +119,11 @@ const INITIAL_VIEW = Object.freeze({
114
119
  pending: null,
115
120
  error: null,
116
121
  attachmentMaxBytes: 0,
117
- profileName: null,
118
- legacySharedStateDetected: false,
122
+ handleRecoveryPhoneEnabled: false,
119
123
  summaries: Object.freeze({}),
124
+ identityAccess: null,
125
+ accessLoading: true,
126
+ accessError: null,
120
127
  recoveryOperationId: null,
121
128
  recoveryProgress: null,
122
129
  });
@@ -178,44 +185,9 @@ function groupReadFailureMessage(failure) {
178
185
  return `${failure.code}:${failure.message}`;
179
186
  }
180
187
  }
181
- function groupRecoveryFailureMessage(failure) {
182
- return failure.code === 'network'
183
- ? '无法连接 AWiki 服务,暂时不能恢复旧群聊身份。'
184
- : '暂时不能检查旧群聊身份,请稍后重试。';
185
- }
186
188
  function conversationPreferenceFailureMessage(_failure) {
187
189
  return '无法保存本机会话设置,请稍后重试。';
188
190
  }
189
- function recoveryFingerprint(parts) {
190
- let hash = 0x811c9dc5;
191
- for (const part of parts) {
192
- for (let index = 0; index < part.length; index += 1) {
193
- hash ^= part.charCodeAt(index);
194
- hash = Math.imul(hash, 0x01000193);
195
- }
196
- hash ^= 0;
197
- hash = Math.imul(hash, 0x01000193);
198
- }
199
- return (hash >>> 0).toString(16).padStart(8, '0');
200
- }
201
- function groupRecoveryProjection(summary, hiddenGroupDids) {
202
- const hiddenItems = summary.items.filter(item => hiddenGroupDids.has(item.groupDid));
203
- const hiddenPending = hiddenItems.filter(item => item.status === 'pending').length;
204
- const hiddenBlocked = hiddenItems.filter(item => item.status === 'blocked').length;
205
- const pending = Math.max(0, summary.pending - hiddenPending);
206
- const blocked = Math.max(0, summary.blocked - hiddenBlocked);
207
- const visibleItems = summary.items
208
- .filter(item => !hiddenGroupDids.has(item.groupDid))
209
- .map(item => `${item.status}:${item.groupDid}`)
210
- .sort();
211
- const signature = `v1:${pending}:${blocked}:${recoveryFingerprint(visibleItems)}`;
212
- if (blocked > 0)
213
- return { view: { status: 'blocked', pending, blocked }, signature };
214
- return {
215
- view: pending > 0 ? { status: 'pending', pending, blocked: 0 } : null,
216
- signature,
217
- };
218
- }
219
191
  function groupReadFailureReason(failure) {
220
192
  switch (failure.code) {
221
193
  case 'group-identity-stale': return 'recovering';
@@ -262,6 +234,20 @@ async function call(operation, failureMessage = failure => `${failure.code}:${
262
234
  };
263
235
  }
264
236
  }
237
+ /** Flatten the carrier and the isolated Guest Integration business result. */
238
+ async function callIntegration(operation) {
239
+ try {
240
+ const carried = await operation();
241
+ if (!carried.ok)
242
+ return { ok: false, error: `连接 AWiki Host 失败:${carried.error.message}` };
243
+ if (!carried.value.ok)
244
+ return { ok: false, error: carried.value.error.message };
245
+ return { ok: true, value: carried.value.value };
246
+ }
247
+ catch {
248
+ return { ok: false, error: '临时消息服务暂时不可用,请稍后重试。' };
249
+ }
250
+ }
265
251
  /** Preserve only the stable business code for controller-level state transitions. */
266
252
  async function callWithFailureCode(operation) {
267
253
  try {
@@ -386,23 +372,15 @@ function sameIdentity(identity, peer) {
386
372
  const GROUP_HISTORY_RETRY_DELAYS_MS = [250, 750, 1_500, 2_500];
387
373
  /** Runtime-only decoded-byte budget that prevents repeat Host calls while browsing. */
388
374
  const BROWSER_IMAGE_ATTACHMENT_CACHE_MAX_BYTES = 32 * 1024 * 1024;
389
- const RECOVERY_OPERATION_STORAGE_PREFIX = 'awiki.handle-recovery.operation.v2:';
390
- function recoveryOperationStorageKey(tenantDomain) {
391
- return `${RECOVERY_OPERATION_STORAGE_PREFIX}${encodeURIComponent(tenantDomain)}`;
375
+ const RECOVERY_OPERATION_STORAGE_KEY = 'awiki.handle-recovery.operation.v1';
376
+ function recoveryOperationStorageKey(tenantId) {
377
+ return tenantId === undefined || tenantId === ''
378
+ ? RECOVERY_OPERATION_STORAGE_KEY
379
+ : `${RECOVERY_OPERATION_STORAGE_KEY}.${encodeURIComponent(tenantId)}`;
392
380
  }
393
- function storedRecoveryOperation(tenantDomain) {
381
+ function storeRecoveryOperation(operationId, tenantId) {
394
382
  try {
395
- return globalThis.localStorage?.getItem(recoveryOperationStorageKey(tenantDomain)) ?? null;
396
- }
397
- catch {
398
- return null;
399
- }
400
- }
401
- function storeRecoveryOperation(tenantDomain, operationId) {
402
- if (tenantDomain === undefined)
403
- return;
404
- try {
405
- const key = recoveryOperationStorageKey(tenantDomain);
383
+ const key = recoveryOperationStorageKey(tenantId);
406
384
  if (operationId === null)
407
385
  globalThis.localStorage?.removeItem(key);
408
386
  else
@@ -458,6 +436,7 @@ function targetOf(conversation) {
458
436
  export class AwikiController {
459
437
  remote;
460
438
  persistentImageCache;
439
+ drafts = new AwikiDraftStore();
461
440
  view = INITIAL_VIEW;
462
441
  listeners = new Set();
463
442
  config = null;
@@ -465,6 +444,10 @@ export class AwikiController {
465
444
  historyCursor;
466
445
  groupMembersCursor;
467
446
  timer;
447
+ opening;
448
+ drawerOpen = false;
449
+ accessRequest;
450
+ tenantTransition;
468
451
  generation = 0;
469
452
  selectionRevision = 0;
470
453
  disposed = false;
@@ -476,18 +459,12 @@ export class AwikiController {
476
459
  directProfiles = new Map();
477
460
  /** Last trustworthy group title for the active identity, keyed by canonical Group DID. */
478
461
  groupTitles = new Map();
479
- /** Current secret-free Core journal state, keyed by canonical Group DID. */
480
- groupRecoveryItems = new Map();
481
462
  /** Identity-scoped product overlays. Core conversations and history remain untouched. */
482
463
  hiddenConversationPreferences = new Map();
483
- dismissedGroupRecoverySignature;
484
- lastGroupRecoverySignature;
485
- lastGroupRecoverySummary;
486
464
  /** Verified image payloads retained outside observable state for instant remounts. */
487
465
  imageAttachments = new Map();
488
466
  imageAttachmentCacheBytes = 0;
489
467
  presentationCacheOwnerDid = null;
490
- groupRecoveryOperation;
491
468
  /**
492
469
  * @param remote - generated Host Remote namespace.
493
470
  * @param persistentImageCache - browser-origin verified preview cache.
@@ -503,51 +480,63 @@ export class AwikiController {
503
480
  this.listeners.add(listener);
504
481
  return () => { this.listeners.delete(listener); };
505
482
  };
483
+ /** Open the deployment-matched dynamic Integration guide. */
484
+ openIntegrationGuide() {
485
+ const url = this.config?.integrationGuideUrl;
486
+ if (url !== undefined)
487
+ window.open(url, '_blank', 'noopener,noreferrer');
488
+ }
506
489
  /** Load Host policy and the shared identity state without starting drawer polling. */
507
- async loadSession() {
490
+ async loadSession(preserveView = false) {
508
491
  if (this.disposed)
509
492
  return { ok: false, error: 'AWiki 插件已卸载' };
510
- this.close();
511
- this.summaryBaselines.clear();
493
+ this.stopPollingLifecycle();
494
+ if (!preserveView)
495
+ this.summaryBaselines.clear();
496
+ const previous = this.view;
497
+ const previousTenant = this.config?.tenantId;
512
498
  const generation = this.generation;
513
- this.publish({ ...INITIAL_VIEW, status: 'loading' });
499
+ if (!preserveView)
500
+ this.publish({ ...INITIAL_VIEW, status: 'loading' });
501
+ const failed = (error) => {
502
+ if (preserveView)
503
+ this.publish({ ...this.view, error });
504
+ else
505
+ this.fail(error);
506
+ return { ok: false, error };
507
+ };
514
508
  const config = await call(() => this.remote.getConfig());
515
509
  if (!this.current(generation))
516
510
  return { ok: true, value: undefined };
517
511
  if (!config.ok)
518
- return this.fail(config.error);
512
+ return failed(config.error);
519
513
  this.config = config.value;
520
514
  const session = await call(() => this.remote.getSession());
521
515
  if (!this.current(generation))
522
516
  return { ok: true, value: undefined };
523
517
  if (!session.ok)
524
- return this.fail(session.error);
518
+ return failed(session.error);
525
519
  const identity = session.value.status === 'active' ? session.value.identity : null;
520
+ const sameOwner = preserveView && previousTenant === config.value.tenantId
521
+ && previous.sessionStatus === session.value.status && previous.identity?.did === identity?.did;
522
+ this.drafts.setScope(config.value.tenantId ?? 'default', identity?.did ?? session.value.status);
526
523
  this.activatePresentationCache(identity);
527
524
  this.publish({
528
- ...this.view,
525
+ ...(sameOwner ? this.view : INITIAL_VIEW),
529
526
  status: 'ready',
530
527
  sessionStatus: session.value.status,
531
528
  identity: session.value.status === 'active' ? session.value.identity : null,
532
529
  error: null,
533
530
  attachmentMaxBytes: config.value.attachmentMaxBytes,
534
- profileName: config.value.profileName ?? null,
535
- legacySharedStateDetected: config.value.legacySharedStateDetected ?? false,
536
- recoveryOperationId: storedRecoveryOperation(config.value.tenantDomain),
531
+ handleRecoveryPhoneEnabled: config.value.handleRecoveryPhoneEnabled,
532
+ recoveryOperationId: previousTenant === config.value.tenantId ? previous.recoveryOperationId : null,
537
533
  });
538
- const operationId = storedRecoveryOperation(config.value.tenantDomain);
539
- if (operationId !== null) {
540
- const recovery = await call(() => this.remote.getRecoveryStatus({ operationId }));
541
- if (this.current(generation) && recovery.ok) {
542
- this.publish({ ...this.view, recoveryOperationId: operationId, recoveryProgress: recovery.value });
543
- if (recovery.value.phase === 'applied') {
544
- storeRecoveryOperation(config.value.tenantDomain, null);
545
- this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: recovery.value });
546
- if (identity === null)
547
- return this.loadSession();
548
- }
549
- }
550
- }
534
+ await this.refreshIdentityAccess();
535
+ if (!this.current(generation))
536
+ return { ok: true, value: undefined };
537
+ if (identity !== null && previous.identity?.did !== identity.did
538
+ && this.view.recoveryOperationId === null && this.view.identityAccess?.recoveries.length === 0)
539
+ this.drafts.clearFlow();
551
540
  if (identity !== null) {
552
541
  await this.loadConversationPreferences(generation);
553
542
  const profile = await call(() => this.remote.getProfile());
@@ -561,19 +550,42 @@ export class AwikiController {
561
550
  * @returns successful readiness or one display-safe Host failure.
562
551
  */
563
552
  async open() {
564
- const loaded = await this.loadSession();
565
- if (!loaded.ok)
566
- return loaded;
553
+ this.drawerOpen = true;
554
+ if (this.tenantTransition !== undefined) {
555
+ return this.tenantTransition.then(() => this.view.error === null ? { ok: true, value: undefined } : { ok: false, error: this.view.error }, () => ({ ok: false, error: this.view.error ?? '租户切换未完成,请重试' }));
556
+ }
557
+ if (this.opening !== undefined)
558
+ return this.opening;
559
+ const opening = this.openOnce();
560
+ this.opening = opening;
561
+ try {
562
+ return await opening;
563
+ }
564
+ finally {
565
+ if (this.opening === opening)
566
+ this.opening = undefined;
567
+ }
568
+ }
569
+ async openOnce() {
570
+ const loading = this.loadSession(this.view.status === 'ready');
567
571
  const generation = this.generation;
568
- if (this.view.identity !== null) {
569
- const listed = await this.refreshConversationsAndRecoverGroups(generation);
570
- if (!listed.ok)
571
- return listed;
572
+ const loaded = await loading;
573
+ if (!loaded.ok && this.view.status !== 'ready')
574
+ return loaded;
575
+ if (!this.current(generation) || !this.drawerOpen)
576
+ return loaded;
577
+ let listed = loaded;
578
+ if (loaded.ok && this.view.identity !== null) {
579
+ listed = await this.refreshConversations(generation, this.view.conversations.length > 0);
580
+ if (this.view.selectedConversationId !== null)
581
+ await this.poll(generation);
572
582
  }
573
- if (this.current(generation)) {
583
+ if (this.current(generation) && this.drawerOpen) {
584
+ if (this.timer !== undefined)
585
+ clearInterval(this.timer);
574
586
  this.timer = setInterval(() => { void this.poll(generation); }, this.config?.pollIntervalMs ?? 3_000);
575
587
  }
576
- return { ok: true, value: undefined };
588
+ return listed;
577
589
  }
578
590
  /** Sign out locally while retaining the SDK-owned identity and database. */
579
591
  async logout(request) {
@@ -582,6 +594,7 @@ export class AwikiController {
582
594
  const result = await call(() => this.remote.logout(request));
583
595
  if (!result.ok)
584
596
  return result;
597
+ this.drafts.clearScope();
585
598
  this.close();
586
599
  this.conversationsCursor = undefined;
587
600
  this.historyCursor = undefined;
@@ -592,7 +605,9 @@ export class AwikiController {
592
605
  status: 'ready',
593
606
  sessionStatus: 'signed-out',
594
607
  attachmentMaxBytes: this.config?.attachmentMaxBytes ?? 0,
608
+ handleRecoveryPhoneEnabled: this.config?.handleRecoveryPhoneEnabled ?? false,
595
609
  });
610
+ await this.loadSession();
596
611
  return result;
597
612
  }
598
613
  /** Resume the preserved local identity and reload its conversations. */
@@ -609,7 +624,52 @@ export class AwikiController {
609
624
  }
610
625
  /** Stop polling and invalidate all in-flight drawer work. */
611
626
  close() {
627
+ this.drawerOpen = false;
628
+ this.opening = undefined;
629
+ this.stopPollingLifecycle();
630
+ }
631
+ /** Fence old requests before switching, then restore the currently visible tenant view. */
632
+ async switchTenant(change) {
633
+ if (this.disposed)
634
+ throw new Error('AWiki 插件已卸载');
635
+ if (this.tenantTransition !== undefined)
636
+ throw new Error('租户正在切换,请稍候');
637
+ this.opening = undefined;
638
+ this.stopPollingLifecycle();
639
+ this.conversationsCursor = undefined;
640
+ this.historyCursor = undefined;
641
+ this.unreadAtOpen.clear();
642
+ this.clearPresentationCache();
643
+ this.publish({ ...INITIAL_VIEW, status: 'loading' });
644
+ const transition = (async () => {
645
+ let switchError;
646
+ try {
647
+ await change();
648
+ }
649
+ catch (error) {
650
+ switchError = error;
651
+ }
652
+ // On failure the Host restores its previous tenant. Reload that exact committed state too.
653
+ if (!this.disposed) {
654
+ const loaded = this.drawerOpen ? await this.openOnce() : await this.loadSession();
655
+ if (!loaded.ok)
656
+ throw new Error(loaded.error);
657
+ }
658
+ if (switchError !== undefined)
659
+ throw switchError;
660
+ })();
661
+ this.tenantTransition = transition;
662
+ try {
663
+ await transition;
664
+ }
665
+ finally {
666
+ if (this.tenantTransition === transition)
667
+ this.tenantTransition = undefined;
668
+ }
669
+ }
670
+ stopPollingLifecycle() {
612
671
  this.generation += 1;
672
+ this.accessRequest = undefined;
613
673
  this.selectionRevision += 1;
614
674
  if (this.timer !== undefined)
615
675
  clearInterval(this.timer);
@@ -626,6 +686,101 @@ export class AwikiController {
626
686
  async sendRegistrationOtp(request) {
627
687
  return this.withPending('发送验证码', () => call(() => this.remote.sendRegistrationOtp(request), registrationOtpFailureMessage));
628
688
  }
689
+ /** Discover Core-owned work without selecting an account or activating a session. */
690
+ refreshIdentityAccess() {
691
+ if (this.accessRequest !== undefined)
692
+ return this.accessRequest;
693
+ const request = this.loadIdentityAccess();
694
+ this.accessRequest = request;
695
+ const settled = () => { if (this.accessRequest === request)
696
+ this.accessRequest = undefined; };
697
+ void request.then(settled, settled);
698
+ return request;
699
+ }
700
+ async loadIdentityAccess() {
701
+ const generation = this.generation;
702
+ const revision = this.recoveryRevision;
703
+ this.publish({ ...this.view, accessLoading: true, accessError: null });
704
+ const result = await call(() => this.remote.getIdentityAccessState());
705
+ if (!this.recoveryCurrent(generation, revision))
706
+ return this.recoveryLeft();
707
+ if (!result.ok) {
708
+ this.publish({ ...this.view, accessLoading: false, accessError: result.error });
709
+ return result;
710
+ }
711
+ const candidates = result.value.recoveries;
712
+ const selected = this.view.recoveryOperationId;
713
+ this.publish({ ...this.view, identityAccess: result.value });
714
+ if (selected !== null) {
715
+ const status = await callWithFailureCode(() => this.remote.getRecoveryStatus({ operationId: selected }));
716
+ if (!this.recoveryCurrent(generation, revision))
717
+ return this.recoveryLeft();
718
+ if (!status.ok) {
719
+ if (status.failureCode === 'not-found' && candidates.length === 0) {
720
+ storeRecoveryOperation(null, this.config?.tenantId);
721
+ this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: null });
722
+ }
723
+ else {
724
+ this.publish({ ...this.view, accessLoading: false, accessError: status.error });
725
+ return { ok: false, error: status.error };
726
+ }
727
+ }
728
+ else {
729
+ this.publish({ ...this.view, recoveryProgress: status.value });
730
+ }
731
+ }
732
+ this.publish({ ...this.view, accessLoading: false, accessError: null });
733
+ return { ok: true, value: undefined };
734
+ }
735
+ async selectRecovery(operationId) {
736
+ if (!this.view.identityAccess?.recoveries.some(value => value.operationId === operationId)) {
737
+ return { ok: false, error: '恢复操作已变化,请刷新后重试。' };
738
+ }
739
+ if (this.view.recoveryOperationId !== null)
740
+ this.leaveRecovery();
741
+ else
742
+ this.recoveryRevision++;
743
+ this.publish({ ...this.view, recoveryOperationId: operationId, recoveryProgress: null });
744
+ const result = await this.refreshRecoveryStatus();
745
+ return result.ok ? { ok: true, value: undefined } : result;
746
+ }
747
+ recoveryRevision = 0;
748
+ pendingRecovery = false;
749
+ recoveryCurrent(generation, revision) {
750
+ return this.current(generation) && revision === this.recoveryRevision;
751
+ }
752
+ recoveryLeft() {
753
+ return { ok: false, error: '已离开此恢复流程;进度保留在本机。' };
754
+ }
755
+ /** Navigation only. Never cancel or delete the Core-owned operation. */
756
+ leaveRecovery() {
757
+ this.recoveryRevision++;
758
+ this.accessRequest = undefined;
759
+ const wasPending = this.pendingRecovery;
760
+ if (wasPending)
761
+ this.pendingRequest = undefined;
762
+ this.pendingRecovery = false;
763
+ this.drafts.clearFlow();
764
+ storeRecoveryOperation(null, this.config?.tenantId);
765
+ this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: null, accessError: null,
766
+ accessLoading: false, pending: wasPending ? null : this.view.pending, error: null });
767
+ }
768
+ /** Inspect this tenant's durable operations before requesting any new OTP for a Handle. */
769
+ async continueRecoveryForHandle(handle) {
770
+ const target = handle.trim().toLowerCase();
771
+ const loaded = await this.refreshIdentityAccess();
772
+ if (!loaded.ok)
773
+ return loaded;
774
+ const matches = this.view.identityAccess?.recoveries.filter(value => target.includes('.')
775
+ ? value.fullHandle.toLowerCase() === target
776
+ : value.fullHandle.slice(0, value.fullHandle.indexOf('.')).toLowerCase() === target) ?? [];
777
+ if (matches.length > 1)
778
+ return { ok: false, error: '此账号的恢复状态存在冲突,请重新检查。' };
779
+ if (matches.length === 0)
780
+ return { ok: true, value: false };
781
+ const selected = await this.selectRecovery(matches[0].operationId);
782
+ return selected.ok ? { ok: true, value: true } : selected;
783
+ }
629
784
  /** Classify one Handle before sending exactly one registration or recovery OTP. */
630
785
  async inspectIdentityAccess(request) {
631
786
  return this.withPending('检查身份', () => call(() => this.remote.inspectIdentityAccess(request), identityAccessInspectionFailureMessage));
@@ -638,15 +793,82 @@ export class AwikiController {
638
793
  async registerIdentity(request) {
639
794
  const generation = this.generation;
640
795
  const result = await this.withPending('注册身份', () => call(() => this.remote.registerIdentity(request), registrationFailureMessage));
641
- if (!result.ok)
796
+ if (!result.ok) {
797
+ if (this.current(generation))
798
+ await this.refreshIdentityAccess();
642
799
  return result;
800
+ }
643
801
  if (!this.current(generation))
644
802
  return result;
645
- this.activatePresentationCache(result.value);
646
- this.publish({ ...this.view, sessionStatus: 'active', identity: result.value, error: null });
647
- await this.refreshConversationsAndRecoverGroups(generation);
803
+ if (result.value.status === 'join-required') {
804
+ this.publish({ ...this.view, identityAccess: { choice: result.value, joining: false, recoveries: [] } });
805
+ }
806
+ if (result.value.status === 'registered') {
807
+ this.drafts.clearScope();
808
+ this.drafts.setScope(this.config?.tenantId ?? 'default', result.value.identity.did);
809
+ this.activatePresentationCache(result.value.identity);
810
+ this.publish({ ...this.view, sessionStatus: 'active', identity: result.value.identity, error: null });
811
+ await this.refreshConversations(generation);
812
+ }
813
+ return result;
814
+ }
815
+ async beginDeviceJoin() {
816
+ const generation = this.generation;
817
+ const result = await this.withPending('开始加入设备', () => call(() => this.remote.beginDeviceJoin()));
818
+ if (result.ok && this.current(generation))
819
+ this.publish({ ...this.view, identityAccess: { choice: null, joining: true, recoveries: [] } });
820
+ return result;
821
+ }
822
+ async getDeviceJoinStatus() {
823
+ const generation = this.generation;
824
+ const result = await call(() => this.remote.getDeviceJoinStatus());
825
+ if (!this.current(generation))
826
+ return { ok: false, error: '身份环境已改变,请重新确认当前流程。' };
827
+ if (result.ok && result.value?.completed) {
828
+ this.drafts.clearFlow();
829
+ await this.open();
830
+ }
648
831
  return result;
649
832
  }
833
+ cancelDeviceJoin() {
834
+ return this.withPending('取消加入设备', async () => {
835
+ const generation = this.generation;
836
+ const result = await call(() => this.remote.cancelDeviceJoin());
837
+ if (result.ok && this.current(generation))
838
+ this.publish({ ...this.view, identityAccess: { choice: null, joining: false, recoveries: [] } });
839
+ return result.ok ? { ok: true, value: undefined } : result;
840
+ });
841
+ }
842
+ retireDeviceIdentityForRejoin() {
843
+ return this.withPending('准备重新加入设备', async () => {
844
+ const result = await call(() => this.remote.retireDeviceIdentityForRejoin());
845
+ if (!result.ok)
846
+ return result;
847
+ this.publish({ ...this.view, sessionStatus: 'unregistered', identity: null, error: null });
848
+ return { ok: true, value: undefined };
849
+ });
850
+ }
851
+ refreshDeviceManagement() {
852
+ return call(() => this.remote.refreshDeviceManagement());
853
+ }
854
+ startDeviceJoinVerification(request) {
855
+ return this.withPending('开始设备验证', () => call(() => this.remote.startDeviceJoinVerification(request)));
856
+ }
857
+ approveDeviceJoin(request) {
858
+ return this.withPending('批准设备', () => call(() => this.remote.approveDeviceJoin(request)));
859
+ }
860
+ rejectDeviceJoin(request) {
861
+ return this.withPending('拒绝设备', () => call(() => this.remote.rejectDeviceJoin(request)));
862
+ }
863
+ revokeDevice(request) {
864
+ return this.withPending('撤销设备', () => call(() => this.remote.revokeDevice(request)));
865
+ }
866
+ prepareRootTransfer(request) {
867
+ return this.withPending('准备授予管理权', () => call(() => this.remote.prepareRootTransfer(request)));
868
+ }
869
+ confirmRootTransfer(request) {
870
+ return this.withPending('验证并授予管理权', () => call(() => this.remote.confirmRootTransfer(request)));
871
+ }
650
872
  /**
651
873
  * Update the deployment identity's public display name.
652
874
  * @param displayName - replacement display name selected by the user.
@@ -686,105 +908,101 @@ export class AwikiController {
686
908
  this.publish({ ...this.view, identity, profile: result.value, error: null });
687
909
  return result;
688
910
  }
689
- /** Request a recovery OTP and persist only its secret-free operation id in the browser. */
911
+ /** Request a dedicated OTP only within the selected account flow. */
690
912
  async sendRecoveryOtp(request) {
691
- const result = await this.withPending('发送恢复验证码', () => call(() => this.remote.sendRecoveryOtp(request), registrationOtpFailureMessage), { publishFailure: false });
692
- if (!result.ok)
913
+ const generation = this.generation;
914
+ const revision = this.recoveryRevision;
915
+ const result = await this.withPending('发送恢复验证码', () => call(() => this.remote.sendRecoveryOtp(request), registrationOtpFailureMessage), { publishFailure: false, recovery: true });
916
+ if (!this.recoveryCurrent(generation, revision))
917
+ return this.recoveryLeft();
918
+ if (!result.ok) {
919
+ await this.continueRecoveryForHandle(request.fullHandle);
693
920
  return result;
694
- storeRecoveryOperation(this.config?.tenantDomain, result.value.operationId);
921
+ }
695
922
  this.publish({ ...this.view, recoveryOperationId: result.value.operationId, recoveryProgress: null });
696
- return result;
697
- }
698
- /** Verify the recovery OTP without attempting the remote identity mutation yet. */
699
- async prepareRecovery(request) {
700
- const operationId = this.view.recoveryOperationId;
701
- if (operationId === null)
702
- return this.fail('请先获取恢复验证码');
703
- const result = await this.withPending('验证恢复信息', () => call(() => this.remote.prepareRecovery({ ...request, operationId }), recoveryPreparationFailureMessage, recoveryCarrierFailureMessage), { publishFailure: false });
704
- if (result.ok)
705
- this.publish({ ...this.view, recoveryProgress: result.value });
706
- return result;
923
+ await this.refreshRecoveryStatus();
924
+ return this.recoveryCurrent(generation, revision) ? result : this.recoveryLeft();
707
925
  }
708
- /** Commit the prepared operation once. Unknown outcomes remain available through status refresh. */
709
- async activateRecovery() {
926
+ /** Publish results only into the exact selected flow; Core work survives navigation. */
927
+ async runRecovery(label, operation, enterWhenApplied = false) {
928
+ const generation = this.generation;
929
+ const revision = this.recoveryRevision;
710
930
  const operationId = this.view.recoveryOperationId;
711
931
  if (operationId === null)
712
- return this.fail('没有可继续的身份恢复操作');
713
- if (this.view.recoveryProgress?.phase !== 'ready_to_commit')
714
- return this.fail('请先完成恢复信息验证');
715
- const phase = this.view.recoveryProgress.phase;
716
- const result = await this.withPending('恢复身份', () => call(() => this.remote.activateRecovery({ operationId }), failure => recoveryContinuationFailureMessage(failure, phase), recoveryCarrierFailureMessage), { publishFailure: false });
717
- if (!result.ok)
932
+ return { ok: false, error: '没有可继续的身份恢复操作' };
933
+ const result = await this.withPending(label, () => operation(operationId), { publishFailure: false, recovery: true });
934
+ if (!this.recoveryCurrent(generation, revision))
935
+ return this.recoveryLeft();
936
+ if (!result.ok) {
937
+ // Reconcile authority after an ambiguous failure, never infer whether Commit occurred.
938
+ const status = await call(() => this.remote.getRecoveryStatus({ operationId }));
939
+ if (!this.recoveryCurrent(generation, revision))
940
+ return this.recoveryLeft();
941
+ this.publish({ ...this.view, recoveryProgress: status.ok ? status.value : this.view.recoveryProgress,
942
+ accessError: status.ok ? null : status.error });
718
943
  return result;
719
- this.publish({ ...this.view, recoveryProgress: result.value });
720
- if (result.value.phase === 'applied') {
721
- storeRecoveryOperation(this.config?.tenantDomain, null);
722
- this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: result.value });
723
- await this.open();
724
944
  }
945
+ this.publish({ ...this.view, recoveryProgress: result.value, accessError: null });
946
+ if (enterWhenApplied && result.value.phase === 'applied')
947
+ return this.enterRecoveredSession();
725
948
  return result;
726
949
  }
727
- /** Refresh Core status without repeating activation. */
950
+ async prepareRecovery(request) {
951
+ if (!this.view.recoveryProgress?.allowedActions?.includes('prepare'))
952
+ return { ok: false, error: '请先检查恢复状态' };
953
+ return this.runRecovery('验证恢复信息', operationId => call(() => this.remote.prepareRecovery({ ...request, operationId }), recoveryPreparationFailureMessage, recoveryCarrierFailureMessage));
954
+ }
955
+ async activateRecovery() {
956
+ const progress = this.view.recoveryProgress;
957
+ if (!progress?.allowedActions?.includes('activate'))
958
+ return { ok: false, error: '请先完成恢复信息验证' };
959
+ return this.runRecovery('恢复身份', operationId => call(() => this.remote.activateRecovery({ operationId }), failure => recoveryContinuationFailureMessage(failure, progress.phase), recoveryCarrierFailureMessage), true);
960
+ }
961
+ /** Read-only even for applied operations. Entering the identity is a separate action. */
728
962
  async refreshRecoveryStatus() {
729
- const operationId = this.view.recoveryOperationId;
730
- if (operationId === null)
731
- return this.fail('没有可查询的身份恢复操作');
732
- const result = await this.withPending('刷新恢复状态', () => call(() => this.remote.getRecoveryStatus({ operationId }), undefined, recoveryCarrierFailureMessage), { publishFailure: false });
733
- if (!result.ok)
734
- return result;
735
- this.publish({ ...this.view, recoveryProgress: result.value });
736
- if (result.value.phase === 'applied') {
737
- storeRecoveryOperation(this.config?.tenantDomain, null);
738
- this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: result.value });
739
- await this.open();
740
- }
741
- return result;
963
+ return this.runRecovery('刷新恢复状态', operationId => call(() => this.remote.getRecoveryStatus({ operationId }), undefined, recoveryCarrierFailureMessage));
742
964
  }
743
- /** Resume only a Core-declared retryable or uncertain phase. */
744
965
  async resumeRecovery() {
745
- const operationId = this.view.recoveryOperationId;
746
966
  const progress = this.view.recoveryProgress;
747
- if (operationId === null || progress === null)
748
- return this.fail('请先刷新恢复状态');
749
- if (!progress.retryable && !['remote_outcome_unknown', 'remote_committed', 'identity_transition_pending'].includes(progress.phase)) {
750
- return this.fail('当前恢复状态不能重试');
967
+ if (!progress?.allowedActions?.includes('resume'))
968
+ return { ok: false, error: '当前恢复状态不能重试' };
969
+ return this.runRecovery('继续恢复身份', operationId => call(() => this.remote.resumeRecovery({ operationId }), failure => recoveryContinuationFailureMessage(failure, progress.phase), recoveryCarrierFailureMessage), true);
970
+ }
971
+ async enterRecoveredSession() {
972
+ const generation = this.generation;
973
+ const revision = this.recoveryRevision;
974
+ const progress = this.view.recoveryProgress;
975
+ if (progress?.phase !== 'applied' || !progress.allowedActions?.includes('activate_identity')) {
976
+ return { ok: false, error: '当前恢复身份尚不可进入,请重新检查。' };
751
977
  }
752
- const result = await this.withPending('继续恢复身份', () => call(() => this.remote.resumeRecovery({ operationId }), failure => recoveryContinuationFailureMessage(failure, progress.phase), recoveryCarrierFailureMessage), { publishFailure: false });
978
+ const result = await this.withPending('进入恢复后的身份', () => call(() => this.remote.enterRecoveredSession({ operationId: progress.operationId })), { publishFailure: false, recovery: true });
979
+ if (!this.recoveryCurrent(generation, revision))
980
+ return this.recoveryLeft();
753
981
  if (!result.ok)
754
982
  return result;
755
- this.publish({ ...this.view, recoveryProgress: result.value });
756
- if (result.value.phase === 'applied') {
757
- storeRecoveryOperation(this.config?.tenantDomain, null);
758
- this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: result.value });
759
- await this.open();
760
- }
761
- return result;
983
+ this.leaveRecovery();
984
+ const opened = await this.open();
985
+ return opened.ok ? result : opened;
762
986
  }
763
- /** Discard only a pre-attempt operation. */
987
+ /** Only Core-authorized pre-attempt cancellation destroys the pending operation. */
764
988
  async discardRecovery() {
989
+ const generation = this.generation;
990
+ const revision = this.recoveryRevision;
765
991
  const operationId = this.view.recoveryOperationId;
766
992
  if (operationId === null)
767
993
  return { ok: true, value: undefined };
768
- const phase = this.view.recoveryProgress?.phase;
769
- if (phase !== undefined && phase !== 'awaiting_factor' && phase !== 'ready_to_commit') {
770
- return this.fail('当前恢复状态不能取消');
994
+ if (!this.view.recoveryProgress?.allowedActions?.includes('discard_pre_attempt')) {
995
+ return { ok: false, error: '当前恢复状态不能取消;可以返回入口,稍后继续。' };
771
996
  }
772
- const result = await this.withPending('取消身份恢复', () => call(() => this.remote.discardRecovery({ operationId })), { publishFailure: false });
997
+ const result = await this.withPending('取消身份恢复', () => call(() => this.remote.discardRecovery({ operationId })), { publishFailure: false, recovery: true });
998
+ if (!this.recoveryCurrent(generation, revision))
999
+ return this.recoveryLeft();
773
1000
  if (!result.ok)
774
1001
  return result;
775
- storeRecoveryOperation(this.config?.tenantDomain, null);
776
- this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: null });
1002
+ this.leaveRecovery();
1003
+ void this.refreshIdentityAccess();
777
1004
  return { ok: true, value: undefined };
778
1005
  }
779
- /** Return cached browser-safe runtime policy, or load it once on demand. */
780
- async getConfig() {
781
- if (this.config !== null)
782
- return { ok: true, value: this.config };
783
- const result = await call(() => this.remote.getConfig(), mailFailureMessage);
784
- if (result.ok)
785
- this.config = result.value;
786
- return result;
787
- }
788
1006
  /** Read the active deployment identity's public mailbox state. */
789
1007
  getMailAccount() {
790
1008
  return call(() => this.remote.getMailAccount(), mailFailureMessage);
@@ -805,16 +1023,6 @@ export class AwikiController {
805
1023
  sendMail(request) {
806
1024
  return call(() => this.remote.sendMail(request), mailFailureMessage);
807
1025
  }
808
- /** Download one explicitly selected mail attachment without retaining its bytes. */
809
- async downloadMailAttachment(request) {
810
- if (this.disposed)
811
- return { ok: false, error: 'AWiki 插件已卸载' };
812
- const generation = this.generation;
813
- const result = await call(() => this.remote.downloadMailAttachment(request), mailFailureMessage);
814
- if (!this.current(generation))
815
- return { ok: false, error: 'AWiki 已关闭' };
816
- return result;
817
- }
818
1026
  /**
819
1027
  * Load another page of the conversation roster.
820
1028
  * @returns successful pagination or one display-safe failure.
@@ -834,33 +1042,10 @@ export class AwikiController {
834
1042
  ...this.view,
835
1043
  conversations: appendUnique(this.view.conversations, conversations.visible, value => value.id),
836
1044
  hiddenConversations: conversations.hidden,
837
- groupRecovery: this.currentGroupRecoveryView(),
838
1045
  conversationsHasMore: result.value.hasMore && result.value.nextCursor !== undefined,
839
1046
  });
840
1047
  return { ok: true, value: undefined };
841
1048
  }
842
- /** Synchronize account projections, then retry Core's durable group-rebind journal. */
843
- async retryGroupRebindRecovery() {
844
- const generation = this.generation;
845
- return this.withPending('恢复群聊身份', async () => {
846
- const listed = await this.refreshConversations(generation, true);
847
- if (!listed.ok)
848
- return { ok: false, error: listed.error };
849
- return this.resumeGroupRebindRecovery(generation);
850
- });
851
- }
852
- /** Dismiss only the current recovery-summary revision; Core recovery remains untouched. */
853
- async dismissGroupRecoveryNotice() {
854
- const signature = this.lastGroupRecoverySignature;
855
- if (signature === undefined || this.view.groupRecovery === null)
856
- return { ok: true, value: undefined };
857
- const result = await call(() => this.remote.updateConversationPreference({ action: 'dismiss-group-recovery', signature }), conversationPreferenceFailureMessage);
858
- if (!result.ok)
859
- return this.fail(result.error);
860
- this.applyConversationPreferences(result.value);
861
- this.publish({ ...this.view, groupRecovery: null, error: null });
862
- return { ok: true, value: undefined };
863
- }
864
1049
  /** Hide one recent row locally without leaving a group or deleting history. */
865
1050
  async hideConversation(conversationId) {
866
1051
  const conversation = this.view.conversations.find(item => item.id === conversationId);
@@ -896,7 +1081,6 @@ export class AwikiController {
896
1081
  refreshing: false,
897
1082
  }
898
1083
  : {}),
899
- groupRecovery: this.currentGroupRecoveryView(),
900
1084
  error: null,
901
1085
  });
902
1086
  return { ok: true, value: undefined };
@@ -918,7 +1102,6 @@ export class AwikiController {
918
1102
  ...this.view,
919
1103
  conversations: appendUnique([conversation], this.view.conversations, item => item.id),
920
1104
  hiddenConversations: this.hiddenConversationsView(),
921
- groupRecovery: this.currentGroupRecoveryView(),
922
1105
  error: null,
923
1106
  });
924
1107
  return { ok: true, value: undefined };
@@ -1074,9 +1257,6 @@ export class AwikiController {
1074
1257
  const generation = this.generation;
1075
1258
  this.publish({ ...this.view, pending: '刷新群成员', error: null });
1076
1259
  const result = await (async () => {
1077
- if (this.view.groupAccess?.status !== 'available' && this.view.groupAccess?.status !== 'loading') {
1078
- await this.resumeGroupRebindRecovery(this.generation);
1079
- }
1080
1260
  const refreshed = await this.loadGroupState(conversation, true, true);
1081
1261
  return refreshed.ok ? { ok: true, value: undefined } : refreshed;
1082
1262
  })();
@@ -1613,28 +1793,90 @@ export class AwikiController {
1613
1793
  }
1614
1794
  return result;
1615
1795
  }
1616
- /** Clear Host-owned local data and immediately remove every cached browser projection. */
1796
+ /** Clear current-tenant local data and only its owned browser projections. */
1617
1797
  async clearLocalData(request) {
1618
1798
  if (this.disposed)
1619
1799
  return { ok: false, error: 'AWiki 插件已卸载' };
1800
+ const generation = this.generation;
1801
+ const ownerDid = this.presentationCacheOwnerDid;
1802
+ const tenantId = this.config?.tenantId;
1620
1803
  const result = await call(() => this.remote.clearLocalData(request));
1621
1804
  if (!result.ok)
1622
1805
  return result;
1623
- await this.persistentImageCache.clear().catch(() => undefined);
1806
+ const owners = [...(result.value.clearedIdentityDids ?? []), ...(ownerDid === null ? [] : [ownerDid])];
1807
+ for (const owner of new Set(owners)) {
1808
+ await this.persistentImageCache.clear(owner).catch(() => undefined);
1809
+ try {
1810
+ clearMailBrowserCache(globalThis.localStorage, owner);
1811
+ }
1812
+ catch { }
1813
+ }
1814
+ storeRecoveryOperation(null, tenantId);
1815
+ if (!this.current(generation))
1816
+ return result;
1817
+ this.drafts.clearScope();
1624
1818
  this.close();
1625
- const tenantDomain = this.config?.tenantDomain;
1626
1819
  this.config = null;
1627
1820
  this.conversationsCursor = undefined;
1628
1821
  this.historyCursor = undefined;
1629
1822
  this.unreadAtOpen.clear();
1630
1823
  this.summaryBaselines.clear();
1631
1824
  this.clearPresentationCache();
1632
- storeRecoveryOperation(tenantDomain, null);
1633
- this.publish({ ...INITIAL_VIEW, status: 'ready' });
1825
+ this.publish({ ...INITIAL_VIEW, status: 'ready', accessLoading: false, identityAccess: { choice: null, joining: false, recoveries: [] } });
1634
1826
  return result;
1635
1827
  }
1828
+ /** Read the Integration without coupling Guest Gateway health to the main AWiki view. */
1829
+ getIntegration() {
1830
+ return callIntegration(() => this.remote.getIntegration());
1831
+ }
1832
+ createIntegration(request) {
1833
+ return callIntegration(() => this.remote.createIntegration(request));
1834
+ }
1835
+ updateIntegration(request) {
1836
+ return callIntegration(() => this.remote.updateIntegration(request));
1837
+ }
1838
+ rotateIntegrationId(request) {
1839
+ return callIntegration(() => this.remote.rotateIntegrationId(request));
1840
+ }
1841
+ closeIntegration(request) {
1842
+ return callIntegration(() => this.remote.closeIntegration(request));
1843
+ }
1844
+ reopenIntegration(request) {
1845
+ return callIntegration(() => this.remote.reopenIntegration(request));
1846
+ }
1847
+ /** Return only locally known groups for which the active identity is authoritative owner. */
1848
+ async listOwnedGroups() {
1849
+ const groups = [];
1850
+ const visited = new Set();
1851
+ let cursor;
1852
+ for (let pageIndex = 0; pageIndex < 20 && groups.length < 20; pageIndex += 1) {
1853
+ const conversations = await call(() => this.remote.listConversations({
1854
+ limit: 100,
1855
+ ...(cursor === undefined ? {} : { cursor }),
1856
+ }));
1857
+ if (!conversations.ok)
1858
+ return conversations;
1859
+ for (const conversation of conversations.value.items) {
1860
+ if (conversation.kind !== 'group' || visited.has(conversation.groupDid))
1861
+ continue;
1862
+ visited.add(conversation.groupDid);
1863
+ const snapshot = await call(() => this.remote.getGroup({ groupDid: conversation.groupDid }));
1864
+ if (!snapshot.ok)
1865
+ continue;
1866
+ if (snapshot.value.myRole === 'owner')
1867
+ groups.push(snapshot.value);
1868
+ if (groups.length >= 20)
1869
+ break;
1870
+ }
1871
+ cursor = conversations.value.nextCursor;
1872
+ if (cursor === undefined)
1873
+ break;
1874
+ }
1875
+ return { ok: true, value: groups };
1876
+ }
1636
1877
  /** Stop timers, invalidate work, and drop subscribers during HMR unload. */
1637
1878
  dispose() {
1879
+ this.drafts.clear();
1638
1880
  this.disposed = true;
1639
1881
  this.close();
1640
1882
  this.listeners.clear();
@@ -1654,25 +1896,12 @@ export class AwikiController {
1654
1896
  hiddenAt: hidden.hiddenAt,
1655
1897
  });
1656
1898
  }
1657
- this.dismissedGroupRecoverySignature = preferences.dismissedGroupRecoverySignature;
1658
1899
  }
1659
1900
  hiddenConversationsView() {
1660
1901
  return [...this.hiddenConversationPreferences.values()]
1661
1902
  .sort((left, right) => right.hiddenAt - left.hiddenAt)
1662
1903
  .map(item => item.conversation);
1663
1904
  }
1664
- currentGroupRecoveryView() {
1665
- const summary = this.lastGroupRecoverySummary;
1666
- if (summary === undefined)
1667
- return this.view.groupRecovery;
1668
- const hiddenGroupDids = new Set([...this.hiddenConversationPreferences.values()]
1669
- .map(item => item.conversation)
1670
- .filter((conversation) => conversation.kind === 'group')
1671
- .map(conversation => conversation.groupDid));
1672
- const projection = groupRecoveryProjection(summary, hiddenGroupDids);
1673
- this.lastGroupRecoverySignature = projection.signature;
1674
- return projection.signature === this.dismissedGroupRecoverySignature ? null : projection.view;
1675
- }
1676
1905
  async reconcileConversationPage(incoming, generation) {
1677
1906
  const current = [...this.view.conversations, ...this.view.hiddenConversations];
1678
1907
  const conversations = incoming.map(item => this.cacheConversation(item, current.find(candidate => candidate.id === item.id)));
@@ -1723,7 +1952,6 @@ export class AwikiController {
1723
1952
  ? refreshed.visible
1724
1953
  : appendUnique(refreshed.visible, this.view.conversations, value => value.id),
1725
1954
  hiddenConversations: refreshed.hidden,
1726
- groupRecovery: this.currentGroupRecoveryView(),
1727
1955
  conversationsHasMore: firstPage
1728
1956
  ? result.value.hasMore && result.value.nextCursor !== undefined
1729
1957
  : this.view.conversationsHasMore,
@@ -1734,14 +1962,18 @@ export class AwikiController {
1734
1962
  /** List the active identity's own conversations and detect a revoked local credential. */
1735
1963
  async listConversationPage(request) {
1736
1964
  const result = await callWithFailureCode(() => this.remote.listConversations(request));
1965
+ if (!result.ok && result.failureCode === 'device-rejoin-required') {
1966
+ this.enterBlockedIdentityState('device-rejoin-required');
1967
+ return { ok: false, error: '当前设备已被撤销,请重新申请加入。' };
1968
+ }
1737
1969
  if (!result.ok && (result.failureCode === 'identity-recovery-required' || result.failureCode === 'forbidden')) {
1738
- this.enterIdentityRecoveryRequired();
1970
+ this.enterBlockedIdentityState('recovery-required');
1739
1971
  return { ok: false, error: '当前设备的 AWiki 身份凭证已失效,请重新恢复身份。' };
1740
1972
  }
1741
1973
  return result.ok ? result : { ok: false, error: result.error };
1742
1974
  }
1743
1975
  /** Replace only visible browser projections; Core identity and SQLite state remain untouched. */
1744
- enterIdentityRecoveryRequired() {
1976
+ enterBlockedIdentityState(status) {
1745
1977
  if (this.view.identity === null)
1746
1978
  return;
1747
1979
  this.close();
@@ -1750,14 +1982,11 @@ export class AwikiController {
1750
1982
  this.groupMembersCursor = undefined;
1751
1983
  this.unreadAtOpen.clear();
1752
1984
  this.summaryBaselines.clear();
1753
- this.groupRecoveryItems.clear();
1754
- this.lastGroupRecoverySignature = undefined;
1755
- this.lastGroupRecoverySummary = undefined;
1756
1985
  this.clearImageAttachments();
1757
1986
  this.publish({
1758
1987
  ...this.view,
1759
1988
  status: 'ready',
1760
- sessionStatus: 'recovery-required',
1989
+ sessionStatus: status,
1761
1990
  conversations: Object.freeze([]),
1762
1991
  hiddenConversations: Object.freeze([]),
1763
1992
  conversationsHasMore: false,
@@ -1766,7 +1995,6 @@ export class AwikiController {
1766
1995
  groupAccess: null,
1767
1996
  groupMembers: Object.freeze([]),
1768
1997
  groupMembersHasMore: false,
1769
- groupRecovery: null,
1770
1998
  messages: Object.freeze([]),
1771
1999
  historyHasMore: false,
1772
2000
  localPending: false,
@@ -1776,49 +2004,6 @@ export class AwikiController {
1776
2004
  summaries: Object.freeze({}),
1777
2005
  });
1778
2006
  }
1779
- /** Hydrate account-projected groups before asking Core to resume their durable rebind jobs. */
1780
- async refreshConversationsAndRecoverGroups(generation, background = false) {
1781
- const listed = await this.refreshConversations(generation, background);
1782
- if (!listed.ok || !this.current(generation))
1783
- return listed;
1784
- await this.resumeGroupRebindRecovery(generation);
1785
- return listed;
1786
- }
1787
- resumeGroupRebindRecovery(generation) {
1788
- const active = this.groupRecoveryOperation;
1789
- if (active !== undefined && active.generation === generation)
1790
- return active.operation;
1791
- let operation;
1792
- operation = call(() => this.remote.resumeGroupRebindRecovery(), groupRecoveryFailureMessage).then((result) => {
1793
- if (!this.current(generation))
1794
- return result;
1795
- if (result.ok) {
1796
- this.lastGroupRecoverySummary = result.value;
1797
- this.groupRecoveryItems.clear();
1798
- for (const item of result.value.items) {
1799
- this.groupRecoveryItems.set(item.groupDid, item.status);
1800
- }
1801
- }
1802
- else {
1803
- this.lastGroupRecoverySummary = undefined;
1804
- this.lastGroupRecoverySignature = 'unavailable';
1805
- }
1806
- this.publish({
1807
- ...this.view,
1808
- groupRecovery: result.ok
1809
- ? this.currentGroupRecoveryView()
1810
- : this.dismissedGroupRecoverySignature === 'unavailable'
1811
- ? null
1812
- : { status: 'unavailable', pending: 0, blocked: 0 },
1813
- });
1814
- return result;
1815
- }).finally(() => {
1816
- if (this.groupRecoveryOperation?.operation === operation)
1817
- this.groupRecoveryOperation = undefined;
1818
- });
1819
- this.groupRecoveryOperation = { generation, operation };
1820
- return operation;
1821
- }
1822
2007
  async loadHistory(older) {
1823
2008
  const conversationId = this.view.selectedConversationId;
1824
2009
  if (conversationId === null)
@@ -1862,13 +2047,7 @@ export class AwikiController {
1862
2047
  return;
1863
2048
  this.polling = true;
1864
2049
  try {
1865
- const knownGroups = new Set(this.view.conversations
1866
- .filter(conversation => conversation.kind === 'group')
1867
- .map(conversation => conversation.groupDid));
1868
- const listed = await this.refreshConversations(generation, true);
1869
- if (listed.ok && this.view.conversations.some(conversation => conversation.kind === 'group' && !knownGroups.has(conversation.groupDid))) {
1870
- await this.resumeGroupRebindRecovery(generation);
1871
- }
2050
+ await this.refreshConversations(generation, true);
1872
2051
  const selected = this.view.selectedConversationId;
1873
2052
  if (selected === null || !this.current(generation))
1874
2053
  return;
@@ -1882,6 +2061,19 @@ export class AwikiController {
1882
2061
  this.publish({ ...this.view, error: 'AWiki 远端消息归属不一致,请重新打开会话。' });
1883
2062
  return;
1884
2063
  }
2064
+ const displayPeers = [...new Set([
2065
+ ...this.view.messages.map(message => message.senderDid),
2066
+ ...this.view.groupMembers.flatMap(member => member.did === undefined ? [] : [member.did]),
2067
+ ])];
2068
+ const displayProfiles = [];
2069
+ for (let start = 0; start < displayPeers.length; start += 100) {
2070
+ const profileResult = await call(() => this.remote.getDisplayProfiles(displayPeers.slice(start, start + 100)));
2071
+ if (!this.current(generation) || this.view.selectedConversationId !== selected)
2072
+ return;
2073
+ if (profileResult.ok)
2074
+ displayProfiles.push(...profileResult.value);
2075
+ }
2076
+ const byDid = new Map(displayProfiles.filter(profile => profile.cacheHit).map(profile => [profile.did, profile]));
1885
2077
  const existingIds = new Set(this.view.messages.map(message => message.id));
1886
2078
  const incoming = result.value.items.filter(message => !existingIds.has(message.id));
1887
2079
  const messages = mergeLatestMessages(this.view.messages, result.value.items);
@@ -1891,7 +2083,22 @@ export class AwikiController {
1891
2083
  }
1892
2084
  this.publish({
1893
2085
  ...this.view,
1894
- messages,
2086
+ messages: messages.map(message => {
2087
+ const profile = byDid.get(message.senderDid);
2088
+ if (profile === undefined)
2089
+ return message;
2090
+ const { senderDisplayName: _snapshot, ...rest } = message;
2091
+ const handle = profile.handle ?? message.senderHandle;
2092
+ return { ...rest, ...profile.displayName === undefined ? {} : { senderDisplayName: profile.displayName }, ...handle === undefined ? {} : { senderHandle: handle } };
2093
+ }),
2094
+ groupMembers: this.view.groupMembers.map(member => {
2095
+ const profile = member.did === undefined ? undefined : byDid.get(member.did);
2096
+ if (profile === undefined)
2097
+ return member;
2098
+ const { displayName: _snapshot, ...rest } = member;
2099
+ const handle = profile.handle ?? member.handle;
2100
+ return { ...rest, ...profile.displayName === undefined ? {} : { displayName: profile.displayName }, ...handle === undefined ? {} : { handle } };
2101
+ }),
1895
2102
  summaries: this.staleSummaries(selected, incoming),
1896
2103
  });
1897
2104
  }
@@ -1899,20 +2106,34 @@ export class AwikiController {
1899
2106
  this.polling = false;
1900
2107
  }
1901
2108
  }
2109
+ pendingRequest;
1902
2110
  async withPending(label, operation, options = {}) {
1903
2111
  if (this.disposed)
1904
2112
  return { ok: false, error: 'AWiki 插件已卸载' };
2113
+ if (this.pendingRequest !== undefined)
2114
+ return { ok: false, error: '上一项操作仍在处理,请稍候再试。' };
2115
+ const token = Symbol(label);
2116
+ this.pendingRequest = token;
2117
+ this.pendingRecovery = options.recovery === true;
1905
2118
  const generation = this.generation;
1906
2119
  this.publish({ ...this.view, pending: label, error: null });
1907
- const result = await operation();
1908
- if (!this.current(generation))
2120
+ try {
2121
+ const result = await operation();
2122
+ if (this.current(generation) && this.pendingRequest === token)
2123
+ this.publish({
2124
+ ...this.view, error: result.ok || options.publishFailure === false ? null : result.error,
2125
+ });
1909
2126
  return result;
1910
- this.publish({
1911
- ...this.view,
1912
- pending: null,
1913
- error: result.ok || options.publishFailure === false ? null : result.error,
1914
- });
1915
- return result;
2127
+ }
2128
+ finally {
2129
+ if (this.pendingRequest === token) {
2130
+ this.pendingRequest = undefined;
2131
+ this.pendingRecovery = false;
2132
+ // Closing/reopening invalidates reads, but the original mutation still settles.
2133
+ if (!this.disposed)
2134
+ this.publish({ ...this.view, pending: null });
2135
+ }
2136
+ }
1916
2137
  }
1917
2138
  appendMessage(message) {
1918
2139
  if (this.view.selectedConversationId !== message.conversationId)
@@ -1963,14 +2184,7 @@ export class AwikiController {
1963
2184
  return selected === null ? undefined : this.view.conversations.find(value => value.id === selected);
1964
2185
  }
1965
2186
  publishGroupAccessFailure(conversation, reason) {
1966
- const journal = this.groupRecoveryItems.get(conversation.groupDid);
1967
- const status = reason === 'not-member'
1968
- ? journal === 'blocked'
1969
- ? 'blocked'
1970
- : journal === 'pending'
1971
- ? 'recovering'
1972
- : 'not-member'
1973
- : reason;
2187
+ const status = reason;
1974
2188
  this.publish({
1975
2189
  ...this.view,
1976
2190
  groupAccess: { groupDid: conversation.groupDid, status },
@@ -1984,11 +2198,7 @@ export class AwikiController {
1984
2198
  return;
1985
2199
  this.directProfiles.clear();
1986
2200
  this.groupTitles.clear();
1987
- this.groupRecoveryItems.clear();
1988
2201
  this.hiddenConversationPreferences.clear();
1989
- this.dismissedGroupRecoverySignature = undefined;
1990
- this.lastGroupRecoverySignature = undefined;
1991
- this.lastGroupRecoverySummary = undefined;
1992
2202
  this.clearImageAttachments();
1993
2203
  this.presentationCacheOwnerDid = ownerDid;
1994
2204
  }
@@ -1996,11 +2206,7 @@ export class AwikiController {
1996
2206
  clearPresentationCache() {
1997
2207
  this.directProfiles.clear();
1998
2208
  this.groupTitles.clear();
1999
- this.groupRecoveryItems.clear();
2000
2209
  this.hiddenConversationPreferences.clear();
2001
- this.dismissedGroupRecoverySignature = undefined;
2002
- this.lastGroupRecoverySignature = undefined;
2003
- this.lastGroupRecoverySummary = undefined;
2004
2210
  this.clearImageAttachments();
2005
2211
  this.presentationCacheOwnerDid = null;
2006
2212
  }