@awiki/dsh-plugin 0.3.0-rc.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -16
- package/README.zh.md +15 -11
- package/THIRD_PARTY_NOTICES.md +0 -7
- package/lib/client.js +5572 -8534
- package/lib/client.js.map +1 -1
- package/lib/index.js +373 -36
- package/lib/provider.js +1 -1
- package/lib/{sdk-adapter-DLhLrLI6.mjs → sdk-adapter-BIbsJmRj.mjs} +28 -4
- package/lib/typert.host.js +221 -81
- package/lib/typert.remote-client.d.ts +5 -1
- package/lib/typert.remote-client.d.ts.map +1 -1
- package/lib/typert.remote-client.js +221 -81
- package/lib/types/client/AwikiGroupAccessNotice.d.ts +12 -0
- package/lib/types/client/AwikiGroupAccessNotice.d.ts.map +1 -0
- package/lib/types/client/AwikiGroupAccessNotice.js +31 -0
- package/lib/types/client/AwikiGroupAccessNotice.js.map +1 -0
- package/lib/types/client/AwikiGroupDetails.d.ts +8 -1
- package/lib/types/client/AwikiGroupDetails.d.ts.map +1 -1
- package/lib/types/client/AwikiGroupDetails.js +40 -4
- package/lib/types/client/AwikiGroupDetails.js.map +1 -1
- package/lib/types/client/AwikiIdentityAccess.d.ts +2 -1
- package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -1
- package/lib/types/client/AwikiIdentityAccess.js +10 -2
- package/lib/types/client/AwikiIdentityAccess.js.map +1 -1
- package/lib/types/client/AwikiMail.d.ts.map +1 -1
- package/lib/types/client/AwikiMail.js +5 -1
- package/lib/types/client/AwikiMail.js.map +1 -1
- package/lib/types/client/AwikiOverlay.d.ts.map +1 -1
- package/lib/types/client/AwikiOverlay.js +72 -11
- package/lib/types/client/AwikiOverlay.js.map +1 -1
- package/lib/types/client/AwikiRecoveryForm.d.ts +3 -0
- package/lib/types/client/AwikiRecoveryForm.d.ts.map +1 -1
- package/lib/types/client/AwikiRecoveryForm.js +10 -6
- package/lib/types/client/AwikiRecoveryForm.js.map +1 -1
- package/lib/types/client/AwikiSettingsSection.d.ts +4 -18
- package/lib/types/client/AwikiSettingsSection.d.ts.map +1 -1
- package/lib/types/client/AwikiSettingsSection.js +7 -271
- package/lib/types/client/AwikiSettingsSection.js.map +1 -1
- package/lib/types/client/awiki-client-bridge.d.ts +18 -0
- package/lib/types/client/awiki-client-bridge.d.ts.map +1 -0
- package/lib/types/client/awiki-client-bridge.js +18 -0
- package/lib/types/client/awiki-client-bridge.js.map +1 -0
- package/lib/types/client/controller.d.ts +38 -2
- package/lib/types/client/controller.d.ts.map +1 -1
- package/lib/types/client/controller.js +473 -47
- package/lib/types/client/controller.js.map +1 -1
- package/lib/types/client/index.d.ts +3 -3
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +7 -46
- package/lib/types/client/index.js.map +1 -1
- package/lib/types/client/settings-locales.d.ts +2 -5
- package/lib/types/client/settings-locales.d.ts.map +1 -1
- package/lib/types/client/settings-locales.js +3 -183
- package/lib/types/client/settings-locales.js.map +1 -1
- package/lib/types/client/slots.d.ts +6 -0
- package/lib/types/client/slots.d.ts.map +1 -1
- package/lib/types/conversation-preferences.d.ts +20 -0
- package/lib/types/conversation-preferences.d.ts.map +1 -0
- package/lib/types/conversation-preferences.js +294 -0
- package/lib/types/conversation-preferences.js.map +1 -0
- package/lib/types/index.d.ts +6 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +39 -0
- package/lib/types/index.js.map +1 -1
- package/lib/types/sdk-adapter.d.ts.map +1 -1
- package/lib/types/sdk-adapter.js +35 -4
- package/lib/types/sdk-adapter.js.map +1 -1
- package/lib/types/types.d.ts +32 -1
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js.map +1 -1
- package/package.json +57 -55
- package/lib/types/client/AwikiOnboarding.d.ts +0 -22
- package/lib/types/client/AwikiOnboarding.d.ts.map +0 -1
- package/lib/types/client/AwikiOnboarding.js +0 -104
- package/lib/types/client/AwikiOnboarding.js.map +0 -1
- package/lib/types/client/RechargeComingSoonDialog.d.ts +0 -11
- package/lib/types/client/RechargeComingSoonDialog.d.ts.map +0 -1
- package/lib/types/client/RechargeComingSoonDialog.js +0 -8
- package/lib/types/client/RechargeComingSoonDialog.js.map +0 -1
- package/lib/types/client/model-availability-controller.d.ts +0 -27
- package/lib/types/client/model-availability-controller.d.ts.map +0 -1
- package/lib/types/client/model-availability-controller.js +0 -111
- package/lib/types/client/model-availability-controller.js.map +0 -1
- package/lib/types/client/model-proxy-controller.d.ts +0 -45
- package/lib/types/client/model-proxy-controller.d.ts.map +0 -1
- package/lib/types/client/model-proxy-controller.js +0 -306
- package/lib/types/client/model-proxy-controller.js.map +0 -1
- package/lib/types/client/recharge-availability.d.ts +0 -6
- package/lib/types/client/recharge-availability.d.ts.map +0 -1
- package/lib/types/client/recharge-availability.js +0 -6
- package/lib/types/client/recharge-availability.js.map +0 -1
|
@@ -44,6 +44,43 @@ function registrationOtpFailureMessage(failure) {
|
|
|
44
44
|
return registrationFailureMessage(failure);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
function recoveryPreparationFailureMessage(failure) {
|
|
48
|
+
switch (failure.code) {
|
|
49
|
+
case 'invalid-request':
|
|
50
|
+
return '恢复信息不匹配,请检查验证码后重试。';
|
|
51
|
+
case 'invalid-otp':
|
|
52
|
+
return '验证码不正确,请检查后重试。';
|
|
53
|
+
case 'challenge-expired':
|
|
54
|
+
return '验证码状态已失效,请重新获取恢复验证码。';
|
|
55
|
+
case 'rate-limited':
|
|
56
|
+
return '恢复验证过于频繁,请稍后重试。';
|
|
57
|
+
case 'network':
|
|
58
|
+
return '无法连接 AWiki 服务,请检查网络后重试。';
|
|
59
|
+
case 'remote':
|
|
60
|
+
return 'AWiki 服务暂时无法验证恢复信息,请稍后重试。';
|
|
61
|
+
default:
|
|
62
|
+
return `${failure.code}:${failure.message}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function recoveryContinuationFailureMessage(failure, phase) {
|
|
66
|
+
const remoteCommitted = phase === 'remote_committed' || phase === 'identity_transition_pending';
|
|
67
|
+
if (remoteCommitted && ['invalid-request', 'conflict', 'forbidden', 'remote'].includes(failure.code)) {
|
|
68
|
+
return '身份已在服务端恢复,但本机切换尚未完成。请保留当前恢复操作,并继续完成本机切换;不要重新获取验证码或创建新身份。';
|
|
69
|
+
}
|
|
70
|
+
switch (failure.code) {
|
|
71
|
+
case 'network':
|
|
72
|
+
return remoteCommitted
|
|
73
|
+
? '身份已在服务端恢复,但当前设备暂时无法完成本机切换。请检查网络后继续完成本机切换。'
|
|
74
|
+
: '恢复请求结果尚未确认。请保留当前恢复操作并重新检查状态,不要重复提交。';
|
|
75
|
+
case 'invalid-request':
|
|
76
|
+
case 'conflict':
|
|
77
|
+
return '身份恢复暂未完成。请保留当前恢复操作并重新检查状态,不要重新获取验证码或创建新身份。';
|
|
78
|
+
case 'rate-limited':
|
|
79
|
+
return '恢复状态检查过于频繁,请稍后继续。';
|
|
80
|
+
default:
|
|
81
|
+
return `${failure.code}:${failure.message}`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
47
84
|
function identityAccessInspectionFailureMessage(failure) {
|
|
48
85
|
switch (failure.code) {
|
|
49
86
|
case 'invalid-request':
|
|
@@ -62,9 +99,11 @@ const INITIAL_VIEW = Object.freeze({
|
|
|
62
99
|
identity: null,
|
|
63
100
|
profile: null,
|
|
64
101
|
conversations: Object.freeze([]),
|
|
102
|
+
hiddenConversations: Object.freeze([]),
|
|
65
103
|
conversationsHasMore: false,
|
|
66
104
|
selectedConversationId: null,
|
|
67
105
|
selectedGroup: null,
|
|
106
|
+
groupAccess: null,
|
|
68
107
|
groupMembers: Object.freeze([]),
|
|
69
108
|
groupMembersHasMore: false,
|
|
70
109
|
groupRecovery: null,
|
|
@@ -142,13 +181,66 @@ function groupRecoveryFailureMessage(failure) {
|
|
|
142
181
|
? '无法连接 AWiki 服务,暂时不能恢复旧群聊身份。'
|
|
143
182
|
: '暂时不能检查旧群聊身份,请稍后重试。';
|
|
144
183
|
}
|
|
145
|
-
function
|
|
146
|
-
|
|
147
|
-
|
|
184
|
+
function conversationPreferenceFailureMessage(_failure) {
|
|
185
|
+
return '无法保存本机会话设置,请稍后重试。';
|
|
186
|
+
}
|
|
187
|
+
function recoveryFingerprint(parts) {
|
|
188
|
+
let hash = 0x811c9dc5;
|
|
189
|
+
for (const part of parts) {
|
|
190
|
+
for (let index = 0; index < part.length; index += 1) {
|
|
191
|
+
hash ^= part.charCodeAt(index);
|
|
192
|
+
hash = Math.imul(hash, 0x01000193);
|
|
193
|
+
}
|
|
194
|
+
hash ^= 0;
|
|
195
|
+
hash = Math.imul(hash, 0x01000193);
|
|
196
|
+
}
|
|
197
|
+
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
198
|
+
}
|
|
199
|
+
function groupRecoveryProjection(summary, hiddenGroupDids) {
|
|
200
|
+
const hiddenItems = summary.items.filter(item => hiddenGroupDids.has(item.groupDid));
|
|
201
|
+
const hiddenPending = hiddenItems.filter(item => item.status === 'pending').length;
|
|
202
|
+
const hiddenBlocked = hiddenItems.filter(item => item.status === 'blocked').length;
|
|
203
|
+
const pending = Math.max(0, summary.pending - hiddenPending);
|
|
204
|
+
const blocked = Math.max(0, summary.blocked - hiddenBlocked);
|
|
205
|
+
const visibleItems = summary.items
|
|
206
|
+
.filter(item => !hiddenGroupDids.has(item.groupDid))
|
|
207
|
+
.map(item => `${item.status}:${item.groupDid}`)
|
|
208
|
+
.sort();
|
|
209
|
+
const signature = `v1:${pending}:${blocked}:${recoveryFingerprint(visibleItems)}`;
|
|
210
|
+
if (blocked > 0)
|
|
211
|
+
return { view: { status: 'blocked', pending, blocked }, signature };
|
|
212
|
+
return {
|
|
213
|
+
view: pending > 0 ? { status: 'pending', pending, blocked: 0 } : null,
|
|
214
|
+
signature,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function groupReadFailureReason(failure) {
|
|
218
|
+
switch (failure.code) {
|
|
219
|
+
case 'group-identity-stale': return 'recovering';
|
|
220
|
+
case 'group-membership-required':
|
|
221
|
+
case 'not-found':
|
|
222
|
+
case 'forbidden': return 'not-member';
|
|
223
|
+
default: return 'network-error';
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async function callGroupRead(operation) {
|
|
227
|
+
try {
|
|
228
|
+
const carried = await operation();
|
|
229
|
+
if (!carried.ok) {
|
|
230
|
+
return { ok: false, error: '暂时无法连接 AWiki 群聊服务,请稍后重新检查。', reason: 'network-error' };
|
|
231
|
+
}
|
|
232
|
+
if (!carried.value.ok) {
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
error: groupReadFailureMessage(carried.value.error),
|
|
236
|
+
reason: groupReadFailureReason(carried.value.error),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return { ok: true, value: carried.value.value };
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return { ok: false, error: '暂时无法连接 AWiki 群聊服务,请稍后重新检查。', reason: 'network-error' };
|
|
148
243
|
}
|
|
149
|
-
return summary.pending > 0
|
|
150
|
-
? { status: 'pending', pending: summary.pending, blocked: 0 }
|
|
151
|
-
: null;
|
|
152
244
|
}
|
|
153
245
|
/** Flatten the carrier and business result once for every controller caller. */
|
|
154
246
|
async function call(operation, failureMessage = failure => `${failure.code}:${failure.message}`, carrierFailureMessage = message => `连接 AWiki Host 失败:${message}`) {
|
|
@@ -168,6 +260,28 @@ async function call(operation, failureMessage = failure => `${failure.code}:${
|
|
|
168
260
|
};
|
|
169
261
|
}
|
|
170
262
|
}
|
|
263
|
+
/** Preserve only the stable business code for controller-level state transitions. */
|
|
264
|
+
async function callWithFailureCode(operation) {
|
|
265
|
+
try {
|
|
266
|
+
const carried = await operation();
|
|
267
|
+
if (!carried.ok)
|
|
268
|
+
return { ok: false, error: `连接 AWiki Host 失败:${carried.error.message}` };
|
|
269
|
+
if (!carried.value.ok) {
|
|
270
|
+
return {
|
|
271
|
+
ok: false,
|
|
272
|
+
error: `${carried.value.error.code}:${carried.value.error.message}`,
|
|
273
|
+
failureCode: carried.value.error.code,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return { ok: true, value: carried.value.value };
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
return {
|
|
280
|
+
ok: false,
|
|
281
|
+
error: error instanceof Error ? `AWiki 调用失败:${error.message}` : 'AWiki 调用失败',
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
171
285
|
function recoveryCarrierFailureMessage(message) {
|
|
172
286
|
return message.includes('business result failed boundary validation')
|
|
173
287
|
? '恢复信息已验证,但暂时无法读取恢复状态。请稍后重试。'
|
|
@@ -354,6 +468,13 @@ export class AwikiController {
|
|
|
354
468
|
directProfiles = new Map();
|
|
355
469
|
/** Last trustworthy group title for the active identity, keyed by canonical Group DID. */
|
|
356
470
|
groupTitles = new Map();
|
|
471
|
+
/** Current secret-free Core journal state, keyed by canonical Group DID. */
|
|
472
|
+
groupRecoveryItems = new Map();
|
|
473
|
+
/** Identity-scoped product overlays. Core conversations and history remain untouched. */
|
|
474
|
+
hiddenConversationPreferences = new Map();
|
|
475
|
+
dismissedGroupRecoverySignature;
|
|
476
|
+
lastGroupRecoverySignature;
|
|
477
|
+
lastGroupRecoverySummary;
|
|
357
478
|
/** Verified image payloads retained outside observable state for instant remounts. */
|
|
358
479
|
imageAttachments = new Map();
|
|
359
480
|
imageAttachmentCacheBytes = 0;
|
|
@@ -405,6 +526,7 @@ export class AwikiController {
|
|
|
405
526
|
recoveryOperationId: storedRecoveryOperation(),
|
|
406
527
|
});
|
|
407
528
|
if (identity !== null) {
|
|
529
|
+
await this.loadConversationPreferences(generation);
|
|
408
530
|
const profile = await call(() => this.remote.getProfile());
|
|
409
531
|
if (this.current(generation) && profile.ok)
|
|
410
532
|
this.publish({ ...this.view, profile: profile.value });
|
|
@@ -556,7 +678,7 @@ export class AwikiController {
|
|
|
556
678
|
}
|
|
557
679
|
/** Request a recovery OTP and persist only its secret-free operation id in the browser. */
|
|
558
680
|
async sendRecoveryOtp(request) {
|
|
559
|
-
const result = await this.withPending('发送恢复验证码', () => call(() => this.remote.sendRecoveryOtp(request), registrationOtpFailureMessage));
|
|
681
|
+
const result = await this.withPending('发送恢复验证码', () => call(() => this.remote.sendRecoveryOtp(request), registrationOtpFailureMessage), { publishFailure: false });
|
|
560
682
|
if (!result.ok)
|
|
561
683
|
return result;
|
|
562
684
|
storeRecoveryOperation(result.value.operationId);
|
|
@@ -568,7 +690,7 @@ export class AwikiController {
|
|
|
568
690
|
const operationId = this.view.recoveryOperationId;
|
|
569
691
|
if (operationId === null)
|
|
570
692
|
return this.fail('请先获取恢复验证码');
|
|
571
|
-
const result = await this.withPending('验证恢复信息', () => call(() => this.remote.prepareRecovery({ ...request, operationId }),
|
|
693
|
+
const result = await this.withPending('验证恢复信息', () => call(() => this.remote.prepareRecovery({ ...request, operationId }), recoveryPreparationFailureMessage, recoveryCarrierFailureMessage), { publishFailure: false });
|
|
572
694
|
if (result.ok)
|
|
573
695
|
this.publish({ ...this.view, recoveryProgress: result.value });
|
|
574
696
|
return result;
|
|
@@ -580,7 +702,8 @@ export class AwikiController {
|
|
|
580
702
|
return this.fail('没有可继续的身份恢复操作');
|
|
581
703
|
if (this.view.recoveryProgress?.phase !== 'ready_to_commit')
|
|
582
704
|
return this.fail('请先完成恢复信息验证');
|
|
583
|
-
const
|
|
705
|
+
const phase = this.view.recoveryProgress.phase;
|
|
706
|
+
const result = await this.withPending('恢复身份', () => call(() => this.remote.activateRecovery({ operationId }), failure => recoveryContinuationFailureMessage(failure, phase), recoveryCarrierFailureMessage), { publishFailure: false });
|
|
584
707
|
if (!result.ok)
|
|
585
708
|
return result;
|
|
586
709
|
this.publish({ ...this.view, recoveryProgress: result.value });
|
|
@@ -596,7 +719,7 @@ export class AwikiController {
|
|
|
596
719
|
const operationId = this.view.recoveryOperationId;
|
|
597
720
|
if (operationId === null)
|
|
598
721
|
return this.fail('没有可查询的身份恢复操作');
|
|
599
|
-
const result = await this.withPending('刷新恢复状态', () => call(() => this.remote.getRecoveryStatus({ operationId }), undefined, recoveryCarrierFailureMessage));
|
|
722
|
+
const result = await this.withPending('刷新恢复状态', () => call(() => this.remote.getRecoveryStatus({ operationId }), undefined, recoveryCarrierFailureMessage), { publishFailure: false });
|
|
600
723
|
if (!result.ok)
|
|
601
724
|
return result;
|
|
602
725
|
this.publish({ ...this.view, recoveryProgress: result.value });
|
|
@@ -616,7 +739,7 @@ export class AwikiController {
|
|
|
616
739
|
if (!progress.retryable && !['remote_outcome_unknown', 'remote_committed', 'identity_transition_pending'].includes(progress.phase)) {
|
|
617
740
|
return this.fail('当前恢复状态不能重试');
|
|
618
741
|
}
|
|
619
|
-
const result = await this.withPending('继续恢复身份', () => call(() => this.remote.resumeRecovery({ operationId }),
|
|
742
|
+
const result = await this.withPending('继续恢复身份', () => call(() => this.remote.resumeRecovery({ operationId }), failure => recoveryContinuationFailureMessage(failure, progress.phase), recoveryCarrierFailureMessage), { publishFailure: false });
|
|
620
743
|
if (!result.ok)
|
|
621
744
|
return result;
|
|
622
745
|
this.publish({ ...this.view, recoveryProgress: result.value });
|
|
@@ -636,7 +759,7 @@ export class AwikiController {
|
|
|
636
759
|
if (phase !== undefined && phase !== 'awaiting_factor' && phase !== 'ready_to_commit') {
|
|
637
760
|
return this.fail('当前恢复状态不能取消');
|
|
638
761
|
}
|
|
639
|
-
const result = await this.withPending('取消身份恢复', () => call(() => this.remote.discardRecovery({ operationId })));
|
|
762
|
+
const result = await this.withPending('取消身份恢复', () => call(() => this.remote.discardRecovery({ operationId })), { publishFailure: false });
|
|
640
763
|
if (!result.ok)
|
|
641
764
|
return result;
|
|
642
765
|
storeRecoveryOperation(null);
|
|
@@ -669,16 +792,20 @@ export class AwikiController {
|
|
|
669
792
|
*/
|
|
670
793
|
async loadMoreConversations() {
|
|
671
794
|
const generation = this.generation;
|
|
672
|
-
const result = await this.withPending('加载更多会话', () =>
|
|
795
|
+
const result = await this.withPending('加载更多会话', () => this.listConversationPage(this.conversationsCursor === undefined ? {} : { cursor: this.conversationsCursor }));
|
|
673
796
|
if (!result.ok)
|
|
674
797
|
return result;
|
|
675
798
|
if (!this.current(generation))
|
|
676
799
|
return { ok: true, value: undefined };
|
|
677
800
|
this.conversationsCursor = result.value.nextCursor;
|
|
678
|
-
const conversations = result.value.items
|
|
801
|
+
const conversations = await this.reconcileConversationPage(result.value.items, generation);
|
|
802
|
+
if (!this.current(generation))
|
|
803
|
+
return { ok: true, value: undefined };
|
|
679
804
|
this.publish({
|
|
680
805
|
...this.view,
|
|
681
|
-
conversations: appendUnique(this.view.conversations, conversations, value => value.id),
|
|
806
|
+
conversations: appendUnique(this.view.conversations, conversations.visible, value => value.id),
|
|
807
|
+
hiddenConversations: conversations.hidden,
|
|
808
|
+
groupRecovery: this.currentGroupRecoveryView(),
|
|
682
809
|
conversationsHasMore: result.value.hasMore && result.value.nextCursor !== undefined,
|
|
683
810
|
});
|
|
684
811
|
return { ok: true, value: undefined };
|
|
@@ -693,6 +820,80 @@ export class AwikiController {
|
|
|
693
820
|
return this.resumeGroupRebindRecovery(generation);
|
|
694
821
|
});
|
|
695
822
|
}
|
|
823
|
+
/** Dismiss only the current recovery-summary revision; Core recovery remains untouched. */
|
|
824
|
+
async dismissGroupRecoveryNotice() {
|
|
825
|
+
const signature = this.lastGroupRecoverySignature;
|
|
826
|
+
if (signature === undefined || this.view.groupRecovery === null)
|
|
827
|
+
return { ok: true, value: undefined };
|
|
828
|
+
const result = await call(() => this.remote.updateConversationPreference({ action: 'dismiss-group-recovery', signature }), conversationPreferenceFailureMessage);
|
|
829
|
+
if (!result.ok)
|
|
830
|
+
return this.fail(result.error);
|
|
831
|
+
this.applyConversationPreferences(result.value);
|
|
832
|
+
this.publish({ ...this.view, groupRecovery: null, error: null });
|
|
833
|
+
return { ok: true, value: undefined };
|
|
834
|
+
}
|
|
835
|
+
/** Hide one recent row locally without leaving a group or deleting history. */
|
|
836
|
+
async hideConversation(conversationId) {
|
|
837
|
+
const conversation = this.view.conversations.find(item => item.id === conversationId);
|
|
838
|
+
if (conversation === undefined)
|
|
839
|
+
return this.fail('该会话已不在当前列表中');
|
|
840
|
+
const generation = this.generation;
|
|
841
|
+
const result = await this.withPending('移除会话', () => call(() => this.remote.updateConversationPreference({ action: 'hide', conversation }), conversationPreferenceFailureMessage));
|
|
842
|
+
if (!result.ok)
|
|
843
|
+
return result;
|
|
844
|
+
if (!this.current(generation))
|
|
845
|
+
return { ok: true, value: undefined };
|
|
846
|
+
this.applyConversationPreferences(result.value);
|
|
847
|
+
const selected = this.view.selectedConversationId === conversationId;
|
|
848
|
+
if (selected) {
|
|
849
|
+
this.selectionRevision += 1;
|
|
850
|
+
this.historyCursor = undefined;
|
|
851
|
+
this.groupMembersCursor = undefined;
|
|
852
|
+
}
|
|
853
|
+
this.publish({
|
|
854
|
+
...this.view,
|
|
855
|
+
conversations: this.view.conversations.filter(item => item.id !== conversationId),
|
|
856
|
+
hiddenConversations: this.hiddenConversationsView(),
|
|
857
|
+
...(selected
|
|
858
|
+
? {
|
|
859
|
+
selectedConversationId: null,
|
|
860
|
+
selectedGroup: null,
|
|
861
|
+
groupAccess: null,
|
|
862
|
+
groupMembers: [],
|
|
863
|
+
groupMembersHasMore: false,
|
|
864
|
+
messages: [],
|
|
865
|
+
historyHasMore: false,
|
|
866
|
+
localPending: false,
|
|
867
|
+
refreshing: false,
|
|
868
|
+
}
|
|
869
|
+
: {}),
|
|
870
|
+
groupRecovery: this.currentGroupRecoveryView(),
|
|
871
|
+
error: null,
|
|
872
|
+
});
|
|
873
|
+
return { ok: true, value: undefined };
|
|
874
|
+
}
|
|
875
|
+
/** Restore one locally hidden row to the recent roster. */
|
|
876
|
+
async restoreConversation(conversationId) {
|
|
877
|
+
const hidden = this.hiddenConversationPreferences.get(conversationId);
|
|
878
|
+
if (hidden === undefined)
|
|
879
|
+
return { ok: true, value: undefined };
|
|
880
|
+
const generation = this.generation;
|
|
881
|
+
const result = await this.withPending('恢复会话', () => call(() => this.remote.updateConversationPreference({ action: 'restore', conversationId }), conversationPreferenceFailureMessage));
|
|
882
|
+
if (!result.ok)
|
|
883
|
+
return result;
|
|
884
|
+
if (!this.current(generation))
|
|
885
|
+
return { ok: true, value: undefined };
|
|
886
|
+
this.applyConversationPreferences(result.value);
|
|
887
|
+
const conversation = this.cacheConversation(hidden.conversation, this.view.conversations.find(item => item.id === conversationId));
|
|
888
|
+
this.publish({
|
|
889
|
+
...this.view,
|
|
890
|
+
conversations: appendUnique([conversation], this.view.conversations, item => item.id),
|
|
891
|
+
hiddenConversations: this.hiddenConversationsView(),
|
|
892
|
+
groupRecovery: this.currentGroupRecoveryView(),
|
|
893
|
+
error: null,
|
|
894
|
+
});
|
|
895
|
+
return { ok: true, value: undefined };
|
|
896
|
+
}
|
|
696
897
|
/**
|
|
697
898
|
* Look up a Handle or DID, then open the matching direct conversation.
|
|
698
899
|
* @param handle - peer Handle or DID typed by the user.
|
|
@@ -709,9 +910,15 @@ export class AwikiController {
|
|
|
709
910
|
return this.fail('请先注册 AWiki 身份');
|
|
710
911
|
if (sameIdentity(identity, peer))
|
|
711
912
|
return this.fail('不能向自己发起私聊');
|
|
712
|
-
const existing = findDirect(this.view.conversations, peer);
|
|
713
|
-
if (existing !== undefined)
|
|
913
|
+
const existing = findDirect([...this.view.conversations, ...this.view.hiddenConversations], peer);
|
|
914
|
+
if (existing !== undefined) {
|
|
915
|
+
if (this.hiddenConversationPreferences.has(existing.id)) {
|
|
916
|
+
const restored = await this.restoreConversation(existing.id);
|
|
917
|
+
if (!restored.ok)
|
|
918
|
+
return restored;
|
|
919
|
+
}
|
|
714
920
|
return this.selectConversation(existing.id);
|
|
921
|
+
}
|
|
715
922
|
const generation = this.generation;
|
|
716
923
|
const resolved = await this.withPending('查找用户', () => call(() => this.remote.resolvePeer({ peer })));
|
|
717
924
|
if (!resolved.ok) {
|
|
@@ -805,7 +1012,8 @@ export class AwikiController {
|
|
|
805
1012
|
await this.refreshConversations(generation);
|
|
806
1013
|
if (!this.current(generation))
|
|
807
1014
|
return result;
|
|
808
|
-
const conversation = this.view.conversations
|
|
1015
|
+
const conversation = [...this.view.conversations, ...this.view.hiddenConversations]
|
|
1016
|
+
.find(value => value.kind === 'group' && value.groupDid === groupDid)
|
|
809
1017
|
?? this.cacheConversation({
|
|
810
1018
|
kind: 'group',
|
|
811
1019
|
id: result.value.conversationId,
|
|
@@ -813,10 +1021,17 @@ export class AwikiController {
|
|
|
813
1021
|
title: result.value.title,
|
|
814
1022
|
unreadCount: 0,
|
|
815
1023
|
});
|
|
816
|
-
this.
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1024
|
+
if (this.hiddenConversationPreferences.has(conversation.id)) {
|
|
1025
|
+
const restored = await this.restoreConversation(conversation.id);
|
|
1026
|
+
if (!restored.ok)
|
|
1027
|
+
return { ok: false, error: restored.error };
|
|
1028
|
+
}
|
|
1029
|
+
else {
|
|
1030
|
+
this.publish({
|
|
1031
|
+
...this.view,
|
|
1032
|
+
conversations: appendUnique([conversation], this.view.conversations, value => value.id),
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
820
1035
|
await this.selectConversation(conversation.id);
|
|
821
1036
|
return result;
|
|
822
1037
|
}
|
|
@@ -825,10 +1040,20 @@ export class AwikiController {
|
|
|
825
1040
|
const conversation = this.selectedConversation();
|
|
826
1041
|
if (conversation?.kind !== 'group')
|
|
827
1042
|
return this.fail('请先打开一个群聊');
|
|
828
|
-
|
|
829
|
-
|
|
1043
|
+
if (this.disposed)
|
|
1044
|
+
return { ok: false, error: 'AWiki 插件已卸载' };
|
|
1045
|
+
const generation = this.generation;
|
|
1046
|
+
this.publish({ ...this.view, pending: '刷新群成员', error: null });
|
|
1047
|
+
const result = await (async () => {
|
|
1048
|
+
if (this.view.groupAccess?.status !== 'available' && this.view.groupAccess?.status !== 'loading') {
|
|
1049
|
+
await this.resumeGroupRebindRecovery(this.generation);
|
|
1050
|
+
}
|
|
1051
|
+
const refreshed = await this.loadGroupState(conversation, true, true);
|
|
830
1052
|
return refreshed.ok ? { ok: true, value: undefined } : refreshed;
|
|
831
|
-
});
|
|
1053
|
+
})();
|
|
1054
|
+
if (this.current(generation))
|
|
1055
|
+
this.publish({ ...this.view, pending: null, error: null });
|
|
1056
|
+
return result;
|
|
832
1057
|
}
|
|
833
1058
|
/** Load the next authoritative member page using Core's opaque cursor. */
|
|
834
1059
|
async loadMoreGroupMembers() {
|
|
@@ -911,6 +1136,7 @@ export class AwikiController {
|
|
|
911
1136
|
...this.view,
|
|
912
1137
|
selectedConversationId: null,
|
|
913
1138
|
selectedGroup: null,
|
|
1139
|
+
groupAccess: null,
|
|
914
1140
|
groupMembers: [],
|
|
915
1141
|
groupMembersHasMore: false,
|
|
916
1142
|
messages: [],
|
|
@@ -940,6 +1166,9 @@ export class AwikiController {
|
|
|
940
1166
|
...this.view,
|
|
941
1167
|
selectedConversationId: conversationId,
|
|
942
1168
|
selectedGroup: null,
|
|
1169
|
+
groupAccess: selected?.kind === 'group'
|
|
1170
|
+
? { groupDid: selected.groupDid, status: 'loading' }
|
|
1171
|
+
: null,
|
|
943
1172
|
groupMembers: [],
|
|
944
1173
|
groupMembersHasMore: false,
|
|
945
1174
|
messages: sameConversation ? this.view.messages : [],
|
|
@@ -985,20 +1214,39 @@ export class AwikiController {
|
|
|
985
1214
|
void this.refreshSelectedDirectProfile(selected, generation, selectionRevision);
|
|
986
1215
|
return { ok: true, value: undefined };
|
|
987
1216
|
}
|
|
988
|
-
async loadGroupState(conversation, reset) {
|
|
1217
|
+
async loadGroupState(conversation, reset, preserveAvailableOnNetworkFailure = false) {
|
|
989
1218
|
const generation = this.generation;
|
|
990
1219
|
const selectionRevision = this.selectionRevision;
|
|
991
|
-
const snapshot = await
|
|
1220
|
+
const snapshot = await callGroupRead(() => this.remote.getGroup({ groupDid: conversation.groupDid }));
|
|
992
1221
|
if (!snapshot.ok) {
|
|
993
|
-
|
|
994
|
-
|
|
1222
|
+
const retainAvailable = preserveAvailableOnNetworkFailure
|
|
1223
|
+
&& snapshot.reason === 'network-error'
|
|
1224
|
+
&& this.view.groupAccess?.groupDid === conversation.groupDid
|
|
1225
|
+
&& this.view.groupAccess.status === 'available';
|
|
1226
|
+
if (!retainAvailable && this.currentSelection(generation, selectionRevision, conversation.id)) {
|
|
1227
|
+
this.publishGroupAccessFailure(conversation, snapshot.reason);
|
|
995
1228
|
}
|
|
996
1229
|
return snapshot;
|
|
997
1230
|
}
|
|
998
|
-
|
|
1231
|
+
if (this.currentSelection(generation, selectionRevision, conversation.id)) {
|
|
1232
|
+
const access = this.view.groupAccess;
|
|
1233
|
+
this.publish({
|
|
1234
|
+
...this.view,
|
|
1235
|
+
selectedGroup: snapshot.value,
|
|
1236
|
+
groupAccess: access?.groupDid === conversation.groupDid && access.status === 'available'
|
|
1237
|
+
? access
|
|
1238
|
+
: { groupDid: conversation.groupDid, status: 'loading' },
|
|
1239
|
+
error: null,
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
const members = await callGroupRead(() => this.remote.listGroupMembers({ groupDid: conversation.groupDid, limit: 50 }));
|
|
999
1243
|
if (!members.ok) {
|
|
1000
|
-
|
|
1001
|
-
|
|
1244
|
+
const retainAvailable = preserveAvailableOnNetworkFailure
|
|
1245
|
+
&& members.reason === 'network-error'
|
|
1246
|
+
&& this.view.groupAccess?.groupDid === conversation.groupDid
|
|
1247
|
+
&& this.view.groupAccess.status === 'available';
|
|
1248
|
+
if (!retainAvailable && this.currentSelection(generation, selectionRevision, conversation.id)) {
|
|
1249
|
+
this.publishGroupAccessFailure(conversation, members.reason);
|
|
1002
1250
|
}
|
|
1003
1251
|
return members;
|
|
1004
1252
|
}
|
|
@@ -1011,6 +1259,7 @@ export class AwikiController {
|
|
|
1011
1259
|
this.publish({
|
|
1012
1260
|
...this.view,
|
|
1013
1261
|
selectedGroup: snapshot.value,
|
|
1262
|
+
groupAccess: { groupDid: conversation.groupDid, status: 'available' },
|
|
1014
1263
|
groupMembers: reset
|
|
1015
1264
|
? members.value.items
|
|
1016
1265
|
: appendUnique(this.view.groupMembers, members.value.items, groupMemberKey),
|
|
@@ -1040,10 +1289,12 @@ export class AwikiController {
|
|
|
1040
1289
|
if (!this.currentSelection(generation, selectionRevision, conversationId))
|
|
1041
1290
|
return;
|
|
1042
1291
|
if (!remote.ok) {
|
|
1292
|
+
if (selected?.kind === 'group')
|
|
1293
|
+
this.publishGroupAccessFailure(selected, remote.reason);
|
|
1043
1294
|
this.publish({
|
|
1044
1295
|
...this.view,
|
|
1045
1296
|
refreshing: false,
|
|
1046
|
-
error: refreshFailureMessage(this.view.messages, remote.error),
|
|
1297
|
+
error: selected?.kind === 'group' ? null : refreshFailureMessage(this.view.messages, remote.error),
|
|
1047
1298
|
});
|
|
1048
1299
|
return;
|
|
1049
1300
|
}
|
|
@@ -1081,17 +1332,22 @@ export class AwikiController {
|
|
|
1081
1332
|
}
|
|
1082
1333
|
async readRemoteHistoryWithGroupReadiness(conversation, conversationId, generation, selectionRevision) {
|
|
1083
1334
|
if (conversation?.kind !== 'group') {
|
|
1084
|
-
|
|
1335
|
+
const result = await call(() => this.remote.getHistory({ conversationId }));
|
|
1336
|
+
return result.ok
|
|
1337
|
+
? result
|
|
1338
|
+
: { ...result, reason: 'network-error' };
|
|
1085
1339
|
}
|
|
1086
1340
|
for (const retryDelay of GROUP_HISTORY_RETRY_DELAYS_MS) {
|
|
1087
|
-
const remote = await
|
|
1341
|
+
const remote = await callGroupRead(() => this.remote.getHistory({ conversationId }));
|
|
1088
1342
|
if (remote.ok || !this.currentSelection(generation, selectionRevision, conversationId))
|
|
1089
1343
|
return remote;
|
|
1344
|
+
if (remote.reason !== 'network-error')
|
|
1345
|
+
return remote;
|
|
1090
1346
|
await delay(retryDelay);
|
|
1091
1347
|
if (!this.currentSelection(generation, selectionRevision, conversationId))
|
|
1092
1348
|
return remote;
|
|
1093
1349
|
}
|
|
1094
|
-
return
|
|
1350
|
+
return callGroupRead(() => this.remote.getHistory({ conversationId }));
|
|
1095
1351
|
}
|
|
1096
1352
|
async refreshSelectedDirectProfile(selected, generation, selectionRevision) {
|
|
1097
1353
|
if (selected?.kind !== 'direct')
|
|
@@ -1145,6 +1401,9 @@ export class AwikiController {
|
|
|
1145
1401
|
if (conversation === undefined || (conversation.unreadCount ?? 0) <= 0) {
|
|
1146
1402
|
return { ok: true, value: undefined };
|
|
1147
1403
|
}
|
|
1404
|
+
if (conversation.kind === 'group' && this.view.groupAccess?.status !== 'available') {
|
|
1405
|
+
return { ok: true, value: undefined };
|
|
1406
|
+
}
|
|
1148
1407
|
const existing = this.markReadInFlight.get(conversation.id);
|
|
1149
1408
|
if (existing !== undefined)
|
|
1150
1409
|
return existing;
|
|
@@ -1237,6 +1496,9 @@ export class AwikiController {
|
|
|
1237
1496
|
const conversation = this.selectedConversation();
|
|
1238
1497
|
if (conversation === undefined)
|
|
1239
1498
|
return this.fail('请先选择会话');
|
|
1499
|
+
if (conversation.kind === 'group' && this.view.groupAccess?.status !== 'available') {
|
|
1500
|
+
return this.fail('当前身份尚未获得这个群聊的发送权限,请先重新检查群成员状态。');
|
|
1501
|
+
}
|
|
1240
1502
|
const conversationId = conversation.id;
|
|
1241
1503
|
const generation = this.generation;
|
|
1242
1504
|
const result = await this.withPending('发送消息', () => call(() => this.remote.sendText({
|
|
@@ -1262,6 +1524,9 @@ export class AwikiController {
|
|
|
1262
1524
|
const conversation = this.selectedConversation();
|
|
1263
1525
|
if (conversation === undefined)
|
|
1264
1526
|
return this.fail('请先选择会话');
|
|
1527
|
+
if (conversation.kind === 'group' && this.view.groupAccess?.status !== 'available') {
|
|
1528
|
+
return this.fail('当前身份尚未获得这个群聊的发送权限,请先重新检查群成员状态。');
|
|
1529
|
+
}
|
|
1265
1530
|
const conversationId = conversation.id;
|
|
1266
1531
|
const generation = this.generation;
|
|
1267
1532
|
const request = {
|
|
@@ -1344,8 +1609,74 @@ export class AwikiController {
|
|
|
1344
1609
|
this.close();
|
|
1345
1610
|
this.listeners.clear();
|
|
1346
1611
|
}
|
|
1612
|
+
async loadConversationPreferences(generation) {
|
|
1613
|
+
const result = await call(() => this.remote.getConversationPreferences(), conversationPreferenceFailureMessage);
|
|
1614
|
+
if (!result.ok || !this.current(generation))
|
|
1615
|
+
return;
|
|
1616
|
+
this.applyConversationPreferences(result.value);
|
|
1617
|
+
this.publish({ ...this.view, hiddenConversations: this.hiddenConversationsView() });
|
|
1618
|
+
}
|
|
1619
|
+
applyConversationPreferences(preferences) {
|
|
1620
|
+
this.hiddenConversationPreferences.clear();
|
|
1621
|
+
for (const hidden of preferences.hiddenConversations) {
|
|
1622
|
+
this.hiddenConversationPreferences.set(hidden.conversation.id, {
|
|
1623
|
+
conversation: { ...hidden.conversation },
|
|
1624
|
+
hiddenAt: hidden.hiddenAt,
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
this.dismissedGroupRecoverySignature = preferences.dismissedGroupRecoverySignature;
|
|
1628
|
+
}
|
|
1629
|
+
hiddenConversationsView() {
|
|
1630
|
+
return [...this.hiddenConversationPreferences.values()]
|
|
1631
|
+
.sort((left, right) => right.hiddenAt - left.hiddenAt)
|
|
1632
|
+
.map(item => item.conversation);
|
|
1633
|
+
}
|
|
1634
|
+
currentGroupRecoveryView() {
|
|
1635
|
+
const summary = this.lastGroupRecoverySummary;
|
|
1636
|
+
if (summary === undefined)
|
|
1637
|
+
return this.view.groupRecovery;
|
|
1638
|
+
const hiddenGroupDids = new Set([...this.hiddenConversationPreferences.values()]
|
|
1639
|
+
.map(item => item.conversation)
|
|
1640
|
+
.filter((conversation) => conversation.kind === 'group')
|
|
1641
|
+
.map(conversation => conversation.groupDid));
|
|
1642
|
+
const projection = groupRecoveryProjection(summary, hiddenGroupDids);
|
|
1643
|
+
this.lastGroupRecoverySignature = projection.signature;
|
|
1644
|
+
return projection.signature === this.dismissedGroupRecoverySignature ? null : projection.view;
|
|
1645
|
+
}
|
|
1646
|
+
async reconcileConversationPage(incoming, generation) {
|
|
1647
|
+
const current = [...this.view.conversations, ...this.view.hiddenConversations];
|
|
1648
|
+
const conversations = incoming.map(item => this.cacheConversation(item, current.find(candidate => candidate.id === item.id)));
|
|
1649
|
+
for (const conversation of conversations) {
|
|
1650
|
+
const hidden = this.hiddenConversationPreferences.get(conversation.id);
|
|
1651
|
+
if (hidden === undefined)
|
|
1652
|
+
continue;
|
|
1653
|
+
const previousActivity = hidden.conversation.lastMessageAt ?? 0;
|
|
1654
|
+
const currentActivity = conversation.lastMessageAt ?? 0;
|
|
1655
|
+
if (currentActivity <= previousActivity)
|
|
1656
|
+
continue;
|
|
1657
|
+
const restored = await call(() => this.remote.updateConversationPreference({
|
|
1658
|
+
action: 'restore',
|
|
1659
|
+
conversationId: conversation.id,
|
|
1660
|
+
}), conversationPreferenceFailureMessage);
|
|
1661
|
+
if (!this.current(generation))
|
|
1662
|
+
return { visible: [], hidden: this.hiddenConversationsView() };
|
|
1663
|
+
if (restored.ok)
|
|
1664
|
+
this.applyConversationPreferences(restored.value);
|
|
1665
|
+
}
|
|
1666
|
+
const visible = [];
|
|
1667
|
+
for (const conversation of conversations) {
|
|
1668
|
+
const hidden = this.hiddenConversationPreferences.get(conversation.id);
|
|
1669
|
+
if (hidden === undefined) {
|
|
1670
|
+
visible.push(conversation);
|
|
1671
|
+
}
|
|
1672
|
+
else {
|
|
1673
|
+
this.hiddenConversationPreferences.set(conversation.id, { ...hidden, conversation });
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
return { visible, hidden: this.hiddenConversationsView() };
|
|
1677
|
+
}
|
|
1347
1678
|
async refreshConversations(generation, background = false) {
|
|
1348
|
-
const result = await
|
|
1679
|
+
const result = await this.listConversationPage({});
|
|
1349
1680
|
if (!this.current(generation))
|
|
1350
1681
|
return { ok: true, value: undefined };
|
|
1351
1682
|
if (!result.ok)
|
|
@@ -1353,15 +1684,16 @@ export class AwikiController {
|
|
|
1353
1684
|
const firstPage = this.view.conversations.length === 0;
|
|
1354
1685
|
if (firstPage)
|
|
1355
1686
|
this.conversationsCursor = result.value.nextCursor;
|
|
1356
|
-
const refreshed = result.value.items
|
|
1357
|
-
|
|
1358
|
-
return
|
|
1359
|
-
});
|
|
1687
|
+
const refreshed = await this.reconcileConversationPage(result.value.items, generation);
|
|
1688
|
+
if (!this.current(generation))
|
|
1689
|
+
return { ok: true, value: undefined };
|
|
1360
1690
|
this.publish({
|
|
1361
1691
|
...this.view,
|
|
1362
1692
|
conversations: firstPage
|
|
1363
|
-
? refreshed
|
|
1364
|
-
: appendUnique(refreshed, this.view.conversations, value => value.id),
|
|
1693
|
+
? refreshed.visible
|
|
1694
|
+
: appendUnique(refreshed.visible, this.view.conversations, value => value.id),
|
|
1695
|
+
hiddenConversations: refreshed.hidden,
|
|
1696
|
+
groupRecovery: this.currentGroupRecoveryView(),
|
|
1365
1697
|
conversationsHasMore: firstPage
|
|
1366
1698
|
? result.value.hasMore && result.value.nextCursor !== undefined
|
|
1367
1699
|
: this.view.conversationsHasMore,
|
|
@@ -1369,6 +1701,51 @@ export class AwikiController {
|
|
|
1369
1701
|
});
|
|
1370
1702
|
return { ok: true, value: undefined };
|
|
1371
1703
|
}
|
|
1704
|
+
/** List the active identity's own conversations and detect a revoked local credential. */
|
|
1705
|
+
async listConversationPage(request) {
|
|
1706
|
+
const result = await callWithFailureCode(() => this.remote.listConversations(request));
|
|
1707
|
+
if (!result.ok && (result.failureCode === 'identity-recovery-required' || result.failureCode === 'forbidden')) {
|
|
1708
|
+
this.enterIdentityRecoveryRequired();
|
|
1709
|
+
return { ok: false, error: '当前设备的 AWiki 身份凭证已失效,请重新恢复身份。' };
|
|
1710
|
+
}
|
|
1711
|
+
return result.ok ? result : { ok: false, error: result.error };
|
|
1712
|
+
}
|
|
1713
|
+
/** Replace only visible browser projections; Core identity and SQLite state remain untouched. */
|
|
1714
|
+
enterIdentityRecoveryRequired() {
|
|
1715
|
+
if (this.view.identity === null)
|
|
1716
|
+
return;
|
|
1717
|
+
this.close();
|
|
1718
|
+
this.conversationsCursor = undefined;
|
|
1719
|
+
this.historyCursor = undefined;
|
|
1720
|
+
this.groupMembersCursor = undefined;
|
|
1721
|
+
this.unreadAtOpen.clear();
|
|
1722
|
+
this.summaryBaselines.clear();
|
|
1723
|
+
this.groupRecoveryItems.clear();
|
|
1724
|
+
this.lastGroupRecoverySignature = undefined;
|
|
1725
|
+
this.lastGroupRecoverySummary = undefined;
|
|
1726
|
+
this.clearImageAttachments();
|
|
1727
|
+
this.publish({
|
|
1728
|
+
...this.view,
|
|
1729
|
+
status: 'ready',
|
|
1730
|
+
sessionStatus: 'recovery-required',
|
|
1731
|
+
conversations: Object.freeze([]),
|
|
1732
|
+
hiddenConversations: Object.freeze([]),
|
|
1733
|
+
conversationsHasMore: false,
|
|
1734
|
+
selectedConversationId: null,
|
|
1735
|
+
selectedGroup: null,
|
|
1736
|
+
groupAccess: null,
|
|
1737
|
+
groupMembers: Object.freeze([]),
|
|
1738
|
+
groupMembersHasMore: false,
|
|
1739
|
+
groupRecovery: null,
|
|
1740
|
+
messages: Object.freeze([]),
|
|
1741
|
+
historyHasMore: false,
|
|
1742
|
+
localPending: false,
|
|
1743
|
+
refreshing: false,
|
|
1744
|
+
pending: null,
|
|
1745
|
+
error: null,
|
|
1746
|
+
summaries: Object.freeze({}),
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1372
1749
|
/** Hydrate account-projected groups before asking Core to resume their durable rebind jobs. */
|
|
1373
1750
|
async refreshConversationsAndRecoverGroups(generation, background = false) {
|
|
1374
1751
|
const listed = await this.refreshConversations(generation, background);
|
|
@@ -1385,11 +1762,24 @@ export class AwikiController {
|
|
|
1385
1762
|
operation = call(() => this.remote.resumeGroupRebindRecovery(), groupRecoveryFailureMessage).then((result) => {
|
|
1386
1763
|
if (!this.current(generation))
|
|
1387
1764
|
return result;
|
|
1765
|
+
if (result.ok) {
|
|
1766
|
+
this.lastGroupRecoverySummary = result.value;
|
|
1767
|
+
this.groupRecoveryItems.clear();
|
|
1768
|
+
for (const item of result.value.items) {
|
|
1769
|
+
this.groupRecoveryItems.set(item.groupDid, item.status);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
else {
|
|
1773
|
+
this.lastGroupRecoverySummary = undefined;
|
|
1774
|
+
this.lastGroupRecoverySignature = 'unavailable';
|
|
1775
|
+
}
|
|
1388
1776
|
this.publish({
|
|
1389
1777
|
...this.view,
|
|
1390
1778
|
groupRecovery: result.ok
|
|
1391
|
-
?
|
|
1392
|
-
:
|
|
1779
|
+
? this.currentGroupRecoveryView()
|
|
1780
|
+
: this.dismissedGroupRecoverySignature === 'unavailable'
|
|
1781
|
+
? null
|
|
1782
|
+
: { status: 'unavailable', pending: 0, blocked: 0 },
|
|
1393
1783
|
});
|
|
1394
1784
|
return result;
|
|
1395
1785
|
}).finally(() => {
|
|
@@ -1403,6 +1793,10 @@ export class AwikiController {
|
|
|
1403
1793
|
const conversationId = this.view.selectedConversationId;
|
|
1404
1794
|
if (conversationId === null)
|
|
1405
1795
|
return this.fail('请先选择会话');
|
|
1796
|
+
const selectedConversation = this.selectedConversation();
|
|
1797
|
+
if (selectedConversation?.kind === 'group' && this.view.groupAccess?.status !== 'available') {
|
|
1798
|
+
return { ok: false, error: '当前群聊仅可查看本机已有记录,请先重新检查群成员状态。' };
|
|
1799
|
+
}
|
|
1406
1800
|
const generation = this.generation;
|
|
1407
1801
|
const request = {
|
|
1408
1802
|
conversationId,
|
|
@@ -1448,6 +1842,9 @@ export class AwikiController {
|
|
|
1448
1842
|
const selected = this.view.selectedConversationId;
|
|
1449
1843
|
if (selected === null || !this.current(generation))
|
|
1450
1844
|
return;
|
|
1845
|
+
const selectedConversation = this.selectedConversation();
|
|
1846
|
+
if (selectedConversation?.kind === 'group' && this.view.groupAccess?.status !== 'available')
|
|
1847
|
+
return;
|
|
1451
1848
|
const result = await call(() => this.remote.getHistory({ conversationId: selected }));
|
|
1452
1849
|
if (!this.current(generation) || !result.ok || this.view.selectedConversationId !== selected)
|
|
1453
1850
|
return;
|
|
@@ -1472,7 +1869,7 @@ export class AwikiController {
|
|
|
1472
1869
|
this.polling = false;
|
|
1473
1870
|
}
|
|
1474
1871
|
}
|
|
1475
|
-
async withPending(label, operation) {
|
|
1872
|
+
async withPending(label, operation, options = {}) {
|
|
1476
1873
|
if (this.disposed)
|
|
1477
1874
|
return { ok: false, error: 'AWiki 插件已卸载' };
|
|
1478
1875
|
const generation = this.generation;
|
|
@@ -1480,7 +1877,11 @@ export class AwikiController {
|
|
|
1480
1877
|
const result = await operation();
|
|
1481
1878
|
if (!this.current(generation))
|
|
1482
1879
|
return result;
|
|
1483
|
-
this.publish({
|
|
1880
|
+
this.publish({
|
|
1881
|
+
...this.view,
|
|
1882
|
+
pending: null,
|
|
1883
|
+
error: result.ok || options.publishFailure === false ? null : result.error,
|
|
1884
|
+
});
|
|
1484
1885
|
return result;
|
|
1485
1886
|
}
|
|
1486
1887
|
appendMessage(message) {
|
|
@@ -1531,6 +1932,21 @@ export class AwikiController {
|
|
|
1531
1932
|
const selected = this.view.selectedConversationId;
|
|
1532
1933
|
return selected === null ? undefined : this.view.conversations.find(value => value.id === selected);
|
|
1533
1934
|
}
|
|
1935
|
+
publishGroupAccessFailure(conversation, reason) {
|
|
1936
|
+
const journal = this.groupRecoveryItems.get(conversation.groupDid);
|
|
1937
|
+
const status = reason === 'not-member'
|
|
1938
|
+
? journal === 'blocked'
|
|
1939
|
+
? 'blocked'
|
|
1940
|
+
: journal === 'pending'
|
|
1941
|
+
? 'recovering'
|
|
1942
|
+
: 'not-member'
|
|
1943
|
+
: reason;
|
|
1944
|
+
this.publish({
|
|
1945
|
+
...this.view,
|
|
1946
|
+
groupAccess: { groupDid: conversation.groupDid, status },
|
|
1947
|
+
error: null,
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1534
1950
|
/** Keep presentation-only cache entries isolated to one authenticated identity. */
|
|
1535
1951
|
activatePresentationCache(identity) {
|
|
1536
1952
|
const ownerDid = identity?.did ?? null;
|
|
@@ -1538,6 +1954,11 @@ export class AwikiController {
|
|
|
1538
1954
|
return;
|
|
1539
1955
|
this.directProfiles.clear();
|
|
1540
1956
|
this.groupTitles.clear();
|
|
1957
|
+
this.groupRecoveryItems.clear();
|
|
1958
|
+
this.hiddenConversationPreferences.clear();
|
|
1959
|
+
this.dismissedGroupRecoverySignature = undefined;
|
|
1960
|
+
this.lastGroupRecoverySignature = undefined;
|
|
1961
|
+
this.lastGroupRecoverySummary = undefined;
|
|
1541
1962
|
this.clearImageAttachments();
|
|
1542
1963
|
this.presentationCacheOwnerDid = ownerDid;
|
|
1543
1964
|
}
|
|
@@ -1545,6 +1966,11 @@ export class AwikiController {
|
|
|
1545
1966
|
clearPresentationCache() {
|
|
1546
1967
|
this.directProfiles.clear();
|
|
1547
1968
|
this.groupTitles.clear();
|
|
1969
|
+
this.groupRecoveryItems.clear();
|
|
1970
|
+
this.hiddenConversationPreferences.clear();
|
|
1971
|
+
this.dismissedGroupRecoverySignature = undefined;
|
|
1972
|
+
this.lastGroupRecoverySignature = undefined;
|
|
1973
|
+
this.lastGroupRecoverySummary = undefined;
|
|
1548
1974
|
this.clearImageAttachments();
|
|
1549
1975
|
this.presentationCacheOwnerDid = null;
|
|
1550
1976
|
}
|