Change order of attachment and mail body on desktop

Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
dab246
2025-07-09 14:20:55 +07:00
committed by Dat H. Pham
parent 296065c0b7
commit 3ce7c60cef
5 changed files with 253 additions and 283 deletions
@@ -1,9 +1,7 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/mail/mail_address.dart';
import 'package:flutter/material.dart';
import 'package:get/get_utils/src/get_utils/get_utils.dart';
import 'package:dartz/dartz.dart';
import 'package:http_parser/http_parser.dart';
@@ -23,6 +21,10 @@ import 'package:tmail_ui_user/main/error/capability_validator.dart';
import 'package:tmail_ui_user/main/routes/route_utils.dart';
class EmailUtils {
static const double desktopItemMaxWidth = 260;
static const double desktopMoreButtonMaxWidth = 70;
static const double maxMobileVisibleAttachments = 3;
EmailUtils._();
static Properties getPropertiesForEmailGetMethod(Session session, AccountId accountId) {
@@ -237,29 +239,29 @@ class EmailUtils {
}
static List<Attachment> getAttachmentDisplayed({
required BuildContext context,
required double maxWidth,
required bool platformIsMobile,
required List<Attachment> attachments,
required ResponsiveUtils responsiveUtils,
required bool isMobile,
}) {
if (attachments.isEmpty) return [];
final bool isMobile = responsiveUtils.isMobile(context);
if (isMobile) {
return attachments.length <= 3 ? attachments : attachments.sublist(0, 3);
return attachments.length <= maxMobileVisibleAttachments
? attachments
: attachments.sublist(0, 3);
}
const double maxWidthItem = 260;
const double buttonMoreMaxWidth = 120;
final displayedCount = maxWidth ~/ desktopItemMaxWidth;
if (displayedCount == attachments.length) {
return attachments;
} else {
final int possibleDisplayedCount =
((maxWidth - desktopMoreButtonMaxWidth) ~/ desktopItemMaxWidth)
.clamp(0, attachments.length);
final int possibleDisplayedCount =
((maxWidth - buttonMoreMaxWidth) ~/ maxWidthItem)
.clamp(0, attachments.length);
return possibleDisplayedCount == 0
? [attachments.first]
: attachments.sublist(0, possibleDisplayedCount);
return possibleDisplayedCount == 0
? [attachments.first]
: attachments.sublist(0, possibleDisplayedCount);
}
}
}
@@ -22,6 +22,7 @@ class AttachmentItemWidget extends StatelessWidget {
final Attachment attachment;
final ImagePaths imagePaths;
final double? width;
final EdgeInsetsGeometry? margin;
final OnDownloadAttachmentFileAction? downloadAttachmentAction;
final OnViewAttachmentFileAction? viewAttachmentAction;
final String? singleEmailControllerTag;
@@ -31,6 +32,7 @@ class AttachmentItemWidget extends StatelessWidget {
required this.attachment,
required this.imagePaths,
this.width,
this.margin,
this.downloadAttachmentAction,
this.viewAttachmentAction,
this.singleEmailControllerTag,
@@ -124,7 +126,7 @@ class AttachmentItemWidget extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 8),
backgroundColor: Colors.transparent,
width: width,
margin: const EdgeInsets.only(top: 8),
margin: margin,
onTapActionCallback:
isLoading ? null : () => _onViewOrDownloadAction(attachment),
child: bodyItemWidget,
@@ -12,6 +12,7 @@ class DraggableAttachmentItemWidget extends StatelessWidget{
final Attachment attachment;
final ImagePaths imagePaths;
final double? width;
final OnDragAttachmentStarted? onDragStarted;
final OnDragAttachmentEnd? onDragEnd;
final OnDownloadAttachmentFileAction? downloadAttachmentAction;
@@ -22,6 +23,7 @@ class DraggableAttachmentItemWidget extends StatelessWidget{
Key? key,
required this.attachment,
required this.imagePaths,
this.width,
this.onDragStarted,
this.onDragEnd,
this.downloadAttachmentAction,
@@ -39,6 +41,7 @@ class DraggableAttachmentItemWidget extends StatelessWidget{
child: AttachmentItemWidget(
attachment: attachment,
imagePaths: imagePaths,
width: width,
downloadAttachmentAction: downloadAttachmentAction,
viewAttachmentAction: viewAttachmentAction,
singleEmailControllerTag: singleEmailControllerTag,
@@ -4,7 +4,7 @@ import 'package:core/presentation/resources/image_paths.dart';
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/utils/platform_info.dart';
import 'package:core/utils/app_logger.dart';
import 'package:filesize/filesize.dart';
import 'package:flutter/material.dart';
import 'package:model/email/attachment.dart';
@@ -46,41 +46,46 @@ class EmailAttachmentsWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
final attachmentRecord = _getDisplayedAndHiddenAttachment(
context,
constraints.maxWidth,
);
final attachmentHeader = AttachmentsInfo(
imagePaths: imagePaths,
numberOfAttachments: attachments.length,
totalSizeInfo: filesize(attachments.totalSize, 1),
responsiveUtils: responsiveUtils,
onTapShowAllAttachmentFile: onTapShowAllAttachmentFile,
onTapDownloadAllButton: showDownloadAllAttachmentsButton
final attachmentHeader = AttachmentsInfo(
imagePaths: imagePaths,
numberOfAttachments: attachments.length,
totalSizeInfo: filesize(attachments.totalSize, 1),
responsiveUtils: responsiveUtils,
onTapShowAllAttachmentFile: onTapShowAllAttachmentFile,
onTapDownloadAllButton: showDownloadAllAttachmentsButton
? onTapDownloadAllButton
: null,
);
if (responsiveUtils.isMobile(context)) {
final attachmentRecord = _getDisplayedAndHiddenAttachment(
context,
responsiveUtils.getDeviceWidth(context),
);
final displayedAttachments = attachmentRecord.displayedAttachments;
final hiddenItemsCount = attachmentRecord.hiddenItemsCount;
late Widget attachmentWidget;
if (responsiveUtils.isMobile(context)) {
attachmentWidget = Column(
return Padding(
padding: const EdgeInsetsDirectional.only(top: 12, bottom: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsetsDirectional.symmetric(horizontal: 12),
child: attachmentHeader,
),
const SizedBox(height: 4),
Padding(
padding: const EdgeInsetsDirectional.symmetric(horizontal: 12),
padding: const EdgeInsetsDirectional.only(
start: 12,
end: 12,
top: 4,
),
child: Column(
children: attachmentRecord.displayedAttachments.map((attachment) {
children: displayedAttachments.map((attachment) {
return AttachmentItemWidget(
attachment: attachment,
imagePaths: imagePaths,
margin: const EdgeInsets.only(top: 8),
downloadAttachmentAction: downloadAttachmentAction,
viewAttachmentAction: viewAttachmentAction,
singleEmailControllerTag: singleEmailControllerTag,
@@ -89,15 +94,14 @@ class EmailAttachmentsWidget extends StatelessWidget {
),
),
const SizedBox(height: 12),
if (attachmentRecord.hiddenItemsCount > 0 &&
showDownloadAllAttachmentsButton)
if (hiddenItemsCount > 0 && showDownloadAllAttachmentsButton)
Padding(
padding: const EdgeInsetsDirectional.symmetric(horizontal: 8),
child: Row(
children: [
TMailButtonWidget.fromText(
text: AppLocalizations.of(context).moreAttachments(
attachmentRecord.hiddenItemsCount,
hiddenItemsCount,
),
backgroundColor: Colors.transparent,
borderRadius: 5,
@@ -117,7 +121,9 @@ class EmailAttachmentsWidget extends StatelessWidget {
children: [
Flexible(
child: TMailButtonWidget(
text: AppLocalizations.of(context).archiveAndDownload,
text: AppLocalizations
.of(context)
.archiveAndDownload,
icon: imagePaths.icDownloadAll,
iconSize: 20,
iconColor: AppColor.steelGrayA540,
@@ -131,9 +137,8 @@ class EmailAttachmentsWidget extends StatelessWidget {
vertical: 3,
horizontal: 5,
),
textStyle: ThemeUtils.textStyleBodyBody1().copyWith(
color: AppColor.steelGray400,
),
textStyle: ThemeUtils.textStyleBodyBody1()
.copyWith(color: AppColor.steelGray400),
onTapActionCallback: onTapDownloadAllButton,
),
),
@@ -143,10 +148,10 @@ class EmailAttachmentsWidget extends StatelessWidget {
],
),
)
else if (attachmentRecord.hiddenItemsCount > 0)
else if (hiddenItemsCount > 0)
TMailButtonWidget.fromText(
text: AppLocalizations.of(context).moreAttachments(
attachmentRecord.hiddenItemsCount,
hiddenItemsCount,
),
backgroundColor: Colors.transparent,
borderRadius: 5,
@@ -160,97 +165,127 @@ class EmailAttachmentsWidget extends StatelessWidget {
onTapActionCallback: onTapShowAllAttachmentFile,
)
else if (showDownloadAllAttachmentsButton)
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Flexible(
child: TMailButtonWidget(
text: AppLocalizations.of(context).archiveAndDownload,
icon: imagePaths.icDownloadAll,
iconSize: 20,
iconColor: AppColor.steelGrayA540,
iconAlignment: TextDirection.rtl,
backgroundColor: Colors.transparent,
borderRadius: 5,
mainAxisSize: MainAxisSize.min,
maxLines: 1,
flexibleText: true,
padding: const EdgeInsets.symmetric(
vertical: 3,
horizontal: 5,
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Flexible(
child: TMailButtonWidget(
text: AppLocalizations.of(context).archiveAndDownload,
icon: imagePaths.icDownloadAll,
iconSize: 20,
iconColor: AppColor.steelGrayA540,
iconAlignment: TextDirection.rtl,
backgroundColor: Colors.transparent,
borderRadius: 5,
mainAxisSize: MainAxisSize.min,
maxLines: 1,
flexibleText: true,
padding: const EdgeInsets.symmetric(
vertical: 3,
horizontal: 5,
),
margin: const EdgeInsetsDirectional.symmetric(
horizontal: 8,
),
textStyle: ThemeUtils.textStyleBodyBody1().copyWith(
color: AppColor.steelGray400,
),
onTapActionCallback: onTapDownloadAllButton,
),
margin: const EdgeInsetsDirectional.symmetric(horizontal: 8),
textStyle: ThemeUtils.textStyleBodyBody1().copyWith(
color: AppColor.steelGray400,
),
onTapActionCallback: onTapDownloadAllButton,
),
),
],
),
],
),
],
);
} else {
attachmentWidget = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsetsDirectional.symmetric(horizontal: 12),
child: attachmentHeader,
),
const SizedBox(height: 4),
Padding(
padding: const EdgeInsetsDirectional.symmetric(horizontal: 12),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: attachmentRecord.displayedAttachments.map((attachment) {
if (responsiveUtils.isDesktop(context)) {
return DraggableAttachmentItemWidget(
attachment: attachment,
imagePaths: imagePaths,
onDragStarted: onDragStarted,
onDragEnd: onDragEnd,
downloadAttachmentAction: downloadAttachmentAction,
viewAttachmentAction: viewAttachmentAction,
singleEmailControllerTag: singleEmailControllerTag,
);
} else {
return AttachmentItemWidget(
attachment: attachment,
imagePaths: imagePaths,
width: 260,
downloadAttachmentAction: downloadAttachmentAction,
viewAttachmentAction: viewAttachmentAction,
singleEmailControllerTag: singleEmailControllerTag,
);
}
}).toList(),
),
),
],
);
}
return Padding(
padding: const EdgeInsetsDirectional.only(top: 12, bottom: 24),
child: attachmentWidget,
),
);
});
} else {
return Padding(
padding: const EdgeInsetsDirectional.only(
start: 16,
end: 16,
top: 16,
bottom: 28,
),
child: LayoutBuilder(
builder: (context, constraints) {
final attachmentRecord = _getDisplayedAndHiddenAttachment(
context,
constraints.maxWidth,
);
final displayedAttachments = attachmentRecord.displayedAttachments;
final hiddenItemsCount = attachmentRecord.hiddenItemsCount;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsetsDirectional.symmetric(horizontal: 12),
child: attachmentHeader,
),
Padding(
padding: const EdgeInsetsDirectional.only(
start: 12,
end: 12,
top: 12,
),
child: Wrap(
spacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
...displayedAttachments.map((attachment) {
if (responsiveUtils.isDesktop(context)) {
return DraggableAttachmentItemWidget(
attachment: attachment,
imagePaths: imagePaths,
width: EmailUtils.desktopItemMaxWidth,
onDragStarted: onDragStarted,
onDragEnd: onDragEnd,
downloadAttachmentAction: downloadAttachmentAction,
viewAttachmentAction: viewAttachmentAction,
singleEmailControllerTag: singleEmailControllerTag,
);
} else {
return AttachmentItemWidget(
attachment: attachment,
imagePaths: imagePaths,
width: EmailUtils.desktopItemMaxWidth,
downloadAttachmentAction: downloadAttachmentAction,
viewAttachmentAction: viewAttachmentAction,
singleEmailControllerTag: singleEmailControllerTag,
);
}
}).toList(),
if (hiddenItemsCount > 0)
TMailButtonWidget.fromText(
text: '+ $hiddenItemsCount',
backgroundColor: Colors.transparent,
borderRadius: 5,
maxWidth: EmailUtils.desktopMoreButtonMaxWidth,
maxLines: 1,
textStyle: ThemeUtils.textStyleM3TitleSmall,
padding: const EdgeInsets.symmetric(
vertical: 3,
horizontal: 5,
),
onTapActionCallback: onTapShowAllAttachmentFile,
),
],
),
),
],
);
}
),
);
}
}
({List<Attachment> displayedAttachments, int hiddenItemsCount})
_getDisplayedAndHiddenAttachment(BuildContext context, double maxWidth) {
const itemSpace = 8.0;
const itemHorizontalPadding = 24;
final maxWidthDesktop = maxWidth - itemHorizontalPadding - itemSpace * 2;
final displayedAttachments = EmailUtils.getAttachmentDisplayed(
context: context,
maxWidth: maxWidthDesktop,
platformIsMobile: PlatformInfo.isMobile,
maxWidth: maxWidth,
attachments: attachments,
responsiveUtils: responsiveUtils,
isMobile: responsiveUtils.isMobile(context),
);
int hiddenItemsCount = attachments.length - displayedAttachments.length;
@@ -259,7 +294,7 @@ class EmailAttachmentsWidget extends StatelessWidget {
} else if (hiddenItemsCount < 0) {
hiddenItemsCount = 0;
}
log('EmailAttachmentsWidget::_getDisplayedAndHiddenAttachment: Displayed: ${displayedAttachments.length}, Hidden: $hiddenItemsCount');
return (
displayedAttachments: displayedAttachments,
hiddenItemsCount: hiddenItemsCount,
@@ -1,160 +1,88 @@
import 'package:core/presentation/utils/responsive_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:model/email/attachment.dart';
import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart';
import 'get_attachment_displayed_test.mocks.dart';
@GenerateMocks([BuildContext, ResponsiveUtils])
void main() {
late MockBuildContext mockContext;
late MockResponsiveUtils mockResponsiveUtils;
late List<Attachment> attachments;
List<Attachment> generateAttachments(int count) =>
List.generate(count, (i) => Attachment(name: 'A$i'));
setUp(() {
mockContext = MockBuildContext();
mockResponsiveUtils = MockResponsiveUtils();
attachments = [
Attachment(name: 'file1.pdf'),
Attachment(name: 'file2.jpg'),
Attachment(name: 'file3.png'),
];
group('EmailUtils::getAttachmentDisplayed::', () {
test('Should returns empty list when attachments list is empty', () {
expect(
EmailUtils.getAttachmentDisplayed(
maxWidth: 1000,
attachments: [],
isMobile: true,
),
[]);
});
group('on mobile devices', () {
test('Should returns all attachments when there are 3 or fewer', () {
final attachments = generateAttachments(2);
final result = EmailUtils.getAttachmentDisplayed(
maxWidth: 1000,
attachments: attachments,
isMobile: true,
);
expect(result, attachments);
});
test('Should returns first 3 attachments when more than 3 exist', () {
final attachments = generateAttachments(5);
final result = EmailUtils.getAttachmentDisplayed(
maxWidth: 1000,
attachments: attachments,
isMobile: true,
);
expect(result, attachments.sublist(0, 3));
});
});
group('on non-mobile (desktop/web)', () {
test('Should returns all attachments if maxWidth can fit all items', () {
final attachments = generateAttachments(2);
final result = EmailUtils.getAttachmentDisplayed(
maxWidth: 700,
attachments: attachments,
isMobile: false,
);
expect(result, attachments);
});
test('Should returns only the number of items that fit in maxWidth', () {
final attachments = generateAttachments(5);
final result = EmailUtils.getAttachmentDisplayed(
maxWidth: 900,
attachments: attachments,
isMobile: false,
);
expect(result, attachments.sublist(0, 3));
});
test(
'Should returns only the first attachment if maxWidth is too small for any',
() {
final attachments = generateAttachments(3);
final result = EmailUtils.getAttachmentDisplayed(
maxWidth: 100,
attachments: attachments,
isMobile: false,
);
expect(result, [attachments.first]);
});
test(
'Should clamps the number of displayed attachments to the list length',
() {
final attachments = generateAttachments(2);
final result = EmailUtils.getAttachmentDisplayed(
maxWidth: 2000,
attachments: attachments,
isMobile: false,
);
expect(result, attachments);
});
});
});
test('should return empty list when attachments are empty', () {
final result = EmailUtils.getAttachmentDisplayed(
context: mockContext,
responsiveUtils: mockResponsiveUtils,
maxWidth: 300.0,
platformIsMobile: true,
attachments: [],
);
expect(result, []);
});
test('should return all attachments when mobile and attachments < 3', () {
when(mockResponsiveUtils.isMobile(mockContext)).thenReturn(true);
final result = EmailUtils.getAttachmentDisplayed(
context: mockContext,
responsiveUtils: mockResponsiveUtils,
maxWidth: 300.0,
platformIsMobile: true,
attachments: attachments.sublist(0, 2),
);
expect(result.length, 2);
expect(result, attachments.sublist(0, 2));
});
test('should return first 3 attachments when mobile and attachments >= 4', () {
when(mockResponsiveUtils.isMobile(mockContext)).thenReturn(true);
final result = EmailUtils.getAttachmentDisplayed(
context: mockContext,
responsiveUtils: mockResponsiveUtils,
maxWidth: 300.0,
platformIsMobile: true,
attachments: attachments,
);
expect(result.length, 3);
expect(result, attachments.sublist(0, 3));
});
test('should return sublist based on maxWidthTabletLarge when responsiveIsTabletLarge is true', () {
when(mockResponsiveUtils.isMobile(mockContext)).thenReturn(false);
when(mockResponsiveUtils.isTablet(mockContext)).thenReturn(false);
when(mockResponsiveUtils.isTabletLarge(mockContext)).thenReturn(true);
final result = EmailUtils.getAttachmentDisplayed(
context: mockContext,
responsiveUtils: mockResponsiveUtils,
maxWidth: 360.0,
platformIsMobile: false,
attachments: attachments,
);
expect(result.length, 1);
expect(result, attachments.sublist(0, 1));
});
test('should return sublist based on maxWidthTablet when responsiveIsTablet is true', () {
when(mockResponsiveUtils.isMobile(mockContext)).thenReturn(false);
when(mockResponsiveUtils.isTablet(mockContext)).thenReturn(true);
when(mockResponsiveUtils.isTabletLarge(mockContext)).thenReturn(false);
final result = EmailUtils.getAttachmentDisplayed(
context: mockContext,
responsiveUtils: mockResponsiveUtils,
maxWidth: 320.0,
platformIsMobile: false,
attachments: attachments,
);
expect(result.length, 1);
expect(result, attachments.sublist(0, 1));
});
test('should return sublist based on maxWidthMobile when not tablet or tabletLarge', () {
when(mockResponsiveUtils.isMobile(mockContext)).thenReturn(false);
when(mockResponsiveUtils.isTablet(mockContext)).thenReturn(false);
when(mockResponsiveUtils.isTabletLarge(mockContext)).thenReturn(false);
final result = EmailUtils.getAttachmentDisplayed(
context: mockContext,
responsiveUtils: mockResponsiveUtils,
maxWidth: 280.0,
platformIsMobile: false,
attachments: attachments,
);
expect(result.length, 1);
expect(result, attachments.sublist(0, 1));
});
test('should return sublist based on maxWidthMobile when platformIsMobile and responsiveIsMobile are true', () {
when(mockResponsiveUtils.isTablet(mockContext)).thenReturn(false);
when(mockResponsiveUtils.isTabletLarge(mockContext)).thenReturn(false);
when(mockResponsiveUtils.isMobile(mockContext)).thenReturn(true);
final result = EmailUtils.getAttachmentDisplayed(
context: mockContext,
responsiveUtils: mockResponsiveUtils,
maxWidth: 280.0,
platformIsMobile: true,
attachments: attachments,
);
expect(result.length, 3);
expect(result, attachments.sublist(0, 3));
});
test('should return sublist based on maxWidthTablet when platformIsMobile is true and responsiveIsMobile is false', () {
when(mockResponsiveUtils.isMobile(mockContext)).thenReturn(false);
when(mockResponsiveUtils.isTablet(mockContext)).thenReturn(false);
when(mockResponsiveUtils.isTabletLarge(mockContext)).thenReturn(false);
final result = EmailUtils.getAttachmentDisplayed(
context: mockContext,
responsiveUtils: mockResponsiveUtils,
maxWidth: 320.0,
platformIsMobile: true,
attachments: attachments,
);
expect(result.length, 1);
expect(result, attachments.sublist(0, 1));
});
test('should return first attachment when possibleDisplayedCount is 0', () {
when(mockResponsiveUtils.isMobile(mockContext)).thenReturn(false);
when(mockResponsiveUtils.isTablet(mockContext)).thenReturn(false);
when(mockResponsiveUtils.isTabletLarge(mockContext)).thenReturn(false);
final result = EmailUtils.getAttachmentDisplayed(
context: mockContext,
responsiveUtils: mockResponsiveUtils,
maxWidth: 100.0,
platformIsMobile: false,
attachments: attachments,
);
expect(result.length, 1);
expect(result.first, attachments.first);
});
}
}