feat: Implement get label changes

This commit is contained in:
dab246
2025-12-02 09:45:29 +07:00
committed by Dat H. Pham
parent 73134d2833
commit 987ea410c5
6 changed files with 314 additions and 23 deletions
@@ -0,0 +1,103 @@
import 'dart:math' hide log;
import 'package:core/utils/app_logger.dart';
import 'package:jmap_dart_client/http/http_client.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:jmap_dart_client/jmap/core/state.dart';
import 'package:jmap_dart_client/jmap/jmap_request.dart';
import 'package:labels/method/get/get_label_method.dart';
import 'package:labels/method/get/get_label_response.dart';
import 'package:labels/model/label.dart';
import 'package:tmail_ui_user/features/base/mixin/handle_error_mixin.dart';
import 'package:tmail_ui_user/features/base/mixin/session_mixin.dart';
mixin BatchGetLabelProcessingMixin on HandleSetErrorMixin, SessionMixin {
Future<
({
List<Label> labels,
List<Id> notFoundIds,
State? state,
})> executeBatchGetLabel({
required Session session,
required AccountId accountId,
required List<Id> labelIds,
required HttpClient httpClient,
String debugLabel = 'executeBatchGetLabel',
}) async {
if (labelIds.isEmpty) {
return (labels: <Label>[], notFoundIds: <Id>[], state: null);
}
final maxObjects = getMaxObjectsInGetMethod(session, accountId);
final totalEmails = labelIds.length;
final batchSize = max(1, min(totalEmails, maxObjects));
final List<Label> allLabels = [];
final List<Id> allNotFoundIds = [];
State? latestState;
for (int start = 0; start < totalEmails; start += batchSize) {
int end =
(start + batchSize < totalEmails) ? start + batchSize : totalEmails;
final currentListIds = labelIds.sublist(start, end);
log('BatchGetLabelProcessingMixin::$debugLabel: processing batch ${start + 1} to $end');
try {
final response = await _executeGetLabelBatch(
accountId: accountId,
httpClient: httpClient,
labelIds: currentListIds,
);
if (response == null) {
throw Exception('GetLabelResponse is null');
}
if (response.list.isNotEmpty) {
allLabels.addAll(response.list);
}
latestState = response.state;
final notFoundIds = response.notFound ?? [];
if (notFoundIds.isNotEmpty) {
allNotFoundIds.addAll(notFoundIds);
}
} catch (e) {
logWarning(
'BatchGetLabelProcessingMixin::$debugLabel: Error processing batch ${start + 1}-$end: $e',
);
}
}
return (labels: allLabels, notFoundIds: allNotFoundIds, state: latestState);
}
Future<GetLabelResponse?> _executeGetLabelBatch({
required AccountId accountId,
required HttpClient httpClient,
required List<Id> labelIds,
}) async {
final getLabelMethod = GetLabelMethod(accountId)..addIds(labelIds.toSet());
final requestBuilder = JmapRequestBuilder(
httpClient,
ProcessingInvocation(),
);
final invocation = requestBuilder.invocation(getLabelMethod);
final response = await (requestBuilder
..usings(getLabelMethod.requiredCapabilities))
.build()
.execute();
return response.parse<GetLabelResponse>(
invocation.methodCallId,
GetLabelResponse.deserialize,
);
}
}
+2 -21
View File
@@ -7,8 +7,6 @@ import 'package:core/utils/app_logger.dart';
import 'package:dartz/dartz.dart';
import 'package:jmap_dart_client/http/http_client.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart';
import 'package:jmap_dart_client/jmap/core/capability/core_capability.dart';
import 'package:jmap_dart_client/jmap/core/error/set_error.dart';
import 'package:jmap_dart_client/jmap/core/filter/filter.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
@@ -33,8 +31,8 @@ import 'package:model/email/email_property.dart';
import 'package:model/extensions/list_email_extension.dart';
import 'package:model/extensions/list_email_id_extension.dart';
import 'package:model/extensions/list_id_extension.dart';
import 'package:model/extensions/session_extension.dart';
import 'package:tmail_ui_user/features/base/mixin/handle_error_mixin.dart';
import 'package:tmail_ui_user/features/base/mixin/session_mixin.dart';
import 'package:tmail_ui_user/features/email/domain/model/move_action.dart';
import 'package:tmail_ui_user/features/mailbox/domain/exceptions/mailbox_exception.dart';
import 'package:tmail_ui_user/features/mailbox/domain/state/move_folder_content_state.dart';
@@ -43,24 +41,7 @@ import 'package:tmail_ui_user/features/thread/data/extensions/list_email_id_exte
import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart';
import 'package:tmail_ui_user/main/error/capability_validator.dart';
mixin MailAPIMixin on HandleSetErrorMixin {
int getMaxObjectsInSetMethod(Session session, AccountId accountId) {
final coreCapability = session.getCapabilityProperties<CoreCapability>(
accountId,
CapabilityIdentifier.jmapCore,
);
final maxObjectsInSetMethod =
coreCapability?.maxObjectsInSet?.value.toInt() ??
CapabilityIdentifierExtension.defaultMaxObjectsInSet;
final minOfMaxObjectsInSetMethod = min(
maxObjectsInSetMethod,
CapabilityIdentifierExtension.defaultMaxObjectsInSet,
);
log('$runtimeType::_getMaxObjectsInSetMethod:minOfMaxObjectsInSetMethod = $minOfMaxObjectsInSetMethod');
return minOfMaxObjectsInSetMethod;
}
mixin MailAPIMixin on HandleSetErrorMixin, SessionMixin {
Future<({List<EmailId> emailIdsSuccess, Map<Id, SetError> mapErrors})>
moveEmailsBetweenMailboxes({
required HttpClient httpClient,
@@ -0,0 +1,41 @@
import 'dart:math' hide log;
import 'package:core/utils/app_logger.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart';
import 'package:jmap_dart_client/jmap/core/capability/core_capability.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:model/extensions/session_extension.dart';
import 'package:tmail_ui_user/main/error/capability_validator.dart';
mixin SessionMixin {
int getMaxObjectsInSetMethod(Session session, AccountId accountId) {
final coreCapability = session.getCapabilityProperties<CoreCapability>(
accountId,
CapabilityIdentifier.jmapCore,
);
final maxObjectsInSetMethod =
coreCapability?.maxObjectsInSet?.value.toInt() ??
CapabilityIdentifierExtension.defaultMaxObjectsInSet;
final minOfMaxObjectsInSetMethod = min(
maxObjectsInSetMethod,
CapabilityIdentifierExtension.defaultMaxObjectsInSet,
);
log('$runtimeType::_getMaxObjectsInSetMethod:minOfMaxObjectsInSetMethod = $minOfMaxObjectsInSetMethod');
return minOfMaxObjectsInSetMethod;
}
int getMaxObjectsInGetMethod(Session session, AccountId accountId) {
final maxObjectsInGetMethod =
session.getMaxObjectsInGet(accountId)?.value.toInt() ??
CapabilityIdentifierExtension.defaultMaxObjectsInGet;
final minOfMaxObjectsInGetMethod = min(
maxObjectsInGetMethod,
CapabilityIdentifierExtension.defaultMaxObjectsInGet,
);
log('$runtimeType::getMaxObjectsInGetMethod:minOfMaxObjectsInGetMethod = $minOfMaxObjectsInGetMethod');
return minOfMaxObjectsInGetMethod;
}
}
@@ -0,0 +1,32 @@
import 'package:equatable/equatable.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/core/state.dart';
import 'package:labels/model/label.dart';
class LabelChangeResponse with EquatableMixin {
final List<Label>? updated;
final List<Label>? created;
final List<Id>? destroyed;
final State? newStateLabel;
final State? newStateChanges;
final bool hasMoreChanges;
LabelChangeResponse({
this.updated,
this.created,
this.destroyed,
this.newStateLabel,
this.newStateChanges,
this.hasMoreChanges = false,
});
@override
List<Object?> get props => [
updated,
created,
destroyed,
newStateLabel,
newStateChanges,
hasMoreChanges,
];
}
+135 -2
View File
@@ -1,15 +1,24 @@
import 'package:core/utils/app_logger.dart';
import 'package:jmap_dart_client/http/http_client.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/core/patch_object.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:jmap_dart_client/jmap/core/state.dart';
import 'package:jmap_dart_client/jmap/core/unsigned_int.dart';
import 'package:jmap_dart_client/jmap/jmap_request.dart';
import 'package:labels/labels.dart';
import 'package:tmail_ui_user/features/base/mixin/batch_get_label_processing_mixin.dart';
import 'package:tmail_ui_user/features/base/mixin/handle_error_mixin.dart';
import 'package:tmail_ui_user/features/labels/domain/model/edit_label_request.dart';
import 'package:tmail_ui_user/features/base/mixin/session_mixin.dart';
import 'package:tmail_ui_user/features/labels/data/model/label_change_response.dart';
import 'package:tmail_ui_user/features/labels/domain/exceptions/label_exceptions.dart';
import 'package:tmail_ui_user/features/labels/domain/model/edit_label_request.dart';
import 'package:uuid/uuid.dart';
class LabelApi with HandleSetErrorMixin {
class LabelApi
with HandleSetErrorMixin, SessionMixin, BatchGetLabelProcessingMixin {
final HttpClient _httpClient;
final Uuid _uuid;
@@ -122,4 +131,128 @@ class LabelApi with HandleSetErrorMixin {
throw parseErrorForSetResponse(response, labelId);
}
}
Future<
({
List<Label>? labels,
State? state,
List<Id> notFoundIds,
})> _fetchLabelsIfNeeded({
required Session session,
required AccountId accountId,
required List<Id> labelIds,
required State sinceState,
required String logPrefix,
}) async {
if (labelIds.isEmpty) {
return (labels: null, state: null, notFoundIds: <Id>[]);
}
final result = await executeBatchGetLabel(
session: session,
accountId: accountId,
httpClient: _httpClient,
labelIds: labelIds,
);
if (result.notFoundIds.isNotEmpty) {
log('LabelAPI::getChanges:notFoundIds$logPrefix = ${result.notFoundIds} | SinceState = ${sinceState.value}');
}
return (
labels: result.labels,
state: result.state,
notFoundIds: result.notFoundIds,
);
}
Future<ChangesLabelResponse> getLabelChanges(
AccountId accountId,
State sinceState,
) async {
final processingInvocation = ProcessingInvocation();
final jmapRequestBuilder = JmapRequestBuilder(
_httpClient,
processingInvocation,
);
final changesLabelMethod = ChangesLabelMethod(
accountId,
sinceState,
maxChanges: UnsignedInt(128),
);
final changesLabelInvocation =
jmapRequestBuilder.invocation(changesLabelMethod);
final result = await (jmapRequestBuilder
..usings(changesLabelMethod.requiredCapabilities))
.build()
.execute();
final resultChanges = result.parse<ChangesLabelResponse>(
changesLabelInvocation.methodCallId,
ChangesLabelResponse.deserialize,
);
if (resultChanges == null) {
throw Exception('Failed to parse ChangesLabelResponse');
}
return resultChanges;
}
Future<LabelChangeResponse> getLabelChangesWithResult(
Session session,
AccountId accountId,
State sinceState,
) async {
final changesResult = await getLabelChanges(accountId, sinceState);
final List<Id> createdIds = changesResult.created.toList();
final List<Id> updatedIds = changesResult.updated.toList();
List<Id> destroyedLabelIds = changesResult.destroyed.toList();
final State newStateChanges = changesResult.newState;
final bool hasMoreChanges = changesResult.hasMoreChanges;
log('LabelAPI::getChanges:createdIds = ${createdIds.length} | updatedIds = ${updatedIds.length} | destroyedIds = ${destroyedLabelIds.length}');
State? newStateLabel;
final updatedResult = await _fetchLabelsIfNeeded(
session: session,
accountId: accountId,
labelIds: updatedIds,
sinceState: sinceState,
logPrefix: 'Updated',
);
if (updatedResult.notFoundIds.isNotEmpty) {
destroyedLabelIds.addAll(updatedResult.notFoundIds);
}
newStateLabel = updatedResult.state ?? newStateLabel;
final createdResult = await _fetchLabelsIfNeeded(
session: session,
accountId: accountId,
labelIds: createdIds,
sinceState: sinceState,
logPrefix: 'Created',
);
if (createdResult.notFoundIds.isNotEmpty) {
destroyedLabelIds.addAll(createdResult.notFoundIds);
}
newStateLabel = createdResult.state ?? newStateLabel;
log('LabelAPI::getChanges:newStateChanges = $newStateChanges | newStateLabel = $newStateLabel | hasMoreChanges = $hasMoreChanges');
log('LabelAPI::getChanges:updatedLabelSize = ${updatedResult.labels?.length} | createdLabelSize = ${createdResult.labels?.length}');
log('LabelAPI::getChanges:destroyedLabelIds = $destroyedLabelIds');
return LabelChangeResponse(
updated: updatedResult.labels,
created: createdResult.labels,
destroyed: destroyedLabelIds,
newStateLabel: newStateLabel,
newStateChanges: newStateChanges,
hasMoreChanges: hasMoreChanges,
);
}
}
+1
View File
@@ -43,6 +43,7 @@ extension CapabilityIdentifierExtension on CapabilityIdentifier {
static const int defaultMaxCallsInRequest = 1;
static const int defaultMaxObjectsInSet = 50;
static const int defaultMaxObjectsInGet = 50;
bool isSupported(Session session, AccountId accountId) {
try {