feat: Implement get label changes
This commit is contained in:
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user