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:dartz/dartz.dart';
|
||||||
import 'package:jmap_dart_client/http/http_client.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/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/error/set_error.dart';
|
||||||
import 'package:jmap_dart_client/jmap/core/filter/filter.dart';
|
import 'package:jmap_dart_client/jmap/core/filter/filter.dart';
|
||||||
import 'package:jmap_dart_client/jmap/core/id.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_extension.dart';
|
||||||
import 'package:model/extensions/list_email_id_extension.dart';
|
import 'package:model/extensions/list_email_id_extension.dart';
|
||||||
import 'package:model/extensions/list_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/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/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/exceptions/mailbox_exception.dart';
|
||||||
import 'package:tmail_ui_user/features/mailbox/domain/state/move_folder_content_state.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/features/thread/domain/model/email_response.dart';
|
||||||
import 'package:tmail_ui_user/main/error/capability_validator.dart';
|
import 'package:tmail_ui_user/main/error/capability_validator.dart';
|
||||||
|
|
||||||
mixin MailAPIMixin on HandleSetErrorMixin {
|
mixin MailAPIMixin on HandleSetErrorMixin, 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<({List<EmailId> emailIdsSuccess, Map<Id, SetError> mapErrors})>
|
Future<({List<EmailId> emailIdsSuccess, Map<Id, SetError> mapErrors})>
|
||||||
moveEmailsBetweenMailboxes({
|
moveEmailsBetweenMailboxes({
|
||||||
required HttpClient httpClient,
|
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/http/http_client.dart';
|
||||||
import 'package:jmap_dart_client/jmap/account_id.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/id.dart';
|
||||||
import 'package:jmap_dart_client/jmap/core/patch_object.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:jmap_dart_client/jmap/jmap_request.dart';
|
||||||
import 'package:labels/labels.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/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/exceptions/label_exceptions.dart';
|
||||||
|
import 'package:tmail_ui_user/features/labels/domain/model/edit_label_request.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class LabelApi with HandleSetErrorMixin {
|
class LabelApi
|
||||||
|
with HandleSetErrorMixin, SessionMixin, BatchGetLabelProcessingMixin {
|
||||||
|
|
||||||
final HttpClient _httpClient;
|
final HttpClient _httpClient;
|
||||||
final Uuid _uuid;
|
final Uuid _uuid;
|
||||||
|
|
||||||
@@ -122,4 +131,128 @@ class LabelApi with HandleSetErrorMixin {
|
|||||||
throw parseErrorForSetResponse(response, labelId);
|
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 defaultMaxCallsInRequest = 1;
|
||||||
static const int defaultMaxObjectsInSet = 50;
|
static const int defaultMaxObjectsInSet = 50;
|
||||||
|
static const int defaultMaxObjectsInGet = 50;
|
||||||
|
|
||||||
bool isSupported(Session session, AccountId accountId) {
|
bool isSupported(Session session, AccountId accountId) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user