TF-3698 Thread Detail Real time update

TF-3698 Thread Detail update reset RefreshThreadDetailAction position

TF-3698 Thread Detail refactor initializeThreadDetailEmails

TF-3698 Thread Detail refactor handleRefreshThreadDetailAction

TF-3698 Thread Detail create listThreadIds in ListEmailExtension
This commit is contained in:
DatDang
2025-05-27 11:03:28 +07:00
committed by Dat H. Pham
parent 1bc45e35f3
commit d210a892f2
28 changed files with 647 additions and 83 deletions
@@ -3,6 +3,7 @@ import 'package:jmap_dart_client/jmap/core/state.dart' as jmap;
import 'package:model/email/email_action_type.dart';
import 'package:model/email/presentation_email.dart';
import 'package:tmail_ui_user/features/base/action/ui_action.dart';
import 'package:tmail_ui_user/features/thread/data/model/email_change_response.dart';
class EmailUIAction extends UIAction {
static final idle = EmailUIAction();
@@ -48,4 +49,13 @@ class PerformEmailActionInThreadDetailAction extends EmailUIAction {
emailActionType,
presentationEmail,
];
}
class RefreshThreadDetailAction extends EmailUIAction {
RefreshThreadDetailAction(this.emailChangeResponse);
final EmailChangeResponse emailChangeResponse;
@override
List<Object?> get props => [emailChangeResponse];
}
@@ -437,9 +437,7 @@ class EmailView extends GetWidget<SingleEmailController> {
),
child: LayoutBuilder(builder: (context, constraints) {
return HtmlContentViewer(
key: PlatformInfo.isIntegrationTesting
? controller.htmlContentViewKey
: null,
key: controller.htmlContentViewKey,
contentHtml: allEmailContents,
initialWidth: constraints.maxWidth,
direction: AppUtils.getCurrentDirection(context),
@@ -478,9 +476,7 @@ class EmailView extends GetWidget<SingleEmailController> {
),
child: LayoutBuilder(builder: (context, constraints) {
return HtmlContentViewer(
key: PlatformInfo.isIntegrationTesting
? controller.htmlContentViewKey
: null,
key: controller.htmlContentViewKey,
contentHtml: allEmailContents,
initialWidth: constraints.maxWidth,
direction: AppUtils.getCurrentDirection(context),
@@ -273,7 +273,7 @@ class ThreadRepositoryImpl extends ThreadRepository {
}
) async* {
log('ThreadRepositoryImpl::refreshChanges(): $currentState');
await _synchronizeCacheWithChanges(
final emailChangeResponse = await _synchronizeCacheWithChanges(
session,
accountId,
currentState,
@@ -305,9 +305,9 @@ class ThreadRepositoryImpl extends ThreadRepository {
propertiesCreated: propertiesCreated,
);
yield networkEmailResponse;
yield networkEmailResponse.copyWith(emailChangeResponse: emailChangeResponse);
} else {
yield newEmailResponse;
yield newEmailResponse.copyWith(emailChangeResponse: emailChangeResponse);
}
}
@@ -396,7 +396,7 @@ class ThreadRepositoryImpl extends ThreadRepository {
return listEmailIdDeleted;
}
Future<void> _synchronizeCacheWithChanges(
Future<EmailChangeResponse?> _synchronizeCacheWithChanges(
Session session,
AccountId accountId,
jmap.State currentState,
@@ -452,6 +452,8 @@ class ThreadRepositoryImpl extends ThreadRepository {
await _updateState(accountId, session.username, emailChangeResponse.newStateEmail!);
}
}
return emailChangeResponse;
}
@override
@@ -2,16 +2,19 @@
import 'package:equatable/equatable.dart';
import 'package:jmap_dart_client/jmap/core/state.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:tmail_ui_user/features/thread/data/model/email_change_response.dart';
class EmailsResponse with EquatableMixin {
final List<Email>? emailList;
final List<EmailId>? notFoundEmailIds;
final State? state;
final EmailChangeResponse? emailChangeResponse;
const EmailsResponse({
this.emailList,
this.notFoundEmailIds,
this.state
this.state,
this.emailChangeResponse,
});
bool hasEmails() => emailList != null && emailList!.isNotEmpty;
@@ -21,5 +24,24 @@ class EmailsResponse with EquatableMixin {
bool get existNotFoundEmails => notFoundEmailIds?.isNotEmpty == true;
@override
List<Object?> get props => [emailList, notFoundEmailIds, state];
List<Object?> get props => [
emailList,
notFoundEmailIds,
state,
emailChangeResponse,
];
EmailsResponse copyWith({
List<Email>? emailList,
List<EmailId>? notFoundEmailIds,
State? state,
EmailChangeResponse? emailChangeResponse,
}) {
return EmailsResponse(
emailList: emailList ?? this.emailList,
notFoundEmailIds: notFoundEmailIds ?? this.notFoundEmailIds,
state: state ?? this.state,
emailChangeResponse: emailChangeResponse ?? this.emailChangeResponse,
);
}
}
@@ -3,6 +3,7 @@ import 'package:core/presentation/state/success.dart';
import 'package:jmap_dart_client/jmap/core/state.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:model/email/presentation_email.dart';
import 'package:tmail_ui_user/features/thread/data/model/email_change_response.dart';
class RefreshChangesAllEmailLoading extends LoadingState {}
@@ -10,18 +11,21 @@ class RefreshChangesAllEmailSuccess extends UIState {
final List<PresentationEmail> emailList;
final State? currentEmailState;
final MailboxId? currentMailboxId;
final EmailChangeResponse? emailChangeResponse;
RefreshChangesAllEmailSuccess({
required this.emailList,
this.currentEmailState,
this.currentMailboxId
this.currentMailboxId,
this.emailChangeResponse,
});
@override
List<Object?> get props => [
emailList,
currentEmailState,
currentMailboxId
currentMailboxId,
emailChangeResponse,
];
}
@@ -59,6 +59,8 @@ class RefreshChangesEmailsInMailboxInteractor {
return Right<Failure, Success>(RefreshChangesAllEmailSuccess(
emailList: presentationEmailList,
currentEmailState: emailResponse.state,
currentMailboxId: currentMailboxId));
currentMailboxId: currentMailboxId,
emailChangeResponse: emailResponse.emailChangeResponse,
));
}
}
@@ -0,0 +1,12 @@
import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart';
import 'package:tmail_ui_user/features/thread/data/model/email_change_response.dart';
import 'package:tmail_ui_user/features/thread/presentation/thread_controller.dart';
extension RefreshThreadDetailExtension on ThreadController {
void refreshThreadDetail(EmailChangeResponse? emailChangeResponse) {
if (emailChangeResponse == null) return;
mailboxDashBoardController
.dispatchEmailUIAction(RefreshThreadDetailAction(emailChangeResponse));
}
}
@@ -61,6 +61,7 @@ import 'package:tmail_ui_user/features/thread/domain/usecases/refresh_changes_em
import 'package:tmail_ui_user/features/thread/domain/usecases/search_email_interactor.dart';
import 'package:tmail_ui_user/features/thread/domain/usecases/search_more_email_interactor.dart';
import 'package:tmail_ui_user/features/thread/presentation/extensions/list_presentation_email_extensions.dart';
import 'package:tmail_ui_user/features/thread/presentation/extensions/refresh_thread_detail_extension.dart';
import 'package:tmail_ui_user/features/thread/presentation/mixin/email_action_controller.dart';
import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart';
import 'package:tmail_ui_user/features/thread/presentation/model/loading_more_status.dart';
@@ -697,7 +698,7 @@ class ThreadController extends BaseController with EmailActionController {
}
Future<Either<Failure, Success>> _refreshChangeListEmailCache() async {
return _refreshChangesEmailsInMailboxInteractor.execute(
final refreshState = await _refreshChangesEmailsInMailboxInteractor.execute(
_session!,
_accountId!,
mailboxDashBoardController.currentEmailState!,
@@ -716,6 +717,17 @@ class ThreadController extends BaseController with EmailActionController {
mailboxId: selectedMailboxId,
),
).last;
refreshState.fold(
(failure) {},
(success) {
if (success is RefreshChangesAllEmailSuccess) {
refreshThreadDetail(success.emailChangeResponse);
}
},
);
return refreshState;
}
Future<void> _refreshChangeListEmail() async {
@@ -25,8 +25,9 @@ class ThreadDetailRepositoryImpl implements ThreadDetailRepository {
Session session,
AccountId accountId,
MailboxId sentMailboxId,
String ownEmailAddress,
) async {
String ownEmailAddress, {
required EmailId? selectedEmailId,
}) async {
final originalEmailIds = await threadDetailDataSource[DataSourceType.network]!
.getThreadById(threadId, accountId);
@@ -43,6 +44,7 @@ class ThreadDetailRepositoryImpl implements ThreadDetailRepository {
emailIds,
sentMailboxId,
ownEmailAddress,
selectedEmailId: selectedEmailId,
))
);
@@ -60,8 +62,9 @@ class ThreadDetailRepositoryImpl implements ThreadDetailRepository {
AccountId accountId,
List<EmailId> emailIds,
MailboxId sentMailboxId,
String ownEmailAddress,
) async {
String ownEmailAddress, {
required EmailId? selectedEmailId,
}) async {
int retry = 3;
while (retry > 0) {
try {
@@ -85,6 +88,7 @@ class ThreadDetailRepositoryImpl implements ThreadDetailRepository {
email,
sentMailboxId,
ownEmailAddress,
selectedEmailId: selectedEmailId,
))
.toList();
} catch (e) {
@@ -99,12 +103,14 @@ class ThreadDetailRepositoryImpl implements ThreadDetailRepository {
bool checkEmailValidForThreadDetail(
Email email,
MailboxId sentMailboxId,
String ownEmailAddress,
) {
String ownEmailAddress, {
required EmailId? selectedEmailId
}) {
return email.id != null && (
!email.inSentMailbox(sentMailboxId)
|| !email.fromMe(ownEmailAddress)
|| !email.recipientsHasMe(ownEmailAddress)
|| email.id == selectedEmailId
);
}
@@ -10,8 +10,9 @@ abstract class ThreadDetailRepository {
Session session,
AccountId accountId,
MailboxId sentMailboxId,
String ownEmailAddress,
);
String ownEmailAddress, {
required EmailId? selectedEmailId,
});
Future<List<Email>> getEmailsByIds(
Session session,
@@ -12,14 +12,27 @@ class GettingEmailsByIds extends LoadingState {
}
class GetEmailsByIdsSuccess extends UIState {
GetEmailsByIdsSuccess(this.presentationEmails);
GetEmailsByIdsSuccess(
this.presentationEmails, {
this.updateCurrentThreadDetail = false,
});
final List<PresentationEmail> presentationEmails;
final bool updateCurrentThreadDetail;
@override
List<Object?> get props => [presentationEmails];
List<Object?> get props => [presentationEmails, updateCurrentThreadDetail];
}
class GetEmailsByIdsFailure extends FeatureFailure {
GetEmailsByIdsFailure({super.exception, super.onRetry});
GetEmailsByIdsFailure({
super.exception,
super.onRetry,
required this.updateCurrentThreadDetail,
});
final bool updateCurrentThreadDetail;
@override
List<Object?> get props => [...super.props, updateCurrentThreadDetail];
}
@@ -2,17 +2,34 @@ import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
class GettingThreadById extends LoadingState {}
class GettingThreadById extends LoadingState {
final bool updateCurrentThreadDetail;
GettingThreadById({this.updateCurrentThreadDetail = false});
@override
List<Object> get props => [updateCurrentThreadDetail];
}
class GetThreadByIdSuccess extends UIState {
final List<EmailId> emailIds;
final bool updateCurrentThreadDetail;
GetThreadByIdSuccess(this.emailIds);
GetThreadByIdSuccess(this.emailIds, {this.updateCurrentThreadDetail = false});
@override
List<Object> get props => [emailIds];
List<Object> get props => [emailIds, updateCurrentThreadDetail];
}
class GetThreadByIdFailure extends FeatureFailure {
GetThreadByIdFailure({super.exception, super.onRetry});
GetThreadByIdFailure({
super.exception,
super.onRetry,
required this.updateCurrentThreadDetail,
});
final bool updateCurrentThreadDetail;
@override
List<Object?> get props => [...super.props, updateCurrentThreadDetail];
}
@@ -23,6 +23,7 @@ class GetEmailsByIdsInteractor {
List<EmailId> emailIds, {
Properties? properties,
int? loadMoreIndex,
bool updateCurrentThreadDetail = false,
}) async* {
try {
if (emailIds.length > 1 && (
@@ -41,12 +42,14 @@ class GetEmailsByIdsInteractor {
);
yield Right(GetEmailsByIdsSuccess(
result.map((e) => e.toPresentationEmail()).toList(),
updateCurrentThreadDetail: updateCurrentThreadDetail,
));
} catch (e) {
logError('GetEmailsByIdsInteractor::execute(): Exception: $e');
yield Left(GetEmailsByIdsFailure(
exception: e,
onRetry: execute(session, accountId, emailIds, properties: properties),
updateCurrentThreadDetail: updateCurrentThreadDetail,
));
}
}
@@ -19,19 +19,27 @@ class GetThreadByIdInteractor {
Session session,
AccountId accountId,
MailboxId sentMailboxId,
String ownEmailAddress,
) async* {
String ownEmailAddress, {
required EmailId? selectedEmailId,
bool updateCurrentThreadDetail = false,
}) async* {
try {
yield Right(GettingThreadById());
yield Right(GettingThreadById(
updateCurrentThreadDetail: updateCurrentThreadDetail,
));
final result = await _threadDetailRepository.getThreadById(
threadId,
session,
accountId,
sentMailboxId,
ownEmailAddress,
selectedEmailId: selectedEmailId,
);
yield Right(GetThreadByIdSuccess(result));
yield Right(GetThreadByIdSuccess(
result,
updateCurrentThreadDetail: updateCurrentThreadDetail,
));
} catch (e) {
logError('GetEmailIdsByThreadIdInteractor::execute(): Exception: $e');
yield Left(GetThreadByIdFailure(
@@ -42,7 +50,9 @@ class GetThreadByIdInteractor {
accountId,
sentMailboxId,
ownEmailAddress,
selectedEmailId: selectedEmailId,
),
updateCurrentThreadDetail: updateCurrentThreadDetail,
));
}
}
@@ -1,16 +1,12 @@
import 'package:core/presentation/views/loading/cupertino_loading_widget.dart';
import 'package:flutter/material.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/state/get_thread_by_id_state.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
extension GetThreadDetailLoadingView on ThreadDetailController {
Widget getThreadDetailLoadingView({
required bool isResponsiveDesktop,
required bool isLoading,
}) {
final isLoading = viewState.value.fold(
(failure) => false,
(success) => success is GettingThreadById
);
if (!isLoading) return const SizedBox.shrink();
return Expanded(
@@ -1,3 +1,4 @@
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/state/get_thread_by_id_state.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
@@ -5,10 +6,27 @@ extension HandleGetEmailIdsByThreadIdSuccess on ThreadDetailController {
void handleGetEmailIdsByThreadIdSuccess(
GetThreadByIdSuccess success,
) {
final newEmailsInThreadDetail = <EmailId>[];
if (success.emailIds.isNotEmpty) {
emailIdsPresentation.value = Map.fromEntries(success.emailIds.map(
(emailId) => MapEntry(emailId, null),
));
if (success.updateCurrentThreadDetail) {
newEmailsInThreadDetail.addAll(success
.emailIds
.where(
(emailId) => !emailIdsPresentation.keys.contains(emailId),
)
);
emailIdsPresentation
..removeWhere(
(key, value) => !success.emailIds.contains(key),
)
..addAll(Map.fromEntries(
newEmailsInThreadDetail.map((emailId) => MapEntry(emailId, null)),
));
} else {
emailIdsPresentation.value = Map.fromEntries(success.emailIds.map(
(emailId) => MapEntry(emailId, null),
));
}
} else if (mailboxDashBoardController.selectedEmail.value?.id != null) {
emailIdsPresentation.value = {
mailboxDashBoardController.selectedEmail.value!.id!: null,
@@ -18,6 +18,15 @@ extension HandleGetEmailsByIdsSuccess on ThreadDetailController {
for (var presentationEmail in success.presentationEmails) {
if (presentationEmail.id == null) continue;
if (success.updateCurrentThreadDetail) {
emailIdsPresentation[presentationEmail.id!] = presentationEmail.copyWith(
emailInThreadStatus: emailIdsPresentation[presentationEmail.id!]
?.emailInThreadStatus ?? EmailInThreadStatus.collapsed,
);
continue;
}
if (presentationEmail.id == selectedEmailId) {
EmailBindings(currentEmailId: presentationEmail.id).dependencies();
currentExpandedEmailId.value = presentationEmail.id;
@@ -5,6 +5,8 @@ import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_
extension HandleGetThreadByIdFailure on ThreadDetailController {
void handleGetThreadByIdFailure(GetThreadByIdFailure failure) {
if (failure.updateCurrentThreadDetail) return;
final selectedEmail = mailboxDashBoardController.selectedEmail.value;
if (selectedEmail != null) {
consumeState(Stream.value(Right(GetEmailsByIdsSuccess([selectedEmail]))));
@@ -0,0 +1,62 @@
import 'package:dartz/dartz.dart';
import 'package:model/extensions/list_email_extension.dart';
import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/state/get_thread_by_id_state.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_by_id_interactor.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
extension HandleRefreshThreadDetailAction on ThreadDetailController {
void handleRefreshThreadDetailAction(
RefreshThreadDetailAction action,
GetThreadByIdInteractor getThreadByIdInteractor,
) {
if (!isThreadDetailEnabled) {
final currentEmailId = mailboxDashBoardController.selectedEmail.value?.id;
if (currentEmailId == null) return;
final updatedEmailIds = action
.emailChangeResponse
.updated
?.listEmailIds ?? [];
if (updatedEmailIds.contains(currentEmailId)) {
consumeState(Stream.value(Right(GetThreadByIdSuccess(
[currentEmailId],
updateCurrentThreadDetail: true,
))));
}
return;
}
final currentThreadId = mailboxDashBoardController.selectedEmail.value?.threadId;
final threadIdsEmailCreated = action
.emailChangeResponse
.created
?.listThreadIds ?? [];
final threadIdsEmailUpdated = action
.emailChangeResponse
.updated
?.listThreadIds ?? [];
final updatedThreadIds = threadIdsEmailCreated + threadIdsEmailUpdated;
final destroyedEmailIds = action.emailChangeResponse.destroyed ?? [];
if (session != null &&
accountId != null &&
sentMailboxId != null &&
ownEmailAddress != null &&
currentThreadId != null && (
updatedThreadIds.contains(currentThreadId) ||
emailIdsPresentation.keys.any(destroyedEmailIds.contains))) {
consumeState(getThreadByIdInteractor.execute(
currentThreadId,
session!,
accountId!,
sentMailboxId!,
ownEmailAddress!,
updateCurrentThreadDetail: true,
selectedEmailId: mailboxDashBoardController.selectedEmail.value?.id,
));
}
}
}
@@ -1,27 +1,47 @@
import 'package:dartz/dartz.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart';
import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/state/get_emails_by_ids_state.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/state/get_thread_by_id_state.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/utils/thread_detail_presentation_utils.dart';
extension InitializeThreadDetailEmails on ThreadDetailController {
void initializeThreadDetailEmails() {
void initializeThreadDetailEmails(GetThreadByIdSuccess success) {
final threadDetailEnabled = isThreadDetailEnabled;
final selectedEmail = mailboxDashBoardController.selectedEmail.value;
if (!threadDetailEnabled && selectedEmail != null) {
if (!threadDetailEnabled &&
selectedEmail != null &&
!success.updateCurrentThreadDetail) {
consumeState(Stream.value(Right(GetEmailsByIdsSuccess([selectedEmail]))));
return;
}
final emailIdsToLoadMetaData = ThreadDetailPresentationUtils.getFirstLoadEmailIds(
emailIdsPresentation.keys.toList(),
selectedEmailId: mailboxDashBoardController.selectedEmail.value?.id,
);
final existingEmailIds = emailIdsPresentation.keys.toList();
final selectedEmailId = mailboxDashBoardController.selectedEmail.value?.id;
List<EmailId> emailIdsToLoadMetaData = [];
if (success.updateCurrentThreadDetail) {
final nonNullEmailIds = emailIdsPresentation.entries
.where((entry) => entry.value != null)
.map((entry) => entry.key)
.toList();
final newEmailIds = success.emailIds.where(
(emailId) => !existingEmailIds.contains(emailId),
);
emailIdsToLoadMetaData = [...nonNullEmailIds, ...newEmailIds];
} else {
emailIdsToLoadMetaData = ThreadDetailPresentationUtils.getFirstLoadEmailIds(
existingEmailIds,
selectedEmailId: selectedEmailId,
);
}
if (accountId == null || session == null) {
consumeState(Stream.value(Left(GetEmailsByIdsFailure(
exception: NotFoundSessionException(),
updateCurrentThreadDetail: false,
))));
return;
}
@@ -33,6 +53,7 @@ extension InitializeThreadDetailEmails on ThreadDetailController {
session!,
accountId!,
).union(additionalProperties),
updateCurrentThreadDetail: success.updateCurrentThreadDetail,
));
}
}
@@ -15,6 +15,7 @@ extension LoadMoreThreadDetailEmails on ThreadDetailController {
if (accountId == null || session == null) {
consumeState(Stream.value(Left(GetEmailsByIdsFailure(
exception: NotFoundSessionException(),
updateCurrentThreadDetail: false,
))));
return;
}
@@ -18,6 +18,7 @@ import 'package:model/extensions/keyword_identifier_extension.dart';
import 'package:model/extensions/session_extension.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:tmail_ui_user/features/base/base_controller.dart';
import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart';
import 'package:tmail_ui_user/features/email/presentation/model/email_loaded.dart';
import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart';
@@ -43,6 +44,7 @@ import 'package:tmail_ui_user/features/thread_detail/presentation/extension/clos
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_get_email_ids_by_thread_id_success.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_get_emails_by_ids_success.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_get_thread_by_id_failure.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/initialize_thread_detail_emails.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/refresh_thread_detail_on_setting_changed.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/model/thread_detail_setting_status.dart';
@@ -164,6 +166,7 @@ class ThreadDetailController extends BaseController {
accountId!,
sentMailboxId!,
ownEmailAddress!,
selectedEmailId: presentationEmail.id,
));
}
});
@@ -188,6 +191,15 @@ class ThreadDetailController extends BaseController {
);
}
});
ever(mailboxDashBoardController.emailUIAction, (action) {
if (action is RefreshThreadDetailAction) {
mailboxDashBoardController.dispatchEmailUIAction(EmailUIAction());
handleRefreshThreadDetailAction(
action,
_getEmailIdsByThreadIdInteractor,
);
}
});
}
void reset() {
@@ -202,7 +214,7 @@ class ThreadDetailController extends BaseController {
void handleSuccessViewState(success) {
if (success is GetThreadByIdSuccess) {
handleGetEmailIdsByThreadIdSuccess(success);
initializeThreadDetailEmails();
initializeThreadDetailEmails(success);
} else if (success is GetEmailsByIdsSuccess) {
handleGetEmailsByIdsSuccess(success);
} else if (success is MarkAsEmailReadSuccess) {
@@ -231,6 +243,7 @@ class ThreadDetailController extends BaseController {
handleGetThreadByIdFailure(failure);
}
if (failure is GetEmailsByIdsFailure) {
if (failure.updateCurrentThreadDetail) return;
showRetryToast(failure);
return;
}
@@ -1,5 +1,7 @@
import 'package:collection/collection.dart';
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
@@ -27,8 +29,7 @@ class ThreadDetailView extends GetWidget<ThreadDetailController> {
child: Column(
children: [
Obx(() {
final currentViewState = controller.viewState.value.fold(id, id);
if (currentViewState is GettingThreadById) {
if (showLoadingView(controller.viewState.value)) {
return const SizedBox.shrink();
}
@@ -68,40 +69,35 @@ class ThreadDetailView extends GetWidget<ThreadDetailController> {
}),
Obx(() => controller.getThreadDetailLoadingView(
isResponsiveDesktop: controller.responsiveUtils.isDesktop(context),
isLoading: showLoadingView(controller.viewState.value),
)),
Obx(() {
return controller.viewState.value.fold(
(failure) => const SizedBox.shrink(),
(success) {
if (success is GettingThreadById) {
return const SizedBox.shrink();
}
if (showLoadingView(controller.viewState.value)) {
return const SizedBox.shrink();
}
return Expanded(
child: Padding(
padding: _padding(context),
child: Stack(
children: [
const Positioned.fill(
child: ColoredBox(color: Colors.white),
),
SingleChildScrollView(
controller: controller.scrollController,
child: Column(
mainAxisSize: MainAxisSize.min,
children: controller.getThreadDetailEmailViews()
),
),
],
return Expanded(
child: Padding(
padding: _padding(context),
child: Stack(
children: [
const Positioned.fill(
child: ColoredBox(color: Colors.white),
),
),
);
},
SingleChildScrollView(
controller: controller.scrollController,
child: Column(
mainAxisSize: MainAxisSize.min,
children: controller.getThreadDetailEmailViews()
),
),
],
),
),
);
}),
Obx(() {
final currentViewState = controller.viewState.value.fold(id, id);
if (currentViewState is GettingThreadById) {
if (showLoadingView(controller.viewState.value)) {
return const SizedBox.shrink();
}
@@ -166,10 +162,12 @@ class ThreadDetailView extends GetWidget<ThreadDetailController> {
));
return Obx(() {
final currentViewState = controller.viewState.value.fold(id, id);
if (currentViewState is GettingThreadById &&
if (showLoadingView(controller.viewState.value) &&
controller.responsiveUtils.isTabletLarge(context)) {
return controller.getThreadDetailLoadingView(isResponsiveDesktop: false);
return controller.getThreadDetailLoadingView(
isResponsiveDesktop: false,
isLoading: true,
);
}
if (controller.emailIdsPresentation.length == 1 &&
@@ -215,4 +213,10 @@ class ThreadDetailView extends GetWidget<ThreadDetailController> {
)
);
}
bool showLoadingView(Either<Failure, Success> viewState) {
final viewStateValue = viewState.fold(id, id);
return viewStateValue is GettingThreadById &&
!viewStateValue.updateCurrentThreadDetail;
}
}
@@ -11,6 +11,8 @@ extension ListEmailExtension on List<Email> {
List<EmailId> get listEmailIds => map((email) => email.id).whereNotNull().toList();
List<ThreadId> get listThreadIds => map((email) => email.threadId).whereNotNull().toList();
Email? findEmailById(EmailId emailId) {
try {
return firstWhere((email) => email.id == emailId);
@@ -28,9 +28,14 @@ void main() {
EmailId(Id('1')),
EmailId(Id('2')),
]);
final mailboxDashBoardController = MockMailboxDashBoardController();
when(threadDetailController.emailIdsPresentation).thenReturn(
<EmailId, PresentationEmail?>{}.obs,
);
when(mailboxDashBoardController.selectedEmail).thenReturn(
Rxn(PresentationEmail(id: EmailId(Id('1')))),
);
when(threadDetailController.mailboxDashBoardController).thenReturn(mailboxDashBoardController);
// act
threadDetailController.handleGetEmailIdsByThreadIdSuccess(success);
@@ -0,0 +1,316 @@
import 'package:core/presentation/state/success.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:model/email/presentation_email.dart';
import 'package:model/extensions/session_extension.dart';
import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/features/thread/data/model/email_change_response.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/state/get_thread_by_id_state.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_by_id_interactor.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_refresh_thread_detail_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
import '../../../../fixtures/account_fixtures.dart';
import '../../../../fixtures/session_fixtures.dart';
import 'handle_refresh_thread_detail_action_test.mocks.dart';
@GenerateNiceMocks([
MockSpec<ThreadDetailController>(),
MockSpec<MailboxDashBoardController>(),
MockSpec<GetThreadByIdInteractor>(),
])
void main() {
late MockThreadDetailController threadDetailController;
late MockMailboxDashBoardController mailboxDashBoardController;
late MockGetThreadByIdInteractor getThreadByIdInteractor;
final sentMailboxId = MailboxId(Id('sent'));
final ownEmailAddress = SessionFixtures.aliceSession.getOwnEmailAddress();
setUp(() {
threadDetailController = MockThreadDetailController();
mailboxDashBoardController = MockMailboxDashBoardController();
getThreadByIdInteractor = MockGetThreadByIdInteractor();
});
group('handle refresh thread detail action test', () {
test(
'should call getThreadByIdInteractor.execute '
'when refreshThreadDetailAction is called '
'and list email created contains currentThreadId '
'and isThreadDetailEnabled is true',
() {
// arrange
final emailId = EmailId(Id('1'));
final threadId = ThreadId(Id('some-id'));
final action = RefreshThreadDetailAction(EmailChangeResponse(
created: [Email(id: emailId, threadId: threadId)],
));
when(threadDetailController.mailboxDashBoardController)
.thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail)
.thenReturn(Rxn(PresentationEmail(id: emailId, threadId: threadId)));
when(threadDetailController.session)
.thenReturn(SessionFixtures.aliceSession);
when(threadDetailController.accountId)
.thenReturn(AccountFixtures.aliceAccountId);
when(threadDetailController.sentMailboxId).thenReturn(sentMailboxId);
when(threadDetailController.ownEmailAddress).thenReturn(ownEmailAddress);
when(threadDetailController.isThreadDetailEnabled).thenReturn(true);
// act
threadDetailController.handleRefreshThreadDetailAction(
action,
getThreadByIdInteractor,
);
// assert
verify(getThreadByIdInteractor.execute(
threadId,
SessionFixtures.aliceSession,
AccountFixtures.aliceAccountId,
sentMailboxId,
ownEmailAddress,
updateCurrentThreadDetail: true,
selectedEmailId: emailId,
)).called(1);
});
test(
'should call getThreadByIdInteractor.execute '
'when refreshThreadDetailAction is called '
'and list email updated contains currentThreadId '
'and isThreadDetailEnabled is true',
() {
// arrange
final emailId = EmailId(Id('1'));
final threadId = ThreadId(Id('some-id'));
final action = RefreshThreadDetailAction(EmailChangeResponse(
updated: [Email(id: emailId, threadId: threadId)],
));
when(threadDetailController.mailboxDashBoardController)
.thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail)
.thenReturn(Rxn(PresentationEmail(id: emailId, threadId: threadId)));
when(threadDetailController.session)
.thenReturn(SessionFixtures.aliceSession);
when(threadDetailController.accountId)
.thenReturn(AccountFixtures.aliceAccountId);
when(threadDetailController.sentMailboxId).thenReturn(sentMailboxId);
when(threadDetailController.ownEmailAddress).thenReturn(ownEmailAddress);
when(threadDetailController.isThreadDetailEnabled).thenReturn(true);
// act
threadDetailController.handleRefreshThreadDetailAction(
action,
getThreadByIdInteractor,
);
// assert
verify(getThreadByIdInteractor.execute(
threadId,
SessionFixtures.aliceSession,
AccountFixtures.aliceAccountId,
sentMailboxId,
ownEmailAddress,
updateCurrentThreadDetail: true,
selectedEmailId: emailId,
)).called(1);
});
test(
'should call getThreadByIdInteractor.execute '
'when refreshThreadDetailAction is called '
'and list email destroyed contains any of emailId in current thread '
'and isThreadDetailEnabled is true',
() {
// arrange
final emailId = EmailId(Id('1'));
final threadId = ThreadId(Id('some-id'));
final action = RefreshThreadDetailAction(EmailChangeResponse(
destroyed: [emailId],
));
when(threadDetailController.mailboxDashBoardController)
.thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail)
.thenReturn(Rxn(PresentationEmail(id: emailId, threadId: threadId)));
when(threadDetailController.session)
.thenReturn(SessionFixtures.aliceSession);
when(threadDetailController.accountId)
.thenReturn(AccountFixtures.aliceAccountId);
when(threadDetailController.sentMailboxId).thenReturn(sentMailboxId);
when(threadDetailController.ownEmailAddress).thenReturn(ownEmailAddress);
when(threadDetailController.emailIdsPresentation).thenReturn({
emailId: null
}.obs);
when(threadDetailController.isThreadDetailEnabled).thenReturn(true);
// act
threadDetailController.handleRefreshThreadDetailAction(
action,
getThreadByIdInteractor,
);
// assert
verify(getThreadByIdInteractor.execute(
threadId,
SessionFixtures.aliceSession,
AccountFixtures.aliceAccountId,
sentMailboxId,
ownEmailAddress,
updateCurrentThreadDetail: true,
selectedEmailId: emailId,
)).called(1);
});
test(
'should consume GetThreadByIdSuccess with selected email id '
'when refreshThreadDetailAction is called '
'and isThreadDetailEnabled is false '
'and selected email id not null '
'and list email updated contains selected email id',
() async {
// arrange
final emailId = EmailId(Id('1'));
final action = RefreshThreadDetailAction(EmailChangeResponse(
updated: [Email(id: emailId)],
));
when(threadDetailController.mailboxDashBoardController)
.thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail)
.thenReturn(Rxn(PresentationEmail(id: emailId)));
when(threadDetailController.isThreadDetailEnabled).thenReturn(false);
// act
threadDetailController.handleRefreshThreadDetailAction(
action,
getThreadByIdInteractor,
);
// assert
verifyNever(getThreadByIdInteractor.execute(
any,
any,
any,
any,
any,
updateCurrentThreadDetail: anyNamed('updateCurrentThreadDetail'),
selectedEmailId: anyNamed('selectedEmailId'),
));
final args = verify(
threadDetailController.consumeState(captureAny),
).captured.first as Stream;
final state = await args.last;
expect(
state,
Right(GetThreadByIdSuccess(
[emailId],
updateCurrentThreadDetail: true,
)),
);
});
test(
'should not call getThreadByIdInteractor.execute '
'and not consume GetThreadByIdSuccess with selected email id'
'when refreshThreadDetailAction is called '
'and list email created does not contain currentThreadId '
'and list email updated does not contain currentThreadId '
'and list email destroyed does not contain any of emailId in current thread '
'and isThreadDetailEnabled is true',
() async {
// arrange
final emailId = EmailId(Id('1'));
final action = RefreshThreadDetailAction(EmailChangeResponse(
created: [
Email(id: EmailId(Id('2')), threadId: ThreadId(Id('some-id-2'))),
],
updated: [
Email(id: EmailId(Id('3')), threadId: ThreadId(Id('some-id-3'))),
],
destroyed: [
EmailId(Id('4')),
],
));
when(threadDetailController.mailboxDashBoardController)
.thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail)
.thenReturn(Rxn(PresentationEmail()));
when(threadDetailController.session)
.thenReturn(SessionFixtures.aliceSession);
when(threadDetailController.accountId)
.thenReturn(AccountFixtures.aliceAccountId);
when(threadDetailController.sentMailboxId).thenReturn(sentMailboxId);
when(threadDetailController.ownEmailAddress).thenReturn(ownEmailAddress);
when(threadDetailController.emailIdsPresentation).thenReturn({
emailId: null,
}.obs);
when(threadDetailController.viewState).thenReturn(Rx(Right(UIState.idle)));
when(threadDetailController.isThreadDetailEnabled).thenReturn(true);
// act
threadDetailController.handleRefreshThreadDetailAction(
action,
getThreadByIdInteractor,
);
// assert
verifyNever(getThreadByIdInteractor.execute(
any,
any,
any,
any,
any,
updateCurrentThreadDetail: anyNamed('updateCurrentThreadDetail'),
selectedEmailId: anyNamed('selectedEmailId'),
));
verifyNever(threadDetailController.consumeState(any));
});
test(
'should not call getThreadByIdInteractor.execute '
'and not consume GetThreadByIdSuccess with selected email id'
'when refreshThreadDetailAction is called '
'and isThreadDetailEnabled is false '
'and updated email ids does not contain selected email id',
() async {
// arrange
final emailId = EmailId(Id('1'));
final action = RefreshThreadDetailAction(EmailChangeResponse(
updated: [
Email(id: EmailId(Id('3')), threadId: ThreadId(Id('some-id-3'))),
],
));
when(threadDetailController.mailboxDashBoardController)
.thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail)
.thenReturn(Rxn(PresentationEmail(id: emailId)));
when(threadDetailController.isThreadDetailEnabled).thenReturn(false);
// act
threadDetailController.handleRefreshThreadDetailAction(
action,
getThreadByIdInteractor,
);
// assert
verifyNever(getThreadByIdInteractor.execute(
any,
any,
any,
any,
any,
updateCurrentThreadDetail: anyNamed('updateCurrentThreadDetail'),
selectedEmailId: anyNamed('selectedEmailId'),
));
verifyNever(threadDetailController.consumeState(any));
});
});
}
@@ -8,6 +8,7 @@ import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
import 'package:model/email/presentation_email.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/state/get_thread_by_id_state.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_emails_by_ids_interactor.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/initialize_thread_detail_emails.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
@@ -65,7 +66,9 @@ void main() {
.thenReturn(Rxn(PresentationEmail()));
// Act
threadDetailController.initializeThreadDetailEmails();
threadDetailController.initializeThreadDetailEmails(
GetThreadByIdSuccess(emailIds),
);
// Assert
final captured = verify(getEmailsByIdsInteractor.execute(
@@ -144,6 +144,7 @@ void main() {
when(controller.accountId).thenReturn(AccountFixtures.aliceAccountId);
when(controller.additionalProperties).thenReturn(Properties.empty());
when(controller.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor);
when(controller.additionalProperties).thenReturn(Properties.empty());
when(controller.mailboxDashBoardController).thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail).thenReturn(Rxn());
@@ -189,6 +190,7 @@ void main() {
when(controller.accountId).thenReturn(AccountFixtures.aliceAccountId);
when(controller.additionalProperties).thenReturn(Properties.empty());
when(controller.getEmailsByIdsInteractor).thenReturn(getEmailsByIdsInteractor);
when(controller.additionalProperties).thenReturn(Properties.empty());
when(controller.mailboxDashBoardController).thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail).thenReturn(Rxn());