Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display limits of email recovery #3281

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions core/lib/utils/duration_utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
class DurationUtils {

// this class is a local copy of the one found at
// https://github.com/desktop-dart/duration/blob/master/lib/src/parse/parse.dart#L6
static Duration parseDuration(String input, {String separator = ','}) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why we need this? how about using dependency directly?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remembered it was asked by @tddang-linagora

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should minimize the using of third party library. If we only need a small function, let's copy it over.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from my point of view, we should use 3rd party lib to get the support from the community, don't need effort in maintaining it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tend to agree with @hoangdat .

Why maintain this code if we have it from a healthy package (size, community, etc..)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'll drop the commit removed package duration usages when squashing then (if ok for you @tddang-linagora)

bool isNegative = false;
if (input.startsWith('-')) {
isNegative = true;
input = input.substring(1);
} else if (input.startsWith('+')) {
input = input.substring(1);
}

final parts = input.split(separator).map((t) => t.trim()).toList();

int? weeks;
int? days;
int? hours;
int? minutes;
int? seconds;
int? milliseconds;
int? microseconds;

for (String part in parts) {
final match = RegExp(r'^(\d+)(w|d|h|min|m|s|ms|us)$').matchAsPrefix(part);
if (match == null) throw const FormatException('Invalid duration format');

int value = int.parse(match.group(1)!);
String? unit = match.group(2);

switch (unit) {
case 'w':
if (weeks != null) {
throw const FormatException('Weeks specified multiple times');
}
weeks = value;
break;
case 'd':
if (days != null) {
throw const FormatException('Days specified multiple times');
}
days = value;
break;
case 'h':
if (hours != null) {
throw const FormatException('Hours specified multiple times');
}
hours = value;
break;
case 'min':
case 'm':
if (minutes != null) {
throw const FormatException('Minutes specified multiple times');
}
minutes = value;
break;
case 's':
if (seconds != null) {
throw const FormatException('Seconds specified multiple times');
}
seconds = value;
break;
case 'ms':
if (milliseconds != null) {
throw const FormatException('Milliseconds specified multiple times');
}
milliseconds = value;
break;
case 'us':
if (microseconds != null) {
throw const FormatException('Microseconds specified multiple times');
}
microseconds = value;
break;
default:
throw FormatException('Invalid duration unit $unit');
}
}

var ret = Duration(
days: (days ?? 0) + (weeks ?? 0) * 7,
hours: hours ?? 0,
minutes: minutes ?? 0,
seconds: seconds ?? 0,
milliseconds: milliseconds ?? 0,
microseconds: microseconds ?? 0);
return isNegative ? -ret : ret;
}
}
18 changes: 18 additions & 0 deletions core/test/utils/duration_utils_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:core/utils/duration_utils.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {

group('parseDuration', () {
test('duration', () {
expect((DurationUtils.parseDuration('20d,10h,30m')).toString(), '490:30:00.000000');
});

test('duration weeks', () {
expect((DurationUtils.parseDuration('2w,20d,10h,30m')).toString(), '826:30:00.000000');
});
test('Negative', () {
expect(DurationUtils.parseDuration('-20d,10h,30m').toString(), '-490:30:00.000000');
});
});
}
2 changes: 1 addition & 1 deletion lib/features/base/mixin/date_range_picker_mixin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ mixin DateRangePickerMixin {
last7daysTitle: AppLocalizations.of(context).last7Days,
last30daysTitle: AppLocalizations.of(context).last30Days,
last6monthsTitle: AppLocalizations.of(context).last6Months,
lastYearTitle: AppLocalizations.of(context).lastYears,
lastYearTitle: AppLocalizations.of(context).last1Year,
initStartDate: initStartDate,
initEndDate: initEndDate,
autoClose: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import 'package:core/presentation/utils/keyboard_utils.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/duration_utils.dart';
import 'package:core/utils/platform_info.dart';
import 'package:email_recovery/email_recovery/capability_deleted_messages_vault.dart';
import 'package:email_recovery/email_recovery/email_recovery_action.dart';
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart';
import 'package:jmap_dart_client/jmap/core/capability/capability_properties.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:jmap_dart_client/jmap/core/utc_date.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
Expand Down Expand Up @@ -33,7 +37,7 @@ class EmailRecoveryController extends BaseController with DateRangePickerMixin {
GetAutoCompleteInteractor? _getAutoCompleteInteractor;
GetDeviceContactSuggestionsInteractor? _getDeviceContactSuggestionsInteractor;

final deletionDateFieldSelected = EmailRecoveryTimeType.last1Year.obs;
final deletionDateFieldSelected = EmailRecoveryTimeType.last7Days.obs;
final receptionDateFieldSelected = EmailRecoveryTimeType.allTime.obs;
final startDeletionDate = Rxn<DateTime>();
final endDeletionDate = Rxn<DateTime>();
Expand Down Expand Up @@ -338,6 +342,35 @@ class EmailRecoveryController extends BaseController with DateRangePickerMixin {
popBack();
}

String getRestorationHorizonAsString() {
return ((arguments!.session
.props[0] as Map<CapabilityIdentifier, CapabilityProperties>?)
?[capabilityDeletedMessagesVault]
?.props[0] as Map<String, dynamic>?)
?['restorationHorizon']
?? "15 days";
}

Duration getRestorationHorizonAsDuration() {
String horizonWithCorrectFormat = getRestorationHorizonAsString()
.replaceAll(" days", "d")
.replaceAll(" day", "d")
.replaceAll(" hours", "h")
.replaceAll(" hour", "h")
.replaceAll(" minutes", "m")
.replaceAll(" minute", "m")
.replaceAll(" seconds", "s")
.replaceAll(" second", "s")
.replaceAll(" milliseconds", "ms")
.replaceAll(" millisecond", "ms");

return DurationUtils.parseDuration(horizonWithCorrectFormat, separator: ' ');
}

DateTime getRestorationHorizonAsDateTime() {
return DateTime.now().subtract(getRestorationHorizonAsDuration());
}

@override
void dispose() {
focusManager.subjectFieldFocusNode.removeListener(_onSubjectFieldFocusChanged);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ enum EmailRecoveryField {
String getHintText(BuildContext context) {
switch (this) {
case EmailRecoveryField.deletionDate:
return AppLocalizations.of(context).last1Year;
return AppLocalizations.of(context).last7Days;
case EmailRecoveryField.receptionDate:
return AppLocalizations.of(context).allTime;
case EmailRecoveryField.subject:
Expand All @@ -40,16 +40,20 @@ enum EmailRecoveryField {
}
}

List<EmailRecoveryTimeType> getSupportedTimeTypes() {
List<EmailRecoveryTimeType> getSupportedTimeTypes(DateTime restorationHorizon) {
switch (this) {
case EmailRecoveryField.deletionDate:
return [
EmailRecoveryTimeType.last1Year,
final supportedTypes = [
EmailRecoveryTimeType.last7Days,
EmailRecoveryTimeType.last15Days,
EmailRecoveryTimeType.last30Days,
EmailRecoveryTimeType.last6Months,
EmailRecoveryTimeType.customRange,
];
EmailRecoveryTimeType.last1Year,
].where((type) => restorationHorizon
.subtract(const Duration(seconds:2)) // to allow "15 days" if restorationHorizon is exactly 15 days
.isBefore(type.toOldestUTCDate()!.value)).toList();

return [...supportedTypes, EmailRecoveryTimeType.customRange];
case EmailRecoveryField.receptionDate:
return [
EmailRecoveryTimeType.allTime,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:jmap_dart_client/jmap/core/utc_date.dart';
enum EmailRecoveryTimeType {
allTime,
last7Days,
last15Days,
last30Days,
last6Months,
last1Year,
Expand All @@ -18,6 +19,8 @@ enum EmailRecoveryTimeType {
return AppLocalizations.of(context).allTime;
case EmailRecoveryTimeType.last7Days:
return AppLocalizations.of(context).last7Days;
case EmailRecoveryTimeType.last15Days:
return AppLocalizations.of(context).last15Days;
case EmailRecoveryTimeType.last30Days:
return AppLocalizations.of(context).last30Days;
case EmailRecoveryTimeType.last6Months:
Expand Down Expand Up @@ -47,6 +50,10 @@ enum EmailRecoveryTimeType {
final today = DateTime.now();
final last7Days = today.subtract(const Duration(days: 7));
return last7Days.toUTCDate();
case EmailRecoveryTimeType.last15Days:
final today = DateTime.now();
final last15Days = today.subtract(const Duration(days: 15));
return last15Days.toUTCDate();
case EmailRecoveryTimeType.last30Days:
final today = DateTime.now();
final last30Days = today.subtract(const Duration(days: 30));
Expand All @@ -68,6 +75,7 @@ enum EmailRecoveryTimeType {
UTCDate? toLatestUTCDate() {
switch(this) {
case EmailRecoveryTimeType.last7Days:
case EmailRecoveryTimeType.last15Days:
case EmailRecoveryTimeType.last30Days:
case EmailRecoveryTimeType.last6Months:
case EmailRecoveryTimeType.last1Year:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class DateSelectionFieldWebWidget extends StatelessWidget {
final EmailRecoveryTimeType? recoveryTimeSelected;
final OnRecoveryTimeSelected? onRecoveryTimeSelected;
final VoidCallback? onTapCalendar;
final DateTime restorationHorizon;

const DateSelectionFieldWebWidget({
super.key,
Expand All @@ -29,6 +30,7 @@ class DateSelectionFieldWebWidget extends StatelessWidget {
this.recoveryTimeSelected,
this.onRecoveryTimeSelected,
this.onTapCalendar,
required this.restorationHorizon
});

@override
Expand All @@ -54,7 +56,7 @@ class DateSelectionFieldWebWidget extends StatelessWidget {
endDate: endDate,
recoveryTimeSelected: recoveryTimeSelected,
onRecoveryTimeSelected: onRecoveryTimeSelected,
items: field.getSupportedTimeTypes(),
items: field.getSupportedTimeTypes(restorationHorizon),
),
),
const SizedBox(width: DateSelectionFieldStyles.icCalenderSpace),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:tmail_ui_user/features/email_recovery/presentation/model/email_r
import 'package:tmail_ui_user/features/email_recovery/presentation/styles/email_recovery_form_styles.dart';
import 'package:tmail_ui_user/features/email_recovery/presentation/widgets/check_box_has_attachment_widget.dart';
import 'package:tmail_ui_user/features/email_recovery/presentation/widgets/date_selection_field/date_selection_field_web_widget.dart';
import 'package:tmail_ui_user/features/email_recovery/presentation/widgets/limits_banner.dart';
import 'package:tmail_ui_user/features/email_recovery/presentation/widgets/list_button_widget.dart';
import 'package:tmail_ui_user/features/email_recovery/presentation/widgets/text_input_field/text_input_field_suggestion_widget.dart';
import 'package:tmail_ui_user/features/email_recovery/presentation/widgets/text_input_field/text_input_field_widget.dart';
Expand Down Expand Up @@ -41,6 +42,10 @@ class EmailRecoveryFormDesktopBuilder extends GetWidget<EmailRecoveryController>
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LimitsBanner(
bannerContent: AppLocalizations.of(context).recoverDeletedMessagesBannerContent(controller.getRestorationHorizonAsString()),
),
const SizedBox(height: 16.0),
Obx(() => DateSelectionFieldWebWidget(
field: EmailRecoveryField.deletionDate,
imagePaths: controller.imagePaths,
Expand All @@ -50,6 +55,7 @@ class EmailRecoveryFormDesktopBuilder extends GetWidget<EmailRecoveryController>
recoveryTimeSelected: controller.deletionDateFieldSelected.value,
onTapCalendar: () => controller.onSelectDeletionDateRange(context),
onRecoveryTimeSelected: (type) => controller.onDeletionDateTypeSelected(context, type),
restorationHorizon: controller.getRestorationHorizonAsDateTime(),
)),
Obx(() => DateSelectionFieldWebWidget(
field: EmailRecoveryField.receptionDate,
Expand All @@ -60,6 +66,7 @@ class EmailRecoveryFormDesktopBuilder extends GetWidget<EmailRecoveryController>
recoveryTimeSelected: controller.receptionDateFieldSelected.value,
onTapCalendar: () => controller.onSelectReceptionDateRange(context),
onRecoveryTimeSelected: (type) => controller.onReceptionDateTypeSelected(context, type),
restorationHorizon: controller.getRestorationHorizonAsDateTime(),
)),
TextInputFieldWidget(
field: EmailRecoveryField.subject,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:tmail_ui_user/features/email_recovery/presentation/model/email_r
import 'package:tmail_ui_user/features/email_recovery/presentation/styles/email_recovery_form_styles.dart';
import 'package:tmail_ui_user/features/email_recovery/presentation/widgets/check_box_has_attachment_widget.dart';
import 'package:tmail_ui_user/features/email_recovery/presentation/widgets/date_selection_field/date_selection_field_mobile_widget.dart';
import 'package:tmail_ui_user/features/email_recovery/presentation/widgets/limits_banner.dart';
import 'package:tmail_ui_user/features/email_recovery/presentation/widgets/list_button_widget.dart';
import 'package:tmail_ui_user/features/email_recovery/presentation/widgets/text_input_field/text_input_field_suggestion_widget.dart';
import 'package:tmail_ui_user/features/email_recovery/presentation/widgets/text_input_field/text_input_field_widget.dart';
Expand Down Expand Up @@ -41,6 +42,10 @@ class EmailRecoveryFormMobileBuilder extends GetWidget<EmailRecoveryController>
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LimitsBanner(
bannerContent: AppLocalizations.of(context).recoverDeletedMessagesBannerContent(controller.getRestorationHorizonAsString()),
),
const SizedBox(height: 16.0),
Obx(() => DateSelectionFieldMobileWidget(
field: EmailRecoveryField.deletionDate,
recoveryTimeSelected: controller.deletionDateFieldSelected.value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:tmail_ui_user/features/email_recovery/presentation/model/email_r
import 'package:tmail_ui_user/features/email_recovery/presentation/styles/email_recovery_form_styles.dart';
import 'package:tmail_ui_user/features/email_recovery/presentation/widgets/check_box_has_attachment_widget.dart';
import 'package:tmail_ui_user/features/email_recovery/presentation/widgets/date_selection_field/date_selection_field_web_widget.dart';
import 'package:tmail_ui_user/features/email_recovery/presentation/widgets/limits_banner.dart';
import 'package:tmail_ui_user/features/email_recovery/presentation/widgets/list_button_widget.dart';
import 'package:tmail_ui_user/features/email_recovery/presentation/widgets/text_input_field/text_input_field_suggestion_widget.dart';
import 'package:tmail_ui_user/features/email_recovery/presentation/widgets/text_input_field/text_input_field_widget.dart';
Expand Down Expand Up @@ -41,6 +42,10 @@ class EmailRecoveryFormTabletBuilder extends GetWidget<EmailRecoveryController>
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LimitsBanner(
bannerContent: AppLocalizations.of(context).recoverDeletedMessagesBannerContent(controller.getRestorationHorizonAsString()),
),
const SizedBox(height: 16.0),
Obx(() => DateSelectionFieldWebWidget(
field: EmailRecoveryField.deletionDate,
imagePaths: controller.imagePaths,
Expand All @@ -50,6 +55,7 @@ class EmailRecoveryFormTabletBuilder extends GetWidget<EmailRecoveryController>
recoveryTimeSelected: controller.deletionDateFieldSelected.value,
onTapCalendar: () => controller.onSelectDeletionDateRange(context),
onRecoveryTimeSelected: (type) => controller.onDeletionDateTypeSelected(context, type),
restorationHorizon: controller.getRestorationHorizonAsDateTime(),
)),
Obx(() => DateSelectionFieldWebWidget(
field: EmailRecoveryField.receptionDate,
Expand All @@ -60,6 +66,7 @@ class EmailRecoveryFormTabletBuilder extends GetWidget<EmailRecoveryController>
recoveryTimeSelected: controller.receptionDateFieldSelected.value,
onTapCalendar: () => controller.onSelectReceptionDateRange(context),
onRecoveryTimeSelected: (type) => controller.onReceptionDateTypeSelected(context, type),
restorationHorizon: controller.getRestorationHorizonAsDateTime(),
)),
TextInputFieldWidget(
field: EmailRecoveryField.subject,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';

class LimitsBanner extends StatelessWidget {
final String bannerContent;

const LimitsBanner({
super.key,
required this.bannerContent,
});

@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
decoration: BoxDecoration(
color: Colors.yellow[400],
borderRadius: BorderRadius.circular(8.0),
),
child: Center(
child: Text(
bannerContent,
style: const TextStyle(
fontSize: 16,
color: Colors.black,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
);
}
}
Loading