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
@@ -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();
}
}