TF-3699 Thread Detail Next previous actions (#3843)

This commit is contained in:
Dat Dang
2025-07-16 17:15:17 +07:00
committed by Dat H. Pham
parent 4d9a44df66
commit 7332b731dc
28 changed files with 581 additions and 241 deletions
@@ -360,6 +360,7 @@ class HtmlContentViewState extends State<HtmlContentViewer> with AutomaticKeepAl
void dispose() {
_loadingBarNotifier.dispose();
_htmlData = null;
_webViewController.dispose();
super.dispose();
}
@@ -127,6 +127,7 @@ import 'package:tmail_ui_user/features/thread_detail/presentation/action/thread_
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/close_thread_detail_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/focus_thread_detail_expanded_email.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/mark_collapsed_email_unread_success.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/on_thread_page_changed.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/update_cached_list_email_loaded.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
import 'package:tmail_ui_user/main/error/capability_validator.dart';
@@ -204,6 +205,9 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
: _threadDetailController?.emailIdsPresentation[_currentEmailId];
}
bool get isOnlyEmailInThread =>
_threadDetailController?.emailIdsPresentation.length == 1;
bool get calendarEventProcessing => viewState.value.fold(
(failure) => false,
(success) => success is CalendarEventReplying);
@@ -680,14 +684,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
emailUnsubscribe.value = null;
}
}
if (currentEmail?.threadId != null &&
currentEmail?.id == mailboxDashBoardController.selectedEmail.value?.id) {
mailboxDashBoardController.dispatchThreadDetailUIAction(
LoadThreadDetailAfterSelectedEmailAction(
currentEmail!.threadId!,
)
);
}
_loadThreadOnGetEmailContentSuccess();
}
void _getEmailContentSuccess(GetEmailContentSuccess success) {
@@ -758,9 +755,14 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
if ((_threadDetailController?.emailIdsPresentation.keys.length ?? 0) > 1 == true) {
_jumpScrollViewToTopOfEmail();
}
_loadThreadOnGetEmailContentSuccess();
}
void _loadThreadOnGetEmailContentSuccess() {
if (currentEmail?.threadId != null &&
currentEmail?.id == mailboxDashBoardController.selectedEmail.value?.id &&
success is! GetEmailContentFromThreadCacheSuccess) {
_threadDetailController?.loadThreadOnThreadChanged == true) {
_threadDetailController?.loadThreadOnThreadChanged = false;
mailboxDashBoardController.dispatchThreadDetailUIAction(
LoadThreadDetailAfterSelectedEmailAction(
currentEmail!.threadId!,
@@ -2391,4 +2393,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
}
);
}
void onScrollHorizontalEnd(bool isLeftDirection) {
if (isLeftDirection) {
_threadDetailController?.onPreviousMobile();
} else {
_threadDetailController?.onNextMobile();
}
}
}
+30 -25
View File
@@ -214,7 +214,7 @@ class EmailView extends GetWidget<SingleEmailController> {
);
}
if (controller.currentEmail == null || !isInsideThreadDetailView) {
if (controller.currentEmail == null || isFirstEmailInThreadDetail) {
return const BoxDecoration(color: Colors.white);
}
@@ -278,35 +278,38 @@ class EmailView extends GetWidget<SingleEmailController> {
onEmailActionClick: (presentationEmail, actionType) => controller.handleEmailAction(context, presentationEmail, actionType),
isInsideThreadDetailView: isInsideThreadDetailView,
emailLoaded: controller.currentEmailLoaded.value,
onMoreActionClick: (presentationEmail, position) => controller.emailActionReactor.handleMoreEmailAction(
mailboxContain: controller.getMailboxContain(presentationEmail),
presentationEmail: presentationEmail,
position: position,
responsiveUtils: controller.responsiveUtils,
imagePaths: controller.imagePaths,
username: controller.session?.username,
handleEmailAction: (email, action) => controller.handleEmailAction(context, email, action),
additionalActions: [
EmailActionType.forward,
EmailActionType.replyAll,
EmailActionType.replyToList,
EmailActionType.printAll,
if (controller.responsiveUtils.isMobile(context))
EmailActionType.moveToMailbox,
if (!controller.responsiveUtils.isDesktop(context)) ...[
EmailActionType.markAsStarred,
EmailActionType.unMarkAsStarred,
EmailActionType.moveToTrash,
EmailActionType.deletePermanently,
],
],
emailIsRead: presentationEmail.hasRead,
),
onMoreActionClick: controller.isOnlyEmailInThread
? null
: (presentationEmail, position) => controller.emailActionReactor.handleMoreEmailAction(
mailboxContain: controller.getMailboxContain(presentationEmail),
presentationEmail: presentationEmail,
position: position,
responsiveUtils: controller.responsiveUtils,
imagePaths: controller.imagePaths,
username: controller.session?.username,
handleEmailAction: (email, action) => controller.handleEmailAction(context, email, action),
additionalActions: [
EmailActionType.forward,
EmailActionType.replyAll,
EmailActionType.replyToList,
EmailActionType.printAll,
if (controller.responsiveUtils.isMobile(context))
EmailActionType.moveToMailbox,
if (!controller.responsiveUtils.isDesktop(context)) ...[
EmailActionType.markAsStarred,
EmailActionType.unMarkAsStarred,
EmailActionType.moveToTrash,
EmailActionType.deletePermanently,
],
],
emailIsRead: presentationEmail.hasRead,
),
onToggleThreadDetailCollapseExpand: onToggleThreadDetailCollapseExpand,
onTapAvatarActionClick: onToggleThreadDetailCollapseExpand,
mailboxContain: presentationEmail.findMailboxContain(
controller.mailboxDashBoardController.mapMailboxById,
),
isOnlyEmailInThread: controller.isOnlyEmailInThread,
)),
Obx(() => MailUnsubscribedBanner(
presentationEmail: controller.currentEmail,
@@ -449,6 +452,7 @@ class EmailView extends GetWidget<SingleEmailController> {
maxHtmlContentHeight: ConstantsUI.htmlContentMaxHeight,
onMailtoDelegateAction: controller.openMailToLink,
onHtmlContentClippedAction: controller.onHtmlContentClippedAction,
onScrollHorizontalEnd: controller.onScrollHorizontalEnd,
keepAlive: isInsideThreadDetailView,
enableQuoteToggle: isInsideThreadDetailView,
);
@@ -489,6 +493,7 @@ class EmailView extends GetWidget<SingleEmailController> {
onMailtoDelegateAction: controller.openMailToLink,
keepAlive: isInsideThreadDetailView,
enableQuoteToggle: isInsideThreadDetailView,
onScrollHorizontalEnd: controller.onScrollHorizontalEnd,
);
})
);
@@ -548,7 +548,7 @@ class EmailActionReactor with MessageDialogActionMixin, PopupContextMenuActionMi
: EmailActionType.markAsStarred,
if (additionalActions.contains(EmailActionType.moveToTrash) &&
additionalActions.contains(EmailActionType.deletePermanently))
_canDeletePermanently(presentationEmail)
_canDeletePermanently(presentationEmail, mailboxContain)
? EmailActionType.deletePermanently
: EmailActionType.moveToTrash,
if (emailIsRead)
@@ -598,8 +598,13 @@ class EmailActionReactor with MessageDialogActionMixin, PopupContextMenuActionMi
}
}
bool _canDeletePermanently(PresentationEmail email) {
return email.mailboxContain?.isTrash
bool _canDeletePermanently(
PresentationEmail email,
PresentationMailbox? mailboxContain,
) {
return mailboxContain?.isTrash
?? mailboxContain?.isSpam
?? email.mailboxContain?.isTrash
?? email.mailboxContain?.isSpam
?? false;
}
@@ -34,6 +34,7 @@ class EmailViewAppBarWidget extends StatelessWidget {
final EdgeInsetsGeometry? iconPadding;
final EdgeInsetsGeometry? iconMargin;
final EdgeInsetsGeometry? padding;
final bool isOnlyEmailInThread;
EmailViewAppBarWidget({
Key? key,
@@ -51,6 +52,7 @@ class EmailViewAppBarWidget extends StatelessWidget {
this.iconPadding,
this.iconMargin,
this.padding,
this.isOnlyEmailInThread = false,
}) : super(key: key);
@override
@@ -251,19 +253,22 @@ class EmailViewAppBarWidget extends StatelessWidget {
_getMoveEmailButton(appLocalizations),
_getMarkStarButton(appLocalizations),
_getDeleteButton(appLocalizations),
_getMoreButton(appLocalizations, isScreenWithShortestSide),
if (onMoreActionClick != null)
_getMoreButton(appLocalizations, isScreenWithShortestSide),
];
}
return [
_getReplyButton(appLocalizations),
if (!isOnlyEmailInThread)
_getReplyButton(appLocalizations),
if (!isResponsiveMobile)
_getMoveEmailButton(appLocalizations),
if (isResponsiveDesktop) ...[
_getMarkStarButton(appLocalizations),
_getDeleteButton(appLocalizations),
],
_getMoreButton(appLocalizations, isScreenWithShortestSide),
if (onMoreActionClick != null)
_getMoreButton(appLocalizations, isScreenWithShortestSide),
];
}
}
@@ -5,7 +5,6 @@ import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:core/presentation/utils/theme_utils.dart';
import 'package:core/presentation/views/button/tmail_button_widget.dart';
import 'package:core/presentation/views/image/avatar_builder.dart';
import 'package:core/utils/platform_info.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_svg/flutter_svg.dart';
@@ -42,6 +41,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget {
final OnTapAvatarActionClick? onTapAvatarActionClick;
final PresentationMailbox? mailboxContain;
final bool showUnreadVisualization;
final bool isOnlyEmailInThread;
const InformationSenderAndReceiverBuilder({
Key? key,
@@ -61,6 +61,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget {
this.onTapAvatarActionClick,
this.mailboxContain,
this.showUnreadVisualization = false,
this.isOnlyEmailInThread = false,
}) : super(key: key);
@override
@@ -164,9 +165,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget {
SizedBox(
height: IconUtils.defaultIconSize,
child: OverflowBox(
maxHeight: PlatformInfo.isIOS
? EmailViewAppBarWidgetStyles.heightIOS(context, responsiveUtils)
: EmailViewAppBarWidgetStyles.height,
maxHeight: EmailViewAppBarWidgetStyles.height,
fit: OverflowBoxFit.deferToChild,
child: EmailViewAppBarWidget(
key: const Key('email_view_app_bar_widget'),
@@ -183,6 +182,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget {
iconPadding: const EdgeInsets.all(8),
iconMargin: EdgeInsets.zero,
padding: EdgeInsets.zero,
isOnlyEmailInThread: isOnlyEmailInThread,
),
),
),
@@ -13,16 +13,28 @@ class GettingThreadById extends LoadingState {
class GetThreadByIdSuccess extends UIState {
final List<EmailId> emailIds;
final ThreadId? threadId;
final bool updateCurrentThreadDetail;
final bool skipLoadingMetadata;
GetThreadByIdSuccess(this.emailIds, {this.updateCurrentThreadDetail = false});
GetThreadByIdSuccess(
this.emailIds, {
required this.threadId,
this.updateCurrentThreadDetail = false,
this.skipLoadingMetadata = false,
});
@override
List<Object> get props => [emailIds, updateCurrentThreadDetail];
List<Object?> get props => [
emailIds,
threadId,
updateCurrentThreadDetail,
skipLoadingMetadata,
];
}
class PreloadEmailIdsInThreadSuccess extends GetThreadByIdSuccess {
PreloadEmailIdsInThreadSuccess(super.emailIds);
PreloadEmailIdsInThreadSuccess(super.emailIds, {required super.threadId});
}
class GetThreadByIdFailure extends FeatureFailure {
@@ -38,6 +38,7 @@ class GetThreadByIdInteractor {
yield Right(GetThreadByIdSuccess(
result,
threadId: threadId,
updateCurrentThreadDetail: updateCurrentThreadDetail,
));
} catch (e) {
@@ -1,36 +0,0 @@
import 'package:core/presentation/views/loading/cupertino_loading_widget.dart';
import 'package:flutter/material.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,
bool isExpanded = true,
}) {
if (!isLoading) return const SizedBox.shrink();
final child = Container(
alignment: Alignment.center,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(20)),
),
margin: isResponsiveDesktop
? const EdgeInsetsDirectional.only(end: 16)
: null,
padding: const EdgeInsets.only(top: 16),
child: const SizedBox(
width: 24,
height: 24,
child: CupertinoLoadingWidget(),
),
);
if (isExpanded) {
return Expanded(child: child);
} else {
return child;
}
}
}
@@ -98,16 +98,29 @@ extension GetThreadDetailEmailViews on ThreadDetailController {
);
}
if (isFirstEmailInThreadDetail) {
return Padding(
padding: const EdgeInsetsDirectional.only(bottom: 16),
child: EmailView(
key: GlobalObjectKey('${presentationEmail.id?.id.value ?? ''}firstInThread'),
isInsideThreadDetailView: true,
emailId: presentationEmail.id,
isFirstEmailInThreadDetail: true,
threadSubject: emailIdsPresentation.values.last?.subject,
onToggleThreadDetailCollapseExpand: () {
toggleThreadDetailCollapeExpand(presentationEmail);
},
scrollController: scrollController,
),
);
}
return Padding(
padding: const EdgeInsetsDirectional.only(bottom: 16),
child: EmailView(
key: GlobalObjectKey(presentationEmail.id?.id.value ?? ''),
isInsideThreadDetailView: emailIdsPresentation.length > 1,
isInsideThreadDetailView: true,
emailId: presentationEmail.id,
isFirstEmailInThreadDetail: isFirstEmailInThreadDetail,
threadSubject: isFirstEmailInThreadDetail
? emailIdsPresentation.values.last?.subject
: null,
onToggleThreadDetailCollapseExpand: () {
toggleThreadDetailCollapeExpand(presentationEmail);
},
@@ -5,7 +5,8 @@ extension HandleGetEmailIdsByThreadIdSuccess on ThreadDetailController {
void handleGetEmailIdsByThreadIdSuccess(
GetThreadByIdSuccess success,
) {
if (success.emailIds.isEmpty) {
final currentThreadId = mailboxDashBoardController.selectedEmail.value?.threadId;
if (success.emailIds.isEmpty || success.threadId != currentThreadId) {
return;
}
@@ -37,6 +37,7 @@ extension HandleGetEmailsByIdsSuccess on ThreadDetailController {
: EmailInThreadStatus.collapsed,
);
}
threadDetailManager.currentMobilePageViewIndex.refresh();
if (_skipScrollJump(isLoadMore)) return;
@@ -32,6 +32,7 @@ extension HandleRefreshThreadDetailAction on ThreadDetailController {
consumeState(Stream.value(Right(GetThreadByIdSuccess(
[currentEmailId],
updateCurrentThreadDetail: true,
threadId: mailboxDashBoardController.selectedEmail.value?.threadId,
))));
}
@@ -74,6 +75,7 @@ extension HandleRefreshThreadDetailAction on ThreadDetailController {
consumeState(Stream.value(Right(GetThreadByIdSuccess(
afterRefreshedEmailIds,
threadId: currentThreadId,
updateCurrentThreadDetail: true,
))));
consumeState(Stream.value(Right(GetEmailsByIdsSuccess(
@@ -9,6 +9,9 @@ import 'package:tmail_ui_user/features/thread_detail/presentation/utils/thread_d
extension InitializeThreadDetailEmails on ThreadDetailController {
void initializeThreadDetailEmails(GetThreadByIdSuccess success) {
final currentThreadId = mailboxDashBoardController.selectedEmail.value?.threadId;
if (success.skipLoadingMetadata || currentThreadId != success.threadId) return;
final selectedEmailId = mailboxDashBoardController.selectedEmail.value?.id;
if (skipLoadThreadMetaData(
selectedEmailId: selectedEmailId,
@@ -0,0 +1,34 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:model/email/presentation_email.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_manager.dart';
extension InitializeThreadDetailManager on ThreadDetailManager {
void initializeThreadDetailManager(
List<PresentationEmail> currentDisplayedEmails,
) {
availableThreadIds.value = currentDisplayedEmails
.map((presentationEmail) => presentationEmail.threadId)
.toSet()
.whereNotNull()
.toList();
if (availableThreadIds.isEmpty) return;
final selectedEmail = mailboxDashBoardController.selectedEmail.value;
if (isThreadDetailEnabled && selectedEmail?.threadId != null) {
final currentThreadIndex = availableThreadIds.indexOf(
selectedEmail!.threadId!,
);
currentMobilePageViewIndex.value = currentThreadIndex;
} else if (selectedEmail?.id != null) {
final currentEmailIndex = currentDisplayedEmails.indexOf(
selectedEmail!,
);
currentMobilePageViewIndex.value = currentEmailIndex;
}
pageController ??= PageController(
initialPage: currentMobilePageViewIndex.value,
);
}
}
@@ -0,0 +1,66 @@
import 'package:core/utils/platform_info.dart';
import 'package:flutter/animation.dart';
import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_next_previous_actions.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
extension OnThreadPageChanged on ThreadDetailController {
void onThreadPageChanged(int index) {
if (index > threadDetailManager.currentMobilePageViewIndex.value) {
onNext();
} else if (index < threadDetailManager.currentMobilePageViewIndex.value) {
onPrevious();
}
}
bool get nextAvailable =>
PlatformInfo.isWeb && threadDetailManager.nextAvailable;
bool get previousAvailable =>
PlatformInfo.isWeb && threadDetailManager.previousAvailable;
void onNext() {
if (currentExpandedEmailId.value != null) {
mailboxDashBoardController.dispatchEmailUIAction(
CollapseEmailInThreadDetailAction(currentExpandedEmailId.value!),
);
}
loadThreadOnThreadChanged = isThreadDetailEnabled;
threadDetailManager.onNext();
}
void onNextMobile() {
if (currentExpandedEmailId.value != null) {
mailboxDashBoardController.dispatchEmailUIAction(
CollapseEmailInThreadDetailAction(currentExpandedEmailId.value!),
);
}
loadThreadOnThreadChanged = isThreadDetailEnabled;
threadDetailManager.pageController?.nextPage(
duration: const Duration(milliseconds: 500),
curve: Curves.linear,
);
}
void onPrevious() {
if (currentExpandedEmailId.value != null) {
mailboxDashBoardController.dispatchEmailUIAction(
CollapseEmailInThreadDetailAction(currentExpandedEmailId.value!),
);
}
loadThreadOnThreadChanged = isThreadDetailEnabled;
threadDetailManager.onPrevious();
}
void onPreviousMobile() {
if (currentExpandedEmailId.value != null) {
mailboxDashBoardController.dispatchEmailUIAction(
CollapseEmailInThreadDetailAction(currentExpandedEmailId.value!),
);
}
loadThreadOnThreadChanged = isThreadDetailEnabled;
threadDetailManager.pageController?.previousPage(
duration: const Duration(milliseconds: 500),
curve: Curves.linear,
);
}
}
@@ -1,6 +1,6 @@
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_manager.dart';
extension RefreshThreadDetailOnSettingChanged on ThreadDetailController {
extension RefreshThreadDetailOnSettingChanged on ThreadDetailManager {
void refreshThreadDetailOnSettingChanged() {
if (threadDetailWasEnabled != isThreadDetailEnabled) {
threadDetailWasEnabled = isThreadDetailEnabled;
@@ -0,0 +1,70 @@
import 'package:collection/collection.dart';
import 'package:core/utils/platform_info.dart';
import 'package:flutter/widgets.dart';
import 'package:model/email/presentation_email.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_manager.dart';
import 'package:tmail_ui_user/main/routes/route_utils.dart';
extension ThreadDetailNextPreviousActions on ThreadDetailManager {
bool get nextAvailable => isThreadDetailEnabled
? currentThreadId != availableThreadIds.lastOrNull
: currentEmailId != currentDisplayedEmails.lastOrNull?.id;
void onNext() {
if (!nextAvailable) return;
final currentIndex = isThreadDetailEnabled
? availableThreadIds.indexOf(currentThreadId!)
: currentDisplayedEmails
.indexOf(mailboxDashBoardController.selectedEmail.value!);
_navigate(currentIndex + 1, isThreadDetailEnabled);
}
bool get previousAvailable => isThreadDetailEnabled
? currentThreadId != availableThreadIds.firstOrNull
: currentEmailId != currentDisplayedEmails.firstOrNull?.id;
void onPrevious() {
if (!previousAvailable) return;
final currentIndex = isThreadDetailEnabled
? availableThreadIds.indexOf(currentThreadId!)
: currentDisplayedEmails
.indexOf(mailboxDashBoardController.selectedEmail.value!);
_navigate(currentIndex - 1, isThreadDetailEnabled);
}
void _preparePageWithIndex(int index) {
currentMobilePageViewIndex.value = index;
pageController?.dispose();
pageController = PageController(initialPage: index);
}
void _goToPageWithEmail(PresentationEmail? presentationEmail) {
mailboxDashBoardController.setSelectedEmail(presentationEmail);
if (PlatformInfo.isWeb && presentationEmail?.routeWeb != null) {
RouteUtils.replaceBrowserHistory(
title: 'Email-${presentationEmail?.id?.id.value ?? ''}',
url: presentationEmail!.routeWeb!);
}
}
void _navigateToEmail(int emailIndex) {
final email = currentDisplayedEmails[emailIndex];
_preparePageWithIndex(emailIndex);
_goToPageWithEmail(email);
}
void _navigateToThread(int threadIndex) {
final threadId = availableThreadIds[threadIndex];
final email = currentDisplayedEmails.firstWhereOrNull(
(presentationEmail) => presentationEmail.threadId == threadId,
);
_preparePageWithIndex(threadIndex);
_goToPageWithEmail(email);
}
void _navigate(int index, bool isThreadDetailEnabled) {
isThreadDetailEnabled ? _navigateToThread(index) : _navigateToEmail(index);
}
}
@@ -5,6 +5,7 @@ import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action
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/domain/usecases/get_thread_by_id_interactor.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/action/thread_detail_ui_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/close_thread_detail_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
@@ -19,20 +20,43 @@ extension ThreadDetailOnSelectedEmailUpdated on ThreadDetailController {
return;
}
if (currentExpandedEmailId.value != null) {
mailboxDashBoardController.dispatchEmailUIAction(
DisposePreviousExpandedEmailAction(
currentExpandedEmailId.value!,
),
);
scrollController ??= ScrollController();
if (currentExpandedEmailId.value == null) {
loadThreadOnThreadChanged = isThreadDetailEnabled;
_preloadSelectedEmail(selectedEmail!);
return;
}
_preloadSelectedEmail(selectedEmail!);
// Thread setting updated, no need to dispose current single email controller
if (currentExpandedEmailId.value == selectedEmail!.id) {
_preloadSelectedEmail(selectedEmail);
if (isThreadDetailEnabled && selectedEmail.threadId != null) {
mailboxDashBoardController.dispatchThreadDetailUIAction(
LoadThreadDetailAfterSelectedEmailAction(
selectedEmail.threadId!,
)
);
}
return;
}
mailboxDashBoardController.dispatchEmailUIAction(
DisposePreviousExpandedEmailAction(
currentExpandedEmailId.value!,
),
);
loadThreadOnThreadChanged = isThreadDetailEnabled;
_preloadSelectedEmail(selectedEmail);
}
void _preloadSelectedEmail(PresentationEmail selectedEmail) {
consumeState(Stream.fromIterable([
Right(PreloadEmailIdsInThreadSuccess([selectedEmail.id!])),
Right(PreloadEmailIdsInThreadSuccess(
[selectedEmail.id!],
threadId: selectedEmail.threadId,
)),
Right(PreloadEmailsByIdsSuccess([selectedEmail])),
]));
}
@@ -11,6 +11,8 @@ extension ToggleThreadDetailCollapeExpand on ThreadDetailController {
if (emailId == null || expansionStatus == null) return;
if (expansionStatus == EmailInThreadStatus.expanded) {
if (emailIdsPresentation.length == 1) return;
emailIdsPresentation[emailId] = presentationEmail.copyWith(
emailInThreadStatus: EmailInThreadStatus.collapsed,
);
@@ -18,6 +20,7 @@ extension ToggleThreadDetailCollapeExpand on ThreadDetailController {
CollapseEmailInThreadDetailAction(emailId),
);
currentExpandedEmailId.value = null;
threadDetailManager.currentMobilePageViewIndex.refresh();
return;
}
@@ -42,5 +45,6 @@ extension ToggleThreadDetailCollapeExpand on ThreadDetailController {
);
}
}
threadDetailManager.currentMobilePageViewIndex.refresh();
}
}
@@ -18,6 +18,7 @@ import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_emails_
import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_by_id_interactor.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_detail_status_interactor.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_manager.dart';
import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart';
import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart';
@@ -31,8 +32,10 @@ class ThreadDetailBindings extends BaseBindings {
@override
void bindingsController() {
Get.put(ThreadDetailController(
Get.put(ThreadDetailManager(
Get.find<GetThreadDetailStatusInteractor>(),
));
Get.put(ThreadDetailController(
Get.find<GetThreadByIdInteractor>(),
Get.find<GetEmailsByIdsInteractor>(),
Get.find<MarkAsEmailReadInteractor>(),
@@ -1,3 +1,4 @@
import 'package:debounce_throttle/debounce_throttle.dart';
import 'package:flutter/material.dart';
import 'dart:async';
@@ -38,20 +39,17 @@ import 'package:tmail_ui_user/features/network_connection/presentation/network_c
import 'package:tmail_ui_user/features/search/email/presentation/search_email_controller.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/state/get_emails_by_ids_state.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/state/get_thread_detail_status_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/domain/usecases/get_emails_by_ids_interactor.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/action/thread_detail_ui_action.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_detail_status_interactor.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_email_moved_action.dart';
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/extension/thread_detail_on_selected_email_updated.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/model/thread_detail_setting_status.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_manager.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/handle_collapsed_email_download_states.dart';
@@ -60,7 +58,6 @@ import 'package:tmail_ui_user/features/thread_detail/presentation/extension/mark
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/quick_create_rule_from_collapsed_email_success.dart';
class ThreadDetailController extends BaseController {
final GetThreadDetailStatusInteractor _getThreadDetailStatusInteractor;
final GetThreadByIdInteractor _getEmailIdsByThreadIdInteractor;
final GetEmailsByIdsInteractor getEmailsByIdsInteractor;
final MarkAsEmailReadInteractor _markAsEmailReadInteractor;
@@ -70,7 +67,6 @@ class ThreadDetailController extends BaseController {
final DownloadAttachmentForWebInteractor _downloadAttachmentForWebInteractor;
ThreadDetailController(
this._getThreadDetailStatusInteractor,
this._getEmailIdsByThreadIdInteractor,
this.getEmailsByIdsInteractor,
this._markAsEmailReadInteractor,
@@ -90,18 +86,34 @@ class ThreadDetailController extends BaseController {
IndividualHeaderIdentifier.listUnsubscribeHeader.value,
});
final cachedEmailLoaded = <EmailId, EmailLoaded>{};
late final _threadGetDebouncer = Debouncer<ThreadId?>(
const Duration(milliseconds: 500),
initialValue: null,
checkEquality: true,
onChanged: (threadId) {
if (_validateLoadThread(threadId)) {
consumeState(_getEmailIdsByThreadIdInteractor.execute(
threadId!,
session!,
accountId!,
sentMailboxId!,
ownEmailAddress!,
selectedEmailId: mailboxDashBoardController.selectedEmail.value?.id,
));
}
},
);
final mailboxDashBoardController = Get.find<MailboxDashBoardController>();
final searchEmailController = Get.find<SearchEmailController>();
final networkConnectionController = Get.find<NetworkConnectionController>();
final threadDetailManager = Get.find<ThreadDetailManager>();
final downloadManager = Get.find<DownloadManager>();
final downloadProgressState = StreamController<Either<Failure, Success>>();
ScrollController? scrollController;
CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor;
AppLifecycleListener? appLifecycleListener;
ThreadDetailSettingStatus threadDetailSettingStatus = ThreadDetailSettingStatus.loading;
bool threadDetailWasEnabled = true;
bool loadThreadOnThreadChanged = false;
AccountId? get accountId => mailboxDashBoardController.accountId.value;
Session? get session => mailboxDashBoardController.sessionCurrent;
@@ -121,21 +133,11 @@ class ThreadDetailController extends BaseController {
bool get networkConnected =>
networkConnectionController.isNetworkConnectionAvailable();
bool get isThreadDetailEnabled =>
threadDetailSettingStatus == ThreadDetailSettingStatus.enabled;
threadDetailManager.isThreadDetailEnabled;
@override
void onInit() {
super.onInit();
consumeState(_getThreadDetailStatusInteractor.execute());
appLifecycleListener = AppLifecycleListener(
onResume: () {
if (threadDetailSettingStatus == ThreadDetailSettingStatus.loading) {
return;
}
consumeState(_getThreadDetailStatusInteractor.execute());
},
);
ever(mailboxDashBoardController.accountId, (accountId) {
if (accountId == null) return;
@@ -172,22 +174,10 @@ class ThreadDetailController extends BaseController {
})
..listUnsubscribeHeader?.clear();
}
} else if (action is UpdatedThreadDetailSettingAction) {
consumeState(_getThreadDetailStatusInteractor.execute());
} else if (action is EmailMovedAction) {
handleEmailMovedAction(action);
} else if (action is LoadThreadDetailAfterSelectedEmailAction) {
if (_validateLoadThread(action)) {
scrollController = ScrollController();
consumeState(_getEmailIdsByThreadIdInteractor.execute(
action.threadId,
session!,
accountId!,
sentMailboxId!,
ownEmailAddress!,
selectedEmailId: mailboxDashBoardController.selectedEmail.value?.id,
));
}
_threadGetDebouncer.value = action.threadId;
}
// Reset [threadDetailUIAction] to original value
mailboxDashBoardController.dispatchThreadDetailUIAction(
@@ -205,9 +195,9 @@ class ThreadDetailController extends BaseController {
});
}
bool _validateLoadThread(LoadThreadDetailAfterSelectedEmailAction action) {
bool _validateLoadThread(ThreadId? threadId) {
return mailboxDashBoardController.selectedEmail.value?.threadId != null &&
action.threadId == mailboxDashBoardController.selectedEmail.value?.threadId &&
threadId == mailboxDashBoardController.selectedEmail.value?.threadId &&
session != null &&
accountId != null &&
sentMailboxId != null &&
@@ -223,6 +213,7 @@ class ThreadDetailController extends BaseController {
currentExpandedEmailId.value = null;
currentEmailLoaded.value = null;
cachedEmailLoaded.clear();
_threadGetDebouncer.value = null;
}
@override
@@ -240,13 +231,6 @@ class ThreadDetailController extends BaseController {
quickCreateRuleFromCollapsedEmailSuccess(success);
} else if (success is DownloadAttachmentForWebSuccess) {
handleDownloadSuccess(success);
} else if (success is GetThreadDetailStatusSuccess) {
threadDetailSettingStatus = success.threadDetailEnabled
? ThreadDetailSettingStatus.enabled
: ThreadDetailSettingStatus.disabled;
refreshThreadDetailOnSettingChanged();
} else if (success is GettingThreadDetailStatus) {
threadDetailSettingStatus = ThreadDetailSettingStatus.loading;
} else {
super.handleSuccessViewState(success);
}
@@ -266,10 +250,6 @@ class ThreadDetailController extends BaseController {
handleDownloadFailure(failure);
return;
}
if (failure is GetThreadDetailStatusFailure) {
threadDetailSettingStatus = ThreadDetailSettingStatus.enabled;
refreshThreadDetailOnSettingChanged();
}
if (failure is PrintEmailFailure) {
if (currentOverlayContext != null && currentContext != null) {
appToast.showToastErrorMessage(
@@ -0,0 +1,121 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/utils/platform_info.dart';
import 'package:flutter/widgets.dart';
import 'package:get/get_instance/get_instance.dart';
import 'package:get/state_manager.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:model/email/presentation_email.dart';
import 'package:tmail_ui_user/features/base/base_controller.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/state/get_thread_detail_status_state.dart';
import 'package:tmail_ui_user/features/thread_detail/domain/usecases/get_thread_detail_status_interactor.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/action/thread_detail_ui_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/initialize_thread_detail_manager.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';
class ThreadDetailManager extends BaseController {
final mailboxDashBoardController = Get.find<MailboxDashBoardController>();
final GetThreadDetailStatusInteractor _getThreadDetailStatusInteractor;
ThreadDetailManager(this._getThreadDetailStatusInteractor);
final availableThreadIds = RxList<ThreadId>();
final currentMobilePageViewIndex = 0.obs;
PageController? pageController;
final threadDetailSettingStatus = ThreadDetailSettingStatus.loading.obs;
AppLifecycleListener? appLifecycleListener;
bool threadDetailWasEnabled = true;
bool get isSearchingOnMobile =>
mailboxDashBoardController.searchController.isSearchEmailRunning &&
PlatformInfo.isMobile;
RxList<PresentationEmail> get currentDisplayedEmails => isSearchingOnMobile
? mailboxDashBoardController.listResultSearch
: mailboxDashBoardController.emailsInCurrentMailbox;
ThreadId? get currentThreadId =>
mailboxDashBoardController.selectedEmail.value?.threadId;
EmailId? get currentEmailId =>
mailboxDashBoardController.selectedEmail.value?.id;
bool get isThreadDetailEnabled =>
threadDetailSettingStatus.value == ThreadDetailSettingStatus.enabled;
@override
void onInit() {
super.onInit();
consumeState(_getThreadDetailStatusInteractor.execute());
appLifecycleListener = AppLifecycleListener(
onResume: () {
if (threadDetailSettingStatus.value == ThreadDetailSettingStatus.loading) {
return;
}
consumeState(_getThreadDetailStatusInteractor.execute());
},
);
ever(mailboxDashBoardController.threadDetailUIAction, (action) {
if (action is UpdatedThreadDetailSettingAction) {
consumeState(_getThreadDetailStatusInteractor.execute());
}
// Reset [threadDetailUIAction] to original value
mailboxDashBoardController.dispatchThreadDetailUIAction(
ThreadDetailUIAction(),
);
});
ever(
currentDisplayedEmails,
(emails) {
if (mailboxDashBoardController.dashboardRoute.value != DashboardRoutes.threadDetailed) {
return;
}
initializeThreadDetailManager(emails);
},
);
ever(
mailboxDashBoardController.dashboardRoute,
(route) {
final selectedEmail = mailboxDashBoardController.selectedEmail.value;
if (route != DashboardRoutes.threadDetailed || selectedEmail == null) {
currentMobilePageViewIndex.value = -1;
pageController?.dispose();
pageController = null;
availableThreadIds.clear();
return;
}
initializeThreadDetailManager(currentDisplayedEmails);
},
);
}
@override
void handleSuccessViewState(Success success) {
if (success is GetThreadDetailStatusSuccess) {
threadDetailSettingStatus.value = success.threadDetailEnabled
? ThreadDetailSettingStatus.enabled
: ThreadDetailSettingStatus.disabled;
refreshThreadDetailOnSettingChanged();
} else if (success is GettingThreadDetailStatus) {
threadDetailSettingStatus.value = ThreadDetailSettingStatus.loading;
} else {
super.handleSuccessViewState(success);
}
}
@override
void handleFailureViewState(Failure failure) {
if (failure is GetThreadDetailStatusFailure) {
threadDetailSettingStatus.value = ThreadDetailSettingStatus.enabled;
refreshThreadDetailOnSettingChanged();
} else {
super.handleFailureViewState(failure);
}
}
}
@@ -2,37 +2,37 @@ 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:core/presentation/views/button/tmail_button_widget.dart';
import 'package:core/utils/platform_info.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:model/email/email_in_thread_status.dart';
import 'package:model/extensions/session_extension.dart';
import 'package:model/email/email_action_type.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart';
import 'package:tmail_ui_user/features/email/presentation/email_view.dart';
import 'package:tmail_ui_user/features/email/presentation/styles/email_view_app_bar_widget_styles.dart';
import 'package:tmail_ui_user/features/email/presentation/widgets/email_view_bottom_bar_widget.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/extension/close_thread_detail_action.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/get_thread_detail_email_mailbox_contains.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/get_thread_detail_loading_view.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/get_thread_details_email_views.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/on_thread_page_changed.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/extension/thread_detail_on_email_action_click.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/thread_detail_controller.dart';
import 'package:tmail_ui_user/features/thread_detail/presentation/widgets/thread_detail_app_bar.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
class ThreadDetailView extends GetWidget<ThreadDetailController> {
const ThreadDetailView({super.key});
@override
Widget build(BuildContext context) {
final multipleEmailsView = SelectionArea(child: SafeArea(
final child = SelectionArea(child: SafeArea(
child: Column(
children: [
Obx(() {
if (showLoadingView(controller.viewState.value)) {
return const SizedBox.shrink();
}
final isLoading = showLoadingView(controller.viewState.value);
return ThreadDetailAppBar(
responsiveUtils: controller.responsiveUtils,
@@ -42,39 +42,84 @@ class ThreadDetailView extends GetWidget<ThreadDetailController> {
lastEmailOfThread: controller.emailIdsPresentation.values.lastOrNull,
ownUserName: controller.session?.getOwnEmailAddress() ?? '',
mailboxContain: _getMailboxContain(),
onEmailActionClick: controller.threadDetailOnEmailActionClick,
onMoreActionClick: (presentationEmail, position) => controller.emailActionReactor.handleMoreEmailAction(
mailboxContain: controller.getThreadDetailEmailMailboxContains(presentationEmail),
presentationEmail: presentationEmail,
position: position,
responsiveUtils: controller.responsiveUtils,
imagePaths: controller.imagePaths,
username: controller.session?.username,
handleEmailAction: controller.threadDetailOnEmailActionClick,
additionalActions: [
if (controller.responsiveUtils.isMobile(context)) ...[
EmailActionType.forward,
EmailActionType.replyAll,
EmailActionType.replyToList,
],
EmailActionType.markAsStarred,
EmailActionType.unMarkAsStarred,
EmailActionType.moveToTrash,
EmailActionType.deletePermanently,
EmailActionType.printAll,
EmailActionType.moveToMailbox,
],
emailIsRead: presentationEmail.hasRead,
),
onEmailActionClick: isLoading
? null
: controller.threadDetailOnEmailActionClick,
onMoreActionClick: (presentationEmail, position) => isLoading
? null
: controller.emailActionReactor.handleMoreEmailAction(
mailboxContain: controller.getThreadDetailEmailMailboxContains(
presentationEmail,
),
presentationEmail: presentationEmail,
position: position,
responsiveUtils: controller.responsiveUtils,
imagePaths: controller.imagePaths,
username: controller.session?.username,
handleEmailAction: controller.threadDetailOnEmailActionClick,
additionalActions: [
if (controller.responsiveUtils.isMobile(context)) ...[
EmailActionType.forward,
EmailActionType.replyAll,
EmailActionType.replyToList,
],
EmailActionType.markAsStarred,
EmailActionType.unMarkAsStarred,
EmailActionType.moveToTrash,
EmailActionType.deletePermanently,
EmailActionType.printAll,
EmailActionType.moveToMailbox,
],
emailIsRead: presentationEmail.hasRead,
),
optionWidgets: [
if (controller.previousAvailable)
TMailButtonWidget.fromIcon(
icon: controller.imagePaths.icNewer,
iconSize: EmailViewAppBarWidgetStyles.buttonIconSize,
iconColor: EmailViewAppBarWidgetStyles.iconColor,
tooltipMessage: AppLocalizations.of(context).newer,
backgroundColor: Colors.transparent,
onTapActionCallback: controller.onPrevious,
),
if (controller.nextAvailable)
TMailButtonWidget.fromIcon(
icon: controller.imagePaths.icOlder,
iconSize: EmailViewAppBarWidgetStyles.buttonIconSize,
iconColor: EmailViewAppBarWidgetStyles.iconColor,
tooltipMessage: AppLocalizations.of(context).older,
backgroundColor: Colors.transparent,
onTapActionCallback: controller.onNext,
),
],
);
}),
Obx(() => controller.getThreadDetailLoadingView(
isResponsiveDesktop: controller.responsiveUtils.isDesktop(context),
isLoading: showLoadingView(controller.viewState.value),
)),
Obx(() {
if (showLoadingView(controller.viewState.value)) {
return const SizedBox.shrink();
if (PlatformInfo.isMobile) {
final manager = controller.threadDetailManager;
final currentIndex = manager.currentMobilePageViewIndex.value;
return Expanded(
child: PageView.builder(
controller: manager.pageController,
itemCount: manager.isThreadDetailEnabled
? manager.availableThreadIds.length
: manager.currentDisplayedEmails.length,
itemBuilder: (context, index) {
if (index != currentIndex) {
return const SizedBox.shrink();
}
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: controller.getThreadDetailEmailViews()
),
);
},
onPageChanged: controller.onThreadPageChanged,
),
);
}
return Expanded(
@@ -98,28 +143,18 @@ class ThreadDetailView extends GetWidget<ThreadDetailController> {
);
}),
Obx(() {
if (showLoadingView(controller.viewState.value)) {
return const SizedBox.shrink();
}
final expandedEmailId = controller.currentExpandedEmailId.value;
if (expandedEmailId == null) {
return _roundBottomPlaceHolder(
isDesktop: controller.responsiveUtils.isDesktop(context),
);
return const SizedBox.shrink();
}
final expandedPresentationEmail = controller.emailIdsPresentation[expandedEmailId];
if (expandedPresentationEmail == null) {
return _roundBottomPlaceHolder(
isDesktop: controller.responsiveUtils.isDesktop(context),
);
return const SizedBox.shrink();
}
final currentEmailLoaded = controller.currentEmailLoaded.value;
if (currentEmailLoaded == null) {
return _roundBottomPlaceHolder(
isDesktop: controller.responsiveUtils.isDesktop(context),
);
return const SizedBox.shrink();
}
return Padding(
@@ -162,29 +197,10 @@ class ThreadDetailView extends GetWidget<ThreadDetailController> {
),
));
return Obx(() {
if (showLoadingView(controller.viewState.value) &&
controller.emailIdsPresentation.isEmpty &&
controller.responsiveUtils.isTabletLarge(context)) {
return controller.getThreadDetailLoadingView(
isResponsiveDesktop: false,
isLoading: true,
isExpanded: false,
);
}
if (controller.emailIdsPresentation.length == 1 &&
controller.emailIdsPresentation.values.firstOrNull?.emailInThreadStatus == EmailInThreadStatus.expanded) {
final emailId = controller.emailIdsPresentation.values.firstOrNull!.id;
return EmailView(
key: ValueKey(emailId?.id.value),
emailId: emailId,
);
}
return multipleEmailsView;
});
return ClipRRect(
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(20)),
child: child,
);
}
EdgeInsetsGeometry _padding(BuildContext context) {
@@ -202,21 +218,6 @@ class ThreadDetailView extends GetWidget<ThreadDetailController> {
return controller.mailboxDashBoardController.selectedMailbox.value;
}
Widget _roundBottomPlaceHolder({required bool isDesktop}) {
return Container(
height: 40,
margin: isDesktop
? const EdgeInsetsDirectional.only(end: 16)
: EdgeInsets.zero,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(20),
),
)
);
}
bool showLoadingView(Either<Failure, Success> viewState) {
final viewStateValue = viewState.fold(id, id);
return viewStateValue is GettingThreadById &&
@@ -139,21 +139,24 @@ class ThreadDetailAppBar extends StatelessWidget {
),
],
if (!responsiveUtils.isMobile(context)) const Spacer(),
...optionWidgets,
] else const Spacer(),
...optionWidgets,
if (lastEmailOfThread != null)
TMailButtonWidget.fromIcon(
icon: imagePaths.icMoreVertical,
iconSize: EmailViewAppBarWidgetStyles.buttonIconSize,
iconColor: EmailViewAppBarWidgetStyles.iconColor,
backgroundColor: Colors.transparent,
tooltipMessage: AppLocalizations.of(context).more,
onTapActionCallback: responsiveUtils.isScreenWithShortestSide(context)
onTapActionCallback: responsiveUtils.isScreenWithShortestSide(context) &&
lastEmailOfThread != null
? () => onMoreActionClick?.call(lastEmailOfThread!, null)
: null,
onTapActionAtPositionCallback: !responsiveUtils.isScreenWithShortestSide(context)
onTapActionAtPositionCallback: !responsiveUtils.isScreenWithShortestSide(context) &&
lastEmailOfThread != null
? (position) => onMoreActionClick?.call(lastEmailOfThread!, position)
: null,
),
] else const Spacer(),
],
),
);
@@ -31,10 +31,14 @@ void main() {
};
when(threadDetailController.emailIdsPresentation)
.thenReturn(initialEmailIdsPresentation.obs);
when(threadDetailController.mailboxDashBoardController)
.thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail)
.thenReturn(Rxn(PresentationEmail(id: EmailId(Id('1')))));
// act
threadDetailController.handleGetEmailIdsByThreadIdSuccess(
GetThreadByIdSuccess([]),
GetThreadByIdSuccess([], threadId: null),
);
// assert
@@ -58,6 +62,10 @@ void main() {
};
when(threadDetailController.emailIdsPresentation)
.thenReturn(initialEmailIdsPresentation.obs);
when(threadDetailController.mailboxDashBoardController)
.thenReturn(mailboxDashBoardController);
when(mailboxDashBoardController.selectedEmail)
.thenReturn(Rxn(PresentationEmail(id: EmailId(Id('1')))));
// act
threadDetailController.handleGetEmailIdsByThreadIdSuccess(
@@ -67,6 +75,7 @@ void main() {
EmailId(Id('3')),
EmailId(Id('4')),
],
threadId: null,
updateCurrentThreadDetail: true,
),
);
@@ -113,6 +122,7 @@ void main() {
EmailId(Id('3')),
EmailId(Id('4')),
],
threadId: null,
updateCurrentThreadDetail: false,
),
);
@@ -156,6 +166,7 @@ void main() {
EmailId(Id('3')),
EmailId(Id('4')),
],
threadId: null,
updateCurrentThreadDetail: false,
),
);
@@ -69,7 +69,7 @@ void main() {
// Act
threadDetailController.initializeThreadDetailEmails(
GetThreadByIdSuccess(emailIds),
GetThreadByIdSuccess(emailIds, threadId: null),
);
// Assert
@@ -85,7 +85,7 @@ void main() {
expect(
streamsConsumed,
emitsInOrder([
Right(PreloadEmailIdsInThreadSuccess([selectedEmail.id!])),
Right(PreloadEmailIdsInThreadSuccess([selectedEmail.id!], threadId: ThreadId(Id('1')))),
Right(PreloadEmailsByIdsSuccess([selectedEmail])),
]),
);