@awiki/dsh-plugin 0.3.16 → 0.3.18
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 +4 -0
- package/THIRD_PARTY_NOTICES.md +6 -0
- package/lib/client.js +3543 -746
- package/lib/client.js.map +1 -1
- package/lib/index.js +218 -41
- package/lib/{sdk-adapter-DGcTiEZz.mjs → package-version.generated-D0MSDRZn.mjs} +83 -10
- package/lib/provider.js +5 -3
- package/lib/typert.host.js +481 -127
- package/lib/typert.remote-client.d.ts +9 -3
- package/lib/typert.remote-client.d.ts.map +1 -1
- package/lib/typert.remote-client.js +481 -127
- package/lib/types/client/AwikiAvatar.d.ts +40 -0
- package/lib/types/client/AwikiAvatar.d.ts.map +1 -0
- package/lib/types/client/AwikiAvatar.js +201 -0
- package/lib/types/client/AwikiAvatar.js.map +1 -0
- package/lib/types/client/AwikiAvatarEditor.d.ts +11 -0
- package/lib/types/client/AwikiAvatarEditor.d.ts.map +1 -0
- package/lib/types/client/AwikiAvatarEditor.js +113 -0
- package/lib/types/client/AwikiAvatarEditor.js.map +1 -0
- package/lib/types/client/AwikiAvatarPreview.d.ts +8 -0
- package/lib/types/client/AwikiAvatarPreview.d.ts.map +1 -0
- package/lib/types/client/AwikiAvatarPreview.js +68 -0
- package/lib/types/client/AwikiAvatarPreview.js.map +1 -0
- package/lib/types/client/AwikiGroupDetails.js +2 -2
- package/lib/types/client/AwikiGroupDetails.js.map +1 -1
- package/lib/types/client/AwikiIdentityAccess.d.ts +1 -0
- 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/AwikiOverlay.d.ts.map +1 -1
- package/lib/types/client/AwikiOverlay.js +92 -27
- package/lib/types/client/AwikiOverlay.js.map +1 -1
- package/lib/types/client/AwikiProfileCard.d.ts +1 -1
- package/lib/types/client/AwikiProfileCard.d.ts.map +1 -1
- package/lib/types/client/AwikiProfileCard.js +6 -2
- package/lib/types/client/AwikiProfileCard.js.map +1 -1
- package/lib/types/client/avatar-cache.d.ts +24 -0
- package/lib/types/client/avatar-cache.d.ts.map +1 -0
- package/lib/types/client/avatar-cache.js +336 -0
- package/lib/types/client/avatar-cache.js.map +1 -0
- package/lib/types/client/avatar-dialog.d.ts +3 -0
- package/lib/types/client/avatar-dialog.d.ts.map +1 -0
- package/lib/types/client/avatar-dialog.js +18 -0
- package/lib/types/client/avatar-dialog.js.map +1 -0
- package/lib/types/client/avatar-image.d.ts +26 -0
- package/lib/types/client/avatar-image.d.ts.map +1 -0
- package/lib/types/client/avatar-image.js +193 -0
- package/lib/types/client/avatar-image.js.map +1 -0
- package/lib/types/client/controller.d.ts +30 -4
- package/lib/types/client/controller.d.ts.map +1 -1
- package/lib/types/client/controller.js +267 -56
- 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 +10 -2
- package/lib/types/client/index.js.map +1 -1
- package/lib/types/client/slots.d.ts +12 -3
- package/lib/types/client/slots.d.ts.map +1 -1
- package/lib/types/conversation-preferences.d.ts.map +1 -1
- package/lib/types/conversation-preferences.js +27 -0
- package/lib/types/conversation-preferences.js.map +1 -1
- package/lib/types/index.d.ts +8 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +76 -11
- package/lib/types/index.js.map +1 -1
- package/lib/types/listener.d.ts.map +1 -1
- package/lib/types/listener.js +9 -8
- package/lib/types/listener.js.map +1 -1
- package/lib/types/package-version.generated.d.ts +2 -2
- package/lib/types/package-version.generated.js +2 -2
- package/lib/types/provider-api.d.ts +9 -3
- package/lib/types/provider-api.d.ts.map +1 -1
- package/lib/types/provider.d.ts.map +1 -1
- package/lib/types/provider.js +5 -2
- package/lib/types/provider.js.map +1 -1
- package/lib/types/sdk-adapter.d.ts +6 -3
- package/lib/types/sdk-adapter.d.ts.map +1 -1
- package/lib/types/sdk-adapter.js +58 -7
- package/lib/types/sdk-adapter.js.map +1 -1
- package/lib/types/tenant-registry.d.ts +2 -0
- package/lib/types/tenant-registry.d.ts.map +1 -1
- package/lib/types/tenant-registry.js +83 -16
- package/lib/types/tenant-registry.js.map +1 -1
- package/lib/types/types.d.ts +63 -7
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js.map +1 -1
- package/lib/types/update-policy.d.ts +2 -2
- package/package.json +33 -34
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { avatarCache } from "./avatar-cache.js";
|
|
1
2
|
import { AwikiDraftStore } from "./drafts.js";
|
|
2
3
|
import { SHORT_HANDLE_INVITE_MESSAGE } from "../registration-policy.js";
|
|
3
4
|
import { IndexedDbAwikiBrowserImageCache, } from "./image-cache.js";
|
|
@@ -12,6 +13,8 @@ function genericFailureMessage(failure) {
|
|
|
12
13
|
return '无法连接 AWiki 服务,请检查网络后重试。';
|
|
13
14
|
case 'rate-limited':
|
|
14
15
|
return 'AWiki 请求过于频繁,请稍后重试。';
|
|
16
|
+
case 'group-member-encryption-not-ready':
|
|
17
|
+
return '尚未加入:对方尚未完成加密准备,请对方打开客户端后重试。';
|
|
15
18
|
case 'not-registered':
|
|
16
19
|
return '请先注册 AWiki 身份后重试。';
|
|
17
20
|
case 'signed-out':
|
|
@@ -337,13 +340,17 @@ function canonicalMessagePage(incoming) {
|
|
|
337
340
|
}
|
|
338
341
|
/** Replace the newest loaded window while preserving older messages before it. */
|
|
339
342
|
function mergeLatestMessages(current, incoming) {
|
|
340
|
-
const
|
|
343
|
+
const previous = new Map(current.map(message => [message.id, message]));
|
|
344
|
+
const page = canonicalMessagePage(incoming).map(message => previous.get(message.id)?.encryptionRequired === true
|
|
345
|
+
? { ...message, encryptionRequired: true } : message);
|
|
341
346
|
const incomingIds = new Set(page.map(message => message.id));
|
|
342
347
|
return [...current.filter(message => !incomingIds.has(message.id)), ...page];
|
|
343
348
|
}
|
|
344
349
|
/** Prepend one chronological continuation page while exact-updating any overlap. */
|
|
345
350
|
function mergeOlderMessages(current, incoming) {
|
|
346
|
-
const
|
|
351
|
+
const previous = new Map(current.map(message => [message.id, message]));
|
|
352
|
+
const page = canonicalMessagePage(incoming).map(message => previous.get(message.id)?.encryptionRequired === true
|
|
353
|
+
? { ...message, encryptionRequired: true } : message);
|
|
347
354
|
const incomingIds = new Set(page.map(message => message.id));
|
|
348
355
|
return [...page, ...current.filter(message => !incomingIds.has(message.id))];
|
|
349
356
|
}
|
|
@@ -352,7 +359,7 @@ function appendMessageById(current, incoming) {
|
|
|
352
359
|
const index = current.findIndex(message => message.id === incoming.id);
|
|
353
360
|
if (index < 0)
|
|
354
361
|
return [...current, incoming];
|
|
355
|
-
return current.map((message, currentIndex) => currentIndex === index ? incoming : message);
|
|
362
|
+
return current.map((message, currentIndex) => currentIndex === index ? { ...incoming, ...(message.encryptionRequired === true ? { encryptionRequired: true } : {}) } : message);
|
|
356
363
|
}
|
|
357
364
|
/** Explain a background failure without implying that visible local messages were lost. */
|
|
358
365
|
function refreshFailureMessage(messages, error) {
|
|
@@ -524,6 +531,7 @@ export class AwikiController {
|
|
|
524
531
|
remote;
|
|
525
532
|
persistentImageCache;
|
|
526
533
|
drafts = new AwikiDraftStore();
|
|
534
|
+
retiredMessageIds = new Set();
|
|
527
535
|
view = INITIAL_VIEW;
|
|
528
536
|
listeners = new Set();
|
|
529
537
|
/** Runtime-only selected operations survive tenant round trips without reopening stale browser hints after reload. */
|
|
@@ -539,8 +547,12 @@ export class AwikiController {
|
|
|
539
547
|
tenantTransition;
|
|
540
548
|
generation = 0;
|
|
541
549
|
selectionRevision = 0;
|
|
550
|
+
groupReadRevision = 0;
|
|
542
551
|
disposed = false;
|
|
543
552
|
polling = false;
|
|
553
|
+
lastAvatarProfileCheck = 0;
|
|
554
|
+
avatarProfileRefreshGeneration = -1;
|
|
555
|
+
avatarProfileRefresh;
|
|
544
556
|
markReadInFlight = new Map();
|
|
545
557
|
unreadAtOpen = new Map();
|
|
546
558
|
summaryBaselines = new Map();
|
|
@@ -583,7 +595,7 @@ export class AwikiController {
|
|
|
583
595
|
if (!preserveView)
|
|
584
596
|
this.summaryBaselines.clear();
|
|
585
597
|
const previous = this.view;
|
|
586
|
-
const previousTenant = this.config?.tenantId;
|
|
598
|
+
const previousTenant = (this.config?.tenantStorageScopeId ?? this.config?.tenantId);
|
|
587
599
|
const generation = this.generation;
|
|
588
600
|
if (!preserveView)
|
|
589
601
|
this.publish({ ...INITIAL_VIEW, status: 'loading' });
|
|
@@ -606,13 +618,14 @@ export class AwikiController {
|
|
|
606
618
|
if (!session.ok)
|
|
607
619
|
return failed(session.error);
|
|
608
620
|
const identity = session.value.status === 'active' ? session.value.identity : null;
|
|
609
|
-
const sameOwner = preserveView && previousTenant === config.value.tenantId
|
|
621
|
+
const sameOwner = preserveView && previousTenant === (config.value.tenantStorageScopeId ?? config.value.tenantId)
|
|
610
622
|
&& previous.sessionStatus === session.value.status && previous.identity?.did === identity?.did;
|
|
611
|
-
this.drafts.setScope(config.value.tenantId ?? 'default', identity?.did ?? session.value.status);
|
|
623
|
+
this.drafts.setScope((config.value.tenantStorageScopeId ?? config.value.tenantId) ?? 'default', identity?.did ?? session.value.status);
|
|
624
|
+
this.restoreGroupCreationDraft();
|
|
612
625
|
this.activatePresentationCache(identity);
|
|
613
|
-
const selectedRecovery = previousTenant === config.value.tenantId
|
|
626
|
+
const selectedRecovery = previousTenant === (config.value.tenantStorageScopeId ?? config.value.tenantId)
|
|
614
627
|
? previous.recoveryOperationId
|
|
615
|
-
: this.recoveryOperationsByTenant.get(recoveryTenantKey(config.value.tenantId)) ?? null;
|
|
628
|
+
: this.recoveryOperationsByTenant.get(recoveryTenantKey((config.value.tenantStorageScopeId ?? config.value.tenantId))) ?? null;
|
|
616
629
|
this.publish({
|
|
617
630
|
...(sameOwner ? this.view : INITIAL_VIEW),
|
|
618
631
|
status: 'ready',
|
|
@@ -622,7 +635,7 @@ export class AwikiController {
|
|
|
622
635
|
attachmentMaxBytes: config.value.attachmentMaxBytes,
|
|
623
636
|
handleRecoveryPhoneEnabled: config.value.handleRecoveryPhoneEnabled,
|
|
624
637
|
recoveryOperationId: selectedRecovery,
|
|
625
|
-
recoveryOtpRetryAt: storedRecoveryOtpRetryAt(selectedRecovery, config.value.tenantId),
|
|
638
|
+
recoveryOtpRetryAt: storedRecoveryOtpRetryAt(selectedRecovery, (config.value.tenantStorageScopeId ?? config.value.tenantId)),
|
|
626
639
|
});
|
|
627
640
|
await this.refreshIdentityAccess();
|
|
628
641
|
if (!this.current(generation))
|
|
@@ -634,7 +647,7 @@ export class AwikiController {
|
|
|
634
647
|
await this.loadConversationPreferences(generation);
|
|
635
648
|
const profile = await call(() => this.remote.getProfile());
|
|
636
649
|
if (this.current(generation) && profile.ok)
|
|
637
|
-
this.
|
|
650
|
+
this.applyProfile(profile.value);
|
|
638
651
|
}
|
|
639
652
|
return { ok: true, value: undefined };
|
|
640
653
|
}
|
|
@@ -808,15 +821,15 @@ export class AwikiController {
|
|
|
808
821
|
const selected = this.view.recoveryOperationId;
|
|
809
822
|
this.publish({ ...this.view, identityAccess: result.value, recoveryOperationId: selected,
|
|
810
823
|
recoveryProgress: selected === this.view.recoveryOperationId ? this.view.recoveryProgress : null,
|
|
811
|
-
recoveryOtpRetryAt: storedRecoveryOtpRetryAt(selected, this.config?.tenantId) });
|
|
824
|
+
recoveryOtpRetryAt: storedRecoveryOtpRetryAt(selected, (this.config?.tenantStorageScopeId ?? this.config?.tenantId)) });
|
|
812
825
|
if (selected !== null) {
|
|
813
826
|
const status = await callWithFailureCode(() => this.remote.getRecoveryStatus({ operationId: selected }));
|
|
814
827
|
if (!this.recoveryCurrent(generation, revision))
|
|
815
828
|
return this.recoveryLeft();
|
|
816
829
|
if (!status.ok) {
|
|
817
830
|
if (status.failureCode === 'not-found' && candidates.length === 0) {
|
|
818
|
-
clearRecoveryOtpRetryAt(selected, this.config?.tenantId);
|
|
819
|
-
this.rememberRecoveryOperation(null, this.config?.tenantId);
|
|
831
|
+
clearRecoveryOtpRetryAt(selected, (this.config?.tenantStorageScopeId ?? this.config?.tenantId));
|
|
832
|
+
this.rememberRecoveryOperation(null, (this.config?.tenantStorageScopeId ?? this.config?.tenantId));
|
|
820
833
|
this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: null, recoveryOtpRetryAt: null });
|
|
821
834
|
}
|
|
822
835
|
else {
|
|
@@ -827,9 +840,9 @@ export class AwikiController {
|
|
|
827
840
|
else {
|
|
828
841
|
const complete = status.value.phase === 'applied';
|
|
829
842
|
if (complete)
|
|
830
|
-
clearRecoveryOtpRetryAt(selected, this.config?.tenantId);
|
|
843
|
+
clearRecoveryOtpRetryAt(selected, (this.config?.tenantStorageScopeId ?? this.config?.tenantId));
|
|
831
844
|
this.publish({ ...this.view, recoveryProgress: status.value,
|
|
832
|
-
recoveryOtpRetryAt: complete ? null : storedRecoveryOtpRetryAt(selected, this.config?.tenantId),
|
|
845
|
+
recoveryOtpRetryAt: complete ? null : storedRecoveryOtpRetryAt(selected, (this.config?.tenantStorageScopeId ?? this.config?.tenantId)),
|
|
833
846
|
identityAccess: result.value });
|
|
834
847
|
}
|
|
835
848
|
}
|
|
@@ -844,9 +857,9 @@ export class AwikiController {
|
|
|
844
857
|
this.leaveRecovery();
|
|
845
858
|
else
|
|
846
859
|
this.recoveryRevision++;
|
|
847
|
-
this.rememberRecoveryOperation(operationId, this.config?.tenantId);
|
|
860
|
+
this.rememberRecoveryOperation(operationId, (this.config?.tenantStorageScopeId ?? this.config?.tenantId));
|
|
848
861
|
this.publish({ ...this.view, recoveryOperationId: operationId, recoveryProgress: null,
|
|
849
|
-
recoveryOtpRetryAt: storedRecoveryOtpRetryAt(operationId, this.config?.tenantId) });
|
|
862
|
+
recoveryOtpRetryAt: storedRecoveryOtpRetryAt(operationId, (this.config?.tenantStorageScopeId ?? this.config?.tenantId)) });
|
|
850
863
|
const result = await this.refreshRecoveryStatus();
|
|
851
864
|
return result.ok ? { ok: true, value: undefined } : result;
|
|
852
865
|
}
|
|
@@ -877,7 +890,7 @@ export class AwikiController {
|
|
|
877
890
|
this.pendingRequest = undefined;
|
|
878
891
|
this.pendingRecovery = false;
|
|
879
892
|
this.drafts.clearFlow();
|
|
880
|
-
this.rememberRecoveryOperation(null, this.config?.tenantId);
|
|
893
|
+
this.rememberRecoveryOperation(null, (this.config?.tenantStorageScopeId ?? this.config?.tenantId));
|
|
881
894
|
this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: null, accessError: null,
|
|
882
895
|
accessLoading: false, pending: wasPending ? null : this.view.pending, error: null });
|
|
883
896
|
}
|
|
@@ -927,9 +940,12 @@ export class AwikiController {
|
|
|
927
940
|
}
|
|
928
941
|
if (result.value.status === 'registered') {
|
|
929
942
|
this.drafts.clearScope();
|
|
930
|
-
this.drafts.setScope(this.config?.tenantId ?? 'default', result.value.identity.did);
|
|
943
|
+
this.drafts.setScope((this.config?.tenantStorageScopeId ?? this.config?.tenantId) ?? 'default', result.value.identity.did);
|
|
931
944
|
this.activatePresentationCache(result.value.identity);
|
|
932
945
|
this.publish({ ...this.view, sessionStatus: 'active', identity: result.value.identity, error: null });
|
|
946
|
+
await this.loadConversationPreferences(generation);
|
|
947
|
+
if (!this.current(generation))
|
|
948
|
+
return result;
|
|
933
949
|
await this.refreshConversations(generation);
|
|
934
950
|
}
|
|
935
951
|
return result;
|
|
@@ -970,7 +986,7 @@ export class AwikiController {
|
|
|
970
986
|
/** Cancel one exact Join choice and send Recovery OTP only while its original browser scope remains current. */
|
|
971
987
|
async beginRecoveryFromDeviceJoin(request) {
|
|
972
988
|
const generation = this.generation;
|
|
973
|
-
const tenantId = this.config?.tenantId;
|
|
989
|
+
const tenantId = (this.config?.tenantStorageScopeId ?? this.config?.tenantId);
|
|
974
990
|
const flowScope = this.drafts.getScope('recovery:retryDeadline');
|
|
975
991
|
const flowEpoch = this.drafts.epoch(flowScope);
|
|
976
992
|
const changed = () => ({
|
|
@@ -978,7 +994,7 @@ export class AwikiController {
|
|
|
978
994
|
error: '恢复能力或身份环境已改变,请重新检查后再选择。',
|
|
979
995
|
});
|
|
980
996
|
const scopeCurrent = () => this.current(generation)
|
|
981
|
-
&& this.config?.tenantId === tenantId
|
|
997
|
+
&& (this.config?.tenantStorageScopeId ?? this.config?.tenantId) === tenantId
|
|
982
998
|
&& this.drafts.getScope('recovery:retryDeadline') === flowScope
|
|
983
999
|
&& this.drafts.epoch(flowScope) === flowEpoch;
|
|
984
1000
|
if (!scopeCurrent())
|
|
@@ -988,7 +1004,7 @@ export class AwikiController {
|
|
|
988
1004
|
return changed();
|
|
989
1005
|
if (!latest.ok)
|
|
990
1006
|
return { ok: false, error: '暂时无法确认服务器的恢复能力,请稍后重试。' };
|
|
991
|
-
if (latest.value.tenantId !== tenantId)
|
|
1007
|
+
if ((latest.value.tenantStorageScopeId ?? latest.value.tenantId) !== tenantId)
|
|
992
1008
|
return changed();
|
|
993
1009
|
this.config = latest.value;
|
|
994
1010
|
this.publish({ ...this.view, handleRecoveryPhoneEnabled: latest.value.handleRecoveryPhoneEnabled });
|
|
@@ -1018,7 +1034,7 @@ export class AwikiController {
|
|
|
1018
1034
|
if (result.value === null)
|
|
1019
1035
|
return result;
|
|
1020
1036
|
this.recordRecoveryOtp(result.value, tenantId);
|
|
1021
|
-
const sameFlow = this.config?.tenantId === tenantId
|
|
1037
|
+
const sameFlow = (this.config?.tenantStorageScopeId ?? this.config?.tenantId) === tenantId
|
|
1022
1038
|
&& this.drafts.getScope('recovery:retryDeadline') === flowScope
|
|
1023
1039
|
&& this.drafts.epoch(flowScope) === flowEpoch;
|
|
1024
1040
|
if (!sameFlow)
|
|
@@ -1090,6 +1106,65 @@ export class AwikiController {
|
|
|
1090
1106
|
this.publish({ ...this.view, identity: result.value, error: null });
|
|
1091
1107
|
return result;
|
|
1092
1108
|
}
|
|
1109
|
+
applyProfile(profile, updateIdentity = false) {
|
|
1110
|
+
if (profile.did !== this.view.identity?.did)
|
|
1111
|
+
return;
|
|
1112
|
+
const current = this.view.profile?.profileVersion;
|
|
1113
|
+
const incoming = profile.profileVersion;
|
|
1114
|
+
if (current !== undefined && incoming !== undefined && /^(0|[1-9][0-9]*)$/u.test(current) && /^(0|[1-9][0-9]*)$/u.test(incoming) && BigInt(incoming) < BigInt(current))
|
|
1115
|
+
return;
|
|
1116
|
+
const identity = updateIdentity ? { ...this.view.identity, displayName: profile.displayName } : this.view.identity;
|
|
1117
|
+
this.publish({ ...this.view, identity, profile, error: null });
|
|
1118
|
+
}
|
|
1119
|
+
refreshAvatarProfile() {
|
|
1120
|
+
if (this.avatarProfileRefresh !== undefined && this.avatarProfileRefreshGeneration === this.generation)
|
|
1121
|
+
return this.avatarProfileRefresh;
|
|
1122
|
+
const generation = this.generation;
|
|
1123
|
+
this.lastAvatarProfileCheck = Date.now();
|
|
1124
|
+
const operation = (async () => {
|
|
1125
|
+
const result = await call(() => this.remote.getProfile());
|
|
1126
|
+
if (!this.current(generation))
|
|
1127
|
+
return { ok: false, error: '账号已切换,请重新打开头像设置' };
|
|
1128
|
+
if (!result.ok)
|
|
1129
|
+
return { ok: false, error: result.error };
|
|
1130
|
+
this.applyProfile(result.value);
|
|
1131
|
+
return { ok: true, value: this.view.profile ?? result.value };
|
|
1132
|
+
})();
|
|
1133
|
+
this.avatarProfileRefresh = operation;
|
|
1134
|
+
this.avatarProfileRefreshGeneration = generation;
|
|
1135
|
+
void operation.finally(() => { if (this.avatarProfileRefresh === operation)
|
|
1136
|
+
this.avatarProfileRefresh = undefined; });
|
|
1137
|
+
return operation;
|
|
1138
|
+
}
|
|
1139
|
+
async setAvatar(request) {
|
|
1140
|
+
return this.mutateAvatar(() => this.remote.setAvatar(request));
|
|
1141
|
+
}
|
|
1142
|
+
async clearAvatar(request) {
|
|
1143
|
+
return this.mutateAvatar(() => this.remote.clearAvatar(request));
|
|
1144
|
+
}
|
|
1145
|
+
async mutateAvatar(operation) {
|
|
1146
|
+
if (this.view.profile?.avatarUploadEnabled !== true)
|
|
1147
|
+
return { ok: false, error: '当前账号或服务暂不支持修改头像' };
|
|
1148
|
+
const generation = this.generation;
|
|
1149
|
+
const result = await this.withPending('保存头像', () => call(operation), { publishFailure: false });
|
|
1150
|
+
if (!this.current(generation))
|
|
1151
|
+
return { ok: false, error: '账号已切换,请重新打开头像设置' };
|
|
1152
|
+
if (result.ok)
|
|
1153
|
+
this.applyProfile(result.value);
|
|
1154
|
+
else
|
|
1155
|
+
await this.refreshAvatarProfile();
|
|
1156
|
+
return result;
|
|
1157
|
+
}
|
|
1158
|
+
async avatarDisplayProfiles(peers) {
|
|
1159
|
+
const generation = this.generation;
|
|
1160
|
+
const result = await call(() => this.remote.getDisplayProfiles(peers.slice(0, 100), true));
|
|
1161
|
+
return this.current(generation) && result.ok ? result.value : [];
|
|
1162
|
+
}
|
|
1163
|
+
async avatarGroup(groupDid) {
|
|
1164
|
+
const generation = this.generation;
|
|
1165
|
+
const result = await call(() => this.remote.getGroup({ groupDid }));
|
|
1166
|
+
return this.current(generation) && result.ok ? result.value : null;
|
|
1167
|
+
}
|
|
1093
1168
|
/** Save all supported public profile fields and keep identity/profile projections aligned. */
|
|
1094
1169
|
async updateProfile(request) {
|
|
1095
1170
|
const displayName = request.displayName.trim();
|
|
@@ -1106,15 +1181,14 @@ export class AwikiController {
|
|
|
1106
1181
|
const result = await this.withPending('保存资料', () => call(() => this.remote.updateProfile({ displayName, bio, tags })));
|
|
1107
1182
|
if (!result.ok || !this.current(generation))
|
|
1108
1183
|
return result;
|
|
1109
|
-
|
|
1110
|
-
this.publish({ ...this.view, identity, profile: result.value, error: null });
|
|
1184
|
+
this.applyProfile(result.value, true);
|
|
1111
1185
|
return result;
|
|
1112
1186
|
}
|
|
1113
1187
|
/** Request a dedicated OTP only within the selected account flow. */
|
|
1114
1188
|
async sendRecoveryOtp(request) {
|
|
1115
1189
|
const generation = this.generation;
|
|
1116
1190
|
const revision = this.recoveryRevision;
|
|
1117
|
-
const tenantId = this.config?.tenantId;
|
|
1191
|
+
const tenantId = (this.config?.tenantStorageScopeId ?? this.config?.tenantId);
|
|
1118
1192
|
const flowScope = this.drafts.getScope('recovery:retryDeadline');
|
|
1119
1193
|
const result = await this.withPending('发送恢复验证码', () => call(() => this.remote.sendRecoveryOtp(request), registrationOtpFailureMessage), { publishFailure: false, recovery: true });
|
|
1120
1194
|
if (!result.ok) {
|
|
@@ -1125,7 +1199,7 @@ export class AwikiController {
|
|
|
1125
1199
|
}
|
|
1126
1200
|
this.recordRecoveryOtp(result.value, tenantId);
|
|
1127
1201
|
if (!this.recoveryCurrent(generation, revision)
|
|
1128
|
-
|| this.config?.tenantId !== tenantId
|
|
1202
|
+
|| (this.config?.tenantStorageScopeId ?? this.config?.tenantId) !== tenantId
|
|
1129
1203
|
|| this.drafts.getScope('recovery:retryDeadline') !== flowScope)
|
|
1130
1204
|
return result;
|
|
1131
1205
|
this.publish({ ...this.view, recoveryOperationId: result.value.operationId, recoveryProgress: null,
|
|
@@ -1138,7 +1212,7 @@ export class AwikiController {
|
|
|
1138
1212
|
storeRecoveryOtpRetryAt(result.operationId, recoveryOtpRetryAt(result), tenantId);
|
|
1139
1213
|
}
|
|
1140
1214
|
/** Publish results only into the exact selected flow; Core work survives navigation. */
|
|
1141
|
-
async runRecovery(label, operation
|
|
1215
|
+
async runRecovery(label, operation) {
|
|
1142
1216
|
const generation = this.generation;
|
|
1143
1217
|
const revision = this.recoveryRevision;
|
|
1144
1218
|
const operationId = this.view.recoveryOperationId;
|
|
@@ -1158,9 +1232,7 @@ export class AwikiController {
|
|
|
1158
1232
|
}
|
|
1159
1233
|
this.publish({ ...this.view, recoveryProgress: result.value, accessError: null });
|
|
1160
1234
|
if (result.value.phase === 'applied')
|
|
1161
|
-
clearRecoveryOtpRetryAt(operationId, this.config?.tenantId);
|
|
1162
|
-
if (enterWhenApplied && result.value.phase === 'applied')
|
|
1163
|
-
return this.enterRecoveredSession();
|
|
1235
|
+
clearRecoveryOtpRetryAt(operationId, (this.config?.tenantStorageScopeId ?? this.config?.tenantId));
|
|
1164
1236
|
return result;
|
|
1165
1237
|
}
|
|
1166
1238
|
async prepareRecovery(request) {
|
|
@@ -1172,7 +1244,7 @@ export class AwikiController {
|
|
|
1172
1244
|
const progress = this.view.recoveryProgress;
|
|
1173
1245
|
if (!progress?.allowedActions?.includes('activate'))
|
|
1174
1246
|
return { ok: false, error: '请先完成恢复信息验证' };
|
|
1175
|
-
return this.runRecovery('恢复身份', operationId => call(() => this.remote.activateRecovery({ operationId }), failure => recoveryContinuationFailureMessage(failure, progress.phase), recoveryCarrierFailureMessage)
|
|
1247
|
+
return this.runRecovery('恢复身份', operationId => call(() => this.remote.activateRecovery({ operationId }), failure => recoveryContinuationFailureMessage(failure, progress.phase), recoveryCarrierFailureMessage));
|
|
1176
1248
|
}
|
|
1177
1249
|
/** Read-only even for applied operations. Entering the identity is a separate action. */
|
|
1178
1250
|
async refreshRecoveryStatus() {
|
|
@@ -1193,14 +1265,14 @@ export class AwikiController {
|
|
|
1193
1265
|
}
|
|
1194
1266
|
this.publish({ ...this.view, recoveryProgress: result.value, accessError: null });
|
|
1195
1267
|
if (result.value.phase === 'applied')
|
|
1196
|
-
clearRecoveryOtpRetryAt(operationId, this.config?.tenantId);
|
|
1268
|
+
clearRecoveryOtpRetryAt(operationId, (this.config?.tenantStorageScopeId ?? this.config?.tenantId));
|
|
1197
1269
|
return result;
|
|
1198
1270
|
}
|
|
1199
1271
|
async resumeRecovery() {
|
|
1200
1272
|
const progress = this.view.recoveryProgress;
|
|
1201
1273
|
if (!progress?.allowedActions?.includes('resume'))
|
|
1202
1274
|
return { ok: false, error: '当前恢复状态不能重试' };
|
|
1203
|
-
return this.runRecovery('继续恢复身份', operationId => call(() => this.remote.resumeRecovery({ operationId }), failure => recoveryContinuationFailureMessage(failure, progress.phase), recoveryCarrierFailureMessage)
|
|
1275
|
+
return this.runRecovery('继续恢复身份', operationId => call(() => this.remote.resumeRecovery({ operationId }), failure => recoveryContinuationFailureMessage(failure, progress.phase), recoveryCarrierFailureMessage));
|
|
1204
1276
|
}
|
|
1205
1277
|
async enterRecoveredSession() {
|
|
1206
1278
|
const generation = this.generation;
|
|
@@ -1214,7 +1286,7 @@ export class AwikiController {
|
|
|
1214
1286
|
return this.recoveryLeft();
|
|
1215
1287
|
if (!result.ok)
|
|
1216
1288
|
return result;
|
|
1217
|
-
clearRecoveryOtpRetryAt(progress.operationId, this.config?.tenantId);
|
|
1289
|
+
clearRecoveryOtpRetryAt(progress.operationId, (this.config?.tenantStorageScopeId ?? this.config?.tenantId));
|
|
1218
1290
|
this.leaveRecovery();
|
|
1219
1291
|
const opened = await this.open();
|
|
1220
1292
|
return opened.ok ? result : opened;
|
|
@@ -1234,7 +1306,7 @@ export class AwikiController {
|
|
|
1234
1306
|
return this.recoveryLeft();
|
|
1235
1307
|
if (!result.ok)
|
|
1236
1308
|
return result;
|
|
1237
|
-
clearRecoveryOtpRetryAt(operationId, this.config?.tenantId);
|
|
1309
|
+
clearRecoveryOtpRetryAt(operationId, (this.config?.tenantStorageScopeId ?? this.config?.tenantId));
|
|
1238
1310
|
this.leaveRecovery();
|
|
1239
1311
|
void this.refreshIdentityAccess();
|
|
1240
1312
|
return { ok: true, value: undefined };
|
|
@@ -1434,7 +1506,7 @@ export class AwikiController {
|
|
|
1434
1506
|
* @param members - Handle or DID values entered by the user.
|
|
1435
1507
|
* @returns the created group and settled invitation outcomes.
|
|
1436
1508
|
*/
|
|
1437
|
-
async createGroup(name, members) {
|
|
1509
|
+
async createGroup(name, members, encrypted = false, draftOperationId) {
|
|
1438
1510
|
if (this.disposed)
|
|
1439
1511
|
return { ok: false, error: 'AWiki 插件已卸载' };
|
|
1440
1512
|
const normalizedName = name.trim();
|
|
@@ -1452,29 +1524,80 @@ export class AwikiController {
|
|
|
1452
1524
|
return this.fail('群成员列表不需要包含自己');
|
|
1453
1525
|
}
|
|
1454
1526
|
const generation = this.generation;
|
|
1527
|
+
const resumeKey = this.groupCreationStorageKey();
|
|
1528
|
+
const operationId = draftOperationId ?? `group-${crypto.randomUUID()}`;
|
|
1529
|
+
const operationKey = `${resumeKey}:${operationId}`;
|
|
1530
|
+
try {
|
|
1531
|
+
const storage = globalThis.localStorage;
|
|
1532
|
+
storage?.setItem(operationKey, JSON.stringify({ name: normalizedName, members: normalizedMembers, encrypted, operationId }));
|
|
1533
|
+
const keys = JSON.parse(storage?.getItem(`${resumeKey}:index`) ?? '[]');
|
|
1534
|
+
storage?.setItem(`${resumeKey}:index`, JSON.stringify([...new Set([...keys, operationKey])]));
|
|
1535
|
+
}
|
|
1536
|
+
catch {
|
|
1537
|
+
return this.fail('无法保存建群恢复标识,请检查本机存储后重试');
|
|
1538
|
+
}
|
|
1455
1539
|
const result = await this.withPending('创建群聊', () => call(() => this.remote.createGroup({
|
|
1456
1540
|
name: normalizedName,
|
|
1457
1541
|
members: normalizedMembers,
|
|
1542
|
+
encrypted,
|
|
1543
|
+
operationId,
|
|
1458
1544
|
}), groupCreateFailureMessage));
|
|
1459
1545
|
if (!result.ok || !this.current(generation))
|
|
1460
1546
|
return result;
|
|
1547
|
+
try {
|
|
1548
|
+
globalThis.localStorage?.removeItem(operationKey);
|
|
1549
|
+
}
|
|
1550
|
+
catch { /* Preserve the resume ID when cleanup is unavailable. */ }
|
|
1551
|
+
this.drafts.write(this.drafts.getScope('compose:groupFrozen'), 'compose:groupFrozen', false, false, true);
|
|
1461
1552
|
const conversation = this.cacheConversation(result.value.conversation);
|
|
1462
1553
|
this.publish({
|
|
1463
1554
|
...this.view,
|
|
1464
1555
|
conversations: appendUnique([conversation], this.view.conversations, value => value.id),
|
|
1465
1556
|
error: null,
|
|
1466
1557
|
});
|
|
1467
|
-
|
|
1558
|
+
// Navigation belongs to the live dialog instance; a closed form cannot redirect it.
|
|
1559
|
+
const selected = draftOperationId === undefined ? await this.selectConversation(result.value.conversation.id) : { ok: true };
|
|
1468
1560
|
if (!this.current(generation))
|
|
1469
1561
|
return result;
|
|
1470
1562
|
const failed = result.value.failedMembers.map(item => item.member);
|
|
1471
1563
|
const warning = failed.length === 0
|
|
1472
1564
|
? selected.ok ? null : '群聊已创建,但暂时无法打开消息历史。'
|
|
1473
|
-
:
|
|
1565
|
+
: `群聊已创建,以下成员的邀请或加密准备未完成,请在群成员中重试:${failed.join('、')}`;
|
|
1474
1566
|
if (warning !== null)
|
|
1475
1567
|
this.publish({ ...this.view, error: warning });
|
|
1476
1568
|
return result;
|
|
1477
1569
|
}
|
|
1570
|
+
groupCreationStorageKey() {
|
|
1571
|
+
return `awiki.group-create.v2:${this.drafts.getScope('compose:groupName')}`;
|
|
1572
|
+
}
|
|
1573
|
+
restoreGroupCreationDraft() {
|
|
1574
|
+
const scope = this.drafts.getScope('compose:groupName');
|
|
1575
|
+
this.drafts.write(scope, 'compose:groupName', '', '', true);
|
|
1576
|
+
this.drafts.write(scope, 'compose:groupMembers', '', '', true);
|
|
1577
|
+
this.drafts.write(scope, 'compose:encryptedGroup', false, false, true);
|
|
1578
|
+
this.drafts.write(scope, 'compose:groupFrozen', false, false, true);
|
|
1579
|
+
// Retain the operation independently of the new form, including legacy records.
|
|
1580
|
+
const generation = this.generation;
|
|
1581
|
+
const prefix = this.groupCreationStorageKey();
|
|
1582
|
+
try {
|
|
1583
|
+
const storage = globalThis.localStorage;
|
|
1584
|
+
if (storage === undefined)
|
|
1585
|
+
return;
|
|
1586
|
+
const keys = [prefix, ...JSON.parse(storage.getItem(`${prefix}:index`) ?? '[]')];
|
|
1587
|
+
for (const key of keys.slice(0, 8)) {
|
|
1588
|
+
const value = JSON.parse(storage.getItem(key) ?? 'null');
|
|
1589
|
+
if (value === null || typeof value.operationId !== 'string' || typeof value.name !== 'string' || !Array.isArray(value.members))
|
|
1590
|
+
continue;
|
|
1591
|
+
void call(() => this.remote.createGroup(value)).then(result => {
|
|
1592
|
+
if (!result.ok || !this.current(generation))
|
|
1593
|
+
return;
|
|
1594
|
+
storage.removeItem(key);
|
|
1595
|
+
void this.refreshConversations(generation);
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
catch { /* Keep unreadable legacy records; they must not freeze a new form. */ }
|
|
1600
|
+
}
|
|
1478
1601
|
/** Join one open group by its canonical DID, then select the refreshed conversation. */
|
|
1479
1602
|
async joinGroup(groupDidInput) {
|
|
1480
1603
|
const groupDid = groupDidInput.trim();
|
|
@@ -1566,9 +1689,9 @@ export class AwikiController {
|
|
|
1566
1689
|
groupDid: group.groupDid,
|
|
1567
1690
|
member,
|
|
1568
1691
|
}));
|
|
1692
|
+
const refreshed = await this.reloadSelectedGroupAfterMutation(group.groupDid);
|
|
1569
1693
|
if (!invited.ok)
|
|
1570
1694
|
return invited;
|
|
1571
|
-
const refreshed = await this.reloadSelectedGroupAfterMutation(group.groupDid);
|
|
1572
1695
|
if (!refreshed.ok) {
|
|
1573
1696
|
return {
|
|
1574
1697
|
ok: false,
|
|
@@ -1689,18 +1812,21 @@ export class AwikiController {
|
|
|
1689
1812
|
async loadGroupState(conversation, reset, preserveAvailableOnNetworkFailure = false) {
|
|
1690
1813
|
const generation = this.generation;
|
|
1691
1814
|
const selectionRevision = this.selectionRevision;
|
|
1815
|
+
const readRevision = ++this.groupReadRevision;
|
|
1816
|
+
const isCurrent = () => this.currentSelection(generation, selectionRevision, conversation.id)
|
|
1817
|
+
&& this.groupReadRevision === readRevision;
|
|
1692
1818
|
const snapshot = await callGroupRead(() => this.remote.getGroup({ groupDid: conversation.groupDid }));
|
|
1693
1819
|
if (!snapshot.ok) {
|
|
1694
1820
|
const retainAvailable = preserveAvailableOnNetworkFailure
|
|
1695
1821
|
&& snapshot.reason === 'network-error'
|
|
1696
1822
|
&& this.view.groupAccess?.groupDid === conversation.groupDid
|
|
1697
1823
|
&& this.view.groupAccess.status === 'available';
|
|
1698
|
-
if (!retainAvailable &&
|
|
1824
|
+
if (!retainAvailable && isCurrent()) {
|
|
1699
1825
|
this.publishGroupAccessFailure(conversation, snapshot.reason);
|
|
1700
1826
|
}
|
|
1701
1827
|
return snapshot;
|
|
1702
1828
|
}
|
|
1703
|
-
if (
|
|
1829
|
+
if (isCurrent()) {
|
|
1704
1830
|
const access = this.view.groupAccess;
|
|
1705
1831
|
this.publish({
|
|
1706
1832
|
...this.view,
|
|
@@ -1717,12 +1843,12 @@ export class AwikiController {
|
|
|
1717
1843
|
&& members.reason === 'network-error'
|
|
1718
1844
|
&& this.view.groupAccess?.groupDid === conversation.groupDid
|
|
1719
1845
|
&& this.view.groupAccess.status === 'available';
|
|
1720
|
-
if (!retainAvailable &&
|
|
1846
|
+
if (!retainAvailable && isCurrent()) {
|
|
1721
1847
|
this.publishGroupAccessFailure(conversation, members.reason);
|
|
1722
1848
|
}
|
|
1723
1849
|
return members;
|
|
1724
1850
|
}
|
|
1725
|
-
if (!
|
|
1851
|
+
if (!isCurrent())
|
|
1726
1852
|
return snapshot;
|
|
1727
1853
|
if (members.value.pageGroup !== undefined && members.value.pageGroup !== conversation.groupDid) {
|
|
1728
1854
|
return this.fail('群成员列表归属不一致,请刷新后重试');
|
|
@@ -1798,7 +1924,7 @@ export class AwikiController {
|
|
|
1798
1924
|
messages: mergeLatestMessages(this.view.messages, committed.value.items),
|
|
1799
1925
|
historyHasMore: remote.value.hasMore && remote.value.nextCursor !== undefined,
|
|
1800
1926
|
refreshing: false,
|
|
1801
|
-
error: existingError?.startsWith('
|
|
1927
|
+
error: existingError?.startsWith('群聊已创建,以下成员的邀请或加密准备未完成,请在群成员中重试:') ? existingError : null,
|
|
1802
1928
|
summaries: this.staleSummaries(conversationId, incoming),
|
|
1803
1929
|
});
|
|
1804
1930
|
}
|
|
@@ -1964,27 +2090,37 @@ export class AwikiController {
|
|
|
1964
2090
|
* @param clientMessageId - optional logical identity shared with the optimistic row.
|
|
1965
2091
|
* @returns successful delivery or one display-safe failure.
|
|
1966
2092
|
*/
|
|
1967
|
-
async sendText(text, clientMessageId, mentions) {
|
|
2093
|
+
async sendText(text, clientMessageId, mentions, security) {
|
|
1968
2094
|
const conversation = this.selectedConversation();
|
|
1969
2095
|
if (conversation === undefined)
|
|
1970
2096
|
return this.fail('请先选择会话');
|
|
2097
|
+
if (conversation.kind === 'direct' && this.view.messageSecurityReady !== true)
|
|
2098
|
+
return this.fail('消息模式尚未就绪,请刷新后重试');
|
|
2099
|
+
if (conversation.kind === 'group' && this.view.selectedGroup?.encrypted && !this.view.selectedGroup.canSendSecure)
|
|
2100
|
+
return this.fail('群加密尚未就绪,请等待群主完成准备或修复');
|
|
1971
2101
|
if (conversation.kind === 'group' && this.view.groupAccess?.status !== 'available') {
|
|
1972
2102
|
return this.fail('当前身份尚未获得这个群聊的发送权限,请先重新检查群成员状态。');
|
|
1973
2103
|
}
|
|
1974
2104
|
const conversationId = conversation.id;
|
|
1975
2105
|
const generation = this.generation;
|
|
1976
2106
|
const result = await this.withPending('发送消息', () => call(() => this.remote.sendText({
|
|
2107
|
+
security: security ?? 'plain',
|
|
1977
2108
|
target: targetOf(conversation),
|
|
1978
2109
|
text,
|
|
1979
2110
|
idempotencyKey: clientMessageId ?? crypto.randomUUID(),
|
|
1980
2111
|
...mentions === undefined || mentions.length === 0 ? {} : { mentions },
|
|
1981
2112
|
})));
|
|
1982
|
-
if (!result.ok)
|
|
2113
|
+
if (!result.ok) {
|
|
2114
|
+
if (conversation.kind === 'group' && this.current(generation) && this.view.selectedGroup?.groupDid === conversation.groupDid) {
|
|
2115
|
+
this.publish({ ...this.view, selectedGroup: { ...this.view.selectedGroup, canSendSecure: false } });
|
|
2116
|
+
await this.loadGroupState(conversation, true, true);
|
|
2117
|
+
}
|
|
1983
2118
|
return result;
|
|
2119
|
+
}
|
|
1984
2120
|
if (!this.current(generation) || this.view.selectedConversationId !== conversationId) {
|
|
1985
2121
|
return { ok: true, value: undefined };
|
|
1986
2122
|
}
|
|
1987
|
-
this.appendMessage(result.value);
|
|
2123
|
+
this.appendMessage(result.value, clientMessageId);
|
|
1988
2124
|
return { ok: true, value: undefined };
|
|
1989
2125
|
}
|
|
1990
2126
|
/**
|
|
@@ -1996,12 +2132,17 @@ export class AwikiController {
|
|
|
1996
2132
|
const conversation = this.selectedConversation();
|
|
1997
2133
|
if (conversation === undefined)
|
|
1998
2134
|
return this.fail('请先选择会话');
|
|
2135
|
+
if (conversation.kind === 'direct' && this.view.messageSecurityReady !== true)
|
|
2136
|
+
return this.fail('消息模式尚未就绪,请刷新后重试');
|
|
2137
|
+
if (conversation.kind === 'group' && this.view.selectedGroup?.encrypted && !this.view.selectedGroup.canSendSecure)
|
|
2138
|
+
return this.fail('群加密尚未就绪,请等待群主完成准备或修复');
|
|
1999
2139
|
if (conversation.kind === 'group' && this.view.groupAccess?.status !== 'available') {
|
|
2000
2140
|
return this.fail('当前身份尚未获得这个群聊的发送权限,请先重新检查群成员状态。');
|
|
2001
2141
|
}
|
|
2002
2142
|
const conversationId = conversation.id;
|
|
2003
2143
|
const generation = this.generation;
|
|
2004
2144
|
const request = {
|
|
2145
|
+
security: file.security ?? 'plain',
|
|
2005
2146
|
target: targetOf(conversation),
|
|
2006
2147
|
fileName: file.fileName,
|
|
2007
2148
|
mimeType: file.mimeType,
|
|
@@ -2010,12 +2151,17 @@ export class AwikiController {
|
|
|
2010
2151
|
idempotencyKey: file.clientMessageId ?? crypto.randomUUID(),
|
|
2011
2152
|
};
|
|
2012
2153
|
const result = await this.withPending('发送附件', () => call(() => this.remote.sendAttachment(request)));
|
|
2013
|
-
if (!result.ok)
|
|
2154
|
+
if (!result.ok) {
|
|
2155
|
+
if (conversation.kind === 'group' && this.current(generation) && this.view.selectedGroup?.groupDid === conversation.groupDid) {
|
|
2156
|
+
this.publish({ ...this.view, selectedGroup: { ...this.view.selectedGroup, canSendSecure: false } });
|
|
2157
|
+
await this.loadGroupState(conversation, true, true);
|
|
2158
|
+
}
|
|
2014
2159
|
return result;
|
|
2160
|
+
}
|
|
2015
2161
|
if (!this.current(generation) || this.view.selectedConversationId !== conversationId) {
|
|
2016
2162
|
return { ok: true, value: undefined };
|
|
2017
2163
|
}
|
|
2018
|
-
this.appendMessage(result.value);
|
|
2164
|
+
this.appendMessage(result.value, file.clientMessageId);
|
|
2019
2165
|
return { ok: true, value: undefined };
|
|
2020
2166
|
}
|
|
2021
2167
|
/**
|
|
@@ -2062,13 +2208,14 @@ export class AwikiController {
|
|
|
2062
2208
|
return { ok: false, error: 'AWiki 插件已卸载' };
|
|
2063
2209
|
const generation = this.generation;
|
|
2064
2210
|
const ownerDid = this.presentationCacheOwnerDid;
|
|
2065
|
-
const tenantId = this.config?.tenantId;
|
|
2211
|
+
const tenantId = (this.config?.tenantStorageScopeId ?? this.config?.tenantId);
|
|
2066
2212
|
const result = await call(() => this.remote.clearLocalData(request));
|
|
2067
2213
|
if (!result.ok)
|
|
2068
2214
|
return result;
|
|
2069
2215
|
const owners = [...(result.value.clearedIdentityDids ?? []), ...(ownerDid === null ? [] : [ownerDid])];
|
|
2070
2216
|
for (const owner of new Set(owners)) {
|
|
2071
2217
|
await this.persistentImageCache.clear(owner).catch(() => undefined);
|
|
2218
|
+
await avatarCache.clear(owner);
|
|
2072
2219
|
try {
|
|
2073
2220
|
clearMailBrowserCache(globalThis.localStorage, owner);
|
|
2074
2221
|
}
|
|
@@ -2148,14 +2295,49 @@ export class AwikiController {
|
|
|
2148
2295
|
this.close();
|
|
2149
2296
|
this.listeners.clear();
|
|
2150
2297
|
}
|
|
2298
|
+
async retryMessageSecurity() {
|
|
2299
|
+
await this.loadConversationPreferences(this.generation);
|
|
2300
|
+
}
|
|
2151
2301
|
async loadConversationPreferences(generation) {
|
|
2302
|
+
this.publish({ ...this.view, messageSecurityReady: false, messageSecurityError: false });
|
|
2152
2303
|
const result = await call(() => this.remote.getConversationPreferences(), conversationPreferenceFailureMessage);
|
|
2153
|
-
if (!
|
|
2304
|
+
if (!this.current(generation))
|
|
2305
|
+
return;
|
|
2306
|
+
if (!result.ok) {
|
|
2307
|
+
this.publish({ ...this.view, messageSecurityError: true });
|
|
2308
|
+
this.fail(result.error);
|
|
2154
2309
|
return;
|
|
2310
|
+
}
|
|
2155
2311
|
this.applyConversationPreferences(result.value);
|
|
2156
2312
|
this.publish({ ...this.view, hiddenConversations: this.hiddenConversationsView() });
|
|
2157
2313
|
}
|
|
2314
|
+
async setMessageSecurity(conversationId, encrypted) {
|
|
2315
|
+
if (this.view.messageSecurityReady !== true)
|
|
2316
|
+
return this.fail('消息模式尚未就绪,请重试读取');
|
|
2317
|
+
const generation = this.generation;
|
|
2318
|
+
const previous = this.view.encryptedConversationIds ?? [];
|
|
2319
|
+
this.publish({ ...this.view, encryptedConversationIds: encrypted ? [...new Set([...previous, conversationId])] : previous.filter(id => id !== conversationId) });
|
|
2320
|
+
const result = await this.withPending('保存消息模式', () => call(() => this.remote.updateConversationPreference({ action: 'security', conversationId, encrypted })));
|
|
2321
|
+
if (!result.ok) {
|
|
2322
|
+
if (this.current(generation))
|
|
2323
|
+
this.publish({ ...this.view, encryptedConversationIds: previous });
|
|
2324
|
+
return result;
|
|
2325
|
+
}
|
|
2326
|
+
if (this.current(generation))
|
|
2327
|
+
this.applyConversationPreferences(result.value);
|
|
2328
|
+
return { ok: true, value: undefined };
|
|
2329
|
+
}
|
|
2330
|
+
async repairSelectedGroupSecurity() {
|
|
2331
|
+
const group = this.view.selectedGroup;
|
|
2332
|
+
if (group === null)
|
|
2333
|
+
return this.fail('请先选择群聊');
|
|
2334
|
+
const result = await this.withPending('修复群加密', () => call(() => this.remote.repairGroupSecurity({ groupDid: group.groupDid })));
|
|
2335
|
+
if (!result.ok)
|
|
2336
|
+
return result;
|
|
2337
|
+
return this.reloadSelectedGroupAfterMutation(group.groupDid);
|
|
2338
|
+
}
|
|
2158
2339
|
applyConversationPreferences(preferences) {
|
|
2340
|
+
this.publish({ ...this.view, messageSecurityReady: true, messageSecurityError: false, encryptedConversationIds: [...(preferences.encryptedConversationIds ?? [])] });
|
|
2159
2341
|
this.hiddenConversationPreferences.clear();
|
|
2160
2342
|
for (const hidden of preferences.hiddenConversations) {
|
|
2161
2343
|
this.hiddenConversationPreferences.set(hidden.conversation.id, {
|
|
@@ -2256,6 +2438,9 @@ export class AwikiController {
|
|
|
2256
2438
|
sessionStatus: status,
|
|
2257
2439
|
conversations: Object.freeze([]),
|
|
2258
2440
|
hiddenConversations: Object.freeze([]),
|
|
2441
|
+
messageSecurityReady: false,
|
|
2442
|
+
messageSecurityError: false,
|
|
2443
|
+
encryptedConversationIds: Object.freeze([]),
|
|
2259
2444
|
conversationsHasMore: false,
|
|
2260
2445
|
selectedConversationId: null,
|
|
2261
2446
|
selectedGroup: null,
|
|
@@ -2313,14 +2498,30 @@ export class AwikiController {
|
|
|
2313
2498
|
if (this.polling || !this.current(generation) || this.view.identity === null)
|
|
2314
2499
|
return;
|
|
2315
2500
|
this.polling = true;
|
|
2501
|
+
if (globalThis.document?.visibilityState !== 'hidden' && Date.now() - this.lastAvatarProfileCheck >= 60_000)
|
|
2502
|
+
void this.refreshAvatarProfile();
|
|
2316
2503
|
try {
|
|
2317
2504
|
await this.refreshConversations(generation, true);
|
|
2318
2505
|
const selected = this.view.selectedConversationId;
|
|
2319
2506
|
if (selected === null || !this.current(generation))
|
|
2320
2507
|
return;
|
|
2321
2508
|
const selectedConversation = this.selectedConversation();
|
|
2322
|
-
if (selectedConversation?.kind === 'group' && this.view.groupAccess?.status !== 'available')
|
|
2323
|
-
|
|
2509
|
+
if (selectedConversation?.kind === 'group' && this.view.groupAccess?.status !== 'available') {
|
|
2510
|
+
const access = this.view.groupAccess?.status;
|
|
2511
|
+
if (access === 'blocked' || access === 'not-member')
|
|
2512
|
+
return;
|
|
2513
|
+
await this.loadGroupState(selectedConversation, false, true);
|
|
2514
|
+
if (!this.current(generation) || this.view.selectedConversationId !== selected
|
|
2515
|
+
|| this.getSnapshot().groupAccess?.status !== 'available')
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
if (selectedConversation?.kind === 'group' && this.view.selectedGroup?.encrypted) {
|
|
2519
|
+
const status = await callGroupRead(() => this.remote.getGroup({ groupDid: selectedConversation.groupDid }));
|
|
2520
|
+
if (!this.current(generation) || this.view.selectedConversationId !== selected)
|
|
2521
|
+
return;
|
|
2522
|
+
if (status.ok)
|
|
2523
|
+
this.publish({ ...this.view, selectedGroup: status.value });
|
|
2524
|
+
}
|
|
2324
2525
|
const result = await call(() => this.remote.getHistory({ conversationId: selected }));
|
|
2325
2526
|
if (!this.current(generation) || !result.ok || this.view.selectedConversationId !== selected)
|
|
2326
2527
|
return;
|
|
@@ -2402,9 +2603,14 @@ export class AwikiController {
|
|
|
2402
2603
|
}
|
|
2403
2604
|
}
|
|
2404
2605
|
}
|
|
2405
|
-
appendMessage(message) {
|
|
2606
|
+
appendMessage(message, logicalId) {
|
|
2406
2607
|
if (this.view.selectedConversationId !== message.conversationId)
|
|
2407
2608
|
return;
|
|
2609
|
+
// Core resolves a queued logical ID to the accepted group event ID. Retire
|
|
2610
|
+
// only that explicit alias, including history reads started before acceptance.
|
|
2611
|
+
if (logicalId !== undefined && logicalId !== message.id) {
|
|
2612
|
+
this.retiredMessageIds.add(`${message.conversationId}/${logicalId}`);
|
|
2613
|
+
}
|
|
2408
2614
|
const isNew = !this.view.messages.some(current => current.id === message.id);
|
|
2409
2615
|
const messages = appendMessageById(this.view.messages, message);
|
|
2410
2616
|
if ((this.unreadAtOpen.get(message.conversationId) ?? 0) > 0 && messages.length > this.view.messages.length) {
|
|
@@ -2463,6 +2669,7 @@ export class AwikiController {
|
|
|
2463
2669
|
const ownerDid = identity?.did ?? null;
|
|
2464
2670
|
if (ownerDid === this.presentationCacheOwnerDid)
|
|
2465
2671
|
return;
|
|
2672
|
+
this.retiredMessageIds.clear();
|
|
2466
2673
|
this.directProfiles.clear();
|
|
2467
2674
|
this.groupTitles.clear();
|
|
2468
2675
|
this.hiddenConversationPreferences.clear();
|
|
@@ -2471,6 +2678,7 @@ export class AwikiController {
|
|
|
2471
2678
|
}
|
|
2472
2679
|
/** Drop every browser projection without touching the Core-owned SQLite cache. */
|
|
2473
2680
|
clearPresentationCache() {
|
|
2681
|
+
this.retiredMessageIds.clear();
|
|
2474
2682
|
this.directProfiles.clear();
|
|
2475
2683
|
this.groupTitles.clear();
|
|
2476
2684
|
this.hiddenConversationPreferences.clear();
|
|
@@ -2574,7 +2782,10 @@ export class AwikiController {
|
|
|
2574
2782
|
/* v8 ignore next -- every asynchronous and public mutation path checks disposal before publishing. */
|
|
2575
2783
|
if (this.disposed)
|
|
2576
2784
|
return;
|
|
2577
|
-
|
|
2785
|
+
const hasRetired = view.messages.some(message => this.retiredMessageIds.has(`${message.conversationId}/${message.id}`));
|
|
2786
|
+
this.view = Object.freeze(hasRetired
|
|
2787
|
+
? { ...view, messages: view.messages.filter(message => !this.retiredMessageIds.has(`${message.conversationId}/${message.id}`)) }
|
|
2788
|
+
: view);
|
|
2578
2789
|
for (const listener of [...this.listeners])
|
|
2579
2790
|
listener();
|
|
2580
2791
|
}
|