@awiki/dsh-plugin 0.2.5 → 0.3.0-rc.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.
- package/README.md +63 -23
- package/README.zh.md +55 -20
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/cordis.patch.yml +0 -8
- package/lib/client.js +5452 -1849
- package/lib/client.js.map +1 -1
- package/lib/index.js +950 -38
- package/lib/model-proxy-contract.js +8 -1
- package/lib/provider.js +3 -1
- package/lib/{sdk-adapter-DKgkyApB.mjs → sdk-adapter-DLhLrLI6.mjs} +219 -2
- package/lib/typert.host.js +796 -49
- package/lib/typert.remote-client.d.ts +33 -1
- package/lib/typert.remote-client.d.ts.map +1 -1
- package/lib/typert.remote-client.js +796 -49
- package/lib/types/client/AwikiGroupDetails.d.ts +16 -0
- package/lib/types/client/AwikiGroupDetails.d.ts.map +1 -0
- package/lib/types/client/AwikiGroupDetails.js +108 -0
- package/lib/types/client/AwikiGroupDetails.js.map +1 -0
- package/lib/types/client/AwikiIdentityAccess.d.ts +21 -0
- package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -0
- package/lib/types/client/AwikiIdentityAccess.js +148 -0
- package/lib/types/client/AwikiIdentityAccess.js.map +1 -0
- package/lib/types/client/AwikiIdentityPage.d.ts +11 -0
- package/lib/types/client/AwikiIdentityPage.d.ts.map +1 -0
- package/lib/types/client/AwikiIdentityPage.js +8 -0
- package/lib/types/client/AwikiIdentityPage.js.map +1 -0
- package/lib/types/client/AwikiMail.d.ts +2 -1
- package/lib/types/client/AwikiMail.d.ts.map +1 -1
- package/lib/types/client/AwikiMail.js +153 -33
- package/lib/types/client/AwikiMail.js.map +1 -1
- package/lib/types/client/AwikiOnboarding.d.ts.map +1 -1
- package/lib/types/client/AwikiOnboarding.js +26 -9
- package/lib/types/client/AwikiOnboarding.js.map +1 -1
- package/lib/types/client/AwikiOverlay.d.ts +0 -5
- package/lib/types/client/AwikiOverlay.d.ts.map +1 -1
- package/lib/types/client/AwikiOverlay.js +136 -124
- package/lib/types/client/AwikiOverlay.js.map +1 -1
- package/lib/types/client/AwikiProfileCard.d.ts +9 -0
- package/lib/types/client/AwikiProfileCard.d.ts.map +1 -0
- package/lib/types/client/AwikiProfileCard.js +90 -0
- package/lib/types/client/AwikiProfileCard.js.map +1 -0
- package/lib/types/client/AwikiRecoveryForm.d.ts +24 -0
- package/lib/types/client/AwikiRecoveryForm.d.ts.map +1 -0
- package/lib/types/client/AwikiRecoveryForm.js +148 -0
- package/lib/types/client/AwikiRecoveryForm.js.map +1 -0
- package/lib/types/client/AwikiSettingsSection.d.ts +0 -2
- package/lib/types/client/AwikiSettingsSection.d.ts.map +1 -1
- package/lib/types/client/AwikiSettingsSection.js +13 -15
- package/lib/types/client/AwikiSettingsSection.js.map +1 -1
- package/lib/types/client/MentionText.d.ts +7 -0
- package/lib/types/client/MentionText.d.ts.map +1 -0
- package/lib/types/client/MentionText.js +10 -0
- package/lib/types/client/MentionText.js.map +1 -0
- package/lib/types/client/controller.d.ts +94 -2
- package/lib/types/client/controller.d.ts.map +1 -1
- package/lib/types/client/controller.js +466 -13
- package/lib/types/client/controller.js.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +21 -4
- package/lib/types/client/index.js.map +1 -1
- package/lib/types/client/mail-list-cache.d.ts +15 -0
- package/lib/types/client/mail-list-cache.d.ts.map +1 -0
- package/lib/types/client/mail-list-cache.js +212 -0
- package/lib/types/client/mail-list-cache.js.map +1 -0
- package/lib/types/client/mentions.d.ts +51 -0
- package/lib/types/client/mentions.d.ts.map +1 -0
- package/lib/types/client/mentions.js +165 -0
- package/lib/types/client/mentions.js.map +1 -0
- package/lib/types/client/model-proxy-controller.d.ts +4 -0
- package/lib/types/client/model-proxy-controller.d.ts.map +1 -1
- package/lib/types/client/model-proxy-controller.js +68 -3
- package/lib/types/client/model-proxy-controller.js.map +1 -1
- package/lib/types/client/settings-locales.js +8 -8
- package/lib/types/client/settings-locales.js.map +1 -1
- package/lib/types/client/slots.d.ts +21 -2
- package/lib/types/client/slots.d.ts.map +1 -1
- package/lib/types/index.d.ts +40 -6
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +523 -23
- package/lib/types/index.js.map +1 -1
- package/lib/types/model-proxy-contract.d.ts +6 -0
- package/lib/types/model-proxy-contract.d.ts.map +1 -1
- package/lib/types/model-proxy-contract.js +6 -0
- package/lib/types/model-proxy-contract.js.map +1 -1
- package/lib/types/provider-api.d.ts +29 -1
- package/lib/types/provider-api.d.ts.map +1 -1
- package/lib/types/provider.d.ts.map +1 -1
- package/lib/types/provider.js +2 -0
- package/lib/types/provider.js.map +1 -1
- package/lib/types/sdk-adapter.d.ts +18 -1
- package/lib/types/sdk-adapter.d.ts.map +1 -1
- package/lib/types/sdk-adapter.js +244 -2
- package/lib/types/sdk-adapter.js.map +1 -1
- package/lib/types/sent-mail-store.d.ts +22 -0
- package/lib/types/sent-mail-store.d.ts.map +1 -0
- package/lib/types/sent-mail-store.js +246 -0
- package/lib/types/sent-mail-store.js.map +1 -0
- package/lib/types/types.d.ts +152 -2
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js.map +1 -1
- package/package.json +15 -26
- package/lib/model-proxy.js +0 -383
- package/lib/types/client/model-account-settings-availability.d.ts +0 -4
- package/lib/types/client/model-account-settings-availability.d.ts.map +0 -1
- package/lib/types/client/model-account-settings-availability.js +0 -4
- package/lib/types/client/model-account-settings-availability.js.map +0 -1
- package/lib/types/model-proxy.d.ts +0 -14
- package/lib/types/model-proxy.d.ts.map +0 -1
- package/lib/types/model-proxy.js +0 -371
- package/lib/types/model-proxy.js.map +0 -1
|
@@ -12,7 +12,7 @@ function registrationFailureMessage(failure) {
|
|
|
12
12
|
case 'challenge-expired':
|
|
13
13
|
return '验证码状态已失效,请重新获取验证码后再注册。';
|
|
14
14
|
case 'handle-unavailable':
|
|
15
|
-
return '该 Handle
|
|
15
|
+
return '该 Handle 刚刚已被注册。请返回身份入口,再按恢复流程重新获取验证码。';
|
|
16
16
|
case 'conflict':
|
|
17
17
|
return '注册冲突:服务端可能已收到上次注册请求,或该手机号 / Handle 已绑定其他身份。请保留当前页面并再次提交;若仍失败,请勿清除本机身份数据,联系管理员并提供失败时间。';
|
|
18
18
|
case 'rate-limited':
|
|
@@ -44,13 +44,30 @@ function registrationOtpFailureMessage(failure) {
|
|
|
44
44
|
return registrationFailureMessage(failure);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
function identityAccessInspectionFailureMessage(failure) {
|
|
48
|
+
switch (failure.code) {
|
|
49
|
+
case 'invalid-request':
|
|
50
|
+
return 'Handle 格式不正确,请检查后重试。';
|
|
51
|
+
case 'network':
|
|
52
|
+
return '无法连接 AWiki 服务,暂时不能确认该 Handle 是否已经存在。';
|
|
53
|
+
case 'remote':
|
|
54
|
+
return 'AWiki 服务暂时无法确认该 Handle 的状态,请稍后重试。';
|
|
55
|
+
default:
|
|
56
|
+
return `${failure.code}:${failure.message}`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
47
59
|
const INITIAL_VIEW = Object.freeze({
|
|
48
60
|
status: 'cold',
|
|
49
61
|
sessionStatus: 'unregistered',
|
|
50
62
|
identity: null,
|
|
63
|
+
profile: null,
|
|
51
64
|
conversations: Object.freeze([]),
|
|
52
65
|
conversationsHasMore: false,
|
|
53
66
|
selectedConversationId: null,
|
|
67
|
+
selectedGroup: null,
|
|
68
|
+
groupMembers: Object.freeze([]),
|
|
69
|
+
groupMembersHasMore: false,
|
|
70
|
+
groupRecovery: null,
|
|
54
71
|
messages: Object.freeze([]),
|
|
55
72
|
historyHasMore: false,
|
|
56
73
|
localPending: false,
|
|
@@ -59,6 +76,8 @@ const INITIAL_VIEW = Object.freeze({
|
|
|
59
76
|
error: null,
|
|
60
77
|
attachmentMaxBytes: 0,
|
|
61
78
|
summaries: Object.freeze({}),
|
|
79
|
+
recoveryOperationId: null,
|
|
80
|
+
recoveryProgress: null,
|
|
62
81
|
});
|
|
63
82
|
/** Turn a closed Host summary failure into one actionable Chinese message. */
|
|
64
83
|
function summaryFailureMessage(failure) {
|
|
@@ -85,12 +104,58 @@ function mailFailureMessage(failure) {
|
|
|
85
104
|
default: return 'AWiki 邮件服务暂时不可用,请稍后重试。';
|
|
86
105
|
}
|
|
87
106
|
}
|
|
107
|
+
/** Turn a rejected group create into a concrete next step instead of exposing Host error text. */
|
|
108
|
+
function groupCreateFailureMessage(failure) {
|
|
109
|
+
switch (failure.code) {
|
|
110
|
+
case 'invalid-request': return '群聊名称或首批成员格式不正确,请检查后重试。';
|
|
111
|
+
case 'not-registered': return '请先注册 AWiki 身份后创建群聊。';
|
|
112
|
+
case 'signed-out': return '当前 AWiki 身份已退出,请重新进入后创建群聊。';
|
|
113
|
+
case 'forbidden': return '当前 AWiki 身份没有创建群聊的权限。';
|
|
114
|
+
case 'rate-limited': return '创建群聊过于频繁,请稍后重试。';
|
|
115
|
+
case 'network': return '无法连接 AWiki 服务,请检查网络后重试。';
|
|
116
|
+
case 'conflict': return '群聊创建状态发生冲突,请刷新群聊列表后确认是否已经创建。';
|
|
117
|
+
case 'remote': return 'AWiki 服务暂时无法创建群聊,请稍后重试。';
|
|
118
|
+
default: return `${failure.code}:${failure.message}`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Keep group authorization failures actionable without exposing raw service text. */
|
|
122
|
+
function groupReadFailureMessage(failure) {
|
|
123
|
+
switch (failure.code) {
|
|
124
|
+
case 'group-membership-required':
|
|
125
|
+
return '当前身份暂时无法访问这个群聊。若刚完成身份恢复,请返回会话列表重试群聊身份恢复;若持续出现,可能已不再是群成员。';
|
|
126
|
+
case 'group-identity-stale':
|
|
127
|
+
return '群聊身份正在恢复,请稍候后重试。';
|
|
128
|
+
case 'not-found':
|
|
129
|
+
return '这个群聊不存在,或当前身份已经无法访问。';
|
|
130
|
+
case 'forbidden':
|
|
131
|
+
return '当前身份没有访问这个群聊的权限。';
|
|
132
|
+
case 'network':
|
|
133
|
+
return '无法连接 AWiki 群聊服务,请检查网络后重试。';
|
|
134
|
+
case 'remote':
|
|
135
|
+
return 'AWiki 暂时无法读取这个群聊,请稍后重试。';
|
|
136
|
+
default:
|
|
137
|
+
return `${failure.code}:${failure.message}`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function groupRecoveryFailureMessage(failure) {
|
|
141
|
+
return failure.code === 'network'
|
|
142
|
+
? '无法连接 AWiki 服务,暂时不能恢复旧群聊身份。'
|
|
143
|
+
: '暂时不能检查旧群聊身份,请稍后重试。';
|
|
144
|
+
}
|
|
145
|
+
function groupRecoveryView(summary) {
|
|
146
|
+
if (summary.blocked > 0) {
|
|
147
|
+
return { status: 'blocked', pending: summary.pending, blocked: summary.blocked };
|
|
148
|
+
}
|
|
149
|
+
return summary.pending > 0
|
|
150
|
+
? { status: 'pending', pending: summary.pending, blocked: 0 }
|
|
151
|
+
: null;
|
|
152
|
+
}
|
|
88
153
|
/** Flatten the carrier and business result once for every controller caller. */
|
|
89
|
-
async function call(operation, failureMessage = failure => `${failure.code}:${failure.message}`) {
|
|
154
|
+
async function call(operation, failureMessage = failure => `${failure.code}:${failure.message}`, carrierFailureMessage = message => `连接 AWiki Host 失败:${message}`) {
|
|
90
155
|
try {
|
|
91
156
|
const carried = await operation();
|
|
92
157
|
if (!carried.ok)
|
|
93
|
-
return { ok: false, error:
|
|
158
|
+
return { ok: false, error: carrierFailureMessage(carried.error.message) };
|
|
94
159
|
if (!carried.value.ok) {
|
|
95
160
|
return { ok: false, error: failureMessage(carried.value.error) };
|
|
96
161
|
}
|
|
@@ -103,6 +168,11 @@ async function call(operation, failureMessage = failure => `${failure.code}:${
|
|
|
103
168
|
};
|
|
104
169
|
}
|
|
105
170
|
}
|
|
171
|
+
function recoveryCarrierFailureMessage(message) {
|
|
172
|
+
return message.includes('business result failed boundary validation')
|
|
173
|
+
? '恢复信息已验证,但暂时无法读取恢复状态。请稍后重试。'
|
|
174
|
+
: `连接 AWiki Host 失败:${message}`;
|
|
175
|
+
}
|
|
106
176
|
/** Append unique values while retaining existing references. */
|
|
107
177
|
function appendUnique(current, incoming, id) {
|
|
108
178
|
const seen = new Set(current.map(id));
|
|
@@ -200,6 +270,26 @@ function sameIdentity(identity, peer) {
|
|
|
200
270
|
const GROUP_HISTORY_RETRY_DELAYS_MS = [250, 750, 1_500, 2_500];
|
|
201
271
|
/** Runtime-only decoded-byte budget that prevents repeat Host calls while browsing. */
|
|
202
272
|
const BROWSER_IMAGE_ATTACHMENT_CACHE_MAX_BYTES = 32 * 1024 * 1024;
|
|
273
|
+
const RECOVERY_OPERATION_STORAGE_KEY = 'awiki.handle-recovery.operation.v1';
|
|
274
|
+
function storedRecoveryOperation() {
|
|
275
|
+
try {
|
|
276
|
+
return globalThis.localStorage?.getItem(RECOVERY_OPERATION_STORAGE_KEY) ?? null;
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function storeRecoveryOperation(operationId) {
|
|
283
|
+
try {
|
|
284
|
+
if (operationId === null)
|
|
285
|
+
globalThis.localStorage?.removeItem(RECOVERY_OPERATION_STORAGE_KEY);
|
|
286
|
+
else
|
|
287
|
+
globalThis.localStorage?.setItem(RECOVERY_OPERATION_STORAGE_KEY, operationId);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// Recovery remains durable in Core even when browser storage is unavailable.
|
|
291
|
+
}
|
|
292
|
+
}
|
|
203
293
|
/** Wait between group-history readiness probes without retaining controller state. */
|
|
204
294
|
function delay(milliseconds) {
|
|
205
295
|
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
@@ -218,6 +308,9 @@ function findDirect(conversations, peer) {
|
|
|
218
308
|
const key = peer.toLowerCase();
|
|
219
309
|
return conversations.find((conversation) => (conversation.kind === 'direct' && directPeerKeys(conversation).includes(key)));
|
|
220
310
|
}
|
|
311
|
+
function groupMemberKey(member) {
|
|
312
|
+
return member.membershipId ?? member.did ?? member.credentialDid ?? member.handle ?? `${member.role ?? ''}:${member.joinedAt ?? ''}`;
|
|
313
|
+
}
|
|
221
314
|
/** True when a group title contains presentation data instead of a protocol fallback. */
|
|
222
315
|
function hasDisplayableGroupTitle(conversation) {
|
|
223
316
|
if (conversation.kind !== 'group')
|
|
@@ -248,6 +341,7 @@ export class AwikiController {
|
|
|
248
341
|
config = null;
|
|
249
342
|
conversationsCursor;
|
|
250
343
|
historyCursor;
|
|
344
|
+
groupMembersCursor;
|
|
251
345
|
timer;
|
|
252
346
|
generation = 0;
|
|
253
347
|
selectionRevision = 0;
|
|
@@ -264,6 +358,7 @@ export class AwikiController {
|
|
|
264
358
|
imageAttachments = new Map();
|
|
265
359
|
imageAttachmentCacheBytes = 0;
|
|
266
360
|
presentationCacheOwnerDid = null;
|
|
361
|
+
groupRecoveryOperation;
|
|
267
362
|
/**
|
|
268
363
|
* @param remote - generated Host Remote namespace.
|
|
269
364
|
* @param persistentImageCache - browser-origin verified preview cache.
|
|
@@ -307,7 +402,26 @@ export class AwikiController {
|
|
|
307
402
|
identity: session.value.status === 'active' ? session.value.identity : null,
|
|
308
403
|
error: null,
|
|
309
404
|
attachmentMaxBytes: config.value.attachmentMaxBytes,
|
|
405
|
+
recoveryOperationId: storedRecoveryOperation(),
|
|
310
406
|
});
|
|
407
|
+
if (identity !== null) {
|
|
408
|
+
const profile = await call(() => this.remote.getProfile());
|
|
409
|
+
if (this.current(generation) && profile.ok)
|
|
410
|
+
this.publish({ ...this.view, profile: profile.value });
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
const operationId = storedRecoveryOperation();
|
|
414
|
+
if (operationId !== null) {
|
|
415
|
+
const recovery = await call(() => this.remote.getRecoveryStatus({ operationId }));
|
|
416
|
+
if (this.current(generation) && recovery.ok) {
|
|
417
|
+
this.publish({ ...this.view, recoveryOperationId: operationId, recoveryProgress: recovery.value });
|
|
418
|
+
if (recovery.value.phase === 'applied') {
|
|
419
|
+
storeRecoveryOperation(null);
|
|
420
|
+
return this.loadSession();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
311
425
|
return { ok: true, value: undefined };
|
|
312
426
|
}
|
|
313
427
|
/**
|
|
@@ -320,7 +434,7 @@ export class AwikiController {
|
|
|
320
434
|
return loaded;
|
|
321
435
|
const generation = this.generation;
|
|
322
436
|
if (this.view.identity !== null) {
|
|
323
|
-
const listed = await this.
|
|
437
|
+
const listed = await this.refreshConversationsAndRecoverGroups(generation);
|
|
324
438
|
if (!listed.ok)
|
|
325
439
|
return listed;
|
|
326
440
|
}
|
|
@@ -370,6 +484,7 @@ export class AwikiController {
|
|
|
370
484
|
this.timer = undefined;
|
|
371
485
|
this.polling = false;
|
|
372
486
|
this.markReadInFlight.clear();
|
|
487
|
+
this.groupMembersCursor = undefined;
|
|
373
488
|
}
|
|
374
489
|
/**
|
|
375
490
|
* Request one phone verification challenge.
|
|
@@ -379,6 +494,10 @@ export class AwikiController {
|
|
|
379
494
|
async sendRegistrationOtp(request) {
|
|
380
495
|
return this.withPending('发送验证码', () => call(() => this.remote.sendRegistrationOtp(request), registrationOtpFailureMessage));
|
|
381
496
|
}
|
|
497
|
+
/** Classify one Handle before sending exactly one registration or recovery OTP. */
|
|
498
|
+
async inspectIdentityAccess(request) {
|
|
499
|
+
return this.withPending('检查身份', () => call(() => this.remote.inspectIdentityAccess(request), identityAccessInspectionFailureMessage));
|
|
500
|
+
}
|
|
382
501
|
/**
|
|
383
502
|
* Register the deployment identity and populate the initial conversation list.
|
|
384
503
|
* @param request - verified Handle, phone number, and one-time code.
|
|
@@ -393,7 +512,7 @@ export class AwikiController {
|
|
|
393
512
|
return result;
|
|
394
513
|
this.activatePresentationCache(result.value);
|
|
395
514
|
this.publish({ ...this.view, sessionStatus: 'active', identity: result.value, error: null });
|
|
396
|
-
await this.
|
|
515
|
+
await this.refreshConversationsAndRecoverGroups(generation);
|
|
397
516
|
return result;
|
|
398
517
|
}
|
|
399
518
|
/**
|
|
@@ -415,6 +534,115 @@ export class AwikiController {
|
|
|
415
534
|
this.publish({ ...this.view, identity: result.value, error: null });
|
|
416
535
|
return result;
|
|
417
536
|
}
|
|
537
|
+
/** Save all supported public profile fields and keep identity/profile projections aligned. */
|
|
538
|
+
async updateProfile(request) {
|
|
539
|
+
const displayName = request.displayName.trim();
|
|
540
|
+
const bio = request.bio.trim();
|
|
541
|
+
const tags = request.tags.map(tag => tag.trim()).filter(tag => tag !== '');
|
|
542
|
+
if (displayName === '' || Array.from(displayName).length > 50)
|
|
543
|
+
return this.fail('昵称需要填写且不能超过 50 个字符');
|
|
544
|
+
if (Array.from(bio).length > 100)
|
|
545
|
+
return this.fail('个人简介不能超过 100 个字符');
|
|
546
|
+
if (tags.length > 5 || new Set(tags.map(tag => tag.toLocaleLowerCase())).size !== tags.length) {
|
|
547
|
+
return this.fail('最多填写 5 个不重复的标签');
|
|
548
|
+
}
|
|
549
|
+
const generation = this.generation;
|
|
550
|
+
const result = await this.withPending('保存资料', () => call(() => this.remote.updateProfile({ displayName, bio, tags })));
|
|
551
|
+
if (!result.ok || !this.current(generation))
|
|
552
|
+
return result;
|
|
553
|
+
const identity = this.view.identity === null ? null : { ...this.view.identity, displayName: result.value.displayName };
|
|
554
|
+
this.publish({ ...this.view, identity, profile: result.value, error: null });
|
|
555
|
+
return result;
|
|
556
|
+
}
|
|
557
|
+
/** Request a recovery OTP and persist only its secret-free operation id in the browser. */
|
|
558
|
+
async sendRecoveryOtp(request) {
|
|
559
|
+
const result = await this.withPending('发送恢复验证码', () => call(() => this.remote.sendRecoveryOtp(request), registrationOtpFailureMessage));
|
|
560
|
+
if (!result.ok)
|
|
561
|
+
return result;
|
|
562
|
+
storeRecoveryOperation(result.value.operationId);
|
|
563
|
+
this.publish({ ...this.view, recoveryOperationId: result.value.operationId, recoveryProgress: null });
|
|
564
|
+
return result;
|
|
565
|
+
}
|
|
566
|
+
/** Verify the recovery OTP without attempting the remote identity mutation yet. */
|
|
567
|
+
async prepareRecovery(request) {
|
|
568
|
+
const operationId = this.view.recoveryOperationId;
|
|
569
|
+
if (operationId === null)
|
|
570
|
+
return this.fail('请先获取恢复验证码');
|
|
571
|
+
const result = await this.withPending('验证恢复信息', () => call(() => this.remote.prepareRecovery({ ...request, operationId }), registrationFailureMessage, recoveryCarrierFailureMessage));
|
|
572
|
+
if (result.ok)
|
|
573
|
+
this.publish({ ...this.view, recoveryProgress: result.value });
|
|
574
|
+
return result;
|
|
575
|
+
}
|
|
576
|
+
/** Commit the prepared operation once. Unknown outcomes remain available through status refresh. */
|
|
577
|
+
async activateRecovery() {
|
|
578
|
+
const operationId = this.view.recoveryOperationId;
|
|
579
|
+
if (operationId === null)
|
|
580
|
+
return this.fail('没有可继续的身份恢复操作');
|
|
581
|
+
if (this.view.recoveryProgress?.phase !== 'ready_to_commit')
|
|
582
|
+
return this.fail('请先完成恢复信息验证');
|
|
583
|
+
const result = await this.withPending('恢复身份', () => call(() => this.remote.activateRecovery({ operationId }), registrationFailureMessage, recoveryCarrierFailureMessage));
|
|
584
|
+
if (!result.ok)
|
|
585
|
+
return result;
|
|
586
|
+
this.publish({ ...this.view, recoveryProgress: result.value });
|
|
587
|
+
if (result.value.phase === 'applied') {
|
|
588
|
+
storeRecoveryOperation(null);
|
|
589
|
+
this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: result.value });
|
|
590
|
+
await this.open();
|
|
591
|
+
}
|
|
592
|
+
return result;
|
|
593
|
+
}
|
|
594
|
+
/** Refresh Core status without repeating activation. */
|
|
595
|
+
async refreshRecoveryStatus() {
|
|
596
|
+
const operationId = this.view.recoveryOperationId;
|
|
597
|
+
if (operationId === null)
|
|
598
|
+
return this.fail('没有可查询的身份恢复操作');
|
|
599
|
+
const result = await this.withPending('刷新恢复状态', () => call(() => this.remote.getRecoveryStatus({ operationId }), undefined, recoveryCarrierFailureMessage));
|
|
600
|
+
if (!result.ok)
|
|
601
|
+
return result;
|
|
602
|
+
this.publish({ ...this.view, recoveryProgress: result.value });
|
|
603
|
+
if (result.value.phase === 'applied') {
|
|
604
|
+
storeRecoveryOperation(null);
|
|
605
|
+
this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: result.value });
|
|
606
|
+
await this.open();
|
|
607
|
+
}
|
|
608
|
+
return result;
|
|
609
|
+
}
|
|
610
|
+
/** Resume only a Core-declared retryable or uncertain phase. */
|
|
611
|
+
async resumeRecovery() {
|
|
612
|
+
const operationId = this.view.recoveryOperationId;
|
|
613
|
+
const progress = this.view.recoveryProgress;
|
|
614
|
+
if (operationId === null || progress === null)
|
|
615
|
+
return this.fail('请先刷新恢复状态');
|
|
616
|
+
if (!progress.retryable && !['remote_outcome_unknown', 'remote_committed', 'identity_transition_pending'].includes(progress.phase)) {
|
|
617
|
+
return this.fail('当前恢复状态不能重试');
|
|
618
|
+
}
|
|
619
|
+
const result = await this.withPending('继续恢复身份', () => call(() => this.remote.resumeRecovery({ operationId }), registrationFailureMessage, recoveryCarrierFailureMessage));
|
|
620
|
+
if (!result.ok)
|
|
621
|
+
return result;
|
|
622
|
+
this.publish({ ...this.view, recoveryProgress: result.value });
|
|
623
|
+
if (result.value.phase === 'applied') {
|
|
624
|
+
storeRecoveryOperation(null);
|
|
625
|
+
this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: result.value });
|
|
626
|
+
await this.open();
|
|
627
|
+
}
|
|
628
|
+
return result;
|
|
629
|
+
}
|
|
630
|
+
/** Discard only a pre-attempt operation. */
|
|
631
|
+
async discardRecovery() {
|
|
632
|
+
const operationId = this.view.recoveryOperationId;
|
|
633
|
+
if (operationId === null)
|
|
634
|
+
return { ok: true, value: undefined };
|
|
635
|
+
const phase = this.view.recoveryProgress?.phase;
|
|
636
|
+
if (phase !== undefined && phase !== 'awaiting_factor' && phase !== 'ready_to_commit') {
|
|
637
|
+
return this.fail('当前恢复状态不能取消');
|
|
638
|
+
}
|
|
639
|
+
const result = await this.withPending('取消身份恢复', () => call(() => this.remote.discardRecovery({ operationId })));
|
|
640
|
+
if (!result.ok)
|
|
641
|
+
return result;
|
|
642
|
+
storeRecoveryOperation(null);
|
|
643
|
+
this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: null });
|
|
644
|
+
return { ok: true, value: undefined };
|
|
645
|
+
}
|
|
418
646
|
/** Read the active deployment identity's public mailbox state. */
|
|
419
647
|
getMailAccount() {
|
|
420
648
|
return call(() => this.remote.getMailAccount(), mailFailureMessage);
|
|
@@ -455,6 +683,16 @@ export class AwikiController {
|
|
|
455
683
|
});
|
|
456
684
|
return { ok: true, value: undefined };
|
|
457
685
|
}
|
|
686
|
+
/** Synchronize account projections, then retry Core's durable group-rebind journal. */
|
|
687
|
+
async retryGroupRebindRecovery() {
|
|
688
|
+
const generation = this.generation;
|
|
689
|
+
return this.withPending('恢复群聊身份', async () => {
|
|
690
|
+
const listed = await this.refreshConversations(generation, true);
|
|
691
|
+
if (!listed.ok)
|
|
692
|
+
return { ok: false, error: listed.error };
|
|
693
|
+
return this.resumeGroupRebindRecovery(generation);
|
|
694
|
+
});
|
|
695
|
+
}
|
|
458
696
|
/**
|
|
459
697
|
* Look up a Handle or DID, then open the matching direct conversation.
|
|
460
698
|
* @param handle - peer Handle or DID typed by the user.
|
|
@@ -523,8 +761,6 @@ export class AwikiController {
|
|
|
523
761
|
if (Array.from(normalizedName).length > 100)
|
|
524
762
|
return this.fail('群聊名称不能超过 100 个字符');
|
|
525
763
|
const normalizedMembers = [...new Set(members.map(normalizeHandle).filter(member => member !== ''))];
|
|
526
|
-
if (normalizedMembers.length === 0)
|
|
527
|
-
return this.fail('请至少添加一位群成员');
|
|
528
764
|
if (normalizedMembers.length > 50)
|
|
529
765
|
return this.fail('首批群成员不能超过 50 位');
|
|
530
766
|
const identity = this.view.identity;
|
|
@@ -537,7 +773,7 @@ export class AwikiController {
|
|
|
537
773
|
const result = await this.withPending('创建群聊', () => call(() => this.remote.createGroup({
|
|
538
774
|
name: normalizedName,
|
|
539
775
|
members: normalizedMembers,
|
|
540
|
-
})));
|
|
776
|
+
}), groupCreateFailureMessage));
|
|
541
777
|
if (!result.ok || !this.current(generation))
|
|
542
778
|
return result;
|
|
543
779
|
const conversation = this.cacheConversation(result.value.conversation);
|
|
@@ -557,6 +793,131 @@ export class AwikiController {
|
|
|
557
793
|
this.publish({ ...this.view, error: warning });
|
|
558
794
|
return result;
|
|
559
795
|
}
|
|
796
|
+
/** Join one open group by its canonical DID, then select the refreshed conversation. */
|
|
797
|
+
async joinGroup(groupDidInput) {
|
|
798
|
+
const groupDid = groupDidInput.trim();
|
|
799
|
+
if (!groupDid.startsWith('did:'))
|
|
800
|
+
return this.fail('请输入有效的群 DID');
|
|
801
|
+
const generation = this.generation;
|
|
802
|
+
const result = await this.withPending('加入群聊', () => call(() => this.remote.joinGroup({ groupDid })));
|
|
803
|
+
if (!result.ok || !this.current(generation))
|
|
804
|
+
return result;
|
|
805
|
+
await this.refreshConversations(generation);
|
|
806
|
+
if (!this.current(generation))
|
|
807
|
+
return result;
|
|
808
|
+
const conversation = this.view.conversations.find(value => value.kind === 'group' && value.groupDid === groupDid)
|
|
809
|
+
?? this.cacheConversation({
|
|
810
|
+
kind: 'group',
|
|
811
|
+
id: result.value.conversationId,
|
|
812
|
+
groupDid,
|
|
813
|
+
title: result.value.title,
|
|
814
|
+
unreadCount: 0,
|
|
815
|
+
});
|
|
816
|
+
this.publish({
|
|
817
|
+
...this.view,
|
|
818
|
+
conversations: appendUnique([conversation], this.view.conversations, value => value.id),
|
|
819
|
+
});
|
|
820
|
+
await this.selectConversation(conversation.id);
|
|
821
|
+
return result;
|
|
822
|
+
}
|
|
823
|
+
/** Refresh the selected group's authoritative snapshot and first member page. */
|
|
824
|
+
async refreshSelectedGroup() {
|
|
825
|
+
const conversation = this.selectedConversation();
|
|
826
|
+
if (conversation?.kind !== 'group')
|
|
827
|
+
return this.fail('请先打开一个群聊');
|
|
828
|
+
return this.withPending('刷新群成员', async () => {
|
|
829
|
+
const refreshed = await this.loadGroupState(conversation, true);
|
|
830
|
+
return refreshed.ok ? { ok: true, value: undefined } : refreshed;
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
/** Load the next authoritative member page using Core's opaque cursor. */
|
|
834
|
+
async loadMoreGroupMembers() {
|
|
835
|
+
const group = this.view.selectedGroup;
|
|
836
|
+
if (group === null || this.groupMembersCursor === undefined)
|
|
837
|
+
return { ok: true, value: undefined };
|
|
838
|
+
const result = await this.withPending('加载更多群成员', () => call(() => this.remote.listGroupMembers({
|
|
839
|
+
groupDid: group.groupDid,
|
|
840
|
+
cursor: this.groupMembersCursor,
|
|
841
|
+
limit: 50,
|
|
842
|
+
})));
|
|
843
|
+
if (!result.ok)
|
|
844
|
+
return result;
|
|
845
|
+
if (result.value.pageGroup !== undefined && result.value.pageGroup !== group.groupDid) {
|
|
846
|
+
return this.fail('群成员分页归属不一致,请刷新后重试');
|
|
847
|
+
}
|
|
848
|
+
this.groupMembersCursor = result.value.nextCursor;
|
|
849
|
+
this.publish({
|
|
850
|
+
...this.view,
|
|
851
|
+
groupMembers: appendUnique(this.view.groupMembers, result.value.items, groupMemberKey),
|
|
852
|
+
groupMembersHasMore: result.value.hasMore && result.value.nextCursor !== undefined,
|
|
853
|
+
});
|
|
854
|
+
return { ok: true, value: undefined };
|
|
855
|
+
}
|
|
856
|
+
/** Invite one ordinary member, then replace snapshot and roster with authoritative reads. */
|
|
857
|
+
async addSelectedGroupMember(memberInput) {
|
|
858
|
+
const group = this.view.selectedGroup;
|
|
859
|
+
const member = normalizeHandle(memberInput);
|
|
860
|
+
if (group === null)
|
|
861
|
+
return this.fail('请先打开一个群聊');
|
|
862
|
+
if (member === '')
|
|
863
|
+
return this.fail('请输入成员 Handle 或 DID');
|
|
864
|
+
if (this.view.identity !== null && (member === this.view.identity.did || sameIdentity(this.view.identity, member))) {
|
|
865
|
+
return this.fail('群成员列表不需要包含自己');
|
|
866
|
+
}
|
|
867
|
+
return await this.withPending('邀请群成员', async () => {
|
|
868
|
+
const invited = await call(() => this.remote.addGroupMember({
|
|
869
|
+
groupDid: group.groupDid,
|
|
870
|
+
member,
|
|
871
|
+
}));
|
|
872
|
+
if (!invited.ok)
|
|
873
|
+
return invited;
|
|
874
|
+
const refreshed = await this.reloadSelectedGroupAfterMutation(group.groupDid);
|
|
875
|
+
if (!refreshed.ok) {
|
|
876
|
+
return {
|
|
877
|
+
ok: false,
|
|
878
|
+
error: `已提交对 ${member} 的邀请,但成员列表刷新失败。请点击刷新查看最新状态。`,
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
return invited;
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
/** Remove one authorized member, then refresh count, roles, and roster from Core. */
|
|
885
|
+
async removeSelectedGroupMember(member) {
|
|
886
|
+
const group = this.view.selectedGroup;
|
|
887
|
+
const reference = member.did ?? member.handle;
|
|
888
|
+
if (group === null || reference === undefined)
|
|
889
|
+
return this.fail('该成员缺少可用的身份标识');
|
|
890
|
+
const result = await this.withPending('移除群成员', () => call(() => this.remote.removeGroupMember({
|
|
891
|
+
groupDid: group.groupDid,
|
|
892
|
+
member: reference,
|
|
893
|
+
})));
|
|
894
|
+
if (result.ok)
|
|
895
|
+
await this.reloadSelectedGroupAfterMutation(group.groupDid);
|
|
896
|
+
return result;
|
|
897
|
+
}
|
|
898
|
+
/** Leave the selected group; Core prevents the owner from leaving. */
|
|
899
|
+
async leaveSelectedGroup() {
|
|
900
|
+
const group = this.view.selectedGroup;
|
|
901
|
+
if (group === null)
|
|
902
|
+
return this.fail('请先打开一个群聊');
|
|
903
|
+
const generation = this.generation;
|
|
904
|
+
const result = await this.withPending('退出群聊', () => call(() => this.remote.leaveGroup({ groupDid: group.groupDid })));
|
|
905
|
+
if (!result.ok)
|
|
906
|
+
return result;
|
|
907
|
+
if (!this.current(generation))
|
|
908
|
+
return { ok: true, value: undefined };
|
|
909
|
+
this.groupMembersCursor = undefined;
|
|
910
|
+
this.publish({
|
|
911
|
+
...this.view,
|
|
912
|
+
selectedConversationId: null,
|
|
913
|
+
selectedGroup: null,
|
|
914
|
+
groupMembers: [],
|
|
915
|
+
groupMembersHasMore: false,
|
|
916
|
+
messages: [],
|
|
917
|
+
});
|
|
918
|
+
await this.refreshConversations(generation);
|
|
919
|
+
return { ok: true, value: undefined };
|
|
920
|
+
}
|
|
560
921
|
/**
|
|
561
922
|
* Select a conversation and load its newest history page.
|
|
562
923
|
* @param conversationId - selected conversation, or `null` to return to the roster.
|
|
@@ -569,6 +930,7 @@ export class AwikiController {
|
|
|
569
930
|
const sameConversation = conversationId !== null && previousConversationId === conversationId;
|
|
570
931
|
const selectionRevision = ++this.selectionRevision;
|
|
571
932
|
this.historyCursor = undefined;
|
|
933
|
+
this.groupMembersCursor = undefined;
|
|
572
934
|
const selected = conversationId === null
|
|
573
935
|
? undefined
|
|
574
936
|
: this.view.conversations.find(conversation => conversation.id === conversationId);
|
|
@@ -577,12 +939,17 @@ export class AwikiController {
|
|
|
577
939
|
this.publish({
|
|
578
940
|
...this.view,
|
|
579
941
|
selectedConversationId: conversationId,
|
|
942
|
+
selectedGroup: null,
|
|
943
|
+
groupMembers: [],
|
|
944
|
+
groupMembersHasMore: false,
|
|
580
945
|
messages: sameConversation ? this.view.messages : [],
|
|
581
946
|
historyHasMore: false,
|
|
582
947
|
localPending: conversationId !== null,
|
|
583
948
|
refreshing: false,
|
|
584
949
|
error: null,
|
|
585
950
|
});
|
|
951
|
+
if (selected?.kind === 'group')
|
|
952
|
+
void this.loadGroupState(selected, true);
|
|
586
953
|
if (conversationId === null)
|
|
587
954
|
return { ok: true, value: undefined };
|
|
588
955
|
const generation = this.generation;
|
|
@@ -618,6 +985,53 @@ export class AwikiController {
|
|
|
618
985
|
void this.refreshSelectedDirectProfile(selected, generation, selectionRevision);
|
|
619
986
|
return { ok: true, value: undefined };
|
|
620
987
|
}
|
|
988
|
+
async loadGroupState(conversation, reset) {
|
|
989
|
+
const generation = this.generation;
|
|
990
|
+
const selectionRevision = this.selectionRevision;
|
|
991
|
+
const snapshot = await call(() => this.remote.getGroup({ groupDid: conversation.groupDid }), groupReadFailureMessage);
|
|
992
|
+
if (!snapshot.ok) {
|
|
993
|
+
if (this.currentSelection(generation, selectionRevision, conversation.id)) {
|
|
994
|
+
this.publish({ ...this.view, error: snapshot.error });
|
|
995
|
+
}
|
|
996
|
+
return snapshot;
|
|
997
|
+
}
|
|
998
|
+
const members = await call(() => this.remote.listGroupMembers({ groupDid: conversation.groupDid, limit: 50 }), groupReadFailureMessage);
|
|
999
|
+
if (!members.ok) {
|
|
1000
|
+
if (this.currentSelection(generation, selectionRevision, conversation.id)) {
|
|
1001
|
+
this.publish({ ...this.view, error: members.error });
|
|
1002
|
+
}
|
|
1003
|
+
return members;
|
|
1004
|
+
}
|
|
1005
|
+
if (!this.currentSelection(generation, selectionRevision, conversation.id))
|
|
1006
|
+
return snapshot;
|
|
1007
|
+
if (members.value.pageGroup !== undefined && members.value.pageGroup !== conversation.groupDid) {
|
|
1008
|
+
return this.fail('群成员列表归属不一致,请刷新后重试');
|
|
1009
|
+
}
|
|
1010
|
+
this.groupMembersCursor = members.value.nextCursor;
|
|
1011
|
+
this.publish({
|
|
1012
|
+
...this.view,
|
|
1013
|
+
selectedGroup: snapshot.value,
|
|
1014
|
+
groupMembers: reset
|
|
1015
|
+
? members.value.items
|
|
1016
|
+
: appendUnique(this.view.groupMembers, members.value.items, groupMemberKey),
|
|
1017
|
+
groupMembersHasMore: members.value.hasMore && members.value.nextCursor !== undefined,
|
|
1018
|
+
});
|
|
1019
|
+
return snapshot;
|
|
1020
|
+
}
|
|
1021
|
+
async reloadSelectedGroupAfterMutation(expectedGroupDid) {
|
|
1022
|
+
const conversation = this.selectedConversation();
|
|
1023
|
+
if (conversation?.kind !== 'group' || conversation.groupDid !== expectedGroupDid) {
|
|
1024
|
+
return { ok: true, value: undefined };
|
|
1025
|
+
}
|
|
1026
|
+
const generation = this.generation;
|
|
1027
|
+
const group = await this.loadGroupState(conversation, true);
|
|
1028
|
+
if (!group.ok)
|
|
1029
|
+
return group;
|
|
1030
|
+
if (!this.current(generation))
|
|
1031
|
+
return { ok: true, value: undefined };
|
|
1032
|
+
await this.refreshConversations(generation, true);
|
|
1033
|
+
return { ok: true, value: undefined };
|
|
1034
|
+
}
|
|
621
1035
|
async reconcileSelectedConversation(conversationId, generation, selectionRevision) {
|
|
622
1036
|
const selected = this.view.conversations.find(conversation => conversation.id === conversationId);
|
|
623
1037
|
const remoteStartedAt = timingStart();
|
|
@@ -670,14 +1084,14 @@ export class AwikiController {
|
|
|
670
1084
|
return call(() => this.remote.getHistory({ conversationId }));
|
|
671
1085
|
}
|
|
672
1086
|
for (const retryDelay of GROUP_HISTORY_RETRY_DELAYS_MS) {
|
|
673
|
-
const remote = await call(() => this.remote.getHistory({ conversationId }));
|
|
1087
|
+
const remote = await call(() => this.remote.getHistory({ conversationId }), groupReadFailureMessage);
|
|
674
1088
|
if (remote.ok || !this.currentSelection(generation, selectionRevision, conversationId))
|
|
675
1089
|
return remote;
|
|
676
1090
|
await delay(retryDelay);
|
|
677
1091
|
if (!this.currentSelection(generation, selectionRevision, conversationId))
|
|
678
1092
|
return remote;
|
|
679
1093
|
}
|
|
680
|
-
return call(() => this.remote.getHistory({ conversationId }));
|
|
1094
|
+
return call(() => this.remote.getHistory({ conversationId }), groupReadFailureMessage);
|
|
681
1095
|
}
|
|
682
1096
|
async refreshSelectedDirectProfile(selected, generation, selectionRevision) {
|
|
683
1097
|
if (selected?.kind !== 'direct')
|
|
@@ -819,7 +1233,7 @@ export class AwikiController {
|
|
|
819
1233
|
* @param clientMessageId - optional logical identity shared with the optimistic row.
|
|
820
1234
|
* @returns successful delivery or one display-safe failure.
|
|
821
1235
|
*/
|
|
822
|
-
async sendText(text, clientMessageId) {
|
|
1236
|
+
async sendText(text, clientMessageId, mentions) {
|
|
823
1237
|
const conversation = this.selectedConversation();
|
|
824
1238
|
if (conversation === undefined)
|
|
825
1239
|
return this.fail('请先选择会话');
|
|
@@ -829,6 +1243,7 @@ export class AwikiController {
|
|
|
829
1243
|
target: targetOf(conversation),
|
|
830
1244
|
text,
|
|
831
1245
|
idempotencyKey: clientMessageId ?? crypto.randomUUID(),
|
|
1246
|
+
...mentions === undefined || mentions.length === 0 ? {} : { mentions },
|
|
832
1247
|
})));
|
|
833
1248
|
if (!result.ok)
|
|
834
1249
|
return result;
|
|
@@ -919,6 +1334,7 @@ export class AwikiController {
|
|
|
919
1334
|
this.unreadAtOpen.clear();
|
|
920
1335
|
this.summaryBaselines.clear();
|
|
921
1336
|
this.clearPresentationCache();
|
|
1337
|
+
storeRecoveryOperation(null);
|
|
922
1338
|
this.publish({ ...INITIAL_VIEW, status: 'ready' });
|
|
923
1339
|
return result;
|
|
924
1340
|
}
|
|
@@ -953,6 +1369,36 @@ export class AwikiController {
|
|
|
953
1369
|
});
|
|
954
1370
|
return { ok: true, value: undefined };
|
|
955
1371
|
}
|
|
1372
|
+
/** Hydrate account-projected groups before asking Core to resume their durable rebind jobs. */
|
|
1373
|
+
async refreshConversationsAndRecoverGroups(generation, background = false) {
|
|
1374
|
+
const listed = await this.refreshConversations(generation, background);
|
|
1375
|
+
if (!listed.ok || !this.current(generation))
|
|
1376
|
+
return listed;
|
|
1377
|
+
await this.resumeGroupRebindRecovery(generation);
|
|
1378
|
+
return listed;
|
|
1379
|
+
}
|
|
1380
|
+
resumeGroupRebindRecovery(generation) {
|
|
1381
|
+
const active = this.groupRecoveryOperation;
|
|
1382
|
+
if (active !== undefined && active.generation === generation)
|
|
1383
|
+
return active.operation;
|
|
1384
|
+
let operation;
|
|
1385
|
+
operation = call(() => this.remote.resumeGroupRebindRecovery(), groupRecoveryFailureMessage).then((result) => {
|
|
1386
|
+
if (!this.current(generation))
|
|
1387
|
+
return result;
|
|
1388
|
+
this.publish({
|
|
1389
|
+
...this.view,
|
|
1390
|
+
groupRecovery: result.ok
|
|
1391
|
+
? groupRecoveryView(result.value)
|
|
1392
|
+
: { status: 'unavailable', pending: 0, blocked: 0 },
|
|
1393
|
+
});
|
|
1394
|
+
return result;
|
|
1395
|
+
}).finally(() => {
|
|
1396
|
+
if (this.groupRecoveryOperation?.operation === operation)
|
|
1397
|
+
this.groupRecoveryOperation = undefined;
|
|
1398
|
+
});
|
|
1399
|
+
this.groupRecoveryOperation = { generation, operation };
|
|
1400
|
+
return operation;
|
|
1401
|
+
}
|
|
956
1402
|
async loadHistory(older) {
|
|
957
1403
|
const conversationId = this.view.selectedConversationId;
|
|
958
1404
|
if (conversationId === null)
|
|
@@ -962,7 +1408,8 @@ export class AwikiController {
|
|
|
962
1408
|
conversationId,
|
|
963
1409
|
...(older && this.historyCursor !== undefined ? { cursor: this.historyCursor } : {}),
|
|
964
1410
|
};
|
|
965
|
-
const
|
|
1411
|
+
const selected = this.view.conversations.find(value => value.id === conversationId);
|
|
1412
|
+
const result = await this.withPending(older ? '加载更早消息' : '加载消息', () => call(() => this.remote.getHistory(request), selected?.kind === 'group' ? groupReadFailureMessage : undefined));
|
|
966
1413
|
if (!result.ok)
|
|
967
1414
|
return result;
|
|
968
1415
|
if (!this.current(generation))
|
|
@@ -991,7 +1438,13 @@ export class AwikiController {
|
|
|
991
1438
|
return;
|
|
992
1439
|
this.polling = true;
|
|
993
1440
|
try {
|
|
994
|
-
|
|
1441
|
+
const knownGroups = new Set(this.view.conversations
|
|
1442
|
+
.filter(conversation => conversation.kind === 'group')
|
|
1443
|
+
.map(conversation => conversation.groupDid));
|
|
1444
|
+
const listed = await this.refreshConversations(generation, true);
|
|
1445
|
+
if (listed.ok && this.view.conversations.some(conversation => conversation.kind === 'group' && !knownGroups.has(conversation.groupDid))) {
|
|
1446
|
+
await this.resumeGroupRebindRecovery(generation);
|
|
1447
|
+
}
|
|
995
1448
|
const selected = this.view.selectedConversationId;
|
|
996
1449
|
if (selected === null || !this.current(generation))
|
|
997
1450
|
return;
|