From c2074065d27cde2440be1088b24fd6dcf4429144 Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 16 Dec 2024 13:26:24 +0700 Subject: [PATCH 01/72] Disable language tool check for text field --- .../views/text/text_field_builder.dart | 82 ++++--------------- .../presentation/composer_controller.dart | 6 +- .../widgets/subject_composer_widget.dart | 5 +- pubspec.lock | 2 +- pubspec.yaml | 7 -- 5 files changed, 22 insertions(+), 80 deletions(-) diff --git a/core/lib/presentation/views/text/text_field_builder.dart b/core/lib/presentation/views/text/text_field_builder.dart index 3f162bc2db..bc31baa14f 100644 --- a/core/lib/presentation/views/text/text_field_builder.dart +++ b/core/lib/presentation/views/text/text_field_builder.dart @@ -1,7 +1,6 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; -import 'package:languagetool_textfield/languagetool_textfield.dart'; class TextFieldBuilder extends StatefulWidget { @@ -26,7 +25,6 @@ class TextFieldBuilder extends StatefulWidget { final TextDirection textDirection; final bool readOnly; final MouseCursor? mouseCursor; - final LanguageToolController? languageToolController; const TextFieldBuilder({ super.key, @@ -42,7 +40,6 @@ class TextFieldBuilder extends StatefulWidget { this.maxLines, this.minLines, this.controller, - this.languageToolController, this.keyboardType, this.focusNode, this.fromValue, @@ -61,66 +58,24 @@ class TextFieldBuilder extends StatefulWidget { class _TextFieldBuilderState extends State { TextEditingController? _controller; - LanguageToolController? _languageToolController; late TextDirection _textDirection; @override void initState() { super.initState(); - if (widget.languageToolController != null) { - _languageToolController = widget.languageToolController; - if (widget.fromValue != null) { - _languageToolController?.value = TextEditingValue(text: widget.fromValue!); - } + if (widget.fromValue != null) { + _controller = TextEditingController.fromValue( + TextEditingValue(text: widget.fromValue!), + ); } else { - if (widget.fromValue != null) { - _controller = TextEditingController.fromValue(TextEditingValue(text: widget.fromValue!)); - } else { - _controller = widget.controller ?? TextEditingController(); - } + _controller = widget.controller ?? TextEditingController(); } _textDirection = widget.textDirection; } @override Widget build(BuildContext context) { - if (_languageToolController != null) { - return LanguageToolTextField( - key: widget.key, - controller: _languageToolController!, - cursorColor: widget.cursorColor, - autocorrect: widget.autocorrect, - textInputAction: widget.textInputAction, - decoration: widget.decoration, - maxLines: widget.maxLines, - minLines: widget.minLines, - keyboardAppearance: widget.keyboardAppearance, - style: widget.textStyle, - keyboardType: widget.keyboardType, - autoFocus: widget.autoFocus, - focusNode: widget.focusNode, - alignCenter: false, - textDirection: _textDirection, - readOnly: widget.readOnly, - mouseCursor: widget.mouseCursor, - onTextChange: (value) { - widget.onTextChange?.call(value); - if (value.isNotEmpty) { - final directionByText = DirectionUtils.getDirectionByEndsText(value); - if (directionByText != _textDirection) { - setState(() { - _textDirection = directionByText; - }); - } - } - }, - onTextSubmitted: widget.onTextSubmitted, - onTap: widget.onTap, - onTapOutside: widget.onTapOutside, - ); - } - return TextField( key: widget.key, controller: _controller, @@ -139,31 +94,30 @@ class _TextFieldBuilderState extends State { textDirection: _textDirection, readOnly: widget.readOnly, mouseCursor: widget.mouseCursor, - onChanged: (value) { - widget.onTextChange?.call(value); - if (value.isNotEmpty) { - final directionByText = DirectionUtils.getDirectionByEndsText(value); - if (directionByText != _textDirection) { - setState(() { - _textDirection = directionByText; - }); - } - } - }, + onChanged: _onTextChanged, onSubmitted: widget.onTextSubmitted, onTap: widget.onTap, onTapOutside: widget.onTapOutside, ); } + void _onTextChanged(String value) { + widget.onTextChange?.call(value); + + if (value.trim().isEmpty) return; + + final directionByText = DirectionUtils.getDirectionByEndsText(value); + if (directionByText != _textDirection) { + setState(() { + _textDirection = directionByText; + }); + } + } @override void dispose() { if (widget.controller == null) { _controller?.dispose(); } - if (widget.languageToolController == null) { - _languageToolController?.dispose(); - } super.dispose(); } } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 4e843153dd..e89a0a86f4 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -24,7 +24,6 @@ import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:languagetool_textfield/languagetool_textfield.dart'; import 'package:model/model.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; @@ -152,9 +151,7 @@ class ComposerController extends BaseController List listBccEmailAddress = []; ContactSuggestionSource _contactSuggestionSource = ContactSuggestionSource.tMailContact; - final subjectEmailInputController = LanguageToolController( - delay: const Duration(milliseconds: 200), - ); + final subjectEmailInputController = TextEditingController(); final toEmailAddressController = TextEditingController(); final ccEmailAddressController = TextEditingController(); final bccEmailAddressController = TextEditingController(); @@ -1947,7 +1944,6 @@ class ComposerController extends BaseController void handleOnFocusHtmlEditorWeb() { FocusManager.instance.primaryFocus?.unfocus(); - subjectEmailInputController.popupWidget?.popupRenderer.dismiss(); richTextWebController?.editorController.setFocus(); richTextWebController?.closeAllMenuPopup(); } diff --git a/lib/features/composer/presentation/widgets/subject_composer_widget.dart b/lib/features/composer/presentation/widgets/subject_composer_widget.dart index 7dfc0e2bc1..cde6fef562 100644 --- a/lib/features/composer/presentation/widgets/subject_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/subject_composer_widget.dart @@ -1,14 +1,13 @@ import 'package:core/presentation/views/text/text_field_builder.dart'; import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; -import 'package:languagetool_textfield/languagetool_textfield.dart'; import 'package:tmail_ui_user/features/composer/presentation/styles/subject_composer_widget_style.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; class SubjectComposerWidget extends StatelessWidget { final FocusNode? focusNode; - final LanguageToolController textController; + final TextEditingController textController; final ValueChanged? onTextChange; final EdgeInsetsGeometry? margin; final EdgeInsetsGeometry? padding; @@ -51,7 +50,7 @@ class SubjectComposerWidget extends StatelessWidget { decoration: const InputDecoration(border: InputBorder.none), textDirection: DirectionUtils.getDirectionByLanguage(context), textStyle: SubjectComposerWidgetStyle.inputTextStyle, - languageToolController: textController, + controller: textController, ) ) ] diff --git a/pubspec.lock b/pubspec.lock index 86c17e2dc2..41311dcd9d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1289,7 +1289,7 @@ packages: source: hosted version: "6.6.1" languagetool_textfield: - dependency: "direct main" + dependency: transitive description: path: "." ref: twake-supported diff --git a/pubspec.yaml b/pubspec.yaml index bf0b4a3a2c..64bc30d354 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -104,13 +104,6 @@ dependencies: url: https://github.com/linagora/flutter_pdf_render.git ref: main - # TODO: We will change it when the PR in upstream repository will be merged - # https://github.com/solid-software/languagetool_textfield/pull/83 - languagetool_textfield: - git: - url: https://github.com/dab246/languagetool_textfield.git - ref: twake-supported - linagora_design_flutter: git: url: https://github.com/linagora/linagora-design-flutter.git From 65720aa649ce733b0748b206c8f7420ee50bdf76 Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 16 Dec 2024 14:16:17 +0700 Subject: [PATCH 02/72] fixup! Disable language tool check for text field --- contact/pubspec.lock | 17 ----------------- core/pubspec.lock | 17 ----------------- core/pubspec.yaml | 7 ------- model/pubspec.lock | 17 ----------------- pubspec.lock | 17 ----------------- 5 files changed, 75 deletions(-) diff --git a/contact/pubspec.lock b/contact/pubspec.lock index a3f209a444..b8ec281b83 100644 --- a/contact/pubspec.lock +++ b/contact/pubspec.lock @@ -688,15 +688,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.1" - languagetool_textfield: - dependency: transitive - description: - path: "." - ref: twake-supported - resolved-ref: f47dd9829e145acc795b4e3e62f8bc668135fcd6 - url: "https://github.com/dab246/languagetool_textfield.git" - source: git - version: "0.1.0" leak_tracker: dependency: transitive description: @@ -1110,14 +1101,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" - throttling: - dependency: transitive - description: - name: throttling - sha256: e48a4c681b1838b8bf99c1a4f822efe43bb69132f9a56091cd5b7d931c862255 - url: "https://pub.dev" - source: hosted - version: "2.0.1" timing: dependency: transitive description: diff --git a/core/pubspec.lock b/core/pubspec.lock index 5f3cc38111..19aac126b7 100644 --- a/core/pubspec.lock +++ b/core/pubspec.lock @@ -656,15 +656,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.9.0" - languagetool_textfield: - dependency: "direct main" - description: - path: "." - ref: twake-supported - resolved-ref: f47dd9829e145acc795b4e3e62f8bc668135fcd6 - url: "https://github.com/dab246/languagetool_textfield.git" - source: git - version: "0.1.0" leak_tracker: dependency: transitive description: @@ -1055,14 +1046,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" - throttling: - dependency: transitive - description: - name: throttling - sha256: e48a4c681b1838b8bf99c1a4f822efe43bb69132f9a56091cd5b7d931c862255 - url: "https://pub.dev" - source: hosted - version: "2.0.1" timing: dependency: transitive description: diff --git a/core/pubspec.yaml b/core/pubspec.yaml index 4502549f20..ad47a2fd3b 100644 --- a/core/pubspec.yaml +++ b/core/pubspec.yaml @@ -27,13 +27,6 @@ dependencies: sdk: flutter ### Dependencies from git ### - # TODO: We will change it when the PR in upstream repository will be merged - # https://github.com/solid-software/languagetool_textfield/pull/83 - languagetool_textfield: - git: - url: https://github.com/dab246/languagetool_textfield.git - ref: twake-supported - # Sanitize_html is restricting Tags and Attributes. So some of our own tags and attributes (signature, public asset,...) will be lost when sanitizing html. # TODO: We will change it when the PR in upstream repository will be merged # https://github.com/google/dart-neats/pull/259 diff --git a/model/pubspec.lock b/model/pubspec.lock index 02e72ad737..3e1b02d95f 100644 --- a/model/pubspec.lock +++ b/model/pubspec.lock @@ -680,15 +680,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.1" - languagetool_textfield: - dependency: transitive - description: - path: "." - ref: twake-supported - resolved-ref: f47dd9829e145acc795b4e3e62f8bc668135fcd6 - url: "https://github.com/dab246/languagetool_textfield.git" - source: git - version: "0.1.0" leak_tracker: dependency: transitive description: @@ -1087,14 +1078,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" - throttling: - dependency: transitive - description: - name: throttling - sha256: e48a4c681b1838b8bf99c1a4f822efe43bb69132f9a56091cd5b7d931c862255 - url: "https://pub.dev" - source: hosted - version: "2.0.1" timing: dependency: transitive description: diff --git a/pubspec.lock b/pubspec.lock index 41311dcd9d..c257e6a85e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1288,15 +1288,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.1" - languagetool_textfield: - dependency: transitive - description: - path: "." - ref: twake-supported - resolved-ref: f47dd9829e145acc795b4e3e62f8bc668135fcd6 - url: "https://github.com/dab246/languagetool_textfield.git" - source: git - version: "0.1.0" leak_tracker: dependency: transitive description: @@ -2008,14 +1999,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" - throttling: - dependency: transitive - description: - name: throttling - sha256: e48a4c681b1838b8bf99c1a4f822efe43bb69132f9a56091cd5b7d931c862255 - url: "https://pub.dev" - source: hosted - version: "2.0.1" timeago: dependency: "direct main" description: From 617dbe02c8dcfde36ba74a8a76ea030895ad5baa Mon Sep 17 00:00:00 2001 From: Dat PHAM HOANG Date: Mon, 16 Dec 2024 16:52:27 +0700 Subject: [PATCH 03/72] Bump version to v0.14.3 --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 231cfd5285..26f90e25ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [0.14.3] - 2024-12-16 +### Fixed +- Disable Spell check API + ## [0.14.2] - 2024-11-20 ### Added - #3010 Highlight search result with SearchSnippet method diff --git a/pubspec.yaml b/pubspec.yaml index 64bc30d354..aba972db20 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.14.2 +version: 0.14.3 environment: sdk: ">=3.0.0 <4.0.0" From b3f3241dff8a8fd35e5c2a44da68d393d3fa14a9 Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 16 Dec 2024 15:39:33 +0700 Subject: [PATCH 04/72] TF-3349 Escape messages when forward and reply email --- .../extensions/html_extension.dart | 17 +++++++ .../extensions/html_extension_test.dart | 36 +++++++++++++++ .../email_action_type_extension.dart | 37 +++++++-------- .../presentation/view/editor_view_mixin.dart | 7 ++- .../view/mobile/mobile_editor_view.dart | 7 ++- .../view/web/web_editor_view.dart | 7 ++- .../extensions/email_address_extension.dart | 11 +++++ .../list_email_address_extension.dart | 13 ++++++ .../email_address_extension_test.dart | 39 ++++++++++++++++ .../list_email_address_extension_test.dart | 46 +++++++++++++++++++ 10 files changed, 196 insertions(+), 24 deletions(-) create mode 100644 core/test/presentation/extensions/html_extension_test.dart create mode 100644 model/test/extensions/email_address_extension_test.dart create mode 100644 model/test/extensions/list_email_address_extension_test.dart diff --git a/core/lib/presentation/extensions/html_extension.dart b/core/lib/presentation/extensions/html_extension.dart index 6256084cbb..5296a807c1 100644 --- a/core/lib/presentation/extensions/html_extension.dart +++ b/core/lib/presentation/extensions/html_extension.dart @@ -1,4 +1,6 @@ +import 'dart:convert'; + extension HtmlExtension on String { static const String editorStartTags = '


'; @@ -40,4 +42,19 @@ extension HtmlExtension on String { 'cite', attribute: 'style="text-align: left;display: block;"' ); +} + +extension HtmlNullableExtension on String? { + String escapeHtmlString({HtmlEscapeMode escapeMode = HtmlEscapeMode.unknown}) { + try { + if (this?.trim().isNotEmpty != true) return ''; + + return HtmlEscape(escapeMode).convert(this!); + } catch (e) { + return ''; + } + } + + String escapeLtGtHtmlString() => + escapeHtmlString(escapeMode: HtmlEscapeMode.element); } \ No newline at end of file diff --git a/core/test/presentation/extensions/html_extension_test.dart b/core/test/presentation/extensions/html_extension_test.dart new file mode 100644 index 0000000000..76958e69bf --- /dev/null +++ b/core/test/presentation/extensions/html_extension_test.dart @@ -0,0 +1,36 @@ +import 'package:core/presentation/extensions/html_extension.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('HtmlNullableExtension::', () { + test('escapeHtmlString should escapes HTML containing onclick attribute', () { + String? input = "Click"; + expect(input.escapeHtmlString(), + '<a onclick="alert('hi')">Click</a>'); + }); + + test('escapeHtmlString should escapes HTML containing multiple events', () { + String? input = "
Hover
"; + expect(input.escapeHtmlString(), + '<div onmouseover="alert('hover')" onclick="doSomething()">Hover</div>'); + }); + + test('escapeLtGtHtmlString should escapes only < and > with events', () { + String? input = ""; + expect(input.escapeLtGtHtmlString(), + '<button onclick=\"run()\">Run</button>'); + }); + + test('escapeHtmlString should handles HTML with empty event attributes', () { + String? input = ""; + expect(input.escapeHtmlString(), + '<img src="image.png" onerror="">'); + }); + + test('escapeHtmlString should handles HTML with invalid syntax in events', () { + String? input = "Click"; + expect(input.escapeHtmlString(), + '<a onclick="alert('unclosed event)>Click</a>'); + }); + }); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/email_action_type_extension.dart b/lib/features/composer/presentation/extensions/email_action_type_extension.dart index 5c0a523d15..b664c37175 100644 --- a/lib/features/composer/presentation/extensions/email_action_type_extension.dart +++ b/lib/features/composer/presentation/extensions/email_action_type_extension.dart @@ -53,62 +53,63 @@ extension EmailActionTypeExtension on EmailActionType { } String? getHeaderEmailQuoted({ - required BuildContext context, + required Locale locale, + required AppLocalizations appLocalizations, required PresentationEmail presentationEmail }) { - final locale = Localizations.localeOf(context).toLanguageTag(); + final languageTag = locale.toLanguageTag(); switch(this) { case EmailActionType.reply: case EmailActionType.replyAll: final receivedAt = presentationEmail.receivedAt; - final emailAddress = presentationEmail.from.listEmailAddressToString(isFullEmailAddress: true); - return AppLocalizations.of(context).header_email_quoted( - receivedAt.formatDateToLocal(pattern: 'MMM d, y h:mm a', locale: locale), + final emailAddress = presentationEmail.from.toEscapeHtmlStringUseCommaSeparator(); + return appLocalizations.header_email_quoted( + receivedAt.formatDateToLocal(pattern: 'MMM d, y h:mm a', locale: languageTag), emailAddress ); case EmailActionType.forward: - var headerQuoted = '------- ${AppLocalizations.of(context).forwarded_message} -------'.addNewLineTag(); + var headerQuoted = '------- ${appLocalizations.forwarded_message} -------'.addNewLineTag(); - final subject = presentationEmail.subject ?? ''; + final subject = presentationEmail.subject?.escapeLtGtHtmlString() ?? ''; final receivedAt = presentationEmail.receivedAt; - final fromEmailAddress = presentationEmail.from.listEmailAddressToString(isFullEmailAddress: true); - final toEmailAddress = presentationEmail.to.listEmailAddressToString(isFullEmailAddress: true); - final ccEmailAddress = presentationEmail.cc.listEmailAddressToString(isFullEmailAddress: true); - final bccEmailAddress = presentationEmail.bcc.listEmailAddressToString(isFullEmailAddress: true); + final fromEmailAddress = presentationEmail.from.toEscapeHtmlStringUseCommaSeparator(); + final toEmailAddress = presentationEmail.to.toEscapeHtmlStringUseCommaSeparator(); + final ccEmailAddress = presentationEmail.cc.toEscapeHtmlStringUseCommaSeparator(); + final bccEmailAddress = presentationEmail.bcc.toEscapeHtmlStringUseCommaSeparator(); if (subject.isNotEmpty) { headerQuoted = headerQuoted - .append('${AppLocalizations.of(context).subject_email}: ') + .append('${appLocalizations.subject_email}: ') .append(subject) .addNewLineTag(); } if (receivedAt != null) { headerQuoted = headerQuoted - .append('${AppLocalizations.of(context).date}: ') - .append(receivedAt.formatDateToLocal(pattern: 'MMM d, y h:mm a', locale: locale)) + .append('${appLocalizations.date}: ') + .append(receivedAt.formatDateToLocal(pattern: 'MMM d, y h:mm a', locale: languageTag)) .addNewLineTag(); } if (fromEmailAddress.isNotEmpty) { headerQuoted = headerQuoted - .append('${AppLocalizations.of(context).from_email_address_prefix}: ') + .append('${appLocalizations.from_email_address_prefix}: ') .append(fromEmailAddress) .addNewLineTag(); } if (toEmailAddress.isNotEmpty) { headerQuoted = headerQuoted - .append('${AppLocalizations.of(context).to_email_address_prefix}: ') + .append('${appLocalizations.to_email_address_prefix}: ') .append(toEmailAddress) .addNewLineTag(); } if (ccEmailAddress.isNotEmpty) { headerQuoted = headerQuoted - .append('${AppLocalizations.of(context).cc_email_address_prefix}: ') + .append('${appLocalizations.cc_email_address_prefix}: ') .append(ccEmailAddress) .addNewLineTag(); } if (bccEmailAddress.isNotEmpty) { headerQuoted = headerQuoted - .append('${AppLocalizations.of(context).bcc_email_address_prefix}: ') + .append('${appLocalizations.bcc_email_address_prefix}: ') .append(bccEmailAddress) .addNewLineTag(); } diff --git a/lib/features/composer/presentation/view/editor_view_mixin.dart b/lib/features/composer/presentation/view/editor_view_mixin.dart index 8e54f1cb01..c319f8dd9a 100644 --- a/lib/features/composer/presentation/view/editor_view_mixin.dart +++ b/lib/features/composer/presentation/view/editor_view_mixin.dart @@ -5,16 +5,19 @@ import 'package:flutter/material.dart'; import 'package:model/email/email_action_type.dart'; import 'package:model/email/presentation_email.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; mixin EditorViewMixin { String getEmailContentQuotedAsHtml({ - required BuildContext context, + required Locale locale, + required AppLocalizations appLocalizations, required String emailContent, required EmailActionType emailActionType, required PresentationEmail presentationEmail, }) { final headerEmailQuoted = emailActionType.getHeaderEmailQuoted( - context: context, + locale: locale, + appLocalizations: appLocalizations, presentationEmail: presentationEmail ); log('EditorViewMixin::getEmailContentQuotedAsHtml:headerEmailQuoted: $headerEmailQuoted'); diff --git a/lib/features/composer/presentation/view/mobile/mobile_editor_view.dart b/lib/features/composer/presentation/view/mobile/mobile_editor_view.dart index b425d86cab..1859f9026f 100644 --- a/lib/features/composer/presentation/view/mobile/mobile_editor_view.dart +++ b/lib/features/composer/presentation/view/mobile/mobile_editor_view.dart @@ -10,6 +10,7 @@ import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/mobi import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_state.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; class MobileEditorView extends StatelessWidget with EditorViewMixin { @@ -87,7 +88,8 @@ class MobileEditorView extends StatelessWidget with EditorViewMixin { return contentViewState!.fold( (failure) { final emailContentQuoted = getEmailContentQuotedAsHtml( - context: context, + locale: Localizations.localeOf(context), + appLocalizations: AppLocalizations.of(context), emailContent: '', emailActionType: arguments!.emailActionType, presentationEmail: arguments!.presentationEmail! @@ -104,7 +106,8 @@ class MobileEditorView extends StatelessWidget with EditorViewMixin { return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0)); } else { final emailContentQuoted = getEmailContentQuotedAsHtml( - context: context, + locale: Localizations.localeOf(context), + appLocalizations: AppLocalizations.of(context), emailContent: success is TransformHtmlEmailContentSuccess ? success.htmlContent : '', diff --git a/lib/features/composer/presentation/view/web/web_editor_view.dart b/lib/features/composer/presentation/view/web/web_editor_view.dart index f04efac66e..f7cd0cca81 100644 --- a/lib/features/composer/presentation/view/web/web_editor_view.dart +++ b/lib/features/composer/presentation/view/web/web_editor_view.dart @@ -13,6 +13,7 @@ import 'package:tmail_ui_user/features/composer/presentation/widgets/web/web_edi import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_state.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; class WebEditorView extends StatelessWidget with EditorViewMixin { @@ -152,7 +153,8 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { return contentViewState!.fold( (failure) { final emailContentQuoted = getEmailContentQuotedAsHtml( - context: context, + locale: Localizations.localeOf(context), + appLocalizations: AppLocalizations.of(context), emailContent: '', emailActionType: arguments!.emailActionType, presentationEmail: arguments!.presentationEmail! @@ -181,7 +183,8 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0)); } else { final emailContentQuoted = getEmailContentQuotedAsHtml( - context: context, + locale: Localizations.localeOf(context), + appLocalizations: AppLocalizations.of(context), emailContent: success is TransformHtmlEmailContentSuccess ? success.htmlContent : '', diff --git a/model/lib/extensions/email_address_extension.dart b/model/lib/extensions/email_address_extension.dart index 2a0b31d9cc..cb1b7a0b19 100644 --- a/model/lib/extensions/email_address_extension.dart +++ b/model/lib/extensions/email_address_extension.dart @@ -27,6 +27,17 @@ extension EmailAddressExtension on EmailAddress { return ''; } + String asFullStringWithLtGtCharacter() { + if (displayName.isNotEmpty && emailAddress.isNotEmpty) { + return '${displayName.capitalizeFirstEach} <$emailAddress>'; + } else if (displayName.isNotEmpty) { + return displayName.capitalizeFirstEach; + } else if (emailAddress.isNotEmpty) { + return '<$emailAddress>'; + } + return ''; + } + String get emailAddress => email ?? ''; String get displayName => name ?? ''; diff --git a/model/lib/extensions/list_email_address_extension.dart b/model/lib/extensions/list_email_address_extension.dart index 84a9465bf3..41b4628e8a 100644 --- a/model/lib/extensions/list_email_address_extension.dart +++ b/model/lib/extensions/list_email_address_extension.dart @@ -1,3 +1,4 @@ +import 'package:core/presentation/extensions/html_extension.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/mailbox/expand_mode.dart'; import 'package:model/extensions/email_address_extension.dart'; @@ -27,6 +28,18 @@ extension SetEmailAddressExtension on Set? { return listEmail.isNotEmpty ? listEmail.join(', ') : ''; } + String toEscapeHtmlString(String separator) { + if (this?.isNotEmpty != true) return ''; + + final listEmail = this + !.map((emailAddress) => emailAddress.asFullStringWithLtGtCharacter().escapeLtGtHtmlString()) + .toList(); + + return listEmail.isNotEmpty ? listEmail.join(separator) : ''; + } + + String toEscapeHtmlStringUseCommaSeparator() => toEscapeHtmlString(', '); + int numberEmailAddress() => this != null ? this!.length : 0; List filterEmailAddress(String emailAddressNotExist) { diff --git a/model/test/extensions/email_address_extension_test.dart b/model/test/extensions/email_address_extension_test.dart new file mode 100644 index 0000000000..af3bbd9204 --- /dev/null +++ b/model/test/extensions/email_address_extension_test.dart @@ -0,0 +1,39 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/extensions/email_address_extension.dart'; + +void main() { + group('EmailAddressExtension::asFullStringWithLtGtCharacter::', () { + test('Should returns displayName and emailAddress formatted correctly', () { + final emailAddress = EmailAddress( + 'john doe', + 'john.doe@example.com', + ); + expect( + emailAddress.asFullStringWithLtGtCharacter(), + 'John Doe ', + ); + }); + + test('Should returns displayName capitalized when emailAddress is empty', () { + final emailAddress = EmailAddress( + 'jane doe', + '', + ); + expect(emailAddress.asFullStringWithLtGtCharacter(), 'Jane Doe'); + }); + + test('Should returns emailAddress enclosed in <> when displayName is empty', () { + final emailAddress = EmailAddress( + '', + 'jane.doe@example.com', + ); + expect(emailAddress.asFullStringWithLtGtCharacter(), ''); + }); + + test('Should returns an empty string when both displayName and emailAddress are empty', () { + final emailAddress = EmailAddress('', ''); + expect(emailAddress.asFullStringWithLtGtCharacter(), ''); + }); + }); +} \ No newline at end of file diff --git a/model/test/extensions/list_email_address_extension_test.dart b/model/test/extensions/list_email_address_extension_test.dart new file mode 100644 index 0000000000..2ade025f69 --- /dev/null +++ b/model/test/extensions/list_email_address_extension_test.dart @@ -0,0 +1,46 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/extensions/list_email_address_extension.dart'; + +void main() { + group('ListEmailAddressExtension::toEscapeHtmlString::', () { + test('Should returns empty string for null or empty list', () { + expect(({}).toEscapeHtmlString(', '), ''); + + Set? listEmails; + expect(listEmails.toEscapeHtmlString(', '), ''); + }); + + test('Should returns joined HTML strings with separator', () { + final emails = { + EmailAddress('John Doe', 'john@example.com'), + EmailAddress('Jane Smith', 'jane@example.com'), + }; + final result = emails.toEscapeHtmlString(' | '); + + expect(result, 'John Doe <john@example.com> | Jane Smith <jane@example.com>'); + }); + + test('Should handles displayName-only or email-only cases', () { + final emails = { + EmailAddress('John Doe', ''), + EmailAddress('', 'jane@example.com'), + }; + final result = emails.toEscapeHtmlString(', '); + + expect(result, 'John Doe, <jane@example.com>'); + }); + }); + + group('ListEmailAddressExtension::toEscapeHtmlStringUseCommaSeparator::', () { + test('Should uses ", " as separator', () { + final emails = { + EmailAddress('John Doe', 'john@example.com'), + EmailAddress('Jane Smith', 'jane@example.com'), + }; + final result = emails.toEscapeHtmlStringUseCommaSeparator(); + + expect(result, 'John Doe <john@example.com>, Jane Smith <jane@example.com>'); + }); + }); +} From 200766571381699969e3adfec959e4a71a3c967e Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 18 Dec 2024 12:10:53 +0700 Subject: [PATCH 05/72] TF-3349 Escape messages when print email --- .../presentation/composer_bindings.dart | 1 + .../print_file_datasource_impl.dart | 30 ++++++++++++++++--- .../usecases/print_email_interactor.dart | 18 +---------- .../presentation/bindings/email_bindings.dart | 1 + .../bindings/mailbox_dashboard_bindings.dart | 1 + .../sending_email_interactor_bindings.dart | 1 + .../bindings/fcm_interactor_bindings.dart | 1 + 7 files changed, 32 insertions(+), 21 deletions(-) diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index 8c401aba80..9c563bce8e 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -119,6 +119,7 @@ class ComposerBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), Get.find() )); Get.lazyPut(() => EmailHiveCacheDataSourceImpl( diff --git a/lib/features/email/data/datasource_impl/print_file_datasource_impl.dart b/lib/features/email/data/datasource_impl/print_file_datasource_impl.dart index 572f4ffb8e..7aed25348a 100644 --- a/lib/features/email/data/datasource_impl/print_file_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/print_file_datasource_impl.dart @@ -1,12 +1,16 @@ import 'package:core/data/model/print_attachment.dart'; import 'package:core/domain/extensions/datetime_extension.dart'; +import 'package:core/presentation/extensions/html_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:core/utils/file_utils.dart'; import 'package:core/utils/print_utils.dart'; import 'package:filesize/filesize.dart'; import 'package:model/email/attachment.dart'; import 'package:model/extensions/email_extension.dart'; import 'package:tmail_ui_user/features/email/data/datasource/print_file_datasource.dart'; +import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; import 'package:tmail_ui_user/features/email/domain/model/email_print.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/attachment_extension.dart'; import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; @@ -16,18 +20,23 @@ class PrintFileDataSourceImpl extends PrintFileDataSource { final PrintUtils _printUtils; final ImagePaths _imagePaths; final FileUtils _fileUtils; + final HtmlAnalyzer _htmlAnalyzer; final ExceptionThrower _exceptionThrower; PrintFileDataSourceImpl( this._printUtils, this._imagePaths, this._fileUtils, + this._htmlAnalyzer, this._exceptionThrower ); @override Future printEmail(EmailPrint emailPrint) { return Future.sync(() async { + final emailContentEscaped = await _transformHtmlEmailContent( + emailPrint.emailContent); + final sender = emailPrint.emailInformation.from?.isNotEmpty == true ? emailPrint.emailInformation.from!.first : null; @@ -43,7 +52,7 @@ class PrintFileDataSourceImpl extends PrintFileDataSource { final iconBase64Data = await _fileUtils.convertImageAssetToBase64(attachment.getIcon(_imagePaths)); final printAttachment = PrintAttachment( iconBase64Data: iconBase64Data, - name: attachment.name ?? '', + name: attachment.name.escapeLtGtHtmlString(), size: filesize(attachment.size?.value) ); listPrintAttachment.add(printAttachment); @@ -53,9 +62,9 @@ class PrintFileDataSourceImpl extends PrintFileDataSource { return await _printUtils.printEmail( appName: emailPrint.appName, userName: emailPrint.userName, - subject: emailPrint.emailInformation.subject ?? '', - emailContent: emailPrint.emailContent, - senderName: sender?.name ?? '', + subject: emailPrint.emailInformation.subject?.escapeLtGtHtmlString() ?? '', + emailContent: emailContentEscaped, + senderName: sender?.name.escapeLtGtHtmlString() ?? '', senderEmailAddress: sender?.email ?? '', dateTime: receiveTime, fromPrefix: emailPrint.fromPrefix, @@ -72,4 +81,17 @@ class PrintFileDataSourceImpl extends PrintFileDataSource { ); }).catchError(_exceptionThrower.throwException); } + + Future _transformHtmlEmailContent(String emailContent) async { + try { + final htmlContentTransformed = await _htmlAnalyzer.transformHtmlEmailContent( + emailContent, + TransformConfiguration.forPrintEmail(), + ); + return htmlContentTransformed; + } catch (e) { + logError('PrintFileDataSourceImpl::_transformHtmlEmailContent: Exception: $e'); + return emailContent; + } + } } \ No newline at end of file diff --git a/lib/features/email/domain/usecases/print_email_interactor.dart b/lib/features/email/domain/usecases/print_email_interactor.dart index 1014b3b5ff..2cdaa4a40b 100644 --- a/lib/features/email/domain/usecases/print_email_interactor.dart +++ b/lib/features/email/domain/usecases/print_email_interactor.dart @@ -1,7 +1,5 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; -import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart'; import 'package:tmail_ui_user/features/email/domain/model/email_print.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; @@ -14,24 +12,10 @@ class PrintEmailInteractor { Stream> execute(EmailPrint emailPrint) async* { try { - final htmlContentTransformed = await _transformHtmlEmailContent(emailPrint.emailContent); - final newEmailPrint = emailPrint.fromEmailContent(htmlContentTransformed); - await emailRepository.printEmail(newEmailPrint); + await emailRepository.printEmail(emailPrint); yield Right(PrintEmailSuccess()); } catch (e) { yield Left(PrintEmailFailure(exception: e)); } } - - Future _transformHtmlEmailContent(String emailContent) async { - try { - final htmlContentTransformed = await emailRepository.transformHtmlEmailContent( - emailContent, - TransformConfiguration.forPrintEmail()); - return htmlContentTransformed; - } catch (e) { - logError('PrintEmailInteractor::_transformHtmlEmailContent: Exception: $e'); - return emailContent; - } - } } \ No newline at end of file diff --git a/lib/features/email/presentation/bindings/email_bindings.dart b/lib/features/email/presentation/bindings/email_bindings.dart index a589ded969..576532f4f8 100644 --- a/lib/features/email/presentation/bindings/email_bindings.dart +++ b/lib/features/email/presentation/bindings/email_bindings.dart @@ -106,6 +106,7 @@ class EmailBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), Get.find() )); Get.lazyPut(() => EmailHiveCacheDataSourceImpl( diff --git a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart index 61f04d8fcc..22dc4b10be 100644 --- a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart +++ b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart @@ -231,6 +231,7 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), Get.find() )); Get.lazyPut(() => MailboxDataSourceImpl( diff --git a/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart b/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart index 1829ad0248..dc8e65f8fb 100644 --- a/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart +++ b/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart @@ -77,6 +77,7 @@ class SendEmailInteractorBindings extends InteractorsBindings { Get.find(), Get.find(), Get.find(), + Get.find(), Get.find(), )); Get.lazyPut(() => EmailHiveCacheDataSourceImpl( diff --git a/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart b/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart index 9b31a99069..eada6b3b1d 100644 --- a/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart +++ b/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart @@ -115,6 +115,7 @@ class FcmInteractorBindings extends InteractorsBindings { Get.find(), Get.find(), Get.find(), + Get.find(), Get.find() )); Get.lazyPut(() => EmailHiveCacheDataSourceImpl( From 0b3f106dda035df45d307cbd5d6e1ddc1d8daed1 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 18 Dec 2024 15:48:50 +0700 Subject: [PATCH 06/72] TF-3349 Add integration test for forward email --- backend-docker/docker-compose.yaml | 4 +- integration_test/robots/email_robot.dart | 12 ++ integration_test/robots/search_robot.dart | 24 ++++ .../scenarios/forward_email_scenario.dart | 107 ++++++++++++++++++ .../tests/compose/forward_email_test.dart | 23 ++++ .../integration_test/eml/forward_email/0.eml | 42 +++++++ .../search_email_with_sort_order}/0.eml | 0 .../search_email_with_sort_order}/1.eml | 0 .../search_email_with_sort_order}/2.eml | 0 .../search_email_with_sort_order}/3.eml | 0 .../search_email_with_sort_order}/4.eml | 0 provisioning/integration_test/provisioning.sh | 28 +++++ .../provisioning.sh | 18 --- .../patrol-integration-test-with-docker.sh | 2 +- ...trol-local-integration-test-with-docker.sh | 2 +- 15 files changed, 240 insertions(+), 22 deletions(-) create mode 100644 integration_test/robots/email_robot.dart create mode 100644 integration_test/scenarios/forward_email_scenario.dart create mode 100644 integration_test/tests/compose/forward_email_test.dart create mode 100644 provisioning/integration_test/eml/forward_email/0.eml rename provisioning/integration_test/{search_email_with_sort_order/eml => eml/search_email_with_sort_order}/0.eml (100%) rename provisioning/integration_test/{search_email_with_sort_order/eml => eml/search_email_with_sort_order}/1.eml (100%) rename provisioning/integration_test/{search_email_with_sort_order/eml => eml/search_email_with_sort_order}/2.eml (100%) rename provisioning/integration_test/{search_email_with_sort_order/eml => eml/search_email_with_sort_order}/3.eml (100%) rename provisioning/integration_test/{search_email_with_sort_order/eml => eml/search_email_with_sort_order}/4.eml (100%) create mode 100755 provisioning/integration_test/provisioning.sh delete mode 100755 provisioning/integration_test/search_email_with_sort_order/provisioning.sh diff --git a/backend-docker/docker-compose.yaml b/backend-docker/docker-compose.yaml index 36f851616e..9ab8f28d86 100644 --- a/backend-docker/docker-compose.yaml +++ b/backend-docker/docker-compose.yaml @@ -10,8 +10,8 @@ services: - ./mailetcontainer.xml:/root/conf/mailetcontainer.xml - ./imapserver.xml:/root/conf/imapserver.xml - ./jmap.properties:/root/conf/jmap.properties - - ../provisioning/integration_test/search_email_with_sort_order/provisioning.sh:/root/conf/integration_test/search_email_with_sort_order/provisioning.sh - - ../provisioning/integration_test/search_email_with_sort_order/eml:/root/conf/integration_test/search_email_with_sort_order/eml + - ../provisioning/integration_test/provisioning.sh:/root/conf/integration_test/provisioning.sh + - ../provisioning/integration_test/eml:/root/conf/integration_test/eml ports: - "80:80" environment: diff --git a/integration_test/robots/email_robot.dart b/integration_test/robots/email_robot.dart new file mode 100644 index 0000000000..2ada2436bf --- /dev/null +++ b/integration_test/robots/email_robot.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import '../base/core_robot.dart'; + +class EmailRobot extends CoreRobot { + EmailRobot(super.$); + + Future onTapForwardEmail() async { + await $(#forward_email_button).tap(); + await $.pump(const Duration(seconds: 2)); + } +} \ No newline at end of file diff --git a/integration_test/robots/search_robot.dart b/integration_test/robots/search_robot.dart index 549ad0f605..363efe5942 100644 --- a/integration_test/robots/search_robot.dart +++ b/integration_test/robots/search_robot.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:core/presentation/views/text/text_field_builder.dart'; import 'package:tmail_ui_user/features/search/email/presentation/search_email_view.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/email_tile_builder.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import '../base/core_robot.dart'; @@ -39,4 +40,27 @@ class SearchRobot extends CoreRobot { await $.waitUntilVisible($(AppLocalizations().showingResultsFor)); await $(AppLocalizations().showingResultsFor).tap(); } + + Future scrollToDateTimeButtonFilter() async { + await $.scrollUntilVisible( + finder: $(#mobile_dateTime_search_filter_button), + view: $(#search_filter_list_view), + scrollDirection: AxisDirection.right, + delta: 300, + ); + } + + Future openDateTimeBottomDialog() async { + await $(#mobile_dateTime_search_filter_button).tap(); + } + + Future selectDateTime(String dateTimeType) async { + await $(find.text(dateTimeType)).tap(); + await $.pump(const Duration(seconds: 2)); + } + + Future openEmail(String subject) async { + await $(find.byType(EmailTileBuilder)).first.tap(); + await $.pump(const Duration(seconds: 2)); + } } \ No newline at end of file diff --git a/integration_test/scenarios/forward_email_scenario.dart b/integration_test/scenarios/forward_email_scenario.dart new file mode 100644 index 0000000000..c38f04e622 --- /dev/null +++ b/integration_test/scenarios/forward_email_scenario.dart @@ -0,0 +1,107 @@ + +import 'package:flutter_test/flutter_test.dart'; +import 'package:rich_text_composer/rich_text_composer.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; +import 'package:tmail_ui_user/features/composer/presentation/composer_view.dart'; +import 'package:tmail_ui_user/features/composer/presentation/view/mobile/mobile_editor_view.dart'; +import 'package:tmail_ui_user/features/email/presentation/email_view.dart'; +import 'package:tmail_ui_user/features/search/email/presentation/search_email_view.dart'; + +import '../base/base_scenario.dart'; +import '../robots/composer_robot.dart'; +import '../robots/email_robot.dart'; +import '../robots/search_robot.dart'; +import '../robots/thread_robot.dart'; +import 'login_with_basic_auth_scenario.dart'; + +class ForwardEmailScenario extends BaseScenario { + const ForwardEmailScenario( + super.$, + { + required this.loginWithBasicAuthScenario, + } + ); + + final LoginWithBasicAuthScenario loginWithBasicAuthScenario; + + @override + Future execute() async { + await loginWithBasicAuthScenario.execute(); + + final threadRobot = ThreadRobot($); + await threadRobot.openSearchView(); + await _expectSearchViewVisible(); + + final searchRobot = SearchRobot($); + await searchRobot.enterQueryString('Forward email'); + await _expectSuggestionSearchListViewVisible(); + await searchRobot.tapOnShowAllResultsText(); + await _expectSearchResultEmailListVisible(); + + await searchRobot.openEmail('Fwd: Forward email'); + await _expectEmailViewVisible(); + await _expectForwardEmailButtonVisible(); + + final emailRobot = EmailRobot($); + await emailRobot.onTapForwardEmail(); + await _expectComposerViewVisible(); + + final composerRobot = ComposerRobot($); + await composerRobot.grantContactPermission(); + await _expectMobileEditorViewVisible(); + + await _expectForwardEmailContentDisplayedCorrectly(); + } + + Future _expectSearchViewVisible() async { + await expectViewVisible($(SearchEmailView)); + } + + Future _expectSuggestionSearchListViewVisible() async { + await expectViewVisible($(#suggestion_search_list_view)); + } + + Future _expectSearchResultEmailListVisible() async { + await expectViewVisible($(#search_email_list_notification_listener)); + await $.pump(const Duration(seconds: 3)); + } + + Future _expectEmailViewVisible() async { + await expectViewVisible($(EmailView)); + } + + Future _expectForwardEmailButtonVisible() async { + await expectViewVisible($(#forward_email_button)); + await $.pump(const Duration(seconds: 3)); + } + + Future _expectComposerViewVisible() async { + await expectViewVisible($(ComposerView)); + } + + Future _expectMobileEditorViewVisible() async { + await expectViewVisible($(#mobile_editor)); + } + + Future _expectForwardEmailContentDisplayedCorrectly() async { + ComposerController? composerController; + await $(ComposerView) + .which((widget) { + composerController = widget.controller; + return true; + }) + .$(MobileEditorView) + .$(HtmlEditor) + .$(InAppWebView).tap(); + + await composerController?.htmlEditorApi?.requestFocusLastChild(); + + final contentHtml = await composerController?.htmlEditorApi?.getText(); + + expect(contentHtml, contains('Subject')); + expect(contentHtml, contains('From')); + expect(contentHtml, contains('To')); + expect(contentHtml, contains('Cc')); + expect(contentHtml, contains('Bcc')); + } +} \ No newline at end of file diff --git a/integration_test/tests/compose/forward_email_test.dart b/integration_test/tests/compose/forward_email_test.dart new file mode 100644 index 0000000000..c24a0549f7 --- /dev/null +++ b/integration_test/tests/compose/forward_email_test.dart @@ -0,0 +1,23 @@ +import '../../base/test_base.dart'; +import '../../scenarios/forward_email_scenario.dart'; +import '../../scenarios/login_with_basic_auth_scenario.dart'; + +void main() { + TestBase().runPatrolTest( + description: 'Should see HTML content contain enough: Subject, From, To, Cc, Bcc, Reply to', + test: ($) async { + final loginWithBasicAuthScenario = LoginWithBasicAuthScenario($, + username: const String.fromEnvironment('USERNAME'), + hostUrl: const String.fromEnvironment('BASIC_AUTH_URL'), + email: const String.fromEnvironment('BASIC_AUTH_EMAIL'), + password: const String.fromEnvironment('PASSWORD'), + ); + final forwardEmailScenario = ForwardEmailScenario( + $, + loginWithBasicAuthScenario: loginWithBasicAuthScenario, + ); + + await forwardEmailScenario.execute(); + }, + ); +} \ No newline at end of file diff --git a/provisioning/integration_test/eml/forward_email/0.eml b/provisioning/integration_test/eml/forward_email/0.eml new file mode 100644 index 0000000000..54646e17dd --- /dev/null +++ b/provisioning/integration_test/eml/forward_email/0.eml @@ -0,0 +1,42 @@ +Return-Path: +MIME-Version: 1.0 +References: +Subject: Fwd: Forward email to Bob +From: emma@example.com +To: "bob@example.com" +Cc: "alice@example.com" +Bcc: "brian@example.com" +Reply-To: emma@example.com +Date: Tue, 17 Dec 2024 16:31:00 +0000 +Message-ID: +User-Agent: Twake-Mail/0.13.2 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; + rv:131.0) Gecko/20100101 Firefox/131.0 +Content-Type: multipart/alternative; + boundary="-=Part.4f.450e1cfbcd355387.192d67ae03d.c367d8042fea24dd=-" + +---=Part.4f.450e1cfbcd355387.192d67ae03d.c367d8042fea24dd=- +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: quoted-printable +Accept-Language: fr-FR, en-US, vi-VN, ru-RU, ar-TN, it-IT +Content-Language: en-US + +Forward email to Bob + +Invite you in to a meeting: Event OPEN TECH TALK: INNOVATION FOR THE TWAKE WORKPLACE + +Our 3rd Open Tech Talk in 2024! + + ► Topic: "Enhancing Multi-Tenancy Security" + ► Topic: "Bringing Desktop Synchronization into TDrive" + ► Topic: "Websocket: Real-time Update + +We are waiting for you! + +---=Part.4f.450e1cfbcd355387.192d67ae03d.c367d8042fea24dd=- +Content-Type: text/html; charset=UTF-8 +Content-Transfer-Encoding: quoted-printable +Accept-Language: fr-FR, en-US, vi-VN, ru-RU, ar-TN, it-IT +Content-Language: en-US + + +---=Part.4f.450e1cfbcd355387.192d67ae03d.c367d8042fea24dd=--- diff --git a/provisioning/integration_test/search_email_with_sort_order/eml/0.eml b/provisioning/integration_test/eml/search_email_with_sort_order/0.eml similarity index 100% rename from provisioning/integration_test/search_email_with_sort_order/eml/0.eml rename to provisioning/integration_test/eml/search_email_with_sort_order/0.eml diff --git a/provisioning/integration_test/search_email_with_sort_order/eml/1.eml b/provisioning/integration_test/eml/search_email_with_sort_order/1.eml similarity index 100% rename from provisioning/integration_test/search_email_with_sort_order/eml/1.eml rename to provisioning/integration_test/eml/search_email_with_sort_order/1.eml diff --git a/provisioning/integration_test/search_email_with_sort_order/eml/2.eml b/provisioning/integration_test/eml/search_email_with_sort_order/2.eml similarity index 100% rename from provisioning/integration_test/search_email_with_sort_order/eml/2.eml rename to provisioning/integration_test/eml/search_email_with_sort_order/2.eml diff --git a/provisioning/integration_test/search_email_with_sort_order/eml/3.eml b/provisioning/integration_test/eml/search_email_with_sort_order/3.eml similarity index 100% rename from provisioning/integration_test/search_email_with_sort_order/eml/3.eml rename to provisioning/integration_test/eml/search_email_with_sort_order/3.eml diff --git a/provisioning/integration_test/search_email_with_sort_order/eml/4.eml b/provisioning/integration_test/eml/search_email_with_sort_order/4.eml similarity index 100% rename from provisioning/integration_test/search_email_with_sort_order/eml/4.eml rename to provisioning/integration_test/eml/search_email_with_sort_order/4.eml diff --git a/provisioning/integration_test/provisioning.sh b/provisioning/integration_test/provisioning.sh new file mode 100755 index 0000000000..20b2dac809 --- /dev/null +++ b/provisioning/integration_test/provisioning.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# Define users and folders +users=("alice" "bob" "brian" "charlotte" "david" "emma") +bobFolders=("Search Emails" "Forward Emails") + +# Add users +for user in "${users[@]}"; do + james-cli AddUser "$user@example.com" "$user" +done + +# Create folders for user Bob +for folderName in "${bobFolders[@]}"; do + echo "Creating $folderName folder for user bob" + james-cli CreateMailbox \#private "bob@example.com" "$folderName" & +done + +# For test search email with sort order +# Import emails into 'Search Emails' folder for user Bob +for eml in {0..4}; do + echo "Importing $eml.eml into 'Search Emails' folder for user bob" + james-cli ImportEml \#private "bob@example.com" "Search Emails" "/root/conf/integration_test/eml/search_email_with_sort_order/$eml.eml" & +done + +# For test forward email +# Import emails into 'Forward Emails' folder for user Bob +echo "Importing 0.eml into 'Forward Emails' folder for user bob" +james-cli ImportEml \#private "bob@example.com" "Forward Emails" "/root/conf/integration_test/eml/forward_email/0.eml" \ No newline at end of file diff --git a/provisioning/integration_test/search_email_with_sort_order/provisioning.sh b/provisioning/integration_test/search_email_with_sort_order/provisioning.sh deleted file mode 100755 index e09ff2200f..0000000000 --- a/provisioning/integration_test/search_email_with_sort_order/provisioning.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -# Define users and folders -users=("alice" "bob" "brian" "charlotte" "david" "emma") - -# Add users -for user in "${users[@]}"; do - james-cli AddUser "$user@example.com" "$user" -done - -# Create search folder for user Bob -james-cli CreateMailbox \#private "bob@example.com" "search" - -# Import emails into search folder for user Bob -for eml in {0..4}; do - echo "Importing $eml.eml into search folder for user bob" - james-cli ImportEml \#private "bob@example.com" "search" "/root/conf/integration_test/search_email_with_sort_order/eml/$eml.eml" & -done diff --git a/scripts/patrol-integration-test-with-docker.sh b/scripts/patrol-integration-test-with-docker.sh index 244248cbc6..3fb4011270 100755 --- a/scripts/patrol-integration-test-with-docker.sh +++ b/scripts/patrol-integration-test-with-docker.sh @@ -41,7 +41,7 @@ export BOB="bob" export ALICE="alice" export DOMAIN="example.com" -docker exec tmail-backend ./root/conf/integration_test/search_email_with_sort_order/provisioning.sh +docker exec tmail-backend ./root/conf/integration_test/provisioning.sh cd .. diff --git a/scripts/patrol-local-integration-test-with-docker.sh b/scripts/patrol-local-integration-test-with-docker.sh index 72b8c589d1..50ddd8241e 100755 --- a/scripts/patrol-local-integration-test-with-docker.sh +++ b/scripts/patrol-local-integration-test-with-docker.sh @@ -41,7 +41,7 @@ export BOB="bob" export ALICE="alice" export DOMAIN="example.com" -docker exec tmail-backend ./root/conf/integration_test/search_email_with_sort_order/provisioning.sh +docker exec tmail-backend ./root/conf/integration_test/provisioning.sh cd .. From 3f52c5d9b3182cbbddb5f01e4a748a561098453f Mon Sep 17 00:00:00 2001 From: Dat PHAM HOANG Date: Wed, 18 Dec 2024 18:04:52 +0700 Subject: [PATCH 07/72] Bump version to v0.14.4 --- CHANGELOG.md | 6 ++++++ pubspec.yaml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26f90e25ac..4c91aeb111 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [0.14.3] - 2024-12-18 +### Fixed +- #3349 Sanitize HTML when forward/reply/replyAll an email +- #3349 Sanitize HTML when print email +- #3349 Add recipients information to the email body when forward/reply/replyAll an email + ## [0.14.3] - 2024-12-16 ### Fixed - Disable Spell check API diff --git a/pubspec.yaml b/pubspec.yaml index aba972db20..5c38a352cb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.14.3 +version: 0.14.4 environment: sdk: ">=3.0.0 <4.0.0" From 9225f9fbd85389e5271ef9a67631487b38ebbbc5 Mon Sep 17 00:00:00 2001 From: DatDang Date: Mon, 16 Dec 2024 16:25:59 +0700 Subject: [PATCH 08/72] TF-3336 Make Echo ping of web socket optional --- docs/adr/0056-web-socket-ping-strategy.md | 24 +++++++++++++++++++ .../ws_echo_ping_configuration.md | 12 ++++++++++ env.file | 3 ++- .../controller/web_socket_controller.dart | 5 +++- lib/main/utils/app_config.dart | 2 ++ 5 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 docs/adr/0056-web-socket-ping-strategy.md create mode 100644 docs/configuration/ws_echo_ping_configuration.md diff --git a/docs/adr/0056-web-socket-ping-strategy.md b/docs/adr/0056-web-socket-ping-strategy.md new file mode 100644 index 0000000000..5c099d2581 --- /dev/null +++ b/docs/adr/0056-web-socket-ping-strategy.md @@ -0,0 +1,24 @@ +# 56. Web Socket Ping Strategy + +Date: 2024-12-16 + +## Status + +Accepted + +## Context + +- Echo ping method takes too much resources from the server +- Server implemented ping frame + +## Decision + +- Twake Mail no longer have to implement Echo ping +- Browser will automatically send pong frame as default implementation +- Echo ping will still be left as an option in `env.file` through `WS_ECHO_PING` + - Set it to `true` if you want to use Echo ping + - Set it to `false` or left it as is if you don't want to use Echo ping + +## Consequences + +- Server resources used will be reduced diff --git a/docs/configuration/ws_echo_ping_configuration.md b/docs/configuration/ws_echo_ping_configuration.md new file mode 100644 index 0000000000..1a51078775 --- /dev/null +++ b/docs/configuration/ws_echo_ping_configuration.md @@ -0,0 +1,12 @@ +## Configuration Web Socket Echo Ping + +### Context +- Echo ping method is optional +### How to config +In [env.file]: +- If you want to use Echo ping: +```WS_ECHO_PING=true``` +- If you don't want to use Echo ping: +```WS_ECHO_PING=false``` + or +```WS_ECHO_PING=``` \ No newline at end of file diff --git a/env.file b/env.file index f3ff169d6c..a072f68967 100644 --- a/env.file +++ b/env.file @@ -6,4 +6,5 @@ APP_GRID_AVAILABLE=supported FCM_AVAILABLE=supported IOS_FCM=supported FORWARD_WARNING_MESSAGE= -PLATFORM=other \ No newline at end of file +PLATFORM=other +WS_ECHO_PING= \ No newline at end of file diff --git a/lib/features/push_notification/presentation/controller/web_socket_controller.dart b/lib/features/push_notification/presentation/controller/web_socket_controller.dart index a262e7508f..f0dbeedc3b 100644 --- a/lib/features/push_notification/presentation/controller/web_socket_controller.dart +++ b/lib/features/push_notification/presentation/controller/web_socket_controller.dart @@ -17,6 +17,7 @@ import 'package:tmail_ui_user/features/push_notification/presentation/extensions import 'package:tmail_ui_user/features/push_notification/presentation/listener/email_change_listener.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/listener/mailbox_change_listener.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; +import 'package:tmail_ui_user/main/utils/app_config.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; class WebSocketController extends PushBaseController { @@ -79,7 +80,9 @@ class WebSocketController extends PushBaseController { _retryRemained = 3; _webSocketChannel = success.webSocketChannel; _enableWebSocketPush(); - _pingWebSocket(); + if (AppConfig.isWebSocketEchoPingEnabled) { + _pingWebSocket(); + } _listenToWebSocket(); } diff --git a/lib/main/utils/app_config.dart b/lib/main/utils/app_config.dart index f749c7c468..9f165e945f 100644 --- a/lib/main/utils/app_config.dart +++ b/lib/main/utils/app_config.dart @@ -68,4 +68,6 @@ class AppConfig { static String get _platformEnv => dotenv.get('PLATFORM', fallback: 'other'); static bool get isSaasPlatForm => _platformEnv.toLowerCase() == saasPlatform; + + static bool get isWebSocketEchoPingEnabled => dotenv.get('WS_ECHO_PING', fallback: 'false') == 'true'; } \ No newline at end of file From 6baff90afb6d1f31fb31ea076824b6fc928de51b Mon Sep 17 00:00:00 2001 From: DatDang Date: Mon, 16 Dec 2024 11:08:10 +0700 Subject: [PATCH 09/72] Format calendar event description --- .../sanitize_autolink_filter.dart | 5 +- .../text/new_line_transformer.dart | 15 ++ ...ze_autolink_unescape_html_transformer.dart | 13 ++ .../calendar_event_repository_impl.dart | 36 ++++- .../repository/calendar_event_repository.dart | 5 + .../parse_calendar_event_interactor.dart | 11 +- .../calendar_event_interactor_bindings.dart | 11 +- .../controller/single_email_controller.dart | 22 ++- .../extensions/calendar_event_extension.dart | 59 +++++++ .../calendar_event_repository_impl_test.dart | 12 +- .../single_email_controller_test.dart | 145 ++++++++++++++++++ 11 files changed, 321 insertions(+), 13 deletions(-) create mode 100644 core/lib/presentation/utils/html_transformer/text/new_line_transformer.dart create mode 100644 core/lib/presentation/utils/html_transformer/text/sanitize_autolink_unescape_html_transformer.dart diff --git a/core/lib/presentation/utils/html_transformer/sanitize_autolink_filter.dart b/core/lib/presentation/utils/html_transformer/sanitize_autolink_filter.dart index 92ab8c2e54..1dcad41e1a 100644 --- a/core/lib/presentation/utils/html_transformer/sanitize_autolink_filter.dart +++ b/core/lib/presentation/utils/html_transformer/sanitize_autolink_filter.dart @@ -7,6 +7,7 @@ import 'package:linkify/linkify.dart'; class SanitizeAutolinkFilter { final HtmlEscape htmlEscape; + final bool escapeHtml; final _linkifyOption = const LinkifyOptions( humanize: true, looseUrl: true, @@ -18,7 +19,7 @@ class SanitizeAutolinkFilter { const UrlLinkifier() ]; - SanitizeAutolinkFilter(this.htmlEscape); + SanitizeAutolinkFilter(this.htmlEscape, {this.escapeHtml = true}); String process(String inputText) { try { @@ -36,7 +37,7 @@ class SanitizeAutolinkFilter { for (var element in elements) { if (element is TextElement) { - final escapedHtml = htmlEscape.convert(element.text); + final escapedHtml = escapeHtml ? htmlEscape.convert(element.text) : element.text; htmlTextBuffer.write(escapedHtml); } else if (element is EmailElement) { final emailLinkTag = _buildEmailLinkTag( diff --git a/core/lib/presentation/utils/html_transformer/text/new_line_transformer.dart b/core/lib/presentation/utils/html_transformer/text/new_line_transformer.dart new file mode 100644 index 0000000000..4b13b76466 --- /dev/null +++ b/core/lib/presentation/utils/html_transformer/text/new_line_transformer.dart @@ -0,0 +1,15 @@ +import 'dart:convert'; + +import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart'; + +class NewLineTransformer extends TextTransformer { + const NewLineTransformer(); + + @override + String process(String text, HtmlEscape htmlEscape) { + return text + .replaceAll('\n', '
') + .replaceAll('\r', ' ') + .replaceAll('\t', ' '); + } +} \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/text/sanitize_autolink_unescape_html_transformer.dart b/core/lib/presentation/utils/html_transformer/text/sanitize_autolink_unescape_html_transformer.dart new file mode 100644 index 0000000000..75aba00855 --- /dev/null +++ b/core/lib/presentation/utils/html_transformer/text/sanitize_autolink_unescape_html_transformer.dart @@ -0,0 +1,13 @@ +import 'dart:convert'; + +import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart'; +import 'package:core/presentation/utils/html_transformer/sanitize_autolink_filter.dart'; + +class SanitizeAutolinkUnescapeHtmlTransformer extends TextTransformer { + const SanitizeAutolinkUnescapeHtmlTransformer(); + + @override + String process(String text, HtmlEscape htmlEscape) { + return SanitizeAutolinkFilter(htmlEscape, escapeHtml: false).process(text); + } +} \ No newline at end of file diff --git a/lib/features/email/data/repository/calendar_event_repository_impl.dart b/lib/features/email/data/repository/calendar_event_repository_impl.dart index 49c59dce72..c0e48e4179 100644 --- a/lib/features/email/data/repository/calendar_event_repository_impl.dart +++ b/lib/features/email/data/repository/calendar_event_repository_impl.dart @@ -1,19 +1,24 @@ import 'package:core/data/model/source_type/data_source_type.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/reply/calendar_event_accept_response.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/reply/calendar_event_maybe_response.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/reply/calendar_event_reject_response.dart'; import 'package:tmail_ui_user/features/email/data/datasource/calendar_event_datasource.dart'; +import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart'; import 'package:tmail_ui_user/features/email/domain/repository/calendar_event_repository.dart'; +import 'package:tmail_ui_user/features/email/presentation/extensions/calendar_event_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/model/blob_calendar_event.dart'; class CalendarEventRepositoryImpl extends CalendarEventRepository { final Map _calendarEventDataSource; + final HtmlDataSource _htmlDataSource; - CalendarEventRepositoryImpl(this._calendarEventDataSource); + CalendarEventRepositoryImpl(this._calendarEventDataSource, this._htmlDataSource); @override Future> parse(AccountId accountId, Set blobIds) { @@ -49,4 +54,33 @@ class CalendarEventRepositoryImpl extends CalendarEventRepository { return _calendarEventDataSource[DataSourceType.network]! .rejectEventInvitation(accountId, blobIds, language); } + + @override + Future> transformCalendarEventDescription( + List blobCalendarEvents, + TransformConfiguration transformConfiguration, + ) async { + return Future.wait(blobCalendarEvents.map((blobCalendarEvent) async { + return BlobCalendarEvent( + blobId: blobCalendarEvent.blobId, + calendarEventList: await Future.wait(blobCalendarEvent.calendarEventList.map((calendarEvent) { + return _transformCalendarEventDescription(calendarEvent, transformConfiguration); + })), + ); + })); + } + + Future _transformCalendarEventDescription( + CalendarEvent calendarEvent, + TransformConfiguration transformConfiguration, + ) async { + return calendarEvent.copyWith( + description: calendarEvent.description?.trim().isNotEmpty == true + ? await _htmlDataSource.transformHtmlEmailContent( + calendarEvent.description!, + transformConfiguration, + ) + : calendarEvent.description, + ); + } } \ No newline at end of file diff --git a/lib/features/email/domain/repository/calendar_event_repository.dart b/lib/features/email/domain/repository/calendar_event_repository.dart index de287e0e42..cf2f193189 100644 --- a/lib/features/email/domain/repository/calendar_event_repository.dart +++ b/lib/features/email/domain/repository/calendar_event_repository.dart @@ -1,4 +1,5 @@ +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/reply/calendar_event_accept_response.dart'; @@ -23,4 +24,8 @@ abstract class CalendarEventRepository { AccountId accountId, Set blobIds, String? language); + + Future> transformCalendarEventDescription( + List blobCalendarEvents, + TransformConfiguration transformConfiguration); } \ No newline at end of file diff --git a/lib/features/email/domain/usecases/parse_calendar_event_interactor.dart b/lib/features/email/domain/usecases/parse_calendar_event_interactor.dart index 55b11e5fb8..f7ceff8849 100644 --- a/lib/features/email/domain/usecases/parse_calendar_event_interactor.dart +++ b/lib/features/email/domain/usecases/parse_calendar_event_interactor.dart @@ -1,5 +1,6 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; @@ -15,15 +16,19 @@ class ParseCalendarEventInteractor { Stream> execute( AccountId accountId, Set blobIds, - String emailContents + TransformConfiguration transformConfiguration, ) async* { try { yield Right(ParseCalendarEventLoading()); final listBlobCalendarEvent = await _calendarEventRepository.parse(accountId, blobIds); + final listBlobCalendarEventWithTransformedDescription = await _calendarEventRepository.transformCalendarEventDescription( + listBlobCalendarEvent, + transformConfiguration, + ); - if (listBlobCalendarEvent.isNotEmpty) { - yield Right(ParseCalendarEventSuccess(listBlobCalendarEvent)); + if (listBlobCalendarEventWithTransformedDescription.isNotEmpty) { + yield Right(ParseCalendarEventSuccess(listBlobCalendarEventWithTransformedDescription)); } else { yield Left(ParseCalendarEventFailure(NotFoundCalendarEventException())); } diff --git a/lib/features/email/presentation/bindings/calendar_event_interactor_bindings.dart b/lib/features/email/presentation/bindings/calendar_event_interactor_bindings.dart index e4718ed820..7d841f8ea8 100644 --- a/lib/features/email/presentation/bindings/calendar_event_interactor_bindings.dart +++ b/lib/features/email/presentation/bindings/calendar_event_interactor_bindings.dart @@ -3,7 +3,10 @@ import 'package:get/get.dart'; import 'package:jmap_dart_client/http/http_client.dart'; import 'package:tmail_ui_user/features/base/interactors_bindings.dart'; import 'package:tmail_ui_user/features/email/data/datasource/calendar_event_datasource.dart'; +import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource_impl/calendar_event_datasource_impl.dart'; +import 'package:tmail_ui_user/features/email/data/datasource_impl/html_datasource_impl.dart'; +import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; import 'package:tmail_ui_user/features/email/data/network/calendar_event_api.dart'; import 'package:tmail_ui_user/features/email/data/repository/calendar_event_repository_impl.dart'; import 'package:tmail_ui_user/features/email/domain/repository/calendar_event_repository.dart'; @@ -11,6 +14,7 @@ import 'package:tmail_ui_user/features/email/domain/usecases/calendar_event_acce import 'package:tmail_ui_user/features/email/domain/usecases/maybe_calendar_event_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/calendar_event_reject_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/parse_calendar_event_interactor.dart'; +import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; class CalendarEventInteractorBindings extends InteractorsBindings { @@ -18,6 +22,7 @@ class CalendarEventInteractorBindings extends InteractorsBindings { @override void bindingsDataSource() { Get.lazyPut(() => Get.find()); + Get.lazyPut(() => Get.find()); } @override @@ -26,6 +31,9 @@ class CalendarEventInteractorBindings extends InteractorsBindings { Get.lazyPut(() => CalendarEventDataSourceImpl( Get.find(), Get.find())); + Get.lazyPut(() => HtmlDataSourceImpl( + Get.find(), + Get.find())); } @override @@ -44,7 +52,8 @@ class CalendarEventInteractorBindings extends InteractorsBindings { @override void bindingsRepositoryImpl() { Get.lazyPut(() => CalendarEventRepositoryImpl( - {DataSourceType.network: Get.find()} + {DataSourceType.network: Get.find()}, + Get.find(), )); } } \ No newline at end of file diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index e40e78fca3..45449b4979 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -4,6 +4,9 @@ import 'dart:typed_data'; import 'package:better_open_file/better_open_file.dart' as open_file; import 'package:core/core.dart'; +import 'package:core/presentation/utils/html_transformer/text/sanitize_autolink_unescape_html_transformer.dart'; +import 'package:core/presentation/utils/html_transformer/text/new_line_transformer.dart'; +import 'package:core/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart'; import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; @@ -519,7 +522,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { _parseCalendarEventAction( accountId: mailboxDashBoardController.accountId.value!, blobIds: success.attachments?.calendarEventBlobIds ?? {}, - emailContents: success.htmlEmailContent ); } else { emailContents.value = success.htmlEmailContent; @@ -564,7 +566,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { _parseCalendarEventAction( accountId: mailboxDashBoardController.accountId.value!, blobIds: success.attachments?.calendarEventBlobIds ?? {}, - emailContents: success.htmlEmailContent ); } else { emailContents.value = success.htmlEmailContent; @@ -1466,13 +1467,26 @@ class SingleEmailController extends BaseController with AppLoaderMixin { CapabilityIdentifier.jamesCalendarEvent.isSupported(session, accountId); } + @visibleForTesting + parseCalendarEventAction({ + required AccountId accountId, + required Set blobIds, + }) => _parseCalendarEventAction(accountId: accountId, blobIds: blobIds); + void _parseCalendarEventAction({ required AccountId accountId, required Set blobIds, - required String emailContents }) { log("SingleEmailController::_parseCalendarEventAction:blobIds: $blobIds"); - consumeState(_parseCalendarEventInteractor!.execute(accountId, blobIds, emailContents)); + consumeState(_parseCalendarEventInteractor!.execute( + accountId, + blobIds, + TransformConfiguration.fromTextTransformers(const [ + SanitizeAutolinkUnescapeHtmlTransformer(), + StandardizeHtmlSanitizingTransformers(), + NewLineTransformer(), + ]) + )); } void _handleParseCalendarEventSuccess(ParseCalendarEventSuccess success) { diff --git a/lib/features/email/presentation/extensions/calendar_event_extension.dart b/lib/features/email/presentation/extensions/calendar_event_extension.dart index 2a7c895e0d..f993c87cb2 100644 --- a/lib/features/email/presentation/extensions/calendar_event_extension.dart +++ b/lib/features/email/presentation/extensions/calendar_event_extension.dart @@ -5,10 +5,21 @@ import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/utils/app_logger.dart'; import 'package:date_format/date_format.dart' as date_format; import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/core/utc_date.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/properties/attendee/calendar_attendee.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/properties/attendee/calendar_attendee_participation_status.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_duration.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_event_status.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_extension_fields.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_free_busy_status.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_organizer.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_priority.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_privacy.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_sequence.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/event_id.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/properties/event_method.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/recurrence_rule/recurrence_rule.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; @@ -364,4 +375,52 @@ extension CalendarEventExtension on CalendarEvent { method == EventMethod.request || method == EventMethod.add || method == EventMethod.counter; + + CalendarEvent copyWith({ + EventId? eventId, + String? title, + String? description, + DateTime? startDate, + DateTime? endDate, + UTCDate? startUtcDate, + UTCDate? endUtcDate, + CalendarDuration? duration, + String? timeZone, + String? location, + EventMethod? method, + CalendarSequence? sequence, + CalendarPrivacy? privacy, + CalendarPriority? priority, + CalendarFreeBusyStatus? freeBusyStatus, + CalendarEventStatus? status, + CalendarOrganizer? organizer, + List? participants, + CalendarExtensionFields? extensionFields, + List? recurrenceRules, + List? excludedCalendarEvents, + }) { + return CalendarEvent( + eventId: eventId ?? this.eventId, + title: title ?? this.title, + description: description ?? this.description, + startDate: startDate ?? this.startDate, + endDate: endDate ?? this.endDate, + startUtcDate: startUtcDate ?? this.startUtcDate, + endUtcDate: endUtcDate ?? this.endUtcDate, + duration: duration ?? this.duration, + timeZone: timeZone ?? this.timeZone, + location: location ?? this.location, + method: method ?? this.method, + sequence: sequence ?? this.sequence, + privacy: privacy ?? this.privacy, + priority: priority ?? this.priority, + freeBusyStatus: freeBusyStatus ?? this.freeBusyStatus, + status: status ?? this.status, + organizer: organizer ?? this.organizer, + participants: participants ?? this.participants, + extensionFields: extensionFields ?? this.extensionFields, + recurrenceRules: recurrenceRules ?? this.recurrenceRules, + excludedCalendarEvents: excludedCalendarEvents ?? this.excludedCalendarEvents, + ); + } } \ No newline at end of file diff --git a/test/features/email/data/repository/calendar_event_repository_impl_test.dart b/test/features/email/data/repository/calendar_event_repository_impl_test.dart index 43b51824ad..67c4bc3d7a 100644 --- a/test/features/email/data/repository/calendar_event_repository_impl_test.dart +++ b/test/features/email/data/repository/calendar_event_repository_impl_test.dart @@ -9,17 +9,25 @@ import 'package:jmap_dart_client/jmap/mail/calendar/reply/calendar_event_reject_ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:tmail_ui_user/features/email/data/datasource/calendar_event_datasource.dart'; +import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart'; import 'package:tmail_ui_user/features/email/data/repository/calendar_event_repository_impl.dart'; import 'package:tmail_ui_user/features/email/domain/exceptions/calendar_event_exceptions.dart'; import 'calendar_event_repository_impl_test.mocks.dart'; -@GenerateNiceMocks([MockSpec()]) +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), +]) void main() { final calendarEventNetworkDataSource = MockCalendarEventDataSource(); + final htmlDatasource = MockHtmlDataSource(); final calendarEventDataSource = { DataSourceType.network: calendarEventNetworkDataSource}; - final calendarEventRepository = CalendarEventRepositoryImpl(calendarEventDataSource); + final calendarEventRepository = CalendarEventRepositoryImpl( + calendarEventDataSource, + htmlDatasource, + ); final accountId = AccountId(Id('123')); final blobId = Id('blobId'); const language = 'en'; diff --git a/test/features/email/presentation/controller/single_email_controller_test.dart b/test/features/email/presentation/controller/single_email_controller_test.dart index 1768bc9342..9ba67d1e37 100644 --- a/test/features/email/presentation/controller/single_email_controller_test.dart +++ b/test/features/email/presentation/controller/single_email_controller_test.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:ui'; import 'package:core/core.dart'; @@ -18,7 +19,12 @@ import 'package:jmap_dart_client/jmap/mail/calendar/reply/calendar_event_accept_ import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:model/email/presentation_email.dart'; import 'package:tmail_ui_user/features/caching/caching_manager.dart'; +import 'package:tmail_ui_user/features/email/data/datasource/calendar_event_datasource.dart'; +import 'package:tmail_ui_user/features/email/data/datasource_impl/html_datasource_impl.dart'; +import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; +import 'package:tmail_ui_user/features/email/data/repository/calendar_event_repository_impl.dart'; import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; import 'package:tmail_ui_user/features/email/domain/state/calendar_event_accept_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/parse_calendar_event_state.dart'; @@ -32,9 +38,11 @@ import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_i import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_star_email_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/parse_calendar_event_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/store_event_attendance_status_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/store_opened_email_interactor.dart'; +import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/email_supervisor_controller.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; import 'package:tmail_ui_user/features/email/presentation/model/blob_calendar_event.dart'; @@ -46,9 +54,11 @@ import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_ import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; +import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; import 'package:tmail_ui_user/main/utils/toast_manager.dart'; import 'package:uuid/uuid.dart'; +import '../../../../fixtures/account_fixtures.dart'; import '../../../../fixtures/email_fixtures.dart'; import 'single_email_controller_test.mocks.dart'; @@ -91,6 +101,8 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(), + MockSpec(), + MockSpec(), ]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -363,4 +375,137 @@ void main() { debugDefaultTargetPlatformOverride = null; }); }); + + group('_parseCalendarEventAction method test:', () { + final calendarEventDataSource = MockCalendarEventDataSource(); + final calendarEventRepository = CalendarEventRepositoryImpl( + {DataSourceType.network :calendarEventDataSource}, + HtmlDataSourceImpl( + HtmlAnalyzer(HtmlTransform( + MockDioClient(), + const HtmlEscape(), + )), + CacheExceptionThrower(), + ), + ); + + setUp(() { + Get.put(ParseCalendarEventInteractor(calendarEventRepository)); + }); + + tearDown(() { + Get.delete(); + }); + + test( + 'should transform all calendar event description url to a tag ' + 'and all new line to
tag', + () async { + // arrange + const eventDescription = '\nhttps://example1.com\nhttps://example2.com'; + const expectedEventDescription = '' + '
' + 'example1.com' + '
' + 'example2.com' + ''; + final blobId = Id('abc123'); + final calendarEvent = CalendarEvent( + description: eventDescription, + ); + final blobCalendarEvents = [ + BlobCalendarEvent( + blobId: blobId, + calendarEventList: [calendarEvent], + ), + ]; + when(calendarEventDataSource.parse(any, any)) + .thenAnswer((_) async => blobCalendarEvents); + + when(mailboxDashboardController.selectedEmail).thenReturn(Rxn(PresentationEmail())); + when(mailboxDashboardController.emailUIAction).thenReturn(Rxn(EmailUIAction())); + when(mailboxDashboardController.viewState).thenReturn(Rx(Right(UIState.idle))); + when(mailboxDashboardController.accountId).thenReturn(Rxn(AccountFixtures.aliceAccountId)); + when(emailSupervisorController.scrollPhysicsPageView).thenReturn(Rxn()); + + singleEmailController.onInit(); + mailboxDashboardController.accountId.refresh(); + + // act + singleEmailController.parseCalendarEventAction( + accountId: AccountFixtures.aliceAccountId, + blobIds: {blobId}, + ); + await untilCalled(calendarEventDataSource.parse(any, any)); + await Future.delayed(Duration.zero); + + // assert + expect( + singleEmailController.blobCalendarEvent.value, + BlobCalendarEvent( + blobId: blobId, + calendarEventList: [CalendarEvent(description: expectedEventDescription)], + ), + ); + }); + + test( + 'should transform all calendar event description url to a tag ' + 'and all new line to
tag ' + 'and remove all xss attempt', + () async { + // arrange + const eventDescription = '\nhttps://example1.com' + '\nhttps://example2.com' + '\n' + '\nhref xss'; + const expectedEventDescription = '' + '
' + 'example1.com' + '
' + 'example2.com' + '
' + '
' + 'href xss' + ''; + final blobId = Id('abc123'); + final calendarEvent = CalendarEvent( + description: eventDescription, + ); + final blobCalendarEvents = [ + BlobCalendarEvent( + blobId: blobId, + calendarEventList: [calendarEvent], + ), + ]; + when(calendarEventDataSource.parse(any, any)) + .thenAnswer((_) async => blobCalendarEvents); + + when(mailboxDashboardController.selectedEmail).thenReturn(Rxn(PresentationEmail())); + when(mailboxDashboardController.emailUIAction).thenReturn(Rxn(EmailUIAction())); + when(mailboxDashboardController.viewState).thenReturn(Rx(Right(UIState.idle))); + when(mailboxDashboardController.accountId).thenReturn(Rxn(AccountFixtures.aliceAccountId)); + when(emailSupervisorController.scrollPhysicsPageView).thenReturn(Rxn()); + + singleEmailController.onInit(); + mailboxDashboardController.accountId.refresh(); + + // act + singleEmailController.parseCalendarEventAction( + accountId: AccountFixtures.aliceAccountId, + blobIds: {blobId}, + ); + await untilCalled(calendarEventDataSource.parse(any, any)); + await Future.delayed(Duration.zero); + + // assert + expect( + singleEmailController.blobCalendarEvent.value, + BlobCalendarEvent( + blobId: blobId, + calendarEventList: [CalendarEvent(description: expectedEventDescription)], + ), + ); + }); + }); } From 3ee3913383f1133f07a809ab20ee92f397b35788 Mon Sep 17 00:00:00 2001 From: DatDang Date: Tue, 17 Dec 2024 14:59:00 +0700 Subject: [PATCH 10/72] TF-3341 Enable web socket for mobile --- .../mailbox_dashboard_controller.dart | 7 +-- .../controller/fcm_message_controller.dart | 23 ---------- .../controller/push_base_controller.dart | 2 + .../controller/web_socket_controller.dart | 43 ++++++++++++++++--- .../presentation/services/fcm_receiver.dart | 5 --- .../presentation/services/fcm_service.dart | 27 ------------ 6 files changed, 43 insertions(+), 64 deletions(-) diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 9a532c1230..1387e4fc4a 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -125,6 +125,7 @@ import 'package:tmail_ui_user/features/push_notification/domain/usecases/delete_ import 'package:tmail_ui_user/features/push_notification/domain/usecases/delete_mailbox_state_to_refresh_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_email_state_to_refresh_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_mailbox_state_to_refresh_interactor.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/controller/web_socket_controller.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/notification/local_notification_manager.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/services/fcm_service.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/utils/fcm_utils.dart'; @@ -627,9 +628,8 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo injectAutoCompleteBindings(session, currentAccountId); injectRuleFilterBindings(session, currentAccountId); injectVacationBindings(session, currentAccountId); - if (PlatformInfo.isWeb) { - injectWebSocket(session, currentAccountId); - } else { + injectWebSocket(session, currentAccountId); + if (PlatformInfo.isMobile) { injectFCMBindings(session, currentAccountId); } @@ -2948,6 +2948,7 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo sessionCurrent = null; mapMailboxById = {}; mapDefaultMailboxIdByRole = {}; + WebSocketController.instance.onClose(); super.onClose(); } } \ No newline at end of file diff --git a/lib/features/push_notification/presentation/controller/fcm_message_controller.dart b/lib/features/push_notification/presentation/controller/fcm_message_controller.dart index a290e4dbe7..5782f3ed59 100644 --- a/lib/features/push_notification/presentation/controller/fcm_message_controller.dart +++ b/lib/features/push_notification/presentation/controller/fcm_message_controller.dart @@ -59,17 +59,9 @@ class FcmMessageController extends PushBaseController { super.initialize(accountId: accountId, session: session); _listenTokenStream(); - _listenForegroundMessageStream(); _listenBackgroundMessageStream(); } - void _listenForegroundMessageStream() { - FcmService.instance.foregroundMessageStreamController - ?.stream - .debounceTime(const Duration(milliseconds: FcmUtils.durationMessageComing)) - .listen(_handleForegroundMessageAction); - } - void _listenBackgroundMessageStream() { FcmService.instance.backgroundMessageStreamController ?.stream @@ -84,21 +76,6 @@ class FcmMessageController extends PushBaseController { .listen(FcmTokenController.instance.onFcmTokenChanged); } - void _handleForegroundMessageAction(Map payloadData) { - log('FcmMessageController::_handleForegroundMessageAction():payloadData: $payloadData | accountId: $accountId'); - if (accountId != null && session?.username != null) { - final stateChange = FcmUtils.instance.convertFirebaseDataMessageToStateChange(payloadData); - final mapTypeState = stateChange.getMapTypeState(accountId!); - mappingTypeStateToAction( - mapTypeState, - accountId!, - emailChangeListener: EmailChangeListener.instance, - mailboxChangeListener: MailboxChangeListener.instance, - session!.username, - session: session); - } - } - void _handleBackgroundMessageAction(Map payloadData) async { log('FcmMessageController::_handleBackgroundMessageAction():payloadData: $payloadData'); final stateChange = FcmUtils.instance.convertFirebaseDataMessageToStateChange(payloadData); diff --git a/lib/features/push_notification/presentation/controller/push_base_controller.dart b/lib/features/push_notification/presentation/controller/push_base_controller.dart index a9c25f16ff..d555c33a9c 100644 --- a/lib/features/push_notification/presentation/controller/push_base_controller.dart +++ b/lib/features/push_notification/presentation/controller/push_base_controller.dart @@ -43,6 +43,8 @@ abstract class PushBaseController { this.session = session; } + void onClose() {} + void mappingTypeStateToAction( Map mapTypeState, AccountId accountId, diff --git a/lib/features/push_notification/presentation/controller/web_socket_controller.dart b/lib/features/push_notification/presentation/controller/web_socket_controller.dart index f0dbeedc3b..821a873cb7 100644 --- a/lib/features/push_notification/presentation/controller/web_socket_controller.dart +++ b/lib/features/push_notification/presentation/controller/web_socket_controller.dart @@ -4,7 +4,9 @@ import 'dart:convert'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:fcm/model/type_name.dart'; +import 'package:flutter/material.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/push/state_change.dart'; @@ -33,13 +35,12 @@ class WebSocketController extends PushBaseController { WebSocketChannel? _webSocketChannel; Timer? _webSocketPingTimer; StreamSubscription? _webSocketSubscription; + AppLifecycleListener? _appLifecycleListener; @override void handleFailureViewState(Failure failure) { logError('WebSocketController::handleFailureViewState():Failure $failure'); - _webSocketSubscription?.cancel(); - _webSocketChannel = null; - _webSocketPingTimer?.cancel(); + _cleanUpWebSocketResources(); if (failure is WebSocketConnectionFailed) { _handleWebSocketConnectionRetry(); } @@ -64,6 +65,31 @@ class WebSocketController extends PushBaseController { super.initialize(accountId: accountId, session: session); _connectWebSocket(accountId, session); + if (PlatformInfo.isMobile) { + _listenToAppLifeCycle(accountId, session); + } + } + + @override + void onClose() { + _cleanUpWebSocketResources(); + _appLifecycleListener?.dispose(); + _appLifecycleListener = null; + super.onClose(); + } + + void _listenToAppLifeCycle(AccountId? accountId, Session? session) { + _appLifecycleListener ??= AppLifecycleListener( + onStateChange: (appLifecycleState) { + switch (appLifecycleState) { + case AppLifecycleState.resumed: + _connectWebSocket(accountId, session); + break; + default: + _cleanUpWebSocketResources(); + } + }, + ); } void _connectWebSocket(AccountId? accountId, Session? session) { @@ -74,9 +100,16 @@ class WebSocketController extends PushBaseController { consumeState(_connectWebSocketInteractor!.execute(session, accountId)); } + + void _cleanUpWebSocketResources() { + _webSocketSubscription?.cancel(); + _webSocketChannel = null; + _webSocketPingTimer?.cancel(); + } void _handleWebSocketConnectionSuccess(WebSocketConnectionSuccess success) { log('WebSocketController::_handleWebSocketConnectionSuccess(): $success'); + _cleanUpWebSocketResources(); _retryRemained = 3; _webSocketChannel = success.webSocketChannel; _enableWebSocketPush(); @@ -87,9 +120,7 @@ class WebSocketController extends PushBaseController { } void _handleWebSocketConnectionRetry() { - _webSocketSubscription?.cancel(); - _webSocketChannel = null; - _webSocketPingTimer?.cancel(); + _cleanUpWebSocketResources(); if (_retryRemained > 0) { _retryRemained--; _connectWebSocket(accountId, session); diff --git a/lib/features/push_notification/presentation/services/fcm_receiver.dart b/lib/features/push_notification/presentation/services/fcm_receiver.dart index 0cb6efd145..a56ee2164a 100644 --- a/lib/features/push_notification/presentation/services/fcm_receiver.dart +++ b/lib/features/push_notification/presentation/services/fcm_receiver.dart @@ -20,16 +20,11 @@ class FcmReceiver { static const int MAX_COUNT_RETRY_TO_GET_FCM_TOKEN = 3; Future onInitialFcmListener() async { - _onForegroundMessage(); _onBackgroundMessage(); await _onHandleFcmToken(); } - void _onForegroundMessage() { - FirebaseMessaging.onMessage.listen(FcmService.instance.handleFirebaseForegroundMessage); - } - void _onBackgroundMessage() { FirebaseMessaging.onBackgroundMessage(handleFirebaseBackgroundMessage); } diff --git a/lib/features/push_notification/presentation/services/fcm_service.dart b/lib/features/push_notification/presentation/services/fcm_service.dart index 147bded91b..2906bf981a 100644 --- a/lib/features/push_notification/presentation/services/fcm_service.dart +++ b/lib/features/push_notification/presentation/services/fcm_service.dart @@ -1,15 +1,11 @@ import 'dart:async'; -import 'dart:convert'; import 'package:core/utils/app_logger.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:tmail_ui_user/features/push_notification/presentation/model/broadcast_message_event_data.dart'; -import 'package:universal_html/html.dart' as html; class FcmService { - StreamController>? foregroundMessageStreamController; StreamController>? backgroundMessageStreamController; StreamController? fcmTokenStreamController; @@ -19,13 +15,6 @@ class FcmService { static FcmService get instance => _instance; - void handleFirebaseForegroundMessage(RemoteMessage newRemoteMessage) { - log('FcmService::handleFirebaseForegroundMessage():data: ${newRemoteMessage.data}'); - if (newRemoteMessage.data.isNotEmpty) { - foregroundMessageStreamController?.add(newRemoteMessage.data); - } - } - void handleFirebaseBackgroundMessage(RemoteMessage newRemoteMessage) { log('FcmService::handleFirebaseBackgroundMessage():data: ${newRemoteMessage.data}'); if (newRemoteMessage.data.isNotEmpty) { @@ -33,19 +22,6 @@ class FcmService { } } - void handleMessageEventBroadcastChannel(html.MessageEvent messageEvent) { - log('FcmService::handleMessageEventBroadcastChannel():TYPE: ${messageEvent.data.runtimeType} | DATA: ${messageEvent.data}'); - try { - final jsonEventData = jsonDecode(jsonEncode(messageEvent.data)) as Map; - final eventData = BroadcastMessageEventData.fromJson(jsonEventData); - if (eventData.data?.isNotEmpty == true) { - foregroundMessageStreamController?.add(eventData.data!); - } - } catch (e) { - logError('FcmService::handleMessageEventBroadcastChannel: Exception = $e'); - } - } - void handleToken(String? token) { log('FcmService::handleToken():token: $token'); fcmTokenStreamController?.add(token); @@ -53,17 +29,14 @@ class FcmService { void initialStreamController() { log('FcmService::initialStreamController:'); - foregroundMessageStreamController = StreamController>.broadcast(); backgroundMessageStreamController = StreamController>.broadcast(); fcmTokenStreamController = StreamController.broadcast(); } void closeStream() { - foregroundMessageStreamController?.close(); backgroundMessageStreamController?.close(); fcmTokenStreamController?.close(); - foregroundMessageStreamController = null; backgroundMessageStreamController = null; fcmTokenStreamController = null; } From 918cfe74b89f42bfeb8174173829970c9b733da2 Mon Sep 17 00:00:00 2001 From: DatDang Date: Tue, 17 Dec 2024 17:13:14 +0700 Subject: [PATCH 11/72] TF-3341 Integration test web socket for mobile --- backend-docker/jmap.properties | 4 +- .../web_socket_update_ui_scenario.dart | 84 +++++++++++++++++++ .../tests/web_socket/web_socket_test.dart | 25 ++++++ .../utils/scenario_utils_mixin.dart | 72 ++++++++++++++-- .../patrol-integration-test-with-docker.sh | 2 + ...trol-local-integration-test-with-docker.sh | 2 + 6 files changed, 179 insertions(+), 10 deletions(-) create mode 100644 integration_test/scenarios/web_socket_update_ui_scenario.dart create mode 100644 integration_test/tests/web_socket/web_socket_test.dart diff --git a/backend-docker/jmap.properties b/backend-docker/jmap.properties index 68282ebaa6..7473fd7578 100644 --- a/backend-docker/jmap.properties +++ b/backend-docker/jmap.properties @@ -1 +1,3 @@ -url.prefix=https://a872-222-252-23-73.ngrok-free.app \ No newline at end of file +authentication.strategy.rfc8621=BasicAuthenticationStrategy,com.linagora.tmail.james.jmap.ticket.TicketAuthenticationStrategy +url.prefix= +websocket.url.prefix= \ No newline at end of file diff --git a/integration_test/scenarios/web_socket_update_ui_scenario.dart b/integration_test/scenarios/web_socket_update_ui_scenario.dart new file mode 100644 index 0000000000..e4783544f5 --- /dev/null +++ b/integration_test/scenarios/web_socket_update_ui_scenario.dart @@ -0,0 +1,84 @@ +import 'package:tmail_ui_user/features/thread/presentation/widgets/email_tile_builder.dart'; + +import '../base/base_scenario.dart'; +import '../models/provisioning_email.dart'; +import '../utils/scenario_utils_mixin.dart'; +import 'login_with_basic_auth_scenario.dart'; + +class WebSocketUpdateUiScenario extends BaseScenario with ScenarioUtilsMixin { + const WebSocketUpdateUiScenario(super.$, { + required this.loginWithBasicAuthScenario, + required this.subject, + required this.content, + }); + + final LoginWithBasicAuthScenario loginWithBasicAuthScenario; + final String subject; + final String content; + + @override + Future execute() async { + await loginWithBasicAuthScenario.execute(); + + await provisionEmail( + [ProvisioningEmail( + toEmail: loginWithBasicAuthScenario.email, + subject: subject, + content: content, + )], + refreshEmailView: false, + ); + await $.pumpAndSettle(); + await _expectEmailVisible(loginWithBasicAuthScenario.email); + await _expectEmailUnreadWithSubject(subject); + await _expectEmailUnstarredWithSubject(subject); + + await simulateUpdateFlagsOfEmailsWithSubjectsFromOutsideCurrentClient( + subjects: [subject], + isRead: true, + ); + await $.pumpAndSettle(); + await _expectEmailReadWithSubject(subject); + + await simulateUpdateFlagsOfEmailsWithSubjectsFromOutsideCurrentClient( + subjects: [subject], + isStar: true, + ); + await $.pumpAndSettle(); + await _expectEmailStarredWithSubject(subject); + } + + Future _expectEmailVisible(String email) => expectViewVisible($(email)); + + Future _expectEmailUnreadWithSubject(String subject) => expectViewVisible( + $(EmailTileBuilder) + .which( + (widget) => widget.presentationEmail.subject == subject + && !widget.presentationEmail.hasRead + ), + ); + + Future _expectEmailReadWithSubject(String subject) => expectViewVisible( + $(EmailTileBuilder) + .which( + (widget) => widget.presentationEmail.subject == subject + && widget.presentationEmail.hasRead + ), + ); + + Future _expectEmailUnstarredWithSubject(String subject) => expectViewVisible( + $(EmailTileBuilder) + .which( + (widget) => widget.presentationEmail.subject == subject + && !widget.presentationEmail.hasStarred + ), + ); + + Future _expectEmailStarredWithSubject(String subject) => expectViewVisible( + $(EmailTileBuilder) + .which( + (widget) => widget.presentationEmail.subject == subject + && widget.presentationEmail.hasStarred + ), + ); +} \ No newline at end of file diff --git a/integration_test/tests/web_socket/web_socket_test.dart b/integration_test/tests/web_socket/web_socket_test.dart new file mode 100644 index 0000000000..0168229aba --- /dev/null +++ b/integration_test/tests/web_socket/web_socket_test.dart @@ -0,0 +1,25 @@ +import '../../base/test_base.dart'; +import '../../scenarios/login_with_basic_auth_scenario.dart'; +import '../../scenarios/web_socket_update_ui_scenario.dart'; + +void main() { + TestBase().runPatrolTest( + description: 'Should see thread view updated per web socket message', + test: ($) async { + final loginWithBasicAuthScenario = LoginWithBasicAuthScenario($, + username: const String.fromEnvironment('USERNAME'), + hostUrl: const String.fromEnvironment('BASIC_AUTH_URL'), + email: const String.fromEnvironment('BASIC_AUTH_EMAIL'), + password: const String.fromEnvironment('PASSWORD'), + ); + + final webSocketUpdateUiScenario = WebSocketUpdateUiScenario($, + loginWithBasicAuthScenario: loginWithBasicAuthScenario, + subject: 'web socket subject', + content: 'web socket content', + ); + + await webSocketUpdateUiScenario.execute(); + } + ); +} \ No newline at end of file diff --git a/integration_test/utils/scenario_utils_mixin.dart b/integration_test/utils/scenario_utils_mixin.dart index 76e5b35d9b..6c6ac4bfec 100644 --- a/integration_test/utils/scenario_utils_mixin.dart +++ b/integration_test/utils/scenario_utils_mixin.dart @@ -1,16 +1,15 @@ import 'dart:async'; -import 'dart:io'; +import 'dart:io' hide HttpClient; import 'package:collection/collection.dart'; import 'package:get/get.dart'; +import 'package:jmap_dart_client/http/http_client.dart'; +import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; +import 'package:jmap_dart_client/jmap/jmap_request.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; -import 'package:model/email/attachment.dart'; -import 'package:model/email/email_action_type.dart'; -import 'package:model/extensions/session_extension.dart'; -import 'package:model/mailbox/presentation_mailbox.dart'; -import 'package:model/extensions/presentation_mailbox_extension.dart'; -import 'package:model/upload/file_info.dart'; +import 'package:jmap_dart_client/jmap/mail/email/set/set_email_method.dart'; +import 'package:model/model.dart'; import 'package:path_provider/path_provider.dart'; import 'package:tmail_ui_user/features/composer/domain/state/upload_attachment_state.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_send_email_interactor.dart'; @@ -22,11 +21,15 @@ import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_ident import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; import 'package:tmail_ui_user/features/thread/presentation/thread_controller.dart'; import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart'; +import 'package:tmail_ui_user/main/error/capability_validator.dart'; import '../models/provisioning_email.dart'; mixin ScenarioUtilsMixin { - Future provisionEmail(List provisioningEmails) async { + Future provisionEmail( + List provisioningEmails, { + bool refreshEmailView = true, + }) async { ComposerBindings().dependencies(); final mailboxDashBoardController = Get.find(); @@ -62,11 +65,62 @@ mixin ScenarioUtilsMixin { })); // Refresh view after provisioning emails - threadController.refreshAllEmail(); + if (refreshEmailView) { + threadController.refreshAllEmail(); + } ComposerBindings().dispose(); } + Future simulateUpdateFlagsOfEmailsWithSubjectsFromOutsideCurrentClient({ + required List subjects, + bool? isRead, + bool? isStar, + }) async { + final mailboxDashBoardController = Get.find(); + final emails = mailboxDashBoardController + .emailsInCurrentMailbox + .where((presentationEmail) => subjects.contains( + presentationEmail.subject + )) + .toList(); + final session = mailboxDashBoardController.sessionCurrent; + final accountId = mailboxDashBoardController.accountId.value; + if (session == null || accountId == null) return; + + final requestBuilder = JmapRequestBuilder( + Get.find(), + ProcessingInvocation(), + ); + final capabilities = {}; + + // Mark as read/unread + if (isRead != null) { + final markEmailAsReadMethod = SetEmailMethod(accountId) + ..addUpdates(emails.listEmailIds.generateMapUpdateObjectMarkAsRead( + isRead ? ReadActions.markAsRead : ReadActions.markAsUnread + )); + requestBuilder.invocation(markEmailAsReadMethod); + capabilities.addAll(markEmailAsReadMethod + .requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId)); + } + + // Mark as star/unstar + if (isStar != null) { + final markEmailAsStarMethod = SetEmailMethod(accountId) + ..addUpdates(emails.listEmailIds.generateMapUpdateObjectMarkAsStar( + isStar ? MarkStarAction.markStar : MarkStarAction.unMarkStar + )); + requestBuilder.invocation(markEmailAsStarMethod); + capabilities.addAll(markEmailAsStarMethod + .requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId)); + } + + await (requestBuilder..usings(capabilities)).build().execute(); + } + Future preparingTxtFile(String content) async { final directory = await getTemporaryDirectory(); final file = File('${directory.path}/test.txt'); diff --git a/scripts/patrol-integration-test-with-docker.sh b/scripts/patrol-integration-test-with-docker.sh index 3fb4011270..bc0c85075f 100755 --- a/scripts/patrol-integration-test-with-docker.sh +++ b/scripts/patrol-integration-test-with-docker.sh @@ -28,7 +28,9 @@ openssl genpkey -algorithm rsa -pkeyopt rsa_keygen_bits:4096 -out jwt_privatekey openssl rsa -in jwt_privatekey -pubout -out jwt_publickey # Replace content of jmap.properties with url.prefix=$BASIC_AUTH_URL +# and websocket.url.prefix=ws${BASIC_AUTH_URL:4} sed -i "s|url.prefix=.*|url.prefix=$BASIC_AUTH_URL|" jmap.properties +sed -i '' "s|websocket.url.prefix=.*|websocket.url.prefix=ws${BASIC_AUTH_URL:4}|" jmap.properties echo "Starting services and adding users..." docker compose up -d diff --git a/scripts/patrol-local-integration-test-with-docker.sh b/scripts/patrol-local-integration-test-with-docker.sh index 50ddd8241e..f35e8f2af0 100755 --- a/scripts/patrol-local-integration-test-with-docker.sh +++ b/scripts/patrol-local-integration-test-with-docker.sh @@ -28,7 +28,9 @@ openssl genpkey -algorithm rsa -pkeyopt rsa_keygen_bits:4096 -out jwt_privatekey openssl rsa -in jwt_privatekey -pubout -out jwt_publickey # Replace content of jmap.properties with url.prefix=$BASIC_AUTH_URL +# and websocket.url.prefix=ws${BASIC_AUTH_URL:4} sed -i '' "s|url.prefix=.*|url.prefix=$BASIC_AUTH_URL|" jmap.properties +sed -i '' "s|websocket.url.prefix=.*|websocket.url.prefix=ws${BASIC_AUTH_URL:4}|" jmap.properties echo "Starting services and adding users..." docker compose up -d From 48a549572e69cf45d3277bfc26aa4a42582a45af Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 18 Dec 2024 12:48:43 +0700 Subject: [PATCH 12/72] TF-3334 Remove resynchronisation when performing actions with emails --- .../presentation/mailbox_controller.dart | 56 ---------------- .../presentation/search_email_controller.dart | 36 ---------- .../presentation/thread_controller.dart | 65 ------------------- 3 files changed, 157 deletions(-) diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index b0501879e4..4503fc2d1b 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -19,15 +19,7 @@ import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:rxdart/transformers.dart'; import 'package:tmail_ui_user/features/base/base_mailbox_controller.dart'; import 'package:tmail_ui_user/features/base/mixin/mailbox_action_handler_mixin.dart'; -import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; -import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; -import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; -import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanently_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails_permanently_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/get_restored_deleted_message_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; import 'package:tmail_ui_user/features/mailbox/domain/constants/mailbox_constants.dart'; @@ -44,7 +36,6 @@ import 'package:tmail_ui_user/features/mailbox/domain/state/create_default_mailb import 'package:tmail_ui_user/features/mailbox/domain/state/create_new_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/delete_multiple_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/get_all_mailboxes_state.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/move_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/refresh_all_mailboxes_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/refresh_changes_all_mailboxes_state.dart'; @@ -72,16 +63,11 @@ import 'package:tmail_ui_user/features/mailbox/presentation/utils/mailbox_utils. import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_creator/presentation/model/mailbox_creator_arguments.dart'; import 'package:tmail_ui_user/features/mailbox_creator/presentation/model/new_mailbox_arguments.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.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/search/mailbox/presentation/search_mailbox_bindings.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/dialog_router.dart'; @@ -248,48 +234,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM _handleNavigationRouteParameters ); - ever(mailboxDashBoardController.viewState, (state) { - state.fold((failure) => null, (success) { - if (success is MarkAsMultipleEmailReadAllSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is MarkAsMultipleEmailReadHasSomeEmailFailure) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is MoveMultipleEmailToMailboxAllSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is MoveMultipleEmailToMailboxHasSomeEmailFailure) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is DeleteMultipleEmailsPermanentlyAllSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is EmptyTrashFolderSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is MarkAsEmailReadSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is MoveToMailboxSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is DeleteEmailPermanentlySuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is SaveEmailAsDraftsSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is RemoveEmailDraftsSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is SendEmailSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is MarkAsMailboxReadAllSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is MarkAsMailboxReadHasSomeEmailFailure) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is UpdateEmailDraftsSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is EmptySpamFolderSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is GetRestoredDeletedMessageSuccess) { - _refreshMailboxChanges(properties: MailboxConstants.propertiesDefault); - } - }); - }); - ever(mailboxDashBoardController.dashBoardAction, (action) { if (action is ClearSearchEmailAction) { _switchBackToMailboxDefault(); diff --git a/lib/features/search/email/presentation/search_email_controller.dart b/lib/features/search/email/presentation/search_email_controller.dart index 6871b68711..aec3fce505 100644 --- a/lib/features/search/email/presentation/search_email_controller.dart +++ b/lib/features/search/email/presentation/search_email_controller.dart @@ -31,13 +31,6 @@ import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_e import 'package:tmail_ui_user/features/contact/presentation/model/contact_arguments.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; -import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanently_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails_permanently_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/store_event_attendance_status_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/unsubscribe_email_state.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; @@ -62,11 +55,6 @@ import 'package:tmail_ui_user/features/search/email/presentation/model/search_mo import 'package:tmail_ui_user/features/search/email/presentation/search_email_bindings.dart'; import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/mark_as_star_multiple_email_state.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_more_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/search_email_interactor.dart'; @@ -246,30 +234,6 @@ class SearchEmailController extends BaseController } void _initWorkerListener() { - dashBoardViewStateWorker = ever(mailboxDashBoardController.viewState, (viewState) { - viewState.map((success) { - if (success is MarkAsEmailReadSuccess || - success is MoveToMailboxSuccess || - success is MarkAsStarEmailSuccess || - success is DeleteEmailPermanentlySuccess || - success is MarkAsMultipleEmailReadAllSuccess || - success is MarkAsMultipleEmailReadHasSomeEmailFailure || - success is MarkAsStarMultipleEmailAllSuccess || - success is MarkAsStarMultipleEmailHasSomeEmailFailure || - success is MoveMultipleEmailToMailboxAllSuccess || - success is MoveMultipleEmailToMailboxHasSomeEmailFailure || - success is EmptyTrashFolderSuccess || - success is EmptySpamFolderSuccess || - success is DeleteMultipleEmailsPermanentlyAllSuccess || - success is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure || - success is UnsubscribeEmailSuccess || - success is StoreEventAttendanceStatusSuccess - ) { - _refreshEmailChanges(); - } - }); - }); - dashBoardActionWorker = ever( mailboxDashBoardController.dashBoardAction, (action) { diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index d58b75886a..61ba7fdbbf 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -18,22 +18,10 @@ import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; -import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; -import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; -import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; -import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanently_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails_permanently_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/store_event_attendance_status_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/unsubscribe_email_state.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart' as search; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; @@ -51,14 +39,9 @@ import 'package:tmail_ui_user/features/thread/domain/model/email_filter.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/model/get_email_request.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_email_by_id_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/load_more_emails_state.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/mark_as_star_multiple_email_state.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/refresh_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/refresh_changes_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; @@ -342,54 +325,6 @@ class ThreadController extends BaseController with EmailActionController { mailboxDashBoardController.clearEmailUIAction(); } }); - - ever(mailboxDashBoardController.viewState, (viewState) { - viewState.map((success) { - if (success is MarkAsEmailReadSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is MoveToMailboxSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is MarkAsStarEmailSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is DeleteEmailPermanentlySuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is SaveEmailAsDraftsSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is RemoveEmailDraftsSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is SendEmailSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is UpdateEmailDraftsSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is MarkAsMailboxReadAllSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is MarkAsMailboxReadHasSomeEmailFailure) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is MoveMultipleEmailToMailboxAllSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is MoveMultipleEmailToMailboxHasSomeEmailFailure) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is DeleteMultipleEmailsPermanentlyAllSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is MarkAsStarMultipleEmailAllSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is MarkAsStarMultipleEmailHasSomeEmailFailure) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is MarkAsMultipleEmailReadAllSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is MarkAsMultipleEmailReadHasSomeEmailFailure) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is EmptyTrashFolderSuccess || success is EmptySpamFolderSuccess) { - refreshAllEmail(); - } else if (success is UnsubscribeEmailSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is StoreEventAttendanceStatusSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } - }); - }); } void _registerBrowserResizeListener() { From 9a6c40bce86077bdeff7b8cdc40be5eb77f723ff Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 18 Dec 2024 13:08:00 +0700 Subject: [PATCH 13/72] TF-3334 Auto resynchronisation search view when receives websocket notification on mobile & tablet responsive web --- .../presentation/search_email_controller.dart | 14 +++++- .../presentation/thread_controller.dart | 44 ++++++++++--------- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/lib/features/search/email/presentation/search_email_controller.dart b/lib/features/search/email/presentation/search_email_controller.dart index aec3fce505..f96f957eaa 100644 --- a/lib/features/search/email/presentation/search_email_controller.dart +++ b/lib/features/search/email/presentation/search_email_controller.dart @@ -31,6 +31,7 @@ import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_e import 'package:tmail_ui_user/features/contact/presentation/model/contact_arguments.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; +import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; @@ -100,7 +101,7 @@ class SearchEmailController extends BaseController final resultSearchViewState = Rx>(Right(UIState.idle)); late Debouncer _deBouncerTime; - late Worker dashBoardViewStateWorker; + late Worker emailUIActionWorker; late Worker dashBoardActionWorker; late SearchMoreState searchMoreState; late bool canSearchMore; @@ -246,6 +247,15 @@ class SearchEmailController extends BaseController } } ); + + emailUIActionWorker = ever( + mailboxDashBoardController.emailUIAction, + (action) { + if (action is RefreshChangeEmailAction) { + _refreshEmailChanges(); + } + }, + ); } void _onSearchTextInputListener() { @@ -966,7 +976,7 @@ class SearchEmailController extends BaseController resultSearchScrollController.dispose(); listSearchFilterScrollController.dispose(); _deBouncerTime.cancel(); - dashBoardViewStateWorker.dispose(); + emailUIActionWorker.dispose(); dashBoardActionWorker.dispose(); super.onClose(); } diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 61ba7fdbbf..8aac45a658 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -316,10 +316,7 @@ class ThreadController extends BaseController with EmailActionController { ever(mailboxDashBoardController.emailUIAction, (action) { if (action is RefreshChangeEmailAction) { - if (action.newState != _currentEmailState) { - _refreshEmailChanges(); - } - mailboxDashBoardController.clearEmailUIAction(); + _refreshEmailChanges(newState: action.newState); } else if (action is RefreshAllEmailAction) { refreshAllEmail(); mailboxDashBoardController.clearEmailUIAction(); @@ -516,28 +513,33 @@ class ThreadController extends BaseController with EmailActionController { return limit; } - void _refreshEmailChanges({jmap.State? currentEmailState}) { - log('ThreadController::_refreshEmailChanges(): currentEmailState: $currentEmailState'); + void _refreshEmailChanges({jmap.State? newState}) { + log('ThreadController::_refreshEmailChanges(): newState: $newState'); if (searchController.isSearchEmailRunning) { _searchEmail(limit: limitEmailFetched, needRefreshSearchState: true); } else { - final newEmailState = currentEmailState ?? _currentEmailState; - log('ThreadController::_refreshEmailChanges(): newEmailState: $newEmailState'); - if (_session != null && _accountId != null && newEmailState != null) { - consumeState(_refreshChangesEmailsInMailboxInteractor.execute( + if (_currentEmailState == null || + _currentEmailState == newState || + _session == null || + _accountId == null) { + return; + } + consumeState(_refreshChangesEmailsInMailboxInteractor.execute( + _session!, + _accountId!, + _currentEmailState!, + sort: EmailSortOrderType.mostRecent.getSortOrder().toNullable(), + propertiesCreated: EmailUtils.getPropertiesForEmailGetMethod( _session!, _accountId!, - newEmailState, - sort: EmailSortOrderType.mostRecent.getSortOrder().toNullable(), - propertiesCreated: EmailUtils.getPropertiesForEmailGetMethod(_session!, _accountId!), - propertiesUpdated: ThreadConstants.propertiesUpdatedDefault, - emailFilter: EmailFilter( - filter: _getFilterCondition(mailboxIdSelected: selectedMailboxId), - filterOption: mailboxDashBoardController.filterMessageOption.value, - mailboxId: selectedMailboxId - ) - )); - } + ), + propertiesUpdated: ThreadConstants.propertiesUpdatedDefault, + emailFilter: EmailFilter( + filter: _getFilterCondition(mailboxIdSelected: selectedMailboxId), + filterOption: mailboxDashBoardController.filterMessageOption.value, + mailboxId: selectedMailboxId, + ), + )); } } From 026e198346ff7b5151b0027ae1b81c42cf823d93 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 18 Dec 2024 13:45:09 +0700 Subject: [PATCH 14/72] TF-3334 Remove resynchronisation when performing actions with mailbox --- .../presentation/mailbox_controller.dart | 178 ++++++++---------- 1 file changed, 74 insertions(+), 104 deletions(-) diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 4503fc2d1b..4458c50ee1 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -9,7 +9,6 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/error/method/error_method_response.dart'; -import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; @@ -32,7 +31,6 @@ import 'package:tmail_ui_user/features/mailbox/domain/model/rename_mailbox_reque import 'package:tmail_ui_user/features/mailbox/domain/model/subscribe_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/subscribe_multiple_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/subscribe_request.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/state/create_default_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/create_new_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/delete_multiple_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/get_all_mailboxes_state.dart'; @@ -104,6 +102,10 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM PresentationEmail? get selectedEmail => mailboxDashBoardController.selectedEmail.value; + AccountId? get accountId => mailboxDashBoardController.accountId.value; + + Session? get session => mailboxDashBoardController.sessionCurrent; + MailboxController( this._createNewMailboxInteractor, this._deleteMultipleMailboxInteractor, @@ -159,11 +161,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM _deleteMultipleMailboxSuccess(success.listMailboxIdDeleted, success.currentMailboxState); } else if (success is DeleteMultipleMailboxHasSomeSuccess) { _deleteMultipleMailboxSuccess(success.listMailboxIdDeleted, success.currentMailboxState); - } else if (success is RenameMailboxSuccess) { - _refreshMailboxChanges( - currentMailboxState: success.currentMailboxState, - properties: MailboxConstants.propertiesDefault - ); } else if (success is MoveMailboxSuccess) { _moveMailboxSuccess(success); } else if (success is SubscribeMailboxSuccess) { @@ -172,8 +169,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM _handleUnsubscribeMultipleMailboxAllSuccess(success); } else if (success is SubscribeMultipleMailboxHasSomeSuccess) { _handleUnsubscribeMultipleMailboxHasSomeSuccess(success); - } else if (success is CreateDefaultMailboxAllSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); } } @@ -188,8 +183,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM _deleteMailboxFailure(failure); } else if (failure is RefreshChangesAllMailboxFailure) { _clearNewFolderId(); - } else if (failure is CreateDefaultMailboxFailure) { - _refreshMailboxChanges(currentMailboxState: failure.currentMailboxState); } } @@ -224,8 +217,8 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM void _registerObxStreamListener() { ever(mailboxDashBoardController.accountId, (accountId) { - if (accountId != null && mailboxDashBoardController.sessionCurrent != null) { - getAllMailbox(mailboxDashBoardController.sessionCurrent!, accountId); + if (accountId != null && session != null) { + getAllMailbox(session!, accountId); } }); @@ -245,9 +238,7 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM _switchBackToMailboxDefault(); mailboxDashBoardController.clearMailboxUIAction(); } else if (action is RefreshChangeMailboxAction) { - if (action.newState != currentMailboxState) { - _refreshMailboxChanges(); - } + _refreshMailboxChanges(newState: action.newState); mailboxDashBoardController.clearMailboxUIAction(); } else if (action is OpenMailboxAction) { if (currentContext != null) { @@ -281,31 +272,29 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM } Future refreshAllMailbox() async { - final session = mailboxDashBoardController.sessionCurrent; - final accountId = mailboxDashBoardController.accountId.value; if (session != null && accountId != null) { - consumeState(getAllMailboxInteractor!.execute(session, accountId)); + consumeState(getAllMailboxInteractor!.execute(session!, accountId!)); } else { consumeState(Stream.value(Left(GetAllMailboxFailure(NotFoundSessionException())))); } } - void _refreshMailboxChanges({jmap.State? currentMailboxState, Properties? properties}) { - log('MailboxController::_refreshMailboxChanges(): currentMailboxState: $currentMailboxState'); - final newMailboxState = currentMailboxState ?? this.currentMailboxState; - log('MailboxController::_refreshMailboxChanges(): newMailboxState: $newMailboxState'); - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; - if (accountId != null && session != null && newMailboxState != null) { - refreshMailboxChanges( - session, - accountId, - newMailboxState, - properties: properties - ); - } else { + void _refreshMailboxChanges({jmap.State? newState}) { + log('MailboxController::_refreshMailboxChanges():newState: $newState'); + if (accountId == null || + session == null || + currentMailboxState == null || + newState == currentMailboxState) { _newFolderId = null; + return; } + + refreshMailboxChanges( + session!, + accountId!, + currentMailboxState!, + properties: MailboxConstants.propertiesDefault, + ); } void _setMapMailbox() { @@ -381,12 +370,10 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM .whereNot((role) => mapDefaultMailboxRole.containsKey(role) || findNodeByNameOnFirstLevel(role.value) != null) .toList(); log('MailboxController::_handleCreateDefaultFolderIfMissing():listRoleMissing: $listRoleMissing'); - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; if (listRoleMissing.isNotEmpty && accountId != null && session != null) { consumeState(_createDefaultMailboxInteractor.execute( - session, - accountId, + session!, + accountId!, listRoleMissing )); } @@ -543,11 +530,9 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM } void goToCreateNewMailboxView(BuildContext context, {PresentationMailbox? parentMailbox}) async { - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; if (session !=null && accountId != null) { final arguments = MailboxCreatorArguments( - accountId, + accountId!, defaultMailboxTree.value, personalMailboxTree.value, teamMailboxesTree.value, @@ -560,9 +545,14 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM : await push(AppRoutes.mailboxCreator, arguments: arguments); if (result != null && result is NewMailboxArguments) { - _createNewMailboxAction(session, accountId, CreateNewMailboxRequest( - result.newName, - parentId: result.mailboxLocation?.id)); + _createNewMailboxAction( + session!, + accountId!, + CreateNewMailboxRequest( + result.newName, + parentId: result.mailboxLocation?.id, + ), + ); } } } @@ -581,8 +571,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM _newFolderId = success.newMailbox.id; } - - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); } void _createNewMailboxFailure(CreateNewMailboxFailure failure) { @@ -714,9 +702,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM } void _deleteMailboxAction(PresentationMailbox presentationMailbox) { - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; - if (session != null && accountId != null) { final tupleMap = MailboxUtils.generateMapDescendantIdsAndMailboxIdList( [presentationMailbox], @@ -726,10 +711,11 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM final listMailboxId = tupleMap.value2; consumeState(_deleteMultipleMailboxInteractor.execute( - session, - accountId, - mapDescendantIds, - listMailboxId)); + session!, + accountId!, + mapDescendantIds, + listMailboxId, + )); } else { _deleteMailboxFailure(DeleteMultipleMailboxFailure(null)); } @@ -752,7 +738,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM _switchBackToMailboxDefault(); _closeEmailViewIfMailboxDisabledOrNotExist(listMailboxIdDeleted); } - _refreshMailboxChanges(currentMailboxState: currentMailboxState); } void _openConfirmationDialogDeleteMultipleMailboxAction( @@ -794,9 +779,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM } void _deleteMultipleMailboxAction(List selectedMailboxList) { - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; - if (session != null && accountId != null) { final tupleMap = MailboxUtils.generateMapDescendantIdsAndMailboxIdList( selectedMailboxList, @@ -805,10 +787,11 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM final mapDescendantIds = tupleMap.value1; final listMailboxId = tupleMap.value2; consumeState(_deleteMultipleMailboxInteractor.execute( - session, - accountId, - mapDescendantIds, - listMailboxId)); + session!, + accountId!, + mapDescendantIds, + listMailboxId, + )); } else { _deleteMailboxFailure(DeleteMultipleMailboxFailure(null)); } @@ -835,13 +818,10 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM } void _renameMailboxAction(PresentationMailbox presentationMailbox, MailboxName newMailboxName) { - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; - if (session != null && accountId != null) { consumeState(_renameMailboxInteractor.execute( - session, - accountId, + session!, + accountId!, RenameMailboxRequest(presentationMailbox.id, newMailboxName)) ); } @@ -890,18 +870,15 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM textColor: Colors.white, actionIcon: SvgPicture.asset(imagePaths.icUndo)); } - - _refreshMailboxChanges( - currentMailboxState: success.currentMailboxState, - properties: MailboxConstants.propertiesDefault - ); } void _undoMovingMailbox(MoveMailboxRequest newMoveRequest) { - final session = mailboxDashBoardController.sessionCurrent; - final accountId = mailboxDashBoardController.accountId.value; if (session != null && accountId != null) { - consumeState(_moveMailboxInteractor.execute(session, accountId, newMoveRequest)); + consumeState(_moveMailboxInteractor.execute( + session!, + accountId!, + newMoveRequest, + )); } } @@ -1034,13 +1011,11 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM PresentationMailbox mailboxSelected, PresentationMailbox? destinationMailbox ) { - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; if (session != null && accountId != null) { _handleMovingMailbox( context, - session, - accountId, + session!, + accountId!, MoveAction.moving, mailboxSelected, destinationMailbox: destinationMailbox @@ -1112,13 +1087,12 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM Future _updateMailboxIdsBlockNotificationToKeychain(List mailboxes) async { _iosSharingManager = getBinding(); - final accountId = mailboxDashBoardController.accountId.value; if (accountId == null || _iosSharingManager == null || mailboxes.isEmpty) { logError('MailboxController::_updateMailboxIdsBlockNotificationToKeychain: AccountId = $accountId | IosSharingManager = $_iosSharingManager | Mailboxes = ${mailboxes.length}'); return; } - if (await _iosSharingManager!.isExistMailboxIdsBlockNotificationInKeyChain(accountId)) { + if (await _iosSharingManager!.isExistMailboxIdsBlockNotificationInKeyChain(accountId!)) { return; } @@ -1128,7 +1102,7 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM .toList(); log('MailboxController::_updateMailboxIdsBlockNotificationToKeychain:MailboxIdsBlockNotification = $mailboxIdsBlockNotification'); _iosSharingManager!.updateMailboxIdsBlockNotificationInKeyChain( - accountId: accountId, + accountId: accountId!, mailboxIds: mailboxIdsBlockNotification); } @@ -1145,8 +1119,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM } void _unsubscribeMailboxAction(MailboxId mailboxId) { - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; if (session != null && accountId != null) { final subscribeRequest = generateSubscribeRequest( mailboxId, @@ -1155,9 +1127,17 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM ); if (subscribeRequest is SubscribeMultipleMailboxRequest) { - consumeState(_subscribeMultipleMailboxInteractor.execute(session, accountId, subscribeRequest)); + consumeState(_subscribeMultipleMailboxInteractor.execute( + session!, + accountId!, + subscribeRequest, + )); } else if (subscribeRequest is SubscribeMailboxRequest) { - consumeState(_subscribeMailboxInteractor.execute(session, accountId, subscribeRequest)); + consumeState(_subscribeMailboxInteractor.execute( + session!, + accountId!, + subscribeRequest, + )); } } } @@ -1171,11 +1151,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM _closeEmailViewIfMailboxDisabledOrNotExist([success.mailboxId]); } } - - _refreshMailboxChanges( - currentMailboxState: success.currentMailboxState, - properties: MailboxConstants.propertiesDefault - ); } void _handleUnsubscribeMultipleMailboxAllSuccess(SubscribeMultipleMailboxAllSuccess success) { @@ -1190,11 +1165,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM _closeEmailViewIfMailboxDisabledOrNotExist(success.mailboxIdsSubscribe); } } - - _refreshMailboxChanges( - currentMailboxState: success.currentMailboxState, - properties: MailboxConstants.propertiesDefault - ); } void _handleUnsubscribeMultipleMailboxHasSomeSuccess(SubscribeMultipleMailboxHasSomeSuccess success) { @@ -1209,11 +1179,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM _closeEmailViewIfMailboxDisabledOrNotExist(success.mailboxIdsSubscribe); } } - - _refreshMailboxChanges( - currentMailboxState: success.currentMailboxState, - properties: MailboxConstants.propertiesDefault - ); } void _closeEmailViewIfMailboxDisabledOrNotExist(List mailboxIdsDisabled) { @@ -1253,9 +1218,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM MailboxId mailboxIdSubscribed, {List? listDescendantMailboxIds} ) { - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; - if (session != null && accountId != null) { SubscribeRequest? subscribeRequest; @@ -1275,9 +1237,17 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM } if (subscribeRequest is SubscribeMultipleMailboxRequest) { - consumeState(_subscribeMultipleMailboxInteractor.execute(session, accountId, subscribeRequest)); + consumeState(_subscribeMultipleMailboxInteractor.execute( + session!, + accountId!, + subscribeRequest, + )); } else if (subscribeRequest is SubscribeMailboxRequest) { - consumeState(_subscribeMailboxInteractor.execute(session, accountId, subscribeRequest)); + consumeState(_subscribeMailboxInteractor.execute( + session!, + accountId!, + subscribeRequest, + )); } } } From ccf49dbf0218436fc60d8944e08859e09c93a93b Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 18 Dec 2024 14:01:10 +0700 Subject: [PATCH 15/72] TF-3334 Auto resynchronisation mailbox search view when receives websocket notification --- .../presentation/mailbox_controller.dart | 1 - .../search_mailbox_controller.dart | 166 +++++++++--------- 2 files changed, 79 insertions(+), 88 deletions(-) diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 4458c50ee1..d6ac440311 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -239,7 +239,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM mailboxDashBoardController.clearMailboxUIAction(); } else if (action is RefreshChangeMailboxAction) { _refreshMailboxChanges(newState: action.newState); - mailboxDashBoardController.clearMailboxUIAction(); } else if (action is OpenMailboxAction) { if (currentContext != null) { _handleOpenMailbox(currentContext!, action.presentationMailbox); diff --git a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart index 9176583661..3447b9ff56 100644 --- a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart +++ b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart @@ -11,7 +11,6 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/error/method/error_method_response.dart'; -import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; @@ -34,7 +33,6 @@ import 'package:tmail_ui_user/features/mailbox/domain/model/subscribe_request.da import 'package:tmail_ui_user/features/mailbox/domain/state/create_new_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/delete_multiple_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/get_all_mailboxes_state.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/move_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/refresh_changes_all_mailboxes_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/rename_mailbox_state.dart'; @@ -88,6 +86,10 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa PresentationEmail? get selectedEmail => dashboardController.selectedEmail.value; + AccountId? get accountId => dashboardController.accountId.value; + + Session? get session => dashboardController.sessionCurrent; + SearchMailboxController( this._searchMailboxInteractor, this._renameMailboxInteractor, @@ -111,6 +113,7 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa void onInit() { super.onInit(); _initializeDebounceTimeTextSearchChange(); + _registerObxStreamListener(); _getAllMailboxAction(); } @@ -144,15 +147,6 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa searchMailboxAction(); } else if (success is SearchMailboxSuccess) { _handleSearchMailboxSuccess(success); - } else if (success is MarkAsMailboxReadAllSuccess) { - _refreshMailboxChanges(mailboxState: success.currentMailboxState); - } else if (success is MarkAsMailboxReadHasSomeEmailFailure) { - _refreshMailboxChanges(mailboxState: success.currentMailboxState); - } else if (success is RenameMailboxSuccess) { - _refreshMailboxChanges( - mailboxState: success.currentMailboxState, - properties: MailboxConstants.propertiesDefault - ); } else if (success is MoveMailboxSuccess) { _moveMailboxSuccess(success); } else if (success is DeleteMultipleMailboxAllSuccess) { @@ -183,27 +177,34 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa }); } + void _registerObxStreamListener() { + ever(dashboardController.mailboxUIAction, (action) { + if (action is RefreshChangeMailboxAction) { + _refreshMailboxChanges(newState: action.newState); + } + }); + } + void _getAllMailboxAction() { - final session = dashboardController.sessionCurrent; - final accountId = dashboardController.accountId.value; if (session != null && accountId != null) { - getAllMailbox(session, accountId); + getAllMailbox(session!, accountId!); } } - void _refreshMailboxChanges({jmap.State? mailboxState, Properties? properties}) { - dashboardController.dispatchMailboxUIAction(RefreshChangeMailboxAction(null)); - final newMailboxState = mailboxState ?? currentMailboxState; - final accountId = dashboardController.accountId.value; - final session = dashboardController.sessionCurrent; - if (session != null && accountId != null && newMailboxState != null) { - refreshMailboxChanges( - session, - accountId, - newMailboxState, - properties: properties - ); + void _refreshMailboxChanges({jmap.State? newState}) { + if (accountId == null || + session == null || + currentMailboxState == null || + newState == currentMailboxState) { + return; } + + refreshMailboxChanges( + session!, + accountId!, + currentMailboxState!, + properties: MailboxConstants.propertiesDefault, + ); } void searchMailboxAction() { @@ -328,13 +329,11 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa } void _renameMailboxAction(PresentationMailbox presentationMailbox, MailboxName newMailboxName) { - final accountId = dashboardController.accountId.value; - final session = dashboardController.sessionCurrent; if (session != null && accountId != null) { consumeState(_renameMailboxInteractor.execute( - session, - accountId, - RenameMailboxRequest(presentationMailbox.id, newMailboxName) + session!, + accountId!, + RenameMailboxRequest(presentationMailbox.id, newMailboxName), )); } } @@ -344,13 +343,11 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa PresentationMailbox mailboxSelected, PresentationMailbox? destinationMailbox ) { - final accountId = dashboardController.accountId.value; - final session = dashboardController.sessionCurrent; if (session != null && accountId != null) { _handleMovingMailbox( context, - session, - accountId, + session!, + accountId!, MoveAction.moving, mailboxSelected, destinationMailbox: destinationMailbox @@ -400,25 +397,19 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa actionIcon: SvgPicture.asset(imagePaths.icUndo) ); } - - _refreshMailboxChanges( - mailboxState: success.currentMailboxState, - properties: MailboxConstants.propertiesDefault - ); } void _undoMovingMailbox(MoveMailboxRequest newMoveRequest) { - final accountId = dashboardController.accountId.value; - final session = dashboardController.sessionCurrent; if (session != null && accountId != null) { - consumeState(_moveMailboxInteractor.execute(session, accountId, newMoveRequest)); + consumeState(_moveMailboxInteractor.execute( + session!, + accountId!, + newMoveRequest, + )); } } void _deleteMailboxAction(PresentationMailbox presentationMailbox) { - final accountId = dashboardController.accountId.value; - final session = dashboardController.sessionCurrent; - if (session != null && accountId != null) { final tupleMap = MailboxUtils.generateMapDescendantIdsAndMailboxIdList( [presentationMailbox], @@ -429,8 +420,8 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa final listMailboxId = tupleMap.value2; consumeState(_deleteMultipleMailboxInteractor.execute( - session, - accountId, + session!, + accountId!, mapDescendantIds, listMailboxId )); @@ -452,8 +443,6 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa dashboardController.selectedMailbox.value = null; dashboardController.dispatchMailboxUIAction(SelectMailboxDefaultAction()); } - - _refreshMailboxChanges(mailboxState: currentMailboxState); } void _deleteMailboxFailure(DeleteMultipleMailboxFailure failure) { @@ -469,16 +458,21 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa MailboxSubscribeState subscribeState, MailboxSubscribeAction subscribeAction ) { - final accountId = dashboardController.accountId.value; - final session = dashboardController.sessionCurrent; - if (session != null && accountId != null) { final subscribeRequest = generateSubscribeRequest(mailboxId, subscribeState, subscribeAction); if (subscribeRequest is SubscribeMultipleMailboxRequest) { - consumeState(_subscribeMultipleMailboxInteractor.execute(session, accountId, subscribeRequest)); + consumeState(_subscribeMultipleMailboxInteractor.execute( + session!, + accountId!, + subscribeRequest, + )); } else if (subscribeRequest is SubscribeMailboxRequest) { - consumeState(_subscribeMailboxInteractor.execute(session, accountId, subscribeRequest)); + consumeState(_subscribeMailboxInteractor.execute( + session!, + accountId!, + subscribeRequest, + )); } } } @@ -502,11 +496,6 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa _closeEmailViewIfMailboxDisabledOrNotExist([success.mailboxId]); } } - - _refreshMailboxChanges( - mailboxState: success.currentMailboxState, - properties: MailboxConstants.propertiesDefault - ); } void _handleSubscribeMultipleMailboxAllSuccess(SubscribeMultipleMailboxAllSuccess success) { @@ -523,11 +512,6 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa _closeEmailViewIfMailboxDisabledOrNotExist(success.mailboxIdsSubscribe); } } - - _refreshMailboxChanges( - mailboxState: success.currentMailboxState, - properties: MailboxConstants.propertiesDefault - ); } void _handleSubscribeMultipleMailboxHasSomeSuccess(SubscribeMultipleMailboxHasSomeSuccess success) { @@ -544,11 +528,6 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa _closeEmailViewIfMailboxDisabledOrNotExist(success.mailboxIdsSubscribe); } } - - _refreshMailboxChanges( - mailboxState: success.currentMailboxState, - properties: MailboxConstants.propertiesDefault - ); } void _showToastSubscribeMailboxSuccess( @@ -587,8 +566,6 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa MailboxId mailboxIdSubscribed, {List? listDescendantMailboxIds} ) { - final accountId = dashboardController.accountId.value; - final session = dashboardController.sessionCurrent; if (session != null && accountId != null) { SubscribeRequest? subscribeRequest; @@ -608,9 +585,17 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa } if (subscribeRequest is SubscribeMultipleMailboxRequest) { - consumeState(_subscribeMultipleMailboxInteractor.execute(session, accountId, subscribeRequest)); + consumeState(_subscribeMultipleMailboxInteractor.execute( + session!, + accountId!, + subscribeRequest, + )); } else if (subscribeRequest is SubscribeMailboxRequest) { - consumeState(_subscribeMailboxInteractor.execute(session, accountId, subscribeRequest)); + consumeState(_subscribeMailboxInteractor.execute( + session!, + accountId!, + subscribeRequest, + )); } } } @@ -619,8 +604,6 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa MailboxId mailboxIdSubscribed, {List? listDescendantMailboxIds} ) { - final accountId = dashboardController.accountId.value; - final session = dashboardController.sessionCurrent; if (session != null && accountId != null) { SubscribeRequest? subscribeRequest; @@ -640,9 +623,17 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa } if (subscribeRequest is SubscribeMultipleMailboxRequest) { - consumeState(_subscribeMultipleMailboxInteractor.execute(session, accountId, subscribeRequest)); + consumeState(_subscribeMultipleMailboxInteractor.execute( + session!, + accountId!, + subscribeRequest, + )); } else if (subscribeRequest is SubscribeMailboxRequest) { - consumeState(_subscribeMailboxInteractor.execute(session, accountId, subscribeRequest)); + consumeState(_subscribeMailboxInteractor.execute( + session!, + accountId!, + subscribeRequest, + )); } } } @@ -660,15 +651,13 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa } void goToCreateNewMailboxView(BuildContext context, {PresentationMailbox? parentMailbox}) async { - final accountId = dashboardController.accountId.value; - final session = dashboardController.sessionCurrent; if (session != null && accountId != null) { final arguments = MailboxCreatorArguments( - accountId, + accountId!, defaultMailboxTree.value, personalMailboxTree.value, teamMailboxesTree.value, - dashboardController.sessionCurrent!, + session!, parentMailbox ); @@ -677,9 +666,14 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa : await push(AppRoutes.mailboxCreator, arguments: arguments); if (result != null && result is NewMailboxArguments) { - _createNewMailboxAction(session, accountId, CreateNewMailboxRequest( - result.newName, - parentId: result.mailboxLocation?.id)); + _createNewMailboxAction( + session!, + accountId!, + CreateNewMailboxRequest( + result.newName, + parentId: result.mailboxLocation?.id, + ), + ); } } } @@ -696,8 +690,6 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa leadingSVGIconColor: Colors.white, leadingSVGIcon: imagePaths.icFolderMailbox); } - - _refreshMailboxChanges(mailboxState: success.currentMailboxState); } void _createNewMailboxFailure(CreateNewMailboxFailure failure) { From 5cf704533475d4aa749d1e037d33e77b0e0becb6 Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 19 Dec 2024 00:50:13 +0700 Subject: [PATCH 16/72] TF-3334 Remove `Email/get` of mark as read & mark as star action --- .../data/datasource/email_datasource.dart | 13 +- .../email_datasource_impl.dart | 24 ++- .../email_hive_cache_datasource_impl.dart | 9 +- .../email/data/network/email_api.dart | 107 +++++----- .../repository/email_repository_impl.dart | 26 ++- .../email/domain/model/event_action.dart | 26 +++ .../domain/repository/email_repository.dart | 13 +- .../state/mark_as_email_read_state.dart | 6 +- .../state/mark_as_email_star_state.dart | 7 +- .../store_event_attendance_status_state.dart | 4 - .../mark_as_email_read_interactor.dart | 39 ++-- .../mark_as_star_email_interactor.dart | 30 +-- ...re_event_attendance_status_interactor.dart | 3 +- .../controller/single_email_controller.dart | 198 +++++++++--------- .../data/datasource/mailbox_datasource.dart | 2 +- .../mailbox_cache_datasource_impl.dart | 2 +- .../mailbox_datasource_impl.dart | 2 +- .../data/network/mailbox_isolate_worker.dart | 36 ++-- .../repository/mailbox_repository_impl.dart | 2 +- .../domain/repository/mailbox_repository.dart | 2 +- .../mailbox_dashboard_controller.dart | 137 ++++++------ ...ark_as_multiple_email_read_interactor.dart | 21 +- ...ark_as_star_multiple_email_interactor.dart | 19 +- .../mixin/email_action_controller.dart | 12 +- .../presentation_email_extension.dart | 7 +- ...ent_attendance_status_interactor_test.dart | 1 - .../controller/thread_controller_test.dart | 16 +- 27 files changed, 419 insertions(+), 345 deletions(-) diff --git a/lib/features/email/data/datasource/email_datasource.dart b/lib/features/email/data/datasource/email_datasource.dart index 2cfa7a9b44..4248f413e6 100644 --- a/lib/features/email/data/datasource/email_datasource.dart +++ b/lib/features/email/data/datasource/email_datasource.dart @@ -43,7 +43,12 @@ abstract class EmailDataSource { } ); - Future> markAsRead(Session session, AccountId accountId, List emails, ReadActions readActions); + Future> markAsRead( + Session session, + AccountId accountId, + List emailIds, + ReadActions readActions, + ); Future> downloadAttachments( List attachments, @@ -72,10 +77,10 @@ abstract class EmailDataSource { Future> moveToMailbox(Session session, AccountId accountId, MoveToMailboxRequest moveRequest); - Future> markAsStar( + Future> markAsStar( Session session, AccountId accountId, - List emails, + List emailIds, MarkStarAction markStarAction ); @@ -144,7 +149,7 @@ abstract class EmailDataSource { Future getRestoredDeletedMessage(EmailRecoveryActionId emailRecoveryActionId); - Future storeEventAttendanceStatus( + Future storeEventAttendanceStatus( Session session, AccountId accountId, EmailId emailId, diff --git a/lib/features/email/data/datasource_impl/email_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_datasource_impl.dart index c64e3ea668..c1b150c679 100644 --- a/lib/features/email/data/datasource_impl/email_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_datasource_impl.dart @@ -75,14 +75,14 @@ class EmailDataSourceImpl extends EmailDataSource { } @override - Future> markAsRead( + Future> markAsRead( Session session, AccountId accountId, - List emails, - ReadActions readActions + List emailIds, + ReadActions readActions, ) { return Future.sync(() async { - return await emailAPI.markAsRead(session, accountId, emails, readActions); + return await emailAPI.markAsRead(session, accountId, emailIds, readActions); }).catchError(_exceptionThrower.throwException); } @@ -119,9 +119,19 @@ class EmailDataSourceImpl extends EmailDataSource { } @override - Future> markAsStar(Session session, AccountId accountId, List emails, MarkStarAction markStarAction) { + Future> markAsStar( + Session session, + AccountId accountId, + List emailIds, + MarkStarAction markStarAction, + ) { return Future.sync(() async { - return await emailAPI.markAsStar(session, accountId, emails, markStarAction); + return await emailAPI.markAsStar( + session, + accountId, + emailIds, + markStarAction, + ); }).catchError(_exceptionThrower.throwException); } @@ -318,7 +328,7 @@ class EmailDataSourceImpl extends EmailDataSource { } @override - Future storeEventAttendanceStatus( + Future storeEventAttendanceStatus( Session session, AccountId accountId, EmailId emailId, diff --git a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart index e866992c15..accf354e53 100644 --- a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart @@ -120,12 +120,17 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { } @override - Future> markAsRead(Session session, AccountId accountId, List emails, ReadActions readActions) { + Future> markAsRead( + Session session, + AccountId accountId, + List emailIds, + ReadActions readActions, + ) { throw UnimplementedError(); } @override - Future> markAsStar(Session session, AccountId accountId, List emails, MarkStarAction markStarAction) { + Future> markAsStar(Session session, AccountId accountId, List emailIds, MarkStarAction markStarAction) { throw UnimplementedError(); } diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index 4ff1280058..f93ff0246b 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -44,13 +44,11 @@ import 'package:model/account/authentication_type.dart'; import 'package:model/download/download_task_id.dart'; import 'package:model/email/attachment.dart'; import 'package:model/email/email_action_type.dart'; -import 'package:model/email/email_property.dart'; import 'package:model/email/mark_star_action.dart'; import 'package:model/email/read_actions.dart'; import 'package:model/extensions/email_extension.dart'; import 'package:model/extensions/email_id_extensions.dart'; import 'package:model/extensions/keyword_identifier_extension.dart'; -import 'package:model/extensions/list_email_extension.dart'; import 'package:model/extensions/list_email_id_extension.dart'; import 'package:model/extensions/mailbox_id_extension.dart'; import 'package:model/extensions/session_extension.dart'; @@ -239,24 +237,18 @@ class EmailAPI with HandleSetErrorMixin { } } - Future> markAsRead( + Future> markAsRead( Session session, AccountId accountId, - List emails, - ReadActions readActions + List emailIds, + ReadActions readActions, ) async { final setEmailMethod = SetEmailMethod(accountId) - ..addUpdates(emails.listEmailIds.generateMapUpdateObjectMarkAsRead(readActions)); - - final getEmailMethod = GetEmailMethod(accountId) - ..addIds(emails.listEmailIds.toIds().toSet()) - ..addProperties(Properties({EmailProperty.keywords})); + ..addUpdates(emailIds.generateMapUpdateObjectMarkAsRead(readActions)); final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); - requestBuilder.invocation(setEmailMethod); - - final getEmailInvocation = requestBuilder.invocation(getEmailMethod); + final setEmailInvocation = requestBuilder.invocation(setEmailMethod); final capabilities = setEmailMethod.requiredCapabilities .toCapabilitiesSupportTeamMailboxes(session, accountId); @@ -266,15 +258,22 @@ class EmailAPI with HandleSetErrorMixin { .build() .execute(); - final getEmailResponse = response.parse( - getEmailInvocation.methodCallId, - GetEmailResponse.deserialize); + final setEmailResponse = response.parse( + setEmailInvocation.methodCallId, + SetEmailResponse.deserialize, + ); + + final emailIdUpdated = setEmailResponse?.updated + ?.keys + .map((id) => EmailId(id)) + .toList() ?? []; + final mapErrors = handleSetResponse([setEmailResponse]); - return Future.sync(() async { - return getEmailResponse!.list; - }).catchError((error) { - throw error; - }); + if (emailIdUpdated.isNotEmpty) { + return emailIdUpdated; + } else { + throw SetMethodException(mapErrors); + } } Future> downloadAttachments( @@ -449,24 +448,18 @@ class EmailAPI with HandleSetErrorMixin { return listEmailIdRequest.where((emailId) => listUpdated.expand((e) => e).toList().contains(emailId.id)).toList(); } - Future> markAsStar( + Future> markAsStar( Session session, AccountId accountId, - List emails, + List emailIds, MarkStarAction markStarAction ) async { final setEmailMethod = SetEmailMethod(accountId) - ..addUpdates(emails.listEmailIds.generateMapUpdateObjectMarkAsStar(markStarAction)); - - final getEmailMethod = GetEmailMethod(accountId) - ..addIds(emails.listEmailIds.toIds().toSet()) - ..addProperties(Properties({EmailProperty.keywords})); + ..addUpdates(emailIds.generateMapUpdateObjectMarkAsStar(markStarAction)); final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); - requestBuilder.invocation(setEmailMethod); - - final getEmailInvocation = requestBuilder.invocation(getEmailMethod); + final setEmailInvocation = requestBuilder.invocation(setEmailMethod); final capabilities = setEmailMethod.requiredCapabilities .toCapabilitiesSupportTeamMailboxes(session, accountId); @@ -476,15 +469,22 @@ class EmailAPI with HandleSetErrorMixin { .build() .execute(); - final getEmailResponse = response.parse( - getEmailInvocation.methodCallId, - GetEmailResponse.deserialize); + final setEmailResponse = response.parse( + setEmailInvocation.methodCallId, + SetEmailResponse.deserialize, + ); - return Future.sync(() async { - return getEmailResponse!.list; - }).catchError((error) { - throw error; - }); + final emailIdUpdated = setEmailResponse?.updated + ?.keys + .map((id) => EmailId(id)) + .toList() ?? []; + final mapErrors = handleSetResponse([setEmailResponse]); + + if (emailIdUpdated.isNotEmpty) { + return emailIdUpdated; + } else { + throw SetMethodException(mapErrors); + } } Future saveEmailAsDrafts( @@ -756,7 +756,7 @@ class EmailAPI with HandleSetErrorMixin { } } - Future storeEventAttendanceStatus( + Future storeEventAttendanceStatus( Session session, AccountId accountId, EmailId emailId, @@ -765,15 +765,9 @@ class EmailAPI with HandleSetErrorMixin { final setEmailMethod = SetEmailMethod(accountId) ..addUpdates(emailId.generateMapUpdateObjectEventAttendanceStatus(eventActionType)); - final getEmailMethod = GetEmailMethod(accountId) - ..addIds({emailId.id}) - ..addProperties(Properties({EmailProperty.keywords})); - final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); - requestBuilder.invocation(setEmailMethod); - - final getEmailInvocation = requestBuilder.invocation(getEmailMethod); + final setEmailInvocation = requestBuilder.invocation(setEmailMethod); final capabilities = setEmailMethod.requiredCapabilities .toCapabilitiesSupportTeamMailboxes(session, accountId); @@ -783,16 +777,19 @@ class EmailAPI with HandleSetErrorMixin { .build() .execute(); - final getEmailResponse = response.parse( - getEmailInvocation.methodCallId, - GetEmailResponse.deserialize); + final setEmailResponse = response.parse( + setEmailInvocation.methodCallId, + SetEmailResponse.deserialize, + ); - final listEmails = getEmailResponse?.list ?? []; + final emailIdUpdated = setEmailResponse?.updated + ?.keys + .map((id) => EmailId(id)) + .toList() ?? []; + final mapErrors = handleSetResponse([setEmailResponse]); - if (listEmails.isNotEmpty) { - return listEmails.first; - } else { - throw NotFoundEmailException(); + if (emailIdUpdated.isEmpty) { + throw SetMethodException(mapErrors); } } } \ No newline at end of file diff --git a/lib/features/email/data/repository/email_repository_impl.dart b/lib/features/email/data/repository/email_repository_impl.dart index 57860f7ef3..cbc7f52015 100644 --- a/lib/features/email/data/repository/email_repository_impl.dart +++ b/lib/features/email/data/repository/email_repository_impl.dart @@ -83,13 +83,18 @@ class EmailRepositoryImpl extends EmailRepository { } @override - Future> markAsRead( + Future> markAsRead( Session session, AccountId accountId, - List emails, - ReadActions readActions + List emailIds, + ReadActions readActions, ) { - return emailDataSource[DataSourceType.network]!.markAsRead(session, accountId, emails, readActions); + return emailDataSource[DataSourceType.network]!.markAsRead( + session, + accountId, + emailIds, + readActions, + ); } @override @@ -124,13 +129,18 @@ class EmailRepositoryImpl extends EmailRepository { } @override - Future> markAsStar( + Future> markAsStar( Session session, AccountId accountId, - List emails, + List emailIds, MarkStarAction markStarAction ) { - return emailDataSource[DataSourceType.network]!.markAsStar(session, accountId, emails, markStarAction); + return emailDataSource[DataSourceType.network]!.markAsStar( + session, + accountId, + emailIds, + markStarAction, + ); } @override @@ -303,7 +313,7 @@ class EmailRepositoryImpl extends EmailRepository { } @override - Future storeEventAttendanceStatus( + Future storeEventAttendanceStatus( Session session, AccountId accountId, EmailId emailId, diff --git a/lib/features/email/domain/model/event_action.dart b/lib/features/email/domain/model/event_action.dart index a14a5ebdb3..098ba6b332 100644 --- a/lib/features/email/domain/model/event_action.dart +++ b/lib/features/email/domain/model/event_action.dart @@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/cupertino.dart'; import 'package:jmap_dart_client/jmap/core/patch_object.dart'; +import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:model/extensions/keyword_identifier_extension.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -61,6 +62,31 @@ enum EventActionType { return ''; } } + + Map getMapKeywords() { + switch(this) { + case EventActionType.yes: + return { + KeyWordIdentifierExtension.acceptedEventAttendance: true, + KeyWordIdentifierExtension.tentativelyAcceptedEventAttendance: false, + KeyWordIdentifierExtension.rejectedEventAttendance: false, + }; + case EventActionType.maybe: + return { + KeyWordIdentifierExtension.acceptedEventAttendance: false, + KeyWordIdentifierExtension.tentativelyAcceptedEventAttendance: true, + KeyWordIdentifierExtension.rejectedEventAttendance: false, + }; + case EventActionType.no: + return { + KeyWordIdentifierExtension.acceptedEventAttendance: false, + KeyWordIdentifierExtension.tentativelyAcceptedEventAttendance: false, + KeyWordIdentifierExtension.rejectedEventAttendance: true, + }; + case EventActionType.mailToAttendees: + return {}; + } + } } class EventAction with EquatableMixin { diff --git a/lib/features/email/domain/repository/email_repository.dart b/lib/features/email/domain/repository/email_repository.dart index 88ae516c27..5e62b863e4 100644 --- a/lib/features/email/domain/repository/email_repository.dart +++ b/lib/features/email/domain/repository/email_repository.dart @@ -45,7 +45,12 @@ abstract class EmailRepository { } ); - Future> markAsRead(Session session, AccountId accountId, List emails, ReadActions readActions); + Future> markAsRead( + Session session, + AccountId accountId, + List emailIds, + ReadActions readActions, + ); Future> downloadAttachments( List attachments, @@ -74,10 +79,10 @@ abstract class EmailRepository { Future> moveToMailbox(Session session, AccountId accountId, MoveToMailboxRequest moveRequest); - Future> markAsStar( + Future> markAsStar( Session session, AccountId accountId, - List emails, + List emailIds, MarkStarAction markStarAction ); @@ -147,7 +152,7 @@ abstract class EmailRepository { Future printEmail(EmailPrint emailPrint); - Future storeEventAttendanceStatus( + Future storeEventAttendanceStatus( Session session, AccountId accountId, EmailId emailId, diff --git a/lib/features/email/domain/state/mark_as_email_read_state.dart b/lib/features/email/domain/state/mark_as_email_read_state.dart index 28e48586cb..2e7ddbd688 100644 --- a/lib/features/email/domain/state/mark_as_email_read_state.dart +++ b/lib/features/email/domain/state/mark_as_email_read_state.dart @@ -7,12 +7,12 @@ import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; class MarkAsEmailReadSuccess extends UIActionState { - final Email updatedEmail; + final EmailId emailId; final ReadActions readActions; final MarkReadAction markReadAction; MarkAsEmailReadSuccess( - this.updatedEmail, + this.emailId, this.readActions, this.markReadAction, { @@ -22,7 +22,7 @@ class MarkAsEmailReadSuccess extends UIActionState { ) : super(currentEmailState, currentMailboxState); @override - List get props => [updatedEmail, readActions, markReadAction, ...super.props]; + List get props => [emailId, readActions, markReadAction, ...super.props]; } class MarkAsEmailReadFailure extends FeatureFailure { diff --git a/lib/features/email/domain/state/mark_as_email_star_state.dart b/lib/features/email/domain/state/mark_as_email_star_state.dart index dbe215734d..c9d639e726 100644 --- a/lib/features/email/domain/state/mark_as_email_star_state.dart +++ b/lib/features/email/domain/state/mark_as_email_star_state.dart @@ -1,15 +1,12 @@ import 'package:core/presentation/state/failure.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; -import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:model/model.dart'; +import 'package:model/email/mark_star_action.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; class MarkAsStarEmailSuccess extends UIActionState { - final Email updatedEmail; final MarkStarAction markStarAction; MarkAsStarEmailSuccess( - this.updatedEmail, this.markStarAction, { jmap.State? currentEmailState, @@ -18,7 +15,7 @@ class MarkAsStarEmailSuccess extends UIActionState { ) : super(currentEmailState, currentMailboxState); @override - List get props => [updatedEmail, markStarAction, ...super.props]; + List get props => [markStarAction, ...super.props]; } class MarkAsStarEmailFailure extends FeatureFailure { diff --git a/lib/features/email/domain/state/store_event_attendance_status_state.dart b/lib/features/email/domain/state/store_event_attendance_status_state.dart index 942e6accb4..1325cf81bd 100644 --- a/lib/features/email/domain/state/store_event_attendance_status_state.dart +++ b/lib/features/email/domain/state/store_event_attendance_status_state.dart @@ -1,7 +1,6 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; -import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; @@ -10,11 +9,9 @@ class StoreEventAttendanceStatusLoading extends LoadingState {} class StoreEventAttendanceStatusSuccess extends UIActionState { final EventActionType eventActionType; - final Email updatedEmail; StoreEventAttendanceStatusSuccess( this.eventActionType, - this.updatedEmail, { jmap.State? currentEmailState, jmap.State? currentMailboxState, @@ -24,7 +21,6 @@ class StoreEventAttendanceStatusSuccess extends UIActionState { @override List get props => [ eventActionType, - updatedEmail, ...super.props]; } diff --git a/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart b/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart index 435f6064c5..7fe15acbb9 100644 --- a/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart +++ b/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart @@ -1,9 +1,10 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:model/model.dart'; +import 'package:model/email/read_actions.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; @@ -15,7 +16,13 @@ class MarkAsEmailReadInteractor { MarkAsEmailReadInteractor(this._emailRepository, this._mailboxRepository); - Stream> execute(Session session, AccountId accountId, Email email, ReadActions readAction, MarkReadAction markReadAction) async* { + Stream> execute( + Session session, + AccountId accountId, + EmailId emailId, + ReadActions readAction, + MarkReadAction markReadAction, + ) async* { try { final listState = await Future.wait([ _mailboxRepository.getMailboxState( session,accountId), @@ -25,18 +32,20 @@ class MarkAsEmailReadInteractor { final currentMailboxState = listState.first; final currentEmailState = listState.last; - final result = await _emailRepository.markAsRead(session, accountId, [email], readAction); - if (result.isNotEmpty) { - final updatedEmail = email.updatedEmail(newKeywords: result.first.keywords); - yield Right(MarkAsEmailReadSuccess( - updatedEmail, - readAction, - markReadAction, - currentEmailState: currentEmailState, - currentMailboxState: currentMailboxState)); - } else { - yield Left(MarkAsEmailReadFailure(readAction)); - } + final result = await _emailRepository.markAsRead( + session, + accountId, + [emailId], + readAction, + ); + + yield Right(MarkAsEmailReadSuccess( + result.first, + readAction, + markReadAction, + currentEmailState: currentEmailState, + currentMailboxState: currentMailboxState, + )); } catch (e) { yield Left(MarkAsEmailReadFailure(readAction, exception: e)); } diff --git a/lib/features/email/domain/usecases/mark_as_star_email_interactor.dart b/lib/features/email/domain/usecases/mark_as_star_email_interactor.dart index df89ea182f..6ca420eb75 100644 --- a/lib/features/email/domain/usecases/mark_as_star_email_interactor.dart +++ b/lib/features/email/domain/usecases/mark_as_star_email_interactor.dart @@ -1,4 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; @@ -12,19 +13,24 @@ class MarkAsStarEmailInteractor { MarkAsStarEmailInteractor(this.emailRepository); - Stream> execute(Session session, AccountId accountId, Email email, MarkStarAction markStarAction) async* { + Stream> execute( + Session session, + AccountId accountId, + EmailId emailId, + MarkStarAction markStarAction, + ) async* { try { final currentEmailState = await emailRepository.getEmailState(session, accountId); - final result = await emailRepository.markAsStar(session, accountId, [email], markStarAction); - if (result.isNotEmpty) { - final updatedEmail = email.updatedEmail(newKeywords: result.first.keywords); - yield Right(MarkAsStarEmailSuccess( - updatedEmail, - markStarAction, - currentEmailState: currentEmailState)); - } else { - yield Left(MarkAsStarEmailFailure(markStarAction)); - } + await emailRepository.markAsStar( + session, + accountId, + [emailId], + markStarAction, + ); + yield Right(MarkAsStarEmailSuccess( + markStarAction, + currentEmailState: currentEmailState, + )); } catch (e) { yield Left(MarkAsStarEmailFailure(markStarAction, exception: e)); } diff --git a/lib/features/email/domain/usecases/store_event_attendance_status_interactor.dart b/lib/features/email/domain/usecases/store_event_attendance_status_interactor.dart index d2abe8422d..d524691aa2 100644 --- a/lib/features/email/domain/usecases/store_event_attendance_status_interactor.dart +++ b/lib/features/email/domain/usecases/store_event_attendance_status_interactor.dart @@ -24,7 +24,7 @@ class StoreEventAttendanceStatusInteractor { final currentEmailState = await _emailRepository.getEmailState(session, accountId); - final updatedEmail = await _emailRepository.storeEventAttendanceStatus( + await _emailRepository.storeEventAttendanceStatus( session, accountId, emailId, @@ -32,7 +32,6 @@ class StoreEventAttendanceStatusInteractor { yield Right(StoreEventAttendanceStatusSuccess( eventActionType, - updatedEmail, currentEmailState: currentEmailState)); } catch (e) { yield Left(StoreEventAttendanceStatusFailure(exception: e)); diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 45449b4979..8988f2df7c 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -23,6 +23,7 @@ import 'package:jmap_dart_client/jmap/mail/calendar/properties/attendee/calendar import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_organizer.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:jmap_dart_client/jmap/mdn/disposition.dart'; import 'package:jmap_dart_client/jmap/mdn/mdn.dart'; import 'package:model/email/eml_attachment.dart'; @@ -62,6 +63,7 @@ import 'package:tmail_ui_user/features/email/domain/state/send_receipt_to_sender import 'package:tmail_ui_user/features/email/domain/state/store_event_attendance_status_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/unsubscribe_email_state.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/calendar_event_accept_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_star_email_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/maybe_calendar_event_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/calendar_event_reject_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart'; @@ -69,7 +71,6 @@ import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment import 'package:tmail_ui_user/features/email/domain/usecases/export_attachment_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart'; -import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_star_email_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/parse_calendar_event_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart'; @@ -169,6 +170,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin { CalendarEvent? get calendarEvent => blobCalendarEvent.value?.calendarEventList.firstOrNull; Id? get _displayingEventBlobId => blobCalendarEvent.value?.blobId; + AccountId? get accountId => mailboxDashBoardController.accountId.value; + + Session? get session => mailboxDashBoardController.sessionCurrent; + SingleEmailController( this._getEmailContentInteractor, this._markAsEmailReadInteractor, @@ -206,7 +211,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } else if (success is GetEmailContentFromCacheSuccess) { _getEmailContentOfflineSuccess(success); } else if (success is MarkAsEmailReadSuccess) { - _markAsEmailReadSuccess(success); + _handleMarkAsEmailReadCompleted(success.readActions); } else if (success is ExportAttachmentSuccess) { _exportAttachmentSuccessAction(success); } else if (success is MoveToMailboxSuccess) { @@ -244,7 +249,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void handleFailureViewState(Failure failure) { super.handleFailureViewState(failure); if (failure is MarkAsEmailReadFailure) { - _markAsEmailReadFailure(failure); + _handleMarkAsEmailReadCompleted(failure.readActions); } else if (failure is DownloadAttachmentsFailure) { _downloadAttachmentsFailure(failure); } else if (failure is ExportAttachmentFailure) { @@ -267,7 +272,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { ever(mailboxDashBoardController.accountId, (accountId) { if (accountId is AccountId) { _injectAndGetInteractorBindings( - mailboxDashBoardController.sessionCurrent, + session, accountId ); } @@ -421,10 +426,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } void _getAllIdentities() { - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; if (accountId != null && session != null) { - consumeState(_getAllIdentitiesInteractor.execute(session, accountId)); + consumeState(_getAllIdentitiesInteractor.execute(session!, accountId!)); } } @@ -477,17 +480,15 @@ class SingleEmailController extends BaseController with AppLoaderMixin { ) ))); } else { - final session = mailboxDashBoardController.sessionCurrent; - final accountId = mailboxDashBoardController.accountId.value; if (session != null && accountId != null) { - final baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl) ?? ''; + final baseDownloadUrl = session!.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl); TransformConfiguration transformConfiguration = PlatformInfo.isWeb ? TransformConfiguration.forPreviewEmailOnWeb() : TransformConfiguration.forPreviewEmail(); consumeState(_getEmailContentInteractor.execute( - session, - accountId, + session!, + accountId!, emailId, baseDownloadUrl, transformConfiguration @@ -585,8 +586,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { ); _storeOpenedEmailAction( - mailboxDashBoardController.sessionCurrent, - mailboxDashBoardController.accountId.value, + session, + accountId, detailedEmail ); } @@ -647,27 +648,24 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } - void markAsEmailRead(PresentationEmail presentationEmail, ReadActions readActions, MarkReadAction markReadAction) async { - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; + void markAsEmailRead( + PresentationEmail presentationEmail, + ReadActions readActions, + MarkReadAction markReadAction, + ) { if (accountId != null && session != null) { - consumeState(_markAsEmailReadInteractor.execute(session, accountId, presentationEmail.toEmail(), readActions, markReadAction)); - } - } - - void _markAsEmailReadSuccess(Success success) { - log('SingleEmailController::_markAsEmailReadSuccess(): $success'); - mailboxDashBoardController.dispatchState(Right(success)); - - if (success is MarkAsEmailReadSuccess - && success.readActions == ReadActions.markAsUnread) { - closeEmailView(context: currentContext); + consumeState(_markAsEmailReadInteractor.execute( + session!, + accountId!, + presentationEmail.id!, + readActions, + markReadAction, + )); } } - void _markAsEmailReadFailure(Failure failure) { - if (failure is MarkAsEmailReadFailure - && failure.readActions == ReadActions.markAsUnread) { + void _handleMarkAsEmailReadCompleted(ReadActions readActions) { + if (readActions == ReadActions.markAsUnread) { closeEmailView(context: currentContext); } } @@ -711,11 +709,16 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } - void _downloadAttachmentsAction(List attachments) async { - final accountId = mailboxDashBoardController.accountId.value; - if (accountId != null && mailboxDashBoardController.sessionCurrent != null) { - final baseDownloadUrl = mailboxDashBoardController.sessionCurrent!.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl); - consumeState(_downloadAttachmentsInteractor.execute(attachments, accountId, baseDownloadUrl)); + void _downloadAttachmentsAction(List attachments) { + if (accountId != null && session != null) { + final baseDownloadUrl = session!.getDownloadUrl( + jmapUrl: dynamicUrlInterceptors.jmapUrl, + ); + consumeState(_downloadAttachmentsInteractor.execute( + attachments, + accountId!, + baseDownloadUrl, + )); } } @@ -760,11 +763,17 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } - void _exportAttachmentAction(Attachment attachment, CancelToken cancelToken) async { - final accountId = mailboxDashBoardController.accountId.value; - if (accountId != null && mailboxDashBoardController.sessionCurrent != null) { - final baseDownloadUrl = mailboxDashBoardController.sessionCurrent!.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl); - consumeState(_exportAttachmentInteractor.execute(attachment, accountId, baseDownloadUrl, cancelToken)); + void _exportAttachmentAction(Attachment attachment, CancelToken cancelToken) { + if (accountId != null && session != null) { + final baseDownloadUrl = session!.getDownloadUrl( + jmapUrl: dynamicUrlInterceptors.jmapUrl, + ); + consumeState(_exportAttachmentInteractor.execute( + attachment, + accountId!, + baseDownloadUrl, + cancelToken, + )); } } @@ -807,15 +816,15 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } void downloadAttachmentForWeb(Attachment attachment) { - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; if (accountId != null && session != null) { - final baseDownloadUrl = session.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl); + final baseDownloadUrl = session!.getDownloadUrl( + jmapUrl: dynamicUrlInterceptors.jmapUrl, + ); final generateTaskId = DownloadTaskId(uuid.v4()); consumeState(_downloadAttachmentForWebInteractor.execute( generateTaskId, attachment, - accountId, + accountId!, baseDownloadUrl, _downloadProgressStateController)); } else { @@ -876,12 +885,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void moveToMailbox(BuildContext context, PresentationEmail email) async { final currentMailbox = getMailboxContain(email); - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; if (currentMailbox != null && accountId != null) { final arguments = DestinationPickerArguments( - accountId, + accountId!, MailboxActions.moveEmail, session, mailboxIdSelected: currentMailbox.mailboxId @@ -893,13 +900,13 @@ class SingleEmailController extends BaseController with AppLoaderMixin { if (destinationMailbox != null && destinationMailbox is PresentationMailbox && - mailboxDashBoardController.sessionCurrent != null && + session != null && context.mounted ) { _dispatchMoveToAction( context, - accountId, - mailboxDashBoardController.sessionCurrent!, + accountId!, + session!, email, currentMailbox, destinationMailbox); @@ -978,24 +985,20 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } void _revertedToOriginalMailbox(MoveToMailboxRequest newMoveRequest) { - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; if (accountId != null && session != null) { - _moveToMailbox(currentContext!, session, accountId, newMoveRequest); + _moveToMailbox(currentContext!, session!, accountId!, newMoveRequest); } } - void moveToTrash(BuildContext context, PresentationEmail email) async { - final session = mailboxDashBoardController.sessionCurrent; - final accountId = mailboxDashBoardController.accountId.value; + void moveToTrash(BuildContext context, PresentationEmail email) { final trashMailboxId = mailboxDashBoardController.getMailboxIdByRole(PresentationMailbox.roleTrash); final currentMailbox = getMailboxContain(email); if (session != null && accountId != null && currentMailbox != null && trashMailboxId != null) { _moveToTrashAction( context, - session, - accountId, + session!, + accountId!, MoveToMailboxRequest( {currentMailbox.id: [email.id!]}, trashMailboxId, @@ -1015,17 +1018,15 @@ class SingleEmailController extends BaseController with AppLoaderMixin { mailboxDashBoardController.moveToMailbox(session, accountId, moveRequest); } - void moveToSpam(BuildContext context, PresentationEmail email) async { - final session = mailboxDashBoardController.sessionCurrent; - final accountId = mailboxDashBoardController.accountId.value; + void moveToSpam(BuildContext context, PresentationEmail email) { final spamMailboxId = mailboxDashBoardController.spamMailboxId; final currentMailbox = getMailboxContain(email); if (session != null && accountId != null && currentMailbox != null && spamMailboxId != null) { _moveToSpamAction( context, - session, - accountId, + session!, + accountId!, MoveToMailboxRequest( {currentMailbox.id: [email.id!]}, spamMailboxId, @@ -1035,17 +1036,15 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } - void unSpam(BuildContext context, PresentationEmail email) async { - final session = mailboxDashBoardController.sessionCurrent; - final accountId = mailboxDashBoardController.accountId.value; + void unSpam(BuildContext context, PresentationEmail email) { final spamMailboxId = mailboxDashBoardController.spamMailboxId; final inboxMailboxId = mailboxDashBoardController.getMailboxIdByRole(PresentationMailbox.roleInbox); if (session != null && accountId != null && spamMailboxId != null && inboxMailboxId != null) { _moveToSpamAction( context, - session, - accountId, + session!, + accountId!, MoveToMailboxRequest( {spamMailboxId: [email.id!]}, inboxMailboxId, @@ -1065,18 +1064,25 @@ class SingleEmailController extends BaseController with AppLoaderMixin { mailboxDashBoardController.moveToMailbox(session, accountId, moveRequest); } - void markAsStarEmail(PresentationEmail presentationEmail, MarkStarAction markStarAction) async { - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; + void markAsStarEmail( + PresentationEmail presentationEmail, + MarkStarAction markStarAction, + ) { if (accountId != null && session != null) { - consumeState(_markAsStarEmailInteractor.execute(session, accountId, presentationEmail.toEmail(), markStarAction)); + consumeState(_markAsStarEmailInteractor.execute( + session!, + accountId!, + presentationEmail.id!, + markStarAction, + )); } } void _markAsEmailStarSuccess(MarkAsStarEmailSuccess success) { - final selectedEmail = mailboxDashBoardController.selectedEmail.value; - mailboxDashBoardController.setSelectedEmail(selectedEmail?.updateKeywords(success.updatedEmail.keywords)); - mailboxDashBoardController.dispatchState(Right(success)); + final newEmail = currentEmail?.updateKeywords({ + KeyWordIdentifier.emailFlagged: true, + }); + mailboxDashBoardController.setSelectedEmail(newEmail); } void handleEmailAction(BuildContext context, PresentationEmail presentationEmail, EmailActionType actionType) { @@ -1210,8 +1216,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } void _handleSendReceiptToSenderAction(BuildContext context) { - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; if (accountId == null || session == null) { return; } @@ -1237,7 +1241,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { return; } - final receiverEmailAddress = _getReceiverEmailAddress(currentEmail!) ?? session.username.value; + final receiverEmailAddress = _getReceiverEmailAddress(currentEmail!) ?? session!.username.value; log('SingleEmailController::_handleSendReceiptToSenderAction():receiverEmailAddress: $receiverEmailAddress'); final mdnToSender = _generateMDN(context, currentEmail!, receiverEmailAddress); final sendReceiptRequest = SendReceiptToSenderRequest( @@ -1246,7 +1250,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { sendId: Id(uuid.v1())); log('SingleEmailController::_handleSendReceiptToSenderAction(): sendReceiptRequest: $sendReceiptRequest'); - consumeState(_sendReceiptToSenderInteractor!.execute(accountId, sendReceiptRequest)); + consumeState(_sendReceiptToSenderInteractor!.execute(accountId!, sendReceiptRequest)); } String? _getReceiverEmailAddress(PresentationEmail presentationEmail) { @@ -1395,12 +1399,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void quickCreatingRule(BuildContext context, EmailAddress emailAddress) async { popBack(); - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; if (accountId != null && session != null) { final arguments = RulesFilterCreatorArguments( - accountId, - session, + accountId!, + session!, emailAddress: emailAddress); final newRuleFilterRequest = PlatformInfo.isWeb @@ -1408,7 +1410,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { : await push(AppRoutes.rulesFilterCreator, arguments: arguments); if (newRuleFilterRequest is CreateNewEmailRuleFilterRequest) { - _createNewRuleFilterAction(accountId, newRuleFilterRequest); + _createNewRuleFilterAction(accountId!, newRuleFilterRequest); } } } @@ -1460,11 +1462,9 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } bool get _isCalendarEventSupported { - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; return session != null && accountId != null && - CapabilityIdentifier.jamesCalendarEvent.isSupported(session, accountId); + CapabilityIdentifier.jamesCalendarEvent.isSupported(session!, accountId!); } @visibleForTesting @@ -1690,21 +1690,21 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void _acceptCalendarEventAction(EmailId emailId) { if (_acceptCalendarEventInteractor == null || _displayingEventBlobId == null - || mailboxDashBoardController.accountId.value == null - || mailboxDashBoardController.sessionCurrent == null - || mailboxDashBoardController.sessionCurrent - !.validateCalendarEventCapability(mailboxDashBoardController.accountId.value!) - .isAvailable == false + || accountId == null + || session == null + || session!.validateCalendarEventCapability(accountId!).isAvailable == false ) { consumeState(Stream.value(Left(CalendarEventAcceptFailure()))); } else { consumeState(_acceptCalendarEventInteractor!.execute( - mailboxDashBoardController.accountId.value!, + accountId!, {_displayingEventBlobId!}, emailId, - mailboxDashBoardController.sessionCurrent!.getLanguageForCalendarEvent( + session!.getLanguageForCalendarEvent( LocalizationService.getLocaleFromLanguage(), - mailboxDashBoardController.accountId.value!))); + accountId!, + ), + )); } } @@ -1768,10 +1768,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } void _showToastMessageEventAttendanceSuccess(StoreEventAttendanceStatusSuccess success) { - final selectedEmail = mailboxDashBoardController.selectedEmail.value; - final newEmail = selectedEmail?.updateKeywords(success.updatedEmail.keywords); + final newEmail = currentEmail?.updateKeywords( + success.eventActionType.getMapKeywords(), + ); mailboxDashBoardController.setSelectedEmail(newEmail); - mailboxDashBoardController.dispatchState(Right(success)); if (currentOverlayContext == null || currentContext == null) { return; diff --git a/lib/features/mailbox/data/datasource/mailbox_datasource.dart b/lib/features/mailbox/data/datasource/mailbox_datasource.dart index 4b9f30c7ef..dc5238178e 100644 --- a/lib/features/mailbox/data/datasource/mailbox_datasource.dart +++ b/lib/features/mailbox/data/datasource/mailbox_datasource.dart @@ -38,7 +38,7 @@ abstract class MailboxDataSource { Future moveMailbox(Session session, AccountId accountId, MoveMailboxRequest request); - Future> markAsMailboxRead( + Future> markAsMailboxRead( Session session, AccountId accountId, MailboxId mailboxId, diff --git a/lib/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart b/lib/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart index b52e767b24..e0c604a39a 100644 --- a/lib/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart +++ b/lib/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart @@ -78,7 +78,7 @@ class MailboxCacheDataSourceImpl extends MailboxDataSource { } @override - Future> markAsMailboxRead( + Future> markAsMailboxRead( Session session, AccountId accountId, MailboxId mailboxId, diff --git a/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart b/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart index 062e7effa3..fdcd4e7f5d 100644 --- a/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart +++ b/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart @@ -87,7 +87,7 @@ class MailboxDataSourceImpl extends MailboxDataSource { } @override - Future> markAsMailboxRead( + Future> markAsMailboxRead( Session session, AccountId accountId, MailboxId mailboxId, diff --git a/lib/features/mailbox/data/network/mailbox_isolate_worker.dart b/lib/features/mailbox/data/network/mailbox_isolate_worker.dart index 58a3306f1f..33b0146ebe 100644 --- a/lib/features/mailbox/data/network/mailbox_isolate_worker.dart +++ b/lib/features/mailbox/data/network/mailbox_isolate_worker.dart @@ -19,6 +19,7 @@ import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/email_property.dart'; import 'package:model/email/read_actions.dart'; +import 'package:model/extensions/list_email_extension.dart'; import 'package:tmail_ui_user/features/base/isolate/background_isolate_binary_messenger/background_isolate_binary_messenger.dart'; import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; @@ -37,7 +38,7 @@ class MailboxIsolateWorker { MailboxIsolateWorker(this._threadApi, this._emailApi, this._isolateExecutor); - Future> markAsMailboxRead( + Future> markAsMailboxRead( Session session, AccountId accountId, MailboxId mailboxId, @@ -80,7 +81,7 @@ class MailboxIsolateWorker { } } - static Future> _handleMarkAsMailboxReadAction( + static Future> _handleMarkAsMailboxReadAction( MailboxMarkAsReadArguments args, TypeSendPort sendPort ) async { @@ -88,7 +89,7 @@ class MailboxIsolateWorker { BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken); await HiveCacheConfig.instance.setUp(); - List emailListCompleted = List.empty(growable: true); + List emailIdsCompleted = List.empty(growable: true); try { bool mailboxHasEmails = true; UTCDate? lastReceivedDate; @@ -134,29 +135,29 @@ class MailboxIsolateWorker { final result = await args.emailAPI.markAsRead( args.session, args.accountId, - listEmailUnread, + listEmailUnread.listEmailIds, ReadActions.markAsRead); log('MailboxIsolateWorker::_handleMarkAsMailboxRead(): MARK_READ: ${result.length}'); - emailListCompleted.addAll(result); - sendPort.send(emailListCompleted); + emailIdsCompleted.addAll(result); + sendPort.send(emailIdsCompleted); } } } catch (e) { log('MailboxIsolateWorker::_handleMarkAsMailboxRead(): ERROR: $e'); } - log('MailboxIsolateWorker::_handleMarkAsMailboxRead(): TOTAL_READ: ${emailListCompleted.length}'); - return emailListCompleted; + log('MailboxIsolateWorker::_handleMarkAsMailboxRead(): TOTAL_READ: ${emailIdsCompleted.length}'); + return emailIdsCompleted; } - Future> _handleMarkAsMailboxReadActionOnWeb( + Future> _handleMarkAsMailboxReadActionOnWeb( Session session, AccountId accountId, MailboxId mailboxId, int totalEmailUnread, StreamController> onProgressController ) async { - List emailListCompleted = List.empty(growable: true); + List emailIdsCompleted = List.empty(growable: true); try { bool mailboxHasEmails = true; UTCDate? lastReceivedDate; @@ -199,20 +200,25 @@ class MailboxIsolateWorker { lastEmailId = listEmailUnread.last.id; lastReceivedDate = listEmailUnread.last.receivedAt; - final result = await _emailApi.markAsRead(session, accountId, listEmailUnread, ReadActions.markAsRead); + final result = await _emailApi.markAsRead( + session, + accountId, + listEmailUnread.listEmailIds, + ReadActions.markAsRead, + ); log('MailboxIsolateWorker::_handleMarkAsMailboxReadActionOnWeb(): MARK_READ: ${result.length}'); - emailListCompleted.addAll(result); + emailIdsCompleted.addAll(result); onProgressController.add(Right(UpdatingMarkAsMailboxReadState( mailboxId: mailboxId, totalUnread: totalEmailUnread, - countRead: emailListCompleted.length))); + countRead: emailIdsCompleted.length))); } } } catch (e) { log('MailboxIsolateWorker::_handleMarkAsMailboxReadActionOnWeb(): ERROR: $e'); } - log('MailboxIsolateWorker::_handleMarkAsMailboxReadActionOnWeb(): TOTAL_READ: ${emailListCompleted.length}'); - return emailListCompleted; + log('MailboxIsolateWorker::_handleMarkAsMailboxReadActionOnWeb(): TOTAL_READ: ${emailIdsCompleted.length}'); + return emailIdsCompleted; } } diff --git a/lib/features/mailbox/data/repository/mailbox_repository_impl.dart b/lib/features/mailbox/data/repository/mailbox_repository_impl.dart index a0fde06d72..bfc6fcc068 100644 --- a/lib/features/mailbox/data/repository/mailbox_repository_impl.dart +++ b/lib/features/mailbox/data/repository/mailbox_repository_impl.dart @@ -216,7 +216,7 @@ class MailboxRepositoryImpl extends MailboxRepository { } @override - Future> markAsMailboxRead( + Future> markAsMailboxRead( Session session, AccountId accountId, MailboxId mailboxId, diff --git a/lib/features/mailbox/domain/repository/mailbox_repository.dart b/lib/features/mailbox/domain/repository/mailbox_repository.dart index a3f40851e4..57b8b17de7 100644 --- a/lib/features/mailbox/domain/repository/mailbox_repository.dart +++ b/lib/features/mailbox/domain/repository/mailbox_repository.dart @@ -31,7 +31,7 @@ abstract class MailboxRepository { Future renameMailbox(Session session, AccountId accountId, RenameMailboxRequest request); - Future> markAsMailboxRead( + Future> markAsMailboxRead( Session session, AccountId accountId, MailboxId mailboxId, diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 1387e4fc4a..d477d0a09d 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -362,12 +362,20 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo } } else if (success is UpdateVacationSuccess) { _handleUpdateVacationSuccess(success); - } else if (success is MarkAsMultipleEmailReadAllSuccess || - success is MarkAsMultipleEmailReadHasSomeEmailFailure) { - _markAsReadSelectedMultipleEmailSuccess(success); - } else if (success is MarkAsStarMultipleEmailAllSuccess || - success is MarkAsStarMultipleEmailHasSomeEmailFailure) { - _markAsStarMultipleEmailSuccess(success); + } else if (success is MarkAsMultipleEmailReadAllSuccess) { + _markAsReadSelectedMultipleEmailSuccess(success.readActions); + } else if (success is MarkAsMultipleEmailReadHasSomeEmailFailure) { + _markAsReadSelectedMultipleEmailSuccess(success.readActions); + } else if (success is MarkAsStarMultipleEmailAllSuccess) { + _markAsStarMultipleEmailSuccess( + success.markStarAction, + success.countMarkStarSuccess, + ); + } else if (success is MarkAsStarMultipleEmailHasSomeEmailFailure) { + _markAsStarMultipleEmailSuccess( + success.markStarAction, + success.countMarkStarSuccess, + ); } else if (success is MoveMultipleEmailToMailboxAllSuccess || success is MoveMultipleEmailToMailboxHasSomeEmailFailure) { _moveSelectedMultipleEmailToMailboxSuccess(success); @@ -867,14 +875,19 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo } } - void markAsEmailRead(PresentationEmail presentationEmail, ReadActions readActions, MarkReadAction markReadAction) async { + void markAsEmailRead( + EmailId emailId, + ReadActions readActions, + MarkReadAction markReadAction, + ) { if (accountId.value != null && sessionCurrent != null) { consumeState(_markAsEmailReadInteractor.execute( sessionCurrent!, accountId.value!, - presentationEmail.toEmail(), + emailId, readActions, - markReadAction)); + markReadAction, + )); } } @@ -883,35 +896,34 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo consumeState(_markAsStarEmailInteractor.execute( sessionCurrent!, accountId.value!, - presentationEmail.toEmail(), + presentationEmail.id!, action)); } } void markAsReadSelectedMultipleEmail(List listPresentationEmail, ReadActions readActions) { - final listEmail = listPresentationEmail - .map((presentationEmail) => presentationEmail.toEmail()) - .toList(); - log('MailboxDashBoardController::markAsReadSelectedMultipleEmail(): listEmail: ${listEmail.length}'); + final listEmailNeedMarkAsRead = listPresentationEmail + .where((email) { + if (readActions == ReadActions.markAsUnread) { + return email.hasRead; + } else { + return !email.hasRead; + } + }) + .toList(); + if (accountId.value != null && sessionCurrent != null) { consumeState(_markAsMultipleEmailReadInteractor.execute( sessionCurrent!, accountId.value!, - listEmail, - readActions)); + listEmailNeedMarkAsRead.listEmailIds, + readActions, + )); } } - void _markAsReadSelectedMultipleEmailSuccess(Success success) { - ReadActions? readActions; - - if (success is MarkAsMultipleEmailReadAllSuccess) { - readActions = success.readActions; - } else if (success is MarkAsMultipleEmailReadHasSomeEmailFailure) { - readActions = success.readActions; - } - - if (readActions != null && currentContext != null && currentOverlayContext != null) { + void _markAsReadSelectedMultipleEmailSuccess(ReadActions readActions) { + if (currentContext != null && currentOverlayContext != null) { final message = readActions == ReadActions.markAsUnread ? AppLocalizations.of(currentContext!).marked_message_toast(AppLocalizations.of(currentContext!).unread) : AppLocalizations.of(currentContext!).marked_message_toast(AppLocalizations.of(currentContext!).read); @@ -926,30 +938,24 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo } } - void _markAsReadEmailSuccess(Success success) { - ReadActions? readActions; - MarkReadAction? markReadAction; - PresentationEmail? presentationEmail; - - if (success is MarkAsEmailReadSuccess) { - readActions = success.readActions; - markReadAction = success.markReadAction; - presentationEmail = success.updatedEmail.toPresentationEmail(); - } - - if (readActions != null && currentContext != null && currentOverlayContext != null && markReadAction == MarkReadAction.swipeOnThread) { - final message = readActions == ReadActions.markAsUnread + void _markAsReadEmailSuccess(MarkAsEmailReadSuccess success) { + if (currentContext != null && + currentOverlayContext != null && + success.markReadAction == MarkReadAction.swipeOnThread) { + final message = success.readActions == ReadActions.markAsUnread ? AppLocalizations.of(currentContext!).markedSingleMessageToast(AppLocalizations.of(currentContext!).unread.toLowerCase()) : AppLocalizations.of(currentContext!).markedSingleMessageToast(AppLocalizations.of(currentContext!).read.toLowerCase()); - final undoAction = readActions == ReadActions.markAsUnread ? ReadActions.markAsRead : ReadActions.markAsUnread; + final undoAction = success.readActions == ReadActions.markAsUnread + ? ReadActions.markAsRead + : ReadActions.markAsUnread; appToast.showToastMessage( currentOverlayContext!, message, actionName: AppLocalizations.of(currentContext!).undo, onActionClick: () { - markAsEmailRead(presentationEmail!, undoAction, MarkReadAction.undo); + markAsEmailRead(success.emailId, undoAction, MarkReadAction.undo); }, leadingSVGIcon: imagePaths.icToastSuccessMessage, backgroundColor: AppColor.toastSuccessBackgroundColor, @@ -960,43 +966,42 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo } void markAsStarSelectedMultipleEmail(List listPresentationEmail, MarkStarAction markStarAction) { - final listEmail = listPresentationEmail - .map((presentationEmail) => presentationEmail.toEmail()) - .toList(); if (accountId.value != null && sessionCurrent != null) { + final listEmailIds = listPresentationEmail + .where((email) { + if (markStarAction == MarkStarAction.unMarkStar) { + return email.hasStarred; + } else { + return !email.hasStarred; + } + }) + .toList() + .listEmailIds; + consumeState(_markAsStarMultipleEmailInteractor.execute( sessionCurrent!, accountId.value!, - listEmail, + listEmailIds, markStarAction)); } } - void _markAsStarMultipleEmailSuccess(Success success) { - MarkStarAction? markStarAction; - int countMarkStarSuccess = 0; - - if (success is MarkAsStarMultipleEmailAllSuccess) { - markStarAction = success.markStarAction; - countMarkStarSuccess = success.countMarkStarSuccess; - } else if (success is MarkAsStarMultipleEmailHasSomeEmailFailure) { - markStarAction = success.markStarAction; - countMarkStarSuccess = success.countMarkStarSuccess; - } - - if (markStarAction != null) { + void _markAsStarMultipleEmailSuccess( + MarkStarAction markStarAction, + int countMarkStarSuccess, + ) { + if (currentOverlayContext != null && currentContext != null) { final message = markStarAction == MarkStarAction.unMarkStar ? AppLocalizations.of(currentContext!).marked_unstar_multiple_item(countMarkStarSuccess) : AppLocalizations.of(currentContext!).marked_star_multiple_item(countMarkStarSuccess); - if (currentOverlayContext != null && currentContext != null) { - appToast.showToastMessage( - currentOverlayContext!, - message, - leadingSVGIcon: markStarAction == MarkStarAction.unMarkStar - ? imagePaths.icUnStar - : imagePaths.icStar); - } + appToast.showToastMessage( + currentOverlayContext!, + message, + leadingSVGIcon: markStarAction == MarkStarAction.unMarkStar + ? imagePaths.icUnStar + : imagePaths.icStar, + ); } } diff --git a/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart b/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart index e99da0356c..68cae75621 100644 --- a/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart +++ b/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart @@ -17,7 +17,7 @@ class MarkAsMultipleEmailReadInteractor { Stream> execute( Session session, AccountId accountId, - List emails, + List emailIds, ReadActions readAction ) async* { try { @@ -31,25 +31,24 @@ class MarkAsMultipleEmailReadInteractor { final currentMailboxState = listState.first; final currentEmailState = listState.last; - final listEmailNeedMarkAsRead = emails - .where((email) => readAction == ReadActions.markAsUnread ? email.hasRead : !email.hasRead) - .toList(); + final result = await _emailRepository.markAsRead( + session, + accountId, + emailIds, + readAction, + ); - final result = await _emailRepository.markAsRead(session, accountId, listEmailNeedMarkAsRead, readAction); - - if (listEmailNeedMarkAsRead.length == result.length) { - final countMarkAsReadSuccess = emails.length; + if (emailIds.length == result.length) { yield Right(MarkAsMultipleEmailReadAllSuccess( - countMarkAsReadSuccess, + result.length, readAction, currentEmailState: currentEmailState, currentMailboxState: currentMailboxState)); } else if (result.isEmpty) { yield Left(MarkAsMultipleEmailReadAllFailure(readAction)); } else { - final countMarkAsReadSuccess = emails.length - (listEmailNeedMarkAsRead.length - result.length); yield Right(MarkAsMultipleEmailReadHasSomeEmailFailure( - countMarkAsReadSuccess, + result.length, readAction, currentEmailState: currentEmailState, currentMailboxState: currentMailboxState)); diff --git a/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart b/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart index 27062847b0..840056fc48 100644 --- a/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart +++ b/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart @@ -1,4 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; @@ -15,7 +16,7 @@ class MarkAsStarMultipleEmailInteractor { Stream> execute( Session session, AccountId accountId, - List emails, + List emailIds, MarkStarAction markStarAction ) async* { try { @@ -23,24 +24,18 @@ class MarkAsStarMultipleEmailInteractor { final currentEmailState = await _emailRepository.getEmailState(session, accountId); - final listEmailNeedMarkStar = emails - .where((email) => markStarAction == MarkStarAction.unMarkStar ? email.hasStarred : !email.hasStarred) - .toList(); + final result = await _emailRepository.markAsStar(session, accountId, emailIds, markStarAction); - final result = await _emailRepository.markAsStar(session, accountId, listEmailNeedMarkStar, markStarAction); - - if (listEmailNeedMarkStar.length == result.length) { - final countMarkStarSuccess = emails.length; + if (emailIds.length == result.length) { yield Right(MarkAsStarMultipleEmailAllSuccess( - countMarkStarSuccess, + emailIds.length, markStarAction, currentEmailState: currentEmailState)); } else if (result.isEmpty) { yield Left(MarkAsStarMultipleEmailAllFailure(markStarAction)); } else { - final countMarkStarSuccess = emails.length - (listEmailNeedMarkStar.length - result.length); yield Right(MarkAsStarMultipleEmailHasSomeEmailFailure( - countMarkStarSuccess, + result.length, markStarAction, currentEmailState: currentEmailState)); } diff --git a/lib/features/thread/presentation/mixin/email_action_controller.dart b/lib/features/thread/presentation/mixin/email_action_controller.dart index d6899667b6..baefee994b 100644 --- a/lib/features/thread/presentation/mixin/email_action_controller.dart +++ b/lib/features/thread/presentation/mixin/email_action_controller.dart @@ -224,8 +224,16 @@ mixin EmailActionController { mailboxDashBoardController.deleteEmailPermanently(email); } - void markAsEmailRead(PresentationEmail presentationEmail, ReadActions readActions, MarkReadAction markReadAction) async { - mailboxDashBoardController.markAsEmailRead(presentationEmail, readActions, markReadAction); + void markAsEmailRead( + PresentationEmail presentationEmail, + ReadActions readActions, + MarkReadAction markReadAction, + ) { + mailboxDashBoardController.markAsEmailRead( + presentationEmail.id!, + readActions, + markReadAction, + ); } void markAsStarEmail(PresentationEmail presentationEmail, MarkStarAction action) { diff --git a/model/lib/extensions/presentation_email_extension.dart b/model/lib/extensions/presentation_email_extension.dart index 0a0fa40266..7b1c5c46e9 100644 --- a/model/lib/extensions/presentation_email_extension.dart +++ b/model/lib/extensions/presentation_email_extension.dart @@ -217,11 +217,14 @@ extension PresentationEmailExtension on PresentationEmail { ..searchSnippetPreview = searchSnippetPreview; } - PresentationEmail updateKeywords(Map? newKeywords) { + PresentationEmail updateKeywords(Map newKeywords) { + final combinedMap = {...(keywords ?? {}), ...newKeywords}; + combinedMap.removeWhere((key, value) => !value); + log('PresentationEmailExtension::updateKeywords:combinedMap = $combinedMap'); return PresentationEmail( id: this.id, blobId: blobId, - keywords: newKeywords, + keywords: combinedMap, size: size, receivedAt: receivedAt, hasAttachment: hasAttachment, diff --git a/test/features/email/domain/usecases/store_event_attendance_status_interactor_test.dart b/test/features/email/domain/usecases/store_event_attendance_status_interactor_test.dart index e30b9a7f96..3deda8c8c6 100644 --- a/test/features/email/domain/usecases/store_event_attendance_status_interactor_test.dart +++ b/test/features/email/domain/usecases/store_event_attendance_status_interactor_test.dart @@ -60,7 +60,6 @@ void main() { Right(StoreEventAttendanceStatusLoading()), Right(StoreEventAttendanceStatusSuccess( eventActionType, - updatedEmail, currentEmailState: currentEmailState)), ]), ); diff --git a/test/features/thread/presentation/controller/thread_controller_test.dart b/test/features/thread/presentation/controller/thread_controller_test.dart index 094c73e95b..7f8ad860d8 100644 --- a/test/features/thread/presentation/controller/thread_controller_test.dart +++ b/test/features/thread/presentation/controller/thread_controller_test.dart @@ -4,22 +4,22 @@ import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/utils/application_manager.dart'; -import 'package:dartz/dartz.dart'; +import 'package:dartz/dartz.dart' hide State; import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:model/email/mark_star_action.dart'; import 'package:model/email/presentation_email.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:model/mailbox/select_mode.dart'; import 'package:tmail_ui_user/features/caching/caching_manager.dart'; -import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart'; +import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/login/data/network/interceptors/authorization_interceptors.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; @@ -266,10 +266,6 @@ void main() { 'AND `mailboxDashBoardController.emailsInCurrentMailbox` should not be cleared', () async { // Arrange - final updatedEmail = Email( - id: EmailId(Id('email1')), - keywords: {KeyWordIdentifier.emailFlagged: true} - ); final emailList = [ PresentationEmail( id: EmailId(Id('email1')), @@ -308,10 +304,8 @@ void main() { // Act threadController.onInit(); - final markAsStarEmailSuccess = MarkAsStarEmailSuccess( - updatedEmail, - MarkStarAction.markStar); - mockMailboxDashBoardController.viewState.value = Right(markAsStarEmailSuccess); + mockMailboxDashBoardController.emailUIAction.value = + RefreshChangeEmailAction(State('new-state')); await untilCalled(mockSearchEmailInteractor.execute( any, From de0c6a96335cee7d4231c969100203512a76ff3a Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 19 Dec 2024 01:08:19 +0700 Subject: [PATCH 17/72] TF-3334 Remove `Email/get` of unsubscribe email action --- .../data/datasource/email_datasource.dart | 2 +- .../email_datasource_impl.dart | 2 +- .../email_hive_cache_datasource_impl.dart | 2 +- .../email/data/network/email_api.dart | 27 ++++++++++--------- .../repository/email_repository_impl.dart | 2 +- .../domain/repository/email_repository.dart | 2 +- .../domain/state/unsubscribe_email_state.dart | 7 ----- .../unsubscribe_email_interactor.dart | 4 +-- .../mailbox_dashboard_controller.dart | 9 ++++--- 9 files changed, 27 insertions(+), 30 deletions(-) diff --git a/lib/features/email/data/datasource/email_datasource.dart b/lib/features/email/data/datasource/email_datasource.dart index 4248f413e6..bdfd90afe7 100644 --- a/lib/features/email/data/datasource/email_datasource.dart +++ b/lib/features/email/data/datasource/email_datasource.dart @@ -143,7 +143,7 @@ abstract class EmailDataSource { Future getStoredSendingEmail(AccountId accountId, UserName userName, String sendingId); - Future unsubscribeMail(Session session, AccountId accountId, EmailId emailId); + Future unsubscribeMail(Session session, AccountId accountId, EmailId emailId); Future restoreDeletedMessage(RestoredDeletedMessageRequest restoredDeletedMessageRequest); diff --git a/lib/features/email/data/datasource_impl/email_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_datasource_impl.dart index c1b150c679..2354d2c415 100644 --- a/lib/features/email/data/datasource_impl/email_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_datasource_impl.dart @@ -307,7 +307,7 @@ class EmailDataSourceImpl extends EmailDataSource { } @override - Future unsubscribeMail(Session session, AccountId accountId, EmailId emailId) { + Future unsubscribeMail(Session session, AccountId accountId, EmailId emailId) { return Future.sync(() async { return await emailAPI.unsubscribeMail(session, accountId, emailId); }).catchError(_exceptionThrower.throwException); diff --git a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart index accf354e53..85df8381d5 100644 --- a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart @@ -339,7 +339,7 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { } @override - Future unsubscribeMail(Session session, AccountId accountId, EmailId emailId) { + Future unsubscribeMail(Session session, AccountId accountId, EmailId emailId) { throw UnimplementedError(); } diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index f93ff0246b..4f764d31cb 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -678,17 +678,13 @@ class EmailAPI with HandleSetErrorMixin { } } - Future unsubscribeMail(Session session, AccountId accountId, EmailId emailId) async { + Future unsubscribeMail(Session session, AccountId accountId, EmailId emailId) async { final setEmailMethod = SetEmailMethod(accountId) ..addUpdates(emailId.generateMapUpdateObjectUnsubscribeMail()); - final getEmailMethod = GetEmailMethod(accountId) - ..addIds({emailId.id}) - ..addProperties(ThreadConstants.propertiesDefault); - final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); requestBuilder.invocation(setEmailMethod); - final getEmailInvocation = requestBuilder.invocation(getEmailMethod); + final setEmailInvocation = requestBuilder.invocation(setEmailMethod); final capabilities = setEmailMethod.requiredCapabilities.toCapabilitiesSupportTeamMailboxes(session, accountId); @@ -697,14 +693,19 @@ class EmailAPI with HandleSetErrorMixin { .build() .execute(); - final getEmailResponse = response.parse( - getEmailInvocation.methodCallId, - GetEmailResponse.deserialize); + final setEmailResponse = response.parse( + setEmailInvocation.methodCallId, + SetEmailResponse.deserialize, + ); - if (getEmailResponse?.list.isNotEmpty == true) { - return getEmailResponse!.list.first; - } else { - throw NotFoundEmailException(); + final emailIdUpdated = setEmailResponse?.updated + ?.keys + .map((id) => EmailId(id)) + .toList() ?? []; + final mapErrors = handleSetResponse([setEmailResponse]); + + if (emailIdUpdated.isEmpty) { + throw SetMethodException(mapErrors); } } diff --git a/lib/features/email/data/repository/email_repository_impl.dart b/lib/features/email/data/repository/email_repository_impl.dart index cbc7f52015..c48b4c3dfe 100644 --- a/lib/features/email/data/repository/email_repository_impl.dart +++ b/lib/features/email/data/repository/email_repository_impl.dart @@ -293,7 +293,7 @@ class EmailRepositoryImpl extends EmailRepository { } @override - Future unsubscribeMail(Session session, AccountId accountId, EmailId emailId) { + Future unsubscribeMail(Session session, AccountId accountId, EmailId emailId) { return emailDataSource[DataSourceType.network]!.unsubscribeMail(session, accountId, emailId); } diff --git a/lib/features/email/domain/repository/email_repository.dart b/lib/features/email/domain/repository/email_repository.dart index 5e62b863e4..7aaf27b9d7 100644 --- a/lib/features/email/domain/repository/email_repository.dart +++ b/lib/features/email/domain/repository/email_repository.dart @@ -144,7 +144,7 @@ abstract class EmailRepository { TransformConfiguration configuration ); - Future unsubscribeMail(Session session, AccountId accountId, EmailId emailId); + Future unsubscribeMail(Session session, AccountId accountId, EmailId emailId); Future restoreDeletedMessage(RestoredDeletedMessageRequest restoredDeletedMessageRequest); diff --git a/lib/features/email/domain/state/unsubscribe_email_state.dart b/lib/features/email/domain/state/unsubscribe_email_state.dart index ee4c94956d..8ec09d478f 100644 --- a/lib/features/email/domain/state/unsubscribe_email_state.dart +++ b/lib/features/email/domain/state/unsubscribe_email_state.dart @@ -1,24 +1,17 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; -import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; class UnsubscribeEmailLoading extends LoadingState {} class UnsubscribeEmailSuccess extends UIActionState { - final Email newEmail; - UnsubscribeEmailSuccess( - this.newEmail, { jmap.State? currentEmailState, jmap.State? currentMailboxState, } ) : super(currentEmailState, currentMailboxState); - - @override - List get props => [newEmail, ...super.props]; } class UnsubscribeEmailFailure extends FeatureFailure { diff --git a/lib/features/email/domain/usecases/unsubscribe_email_interactor.dart b/lib/features/email/domain/usecases/unsubscribe_email_interactor.dart index cf74151d34..4e1720b989 100644 --- a/lib/features/email/domain/usecases/unsubscribe_email_interactor.dart +++ b/lib/features/email/domain/usecases/unsubscribe_email_interactor.dart @@ -16,8 +16,8 @@ class UnsubscribeEmailInteractor { try { yield Right(UnsubscribeEmailLoading()); final currentEmailState = await emailRepository.getEmailState(session, accountId); - final newEmail = await emailRepository.unsubscribeMail(session, accountId, emailId); - yield Right(UnsubscribeEmailSuccess(newEmail, currentEmailState: currentEmailState)); + await emailRepository.unsubscribeMail(session, accountId, emailId); + yield Right(UnsubscribeEmailSuccess(currentEmailState: currentEmailState)); } catch (e) { yield Left(UnsubscribeEmailFailure(exception: e)); } diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index d477d0a09d..7a81611656 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -401,7 +401,7 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo } else if (success is DeleteSendingEmailSuccess) { getAllSendingEmails(); } else if (success is UnsubscribeEmailSuccess) { - _handleUnsubscribeMailSuccess(success.newEmail); + _handleUnsubscribeMailSuccess(); } else if (success is RestoreDeletedMessageSuccess) { dispatchMailboxUIAction(RefreshChangeMailboxAction(success.currentMailboxState)); _handleRestoreDeletedMessageSuccess(success.emailRecoveryAction.id!); @@ -2599,13 +2599,16 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo } } - void _handleUnsubscribeMailSuccess(Email email) { + void _handleUnsubscribeMailSuccess() { if (currentContext != null && currentOverlayContext != null) { appToast.showToastSuccessMessage( currentOverlayContext!, AppLocalizations.of(currentContext!).unsubscribedFromThisMailingList); } - setSelectedEmail(email.toPresentationEmail()); + final newEmail = selectedEmail.value?.updateKeywords({ + KeyWordIdentifierExtension.unsubscribeMail: true, + }); + setSelectedEmail(newEmail); } void _replaceBrowserHistory({Uri? uri}) { From 73fe16a70197e57e11e6179be0de7ef1656471f2 Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 23 Dec 2024 12:24:50 +0700 Subject: [PATCH 18/72] TF-3334 Use queue to handle multiple refresh changes mailbox from incoming websocket --- core/lib/core.dart | 1 + .../either_view_state_extension.dart | 12 ++ lib/features/base/base_controller.dart | 28 +-- .../base/base_mailbox_controller.dart | 3 +- .../destination_picker_controller.dart | 4 +- .../controller/single_email_controller.dart | 55 +++-- .../presentation/mailbox_controller.dart | 100 ++++++--- .../mailbox_dashboard_controller.dart | 8 + .../mailbox_visibility_controller.dart | 4 +- .../controller/web_socket_controller.dart | 43 +++- .../websocket/web_socket_message.dart | 13 ++ .../websocket/web_socket_queue_handler.dart | 121 +++++++++++ .../rules_filter_creator_controller.dart | 2 +- .../presentation/search_email_controller.dart | 84 ++++++-- .../search_mailbox_controller.dart | 4 +- .../domain/constants/thread_constants.dart | 1 + .../presentation/thread_controller.dart | 158 ++++++++++---- .../web_socket_queue_handler_test.dart | 197 ++++++++++++++++++ .../controller/thread_controller_test.dart | 1 + 19 files changed, 696 insertions(+), 143 deletions(-) create mode 100644 core/lib/presentation/extensions/either_view_state_extension.dart create mode 100644 lib/features/push_notification/presentation/websocket/web_socket_message.dart create mode 100644 lib/features/push_notification/presentation/websocket/web_socket_queue_handler.dart create mode 100644 test/features/push_notification/presentation/websocket/web_socket_queue_handler_test.dart diff --git a/core/lib/core.dart b/core/lib/core.dart index bfe0664e63..1d92e3eef8 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -16,6 +16,7 @@ export 'presentation/extensions/string_extension.dart'; export 'presentation/extensions/tap_down_details_extension.dart'; export 'domain/extensions/media_type_extension.dart'; export 'presentation/extensions/map_extensions.dart'; +export 'presentation/extensions/either_view_state_extension.dart'; // Exceptions export 'domain/exceptions/download_file_exception.dart'; diff --git a/core/lib/presentation/extensions/either_view_state_extension.dart b/core/lib/presentation/extensions/either_view_state_extension.dart new file mode 100644 index 0000000000..76faf2f419 --- /dev/null +++ b/core/lib/presentation/extensions/either_view_state_extension.dart @@ -0,0 +1,12 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; + +extension EitherViewStateExtension on Either { + dynamic foldSuccessWithResult() { + return fold( + (failure) => failure, + (success) => success is T ? success as T : null, + ); + } +} \ No newline at end of file diff --git a/lib/features/base/base_controller.dart b/lib/features/base/base_controller.dart index 1c8f904e8a..9c8ca16689 100644 --- a/lib/features/base/base_controller.dart +++ b/lib/features/base/base_controller.dart @@ -143,20 +143,7 @@ abstract class BaseController extends GetxController void onData(Either newState) { viewState.value = newState; - viewState.value.fold( - (failure) { - if (failure is FeatureFailure) { - final isUrgentException = validateUrgentException(failure.exception); - if (isUrgentException) { - handleUrgentException(failure: failure, exception: failure.exception); - } else { - handleFailureViewState(failure); - } - } else { - handleFailureViewState(failure); - } - }, - handleSuccessViewState); + viewState.value.fold(onDataFailureViewState, handleSuccessViewState); } void onError(dynamic error, StackTrace stackTrace) { @@ -272,6 +259,19 @@ abstract class BaseController extends GetxController } } + void onDataFailureViewState(Failure failure) { + if (failure is FeatureFailure) { + final isUrgentException = validateUrgentException(failure.exception); + if (isUrgentException) { + handleUrgentException(failure: failure, exception: failure.exception); + } else { + handleFailureViewState(failure); + } + } else { + handleFailureViewState(failure); + } + } + void handleFailureViewState(Failure failure) async { logError('$runtimeType::handleFailureViewState():Failure = $failure'); if (failure is LogoutOidcFailure) { diff --git a/lib/features/base/base_mailbox_controller.dart b/lib/features/base/base_mailbox_controller.dart index 854eca61a7..aa62a5145a 100644 --- a/lib/features/base/base_mailbox_controller.dart +++ b/lib/features/base/base_mailbox_controller.dart @@ -104,8 +104,7 @@ abstract class BaseMailboxController extends BaseController { teamMailboxesTree.value = tupleTree.value3; } - Future syncAllMailboxWithDisplayName(BuildContext context) async { - log("BaseMailboxController::syncAllMailboxWithDisplayName"); + void syncAllMailboxWithDisplayName(BuildContext context) { final syncedMailbox = allMailboxes .map((mailbox) => mailbox.withDisplayName(mailbox.getDisplayName(context))) .toList(); diff --git a/lib/features/destination_picker/presentation/destination_picker_controller.dart b/lib/features/destination_picker/presentation/destination_picker_controller.dart index eb77e50a25..28d6ede527 100644 --- a/lib/features/destination_picker/presentation/destination_picker_controller.dart +++ b/lib/features/destination_picker/presentation/destination_picker_controller.dart @@ -112,12 +112,12 @@ class DestinationPickerController extends BaseMailboxController { await buildTree(success.mailboxList.listSubscribedMailboxesAndDefaultMailboxes); } if (currentContext != null) { - await syncAllMailboxWithDisplayName(currentContext!); + syncAllMailboxWithDisplayName(currentContext!); } } else if (success is RefreshChangesAllMailboxSuccess) { await refreshTree(success.mailboxList.listSubscribedMailboxesAndDefaultMailboxes); if (currentContext != null) { - await syncAllMailboxWithDisplayName(currentContext!); + syncAllMailboxWithDisplayName(currentContext!); } } else if (success is SearchMailboxSuccess) { _searchMailboxSuccess(success); diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 8988f2df7c..6918ee5256 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -1711,57 +1711,54 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void _rejectCalendarEventAction(EmailId emailId) { if (_rejectCalendarEventInteractor == null || _displayingEventBlobId == null - || mailboxDashBoardController.accountId.value == null - || mailboxDashBoardController.sessionCurrent == null - || mailboxDashBoardController.sessionCurrent - !.validateCalendarEventCapability(mailboxDashBoardController.accountId.value!) - .isAvailable == false + || accountId == null + || session == null + || session!.validateCalendarEventCapability(accountId!).isAvailable == false ) { consumeState(Stream.value(Left(CalendarEventRejectFailure()))); } else { consumeState(_rejectCalendarEventInteractor!.execute( - mailboxDashBoardController.accountId.value!, + accountId!, {_displayingEventBlobId!}, emailId, - mailboxDashBoardController.sessionCurrent!.getLanguageForCalendarEvent( + session!.getLanguageForCalendarEvent( LocalizationService.getLocaleFromLanguage(), - mailboxDashBoardController.accountId.value!))); + accountId!, + ), + )); } } void _maybeCalendarEventAction(EmailId emailId) { if (_maybeCalendarEventInteractor == null || _displayingEventBlobId == null - || mailboxDashBoardController.accountId.value == null - || mailboxDashBoardController.sessionCurrent == null - || mailboxDashBoardController.sessionCurrent - !.validateCalendarEventCapability(mailboxDashBoardController.accountId.value!) - .isAvailable == false + || accountId == null + || session == null + || session!.validateCalendarEventCapability(accountId!).isAvailable == false ) { consumeState(Stream.value(Left(CalendarEventMaybeFailure()))); } else { consumeState(_maybeCalendarEventInteractor!.execute( - mailboxDashBoardController.accountId.value!, + accountId!, {_displayingEventBlobId!}, emailId, - mailboxDashBoardController.sessionCurrent!.getLanguageForCalendarEvent( + session!.getLanguageForCalendarEvent( LocalizationService.getLocaleFromLanguage(), - mailboxDashBoardController.accountId.value!))); + accountId!, + ), + )); } } void calendarEventSuccess(CalendarEventReplySuccess success) { - final session = mailboxDashBoardController.sessionCurrent; - final accountId = mailboxDashBoardController.accountId.value; - if (session == null || accountId == null) { consumeState(Stream.value(Left(StoreEventAttendanceStatusFailure(exception: NotFoundSessionException())))); return; } consumeState(_storeEventAttendanceStatusInteractor.execute( - session, - accountId, + session!, + accountId!, success.emailId, success.getEventActionType() )); @@ -1839,16 +1836,15 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } Future previewPDFFileAction(BuildContext context, Attachment attachment) async { - final accountId = mailboxDashBoardController.accountId.value; - final downloadUrl = mailboxDashBoardController.sessionCurrent - ?.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl); - - if (accountId == null || downloadUrl == null) { + if (accountId == null || session == null) { appToast.showToastErrorMessage( context, AppLocalizations.of(context).noPreviewAvailable); return; } + final downloadUrl = session!.getDownloadUrl( + jmapUrl: dynamicUrlInterceptors.jmapUrl, + ); await Get.generalDialog( barrierColor: Colors.black.withOpacity(0.8), @@ -1856,7 +1852,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { return PointerInterceptor( child: PDFViewer( attachment: attachment, - accountId: accountId, + accountId: accountId!, downloadUrl: downloadUrl, downloadAction: _downloadPDFFile, printAction: _printPDFFile, @@ -1876,7 +1872,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } final listEmailAddressMailTo = listEmailAddressAttendees - .where((emailAddress) => emailAddress.emailAddress.isNotEmpty && emailAddress.emailAddress != mailboxDashBoardController.sessionCurrent?.username.value) + .where((emailAddress) { + return emailAddress.emailAddress.isNotEmpty && + emailAddress.emailAddress != session?.username.value; + }) .toSet() .toList(); diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index d6ac440311..f117eae647 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -64,6 +64,8 @@ import 'package:tmail_ui_user/features/mailbox_creator/presentation/model/new_ma import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.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/push_notification/presentation/websocket/web_socket_message.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/websocket/web_socket_queue_handler.dart'; import 'package:tmail_ui_user/features/search/mailbox/presentation/search_mailbox_bindings.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -94,6 +96,7 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM MailboxId? _newFolderId; NavigationRouter? _navigationRouter; + WebSocketQueueHandler? _webSocketQueueHandler; final _openMailboxEventController = StreamController(); final mailboxListScrollController = ScrollController(); @@ -128,6 +131,7 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM @override void onInit() { _registerObxStreamListener(); + _initWebSocketQueueHandler(); super.onInit(); } @@ -145,6 +149,7 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM void onClose() { _openMailboxEventController.close(); mailboxListScrollController.dispose(); + _webSocketQueueHandler?.dispose(); super.onClose(); } @@ -153,8 +158,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM super.handleSuccessViewState(success); if (success is GetAllMailboxSuccess) { _handleGetAllMailboxSuccess(success); - } else if (success is RefreshChangesAllMailboxSuccess) { - _handleRefreshChangesAllMailboxSuccess(success); } else if (success is CreateNewMailboxSuccess) { _createNewMailboxSuccess(success); } else if (success is DeleteMultipleMailboxAllSuccess) { @@ -181,8 +184,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM _renameMailboxFailure(failure); } else if (failure is DeleteMultipleMailboxFailure) { _deleteMailboxFailure(failure); - } else if (failure is RefreshChangesAllMailboxFailure) { - _clearNewFolderId(); } } @@ -204,13 +205,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM if (PlatformInfo.isIOS) { _updateMailboxIdsBlockNotificationToKeychain(success.mailboxList); } - } else if (success is RefreshChangesAllMailboxSuccess) { - _selectSelectedMailboxDefault(); - mailboxDashBoardController.refreshSpamReportBanner(); - - if (_newFolderId != null) { - _redirectToNewFolder(); - } } }); } @@ -258,6 +252,13 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM }); } + void _initWebSocketQueueHandler() { + _webSocketQueueHandler = WebSocketQueueHandler( + processMessageCallback: _handleWebSocketMessage, + onErrorCallback: onError, + ); + } + void _initCollapseMailboxCategories() { if (kIsWeb && currentContext != null && (responsiveUtils.isMobile(currentContext!) || responsiveUtils.isTablet(currentContext!))) { @@ -283,17 +284,66 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM if (accountId == null || session == null || currentMailboxState == null || - newState == currentMailboxState) { + newState == null) { _newFolderId = null; return; } - refreshMailboxChanges( - session!, - accountId!, - currentMailboxState!, - properties: MailboxConstants.propertiesDefault, - ); + _webSocketQueueHandler?.enqueue(WebSocketMessage(newState: newState)); + } + + Future _handleWebSocketMessage(WebSocketMessage message) async { + try { + if (currentMailboxState == message.newState) { + log('MailboxController::_handleWebSocketMessage:Skipping redundant state: ${message.newState}'); + return Future.value(); + } + + final refreshViewState = await refreshAllMailboxInteractor!.execute( + session!, + accountId!, + currentMailboxState!, + properties: MailboxConstants.propertiesDefault, + ).last; + + final refreshState = refreshViewState + .foldSuccessWithResult(); + + if (refreshState is RefreshChangesAllMailboxSuccess) { + await _handleRefreshChangeMailboxSuccess(refreshState); + } else { + _clearNewFolderId(); + onDataFailureViewState(refreshState); + } + } catch (e, stackTrace) { + logError('MailboxController::_processMailboxStateQueue:Error processing state: $e'); + onError(e, stackTrace); + } + if (currentMailboxState != null) { + _webSocketQueueHandler?.removeMessagesUpToCurrent(currentMailboxState!.value); + } + } + + Future _handleRefreshChangeMailboxSuccess(RefreshChangesAllMailboxSuccess success) async { + currentMailboxState = success.currentMailboxState; + log('MailboxController::_handleRefreshChangeMailboxSuccess:currentMailboxState: $currentMailboxState'); + final listMailboxDisplayed = success + .mailboxList + .listSubscribedMailboxesAndDefaultMailboxes; + + await refreshTree(listMailboxDisplayed); + + if (currentContext != null) { + syncAllMailboxWithDisplayName(currentContext!); + } + _setMapMailbox(); + _setOutboxMailbox(); + _selectSelectedMailboxDefault(); + mailboxDashBoardController.refreshSpamReportBanner(); + + if (_newFolderId != null) { + _redirectToNewFolder(); + } } void _setMapMailbox() { @@ -1078,7 +1128,7 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM final listMailboxDisplayed = success.mailboxList.listSubscribedMailboxesAndDefaultMailboxes; await buildTree(listMailboxDisplayed); if (currentContext != null) { - await syncAllMailboxWithDisplayName(currentContext!); + syncAllMailboxWithDisplayName(currentContext!); } _setMapMailbox(); _setOutboxMailbox(); @@ -1105,18 +1155,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM mailboxIds: mailboxIdsBlockNotification); } - void _handleRefreshChangesAllMailboxSuccess(RefreshChangesAllMailboxSuccess success) async { - currentMailboxState = success.currentMailboxState; - log('MailboxController::_handleRefreshChangesAllMailboxSuccess:currentMailboxState: $currentMailboxState'); - final listMailboxDisplayed = success.mailboxList.listSubscribedMailboxesAndDefaultMailboxes; - await refreshTree(listMailboxDisplayed); - if (currentContext != null) { - await syncAllMailboxWithDisplayName(currentContext!); - } - _setMapMailbox(); - _setOutboxMailbox(); - } - void _unsubscribeMailboxAction(MailboxId mailboxId) { if (session != null && accountId != null) { final subscribeRequest = generateSubscribeRequest( diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 7a81611656..d97e43b1ae 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -243,6 +243,7 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo PresentationMailbox? outboxMailbox; ComposerArguments? composerArguments; List? _identities; + jmap.State? _currentEmailState; ScrollController? listSearchFilterScrollController; StreamSubscription? _pendingSharedFileInfoSubscription; StreamSubscription? _receivingFileSharingStreamSubscription; @@ -2930,6 +2931,12 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo accountId: accountId.value!); } + void setCurrentEmailState(jmap.State? newState) { + _currentEmailState = newState; + } + + jmap.State? get currentEmailState => _currentEmailState; + @override void onClose() { if (PlatformInfo.isWeb) { @@ -2957,6 +2964,7 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo mapMailboxById = {}; mapDefaultMailboxIdByRole = {}; WebSocketController.instance.onClose(); + _currentEmailState = null; super.onClose(); } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_controller.dart b/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_controller.dart index ceb7b0669d..d405b92fa6 100644 --- a/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_controller.dart +++ b/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_controller.dart @@ -73,7 +73,7 @@ class MailboxVisibilityController extends BaseMailboxController { currentMailboxState = success.currentMailboxState; await refreshTree(success.mailboxList); if (currentContext != null) { - await syncAllMailboxWithDisplayName(currentContext!); + syncAllMailboxWithDisplayName(currentContext!); } } else if (success is SubscribeMailboxSuccess) { _subscribeMailboxSuccess(success); @@ -99,7 +99,7 @@ class MailboxVisibilityController extends BaseMailboxController { await buildTree(mailboxList); dispatchState(Right(BuildTreeMailboxVisibilitySuccess())); if (currentContext != null) { - await syncAllMailboxWithDisplayName(currentContext!); + syncAllMailboxWithDisplayName(currentContext!); } } diff --git a/lib/features/push_notification/presentation/controller/web_socket_controller.dart b/lib/features/push_notification/presentation/controller/web_socket_controller.dart index 821a873cb7..afdada5c6e 100644 --- a/lib/features/push_notification/presentation/controller/web_socket_controller.dart +++ b/lib/features/push_notification/presentation/controller/web_socket_controller.dart @@ -5,6 +5,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; +import 'package:debounce_throttle/debounce_throttle.dart'; import 'package:fcm/model/type_name.dart'; import 'package:flutter/material.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; @@ -18,6 +19,7 @@ import 'package:tmail_ui_user/features/push_notification/presentation/controller import 'package:tmail_ui_user/features/push_notification/presentation/extensions/state_change_extension.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/listener/email_change_listener.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/listener/mailbox_change_listener.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/utils/fcm_utils.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; @@ -36,6 +38,7 @@ class WebSocketController extends PushBaseController { Timer? _webSocketPingTimer; StreamSubscription? _webSocketSubscription; AppLifecycleListener? _appLifecycleListener; + Debouncer? _stateChangeDebouncer; @override void handleFailureViewState(Failure failure) { @@ -105,8 +108,9 @@ class WebSocketController extends PushBaseController { _webSocketSubscription?.cancel(); _webSocketChannel = null; _webSocketPingTimer?.cancel(); + _stateChangeDebouncer?.cancel(); } - + void _handleWebSocketConnectionSuccess(WebSocketConnectionSuccess success) { log('WebSocketController::_handleWebSocketConnectionSuccess(): $success'); _cleanUpWebSocketResources(); @@ -117,6 +121,7 @@ class WebSocketController extends PushBaseController { _pingWebSocket(); } _listenToWebSocket(); + _initStateChangeDeouncerTimer(); } void _handleWebSocketConnectionRetry() { @@ -150,14 +155,7 @@ class WebSocketController extends PushBaseController { try { final stateChange = StateChange.fromJson(data); - final mapTypeState = stateChange.getMapTypeState(accountId!); - mappingTypeStateToAction( - mapTypeState, - accountId!, - emailChangeListener: EmailChangeListener.instance, - mailboxChangeListener: MailboxChangeListener.instance, - session!.username, - session: session); + _stateChangeDebouncer?.value = stateChange; } catch (e) { logError('WebSocketController::_listenToWebSocket(): Data is not StateChange'); } @@ -173,4 +171,31 @@ class WebSocketController extends PushBaseController { }, ); } + + void _initStateChangeDeouncerTimer() { + _stateChangeDebouncer = Debouncer( + const Duration(milliseconds: FcmUtils.durationMessageComing), + initialValue: null, + ); + + _stateChangeDebouncer?.values.listen(_handleStateChange); + } + + void _handleStateChange(StateChange? stateChange) { + try { + if (stateChange == null || accountId == null || session == null) return; + + final mapTypeState = stateChange.getMapTypeState(accountId!); + mappingTypeStateToAction( + mapTypeState, + accountId!, + emailChangeListener: EmailChangeListener.instance, + mailboxChangeListener: MailboxChangeListener.instance, + session!.username, + session: session, + ); + } catch (e) { + logError('WebSocketController::_handleStateChange:Exception = $e'); + } + } } \ No newline at end of file diff --git a/lib/features/push_notification/presentation/websocket/web_socket_message.dart b/lib/features/push_notification/presentation/websocket/web_socket_message.dart new file mode 100644 index 0000000000..58532f855c --- /dev/null +++ b/lib/features/push_notification/presentation/websocket/web_socket_message.dart @@ -0,0 +1,13 @@ +import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; + +class WebSocketMessage with EquatableMixin { + final jmap.State newState; + + WebSocketMessage({required this.newState}); + + String get id => newState.value; + + @override + List get props => [newState]; +} \ No newline at end of file diff --git a/lib/features/push_notification/presentation/websocket/web_socket_queue_handler.dart b/lib/features/push_notification/presentation/websocket/web_socket_queue_handler.dart new file mode 100644 index 0000000000..155b0deb6b --- /dev/null +++ b/lib/features/push_notification/presentation/websocket/web_socket_queue_handler.dart @@ -0,0 +1,121 @@ + +import 'dart:async'; +import 'dart:collection'; + +import 'package:core/utils/app_logger.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/websocket/web_socket_message.dart'; + +typedef ProcessMessageCallback = Future Function(WebSocketMessage message); +typedef OnErrorCallback = void Function(dynamic error, StackTrace stackTrace); + +class WebSocketQueueHandler { + static const int _maxQueueSize = 128; + static const int _maxProcessedIdsSize = 128; + + final Queue _messageQueue = Queue(); + final Queue _processedMessageIds = Queue(); + + Completer? _processingLock; + + final _queueController = StreamController.broadcast(); + + final ProcessMessageCallback processMessageCallback; + final OnErrorCallback? onErrorCallback; + + WebSocketQueueHandler({ + required this.processMessageCallback, + this.onErrorCallback, + }) { + _queueController.stream.listen((_) { + _processQueue(); + }); + } + + void enqueue(WebSocketMessage message) { + if (isMessageProcessed(message.id)) { + log('WebSocketQueueHandler::enqueue:Message ${message.id} already processed, skipping'); + return; + } + + if (queueSize >= _maxQueueSize) { + log('WebSocketQueueHandler::enqueue:Queue full, removing oldest message'); + _messageQueue.removeFirst(); + } + + _messageQueue.add(message); + _queueController.add(message); + } + + Future _processQueue() async { + if (_processingLock != null) { + return; + } + + _processingLock = Completer(); + + try { + while (queueSize > 0) { + final message = _messageQueue.removeFirst(); + + try { + await processMessageCallback(message); + } catch (e, stackTrace) { + logError('WebSocketQueueHandler::_processQueue:Error processing message ${message.id}: $e'); + onErrorCallback?.call(e, stackTrace); + } finally { + _addToProcessedMessages(message.id); + } + } + } finally { + _processingLock?.complete(); + _processingLock = null; + + if (queueSize > 0) { + scheduleMicrotask(() => _queueController.add(_messageQueue.first)); + } + } + } + + void _addToProcessedMessages(String messageId) { + if (_processedMessageIds.length >= _maxProcessedIdsSize) { + _processedMessageIds.removeFirst(); + } + _processedMessageIds.add(messageId); + } + + void removeMessagesUpToCurrent(String messageId) { + final isCurrentStateExist = _messageQueue + .any((message) => message.id == messageId); + + if (!isCurrentStateExist) { + log('WebSocketQueueHandler::removeMessagesUpToCurrent:Current state $messageId not found in the queue.'); + return; + } + while (queueSize > 0) { + final removedMessage = _messageQueue.removeFirst(); + if (removedMessage.id == messageId) { + break; + } + } + log('WebSocketQueueHandler::removeMessagesUpToCurrent:Updated Queue: $queueSize'); + } + + @visibleForTesting + Future waitForEmpty() async { + while (_messageQueue.isNotEmpty || _processingLock != null) { + if (_processingLock != null) { + await _processingLock!.future; + } + await Future.delayed(const Duration(milliseconds: 100)); + } + } + + int get queueSize => _messageQueue.length; + + bool isMessageProcessed(String messageId) => _processedMessageIds.contains(messageId); + + void dispose() { + _queueController.close(); + } +} diff --git a/lib/features/rules_filter_creator/presentation/rules_filter_creator_controller.dart b/lib/features/rules_filter_creator/presentation/rules_filter_creator_controller.dart index e9f535deaf..bea0723471 100644 --- a/lib/features/rules_filter_creator/presentation/rules_filter_creator_controller.dart +++ b/lib/features/rules_filter_creator/presentation/rules_filter_creator_controller.dart @@ -127,7 +127,7 @@ class RulesFilterCreatorController extends BaseMailboxController { if (success is GetAllMailboxSuccess) { await buildTree(success.mailboxList); if (currentContext != null) { - await syncAllMailboxWithDisplayName(currentContext!); + syncAllMailboxWithDisplayName(currentContext!); } } else if (success is GetAllRulesSuccess) { log('RulesFilterCreatorController::handleSuccessViewState():GetAllRulesSuccess: ${success.rules}'); diff --git a/lib/features/search/email/presentation/search_email_controller.dart b/lib/features/search/email/presentation/search_email_controller.dart index f96f957eaa..37c9081eb9 100644 --- a/lib/features/search/email/presentation/search_email_controller.dart +++ b/lib/features/search/email/presentation/search_email_controller.dart @@ -1,4 +1,5 @@ +import 'package:core/presentation/extensions/either_view_state_extension.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/keyboard_utils.dart'; @@ -10,6 +11,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/core/utc_date.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; @@ -50,6 +52,8 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/sear import 'package:tmail_ui_user/features/manage_account/presentation/extensions/datetime_extension.dart'; import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart' if (dart.library.html) 'package:tmail_ui_user/features/network_connection/presentation/web_network_connection_controller.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/websocket/web_socket_message.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/websocket/web_socket_queue_handler.dart'; import 'package:tmail_ui_user/features/search/email/domain/state/refresh_changes_search_email_state.dart'; import 'package:tmail_ui_user/features/search/email/domain/usecases/refresh_changes_search_email_interactor.dart'; import 'package:tmail_ui_user/features/search/email/presentation/model/search_more_state.dart'; @@ -105,6 +109,7 @@ class SearchEmailController extends BaseController late Worker dashBoardActionWorker; late SearchMoreState searchMoreState; late bool canSearchMore; + WebSocketQueueHandler? _webSocketQueueHandler; PresentationMailbox? get currentMailbox => mailboxDashBoardController.selectedMailbox.value; @@ -145,6 +150,7 @@ class SearchEmailController extends BaseController _initializeDebounceTimeTextSearchChange(); _initializeTextInputFocus(); _initWorkerListener(); + _initWebSocketQueueHandler(); } @override @@ -165,8 +171,6 @@ class SearchEmailController extends BaseController searchMoreState = SearchMoreState.waiting; } else if (success is SearchMoreEmailSuccess) { _searchMoreEmailsSuccess(success); - } else if (success is RefreshChangesSearchEmailSuccess) { - _refreshChangesSearchEmailsSuccess(success); } else if (success is SearchingState) { resultSearchViewState.value = Right(success); } @@ -252,28 +256,55 @@ class SearchEmailController extends BaseController mailboxDashBoardController.emailUIAction, (action) { if (action is RefreshChangeEmailAction) { - _refreshEmailChanges(); + _refreshEmailChanges(newState: action.newState); } }, ); } - void _onSearchTextInputListener() { - if (textInputSearchFocus.hasFocus) { - searchIsRunning.value = false; + void _refreshEmailChanges({jmap.State? newState}) { + log('SearchEmailController::_refreshEmailChanges(): newState: $newState'); + if (accountId == null || + session == null || + mailboxDashBoardController.currentEmailState == null || + newState == null || + searchIsRunning.isFalse) { + return; } + + _webSocketQueueHandler?.enqueue(WebSocketMessage(newState: newState)); } - void _refreshEmailChanges() { - if (searchIsRunning.isTrue && session != null && accountId != null) { + void _initWebSocketQueueHandler() { + _webSocketQueueHandler = WebSocketQueueHandler( + processMessageCallback: _handleWebSocketMessage, + onErrorCallback: onError, + ); + } + + Future _handleWebSocketMessage(WebSocketMessage message) async { + try { + if (mailboxDashBoardController.currentEmailState == null || + mailboxDashBoardController.currentEmailState == message.newState) { + log('SearchEmailController::_handleWebSocketMessage:Skipping redundant state: ${message.newState}'); + return Future.value(); + } + final limit = listResultSearch.isNotEmpty - ? UnsignedInt(listResultSearch.length) - : ThreadConstants.defaultLimit; + ? UnsignedInt(listResultSearch.length) + : ThreadConstants.defaultLimit; + + if (limit.value > ThreadConstants.maximumEmailQueryLimit && + resultSearchScrollController.hasClients) { + resultSearchScrollController.jumpTo(0); + } + _updateSimpleSearchFilter( beforeOption: const None(), - positionOption: option(searchEmailFilter.value.sortOrderType.isScrollByPosition(), 0) + positionOption: option(searchEmailFilter.value.sortOrderType.isScrollByPosition(), 0), ); - consumeState(_refreshChangesSearchEmailInteractor.execute( + + final searchViewState = await _refreshChangesSearchEmailInteractor.execute( session!, accountId!, limit: limit, @@ -281,11 +312,32 @@ class SearchEmailController extends BaseController sort: searchEmailFilter.value.sortOrderType.getSortOrder().toNullable(), filter: searchEmailFilter.value.mappingToEmailFilterCondition(), properties: EmailUtils.getPropertiesForEmailGetMethod(session!, accountId!), - )); + ).last; + + final searchState = searchViewState + .foldSuccessWithResult(); + + if (searchState is RefreshChangesSearchEmailSuccess) { + _handleRefreshChangesSearchEmailsSuccess(searchState); + } + } catch (e, stackTrace) { + logError('SearchEmailController::_handleWebSocketMessage:Error processing state: $e'); + onError(e, stackTrace); + } finally { + if (mailboxDashBoardController.currentEmailState != null) { + _webSocketQueueHandler?.removeMessagesUpToCurrent( + mailboxDashBoardController.currentEmailState!.value); + } + } + } + + void _onSearchTextInputListener() { + if (textInputSearchFocus.hasFocus) { + searchIsRunning.value = false; } } - void _refreshChangesSearchEmailsSuccess(RefreshChangesSearchEmailSuccess success) { + void _handleRefreshChangesSearchEmailsSuccess(RefreshChangesSearchEmailSuccess success) { final resultEmailSearchList = success.emailList .map((email) => email.toSearchPresentationEmail(mailboxDashBoardController.mapMailboxById)) .toList(); @@ -306,8 +358,8 @@ class SearchEmailController extends BaseController return _getAllRecentSearchLatestInteractor .execute(pattern: pattern) .then((result) => result.fold( - (failure) => [], - (success) => success is GetAllRecentSearchLatestSuccess + (failure) => [], + (success) => success is GetAllRecentSearchLatestSuccess ? success.listRecentSearch : [])); } diff --git a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart index 3447b9ff56..2ae4bc4455 100644 --- a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart +++ b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart @@ -136,13 +136,13 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa currentMailboxState = success.currentMailboxState; await buildTree(success.mailboxList); if (currentContext != null) { - await syncAllMailboxWithDisplayName(currentContext!); + syncAllMailboxWithDisplayName(currentContext!); } } else if (success is RefreshChangesAllMailboxSuccess) { currentMailboxState = success.currentMailboxState; await refreshTree(success.mailboxList); if (currentContext != null) { - await syncAllMailboxWithDisplayName(currentContext!); + syncAllMailboxWithDisplayName(currentContext!); } searchMailboxAction(); } else if (success is SearchMailboxSuccess) { diff --git a/lib/features/thread/domain/constants/thread_constants.dart b/lib/features/thread/domain/constants/thread_constants.dart index 2e5791697e..9a5618b929 100644 --- a/lib/features/thread/domain/constants/thread_constants.dart +++ b/lib/features/thread/domain/constants/thread_constants.dart @@ -5,6 +5,7 @@ import 'package:model/email/email_property.dart'; class ThreadConstants { static const maxCountEmails = 20; + static const maximumEmailQueryLimit = 256; static final defaultLimit = UnsignedInt(maxCountEmails); static final propertiesDefault = Properties({ EmailProperty.id, diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 8aac45a658..c23994fcd3 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:core/presentation/extensions/either_view_state_extension.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; @@ -32,6 +33,8 @@ import 'package:tmail_ui_user/features/manage_account/domain/state/create_new_ru import 'package:tmail_ui_user/features/manage_account/domain/usecases/create_new_email_rule_filter_interactor.dart'; import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart' if (dart.library.html) 'package:tmail_ui_user/features/network_connection/presentation/web_network_connection_controller.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/websocket/web_socket_message.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/websocket/web_socket_queue_handler.dart'; import 'package:tmail_ui_user/features/rules_filter_creator/presentation/model/rules_filter_creator_arguments.dart'; import 'package:tmail_ui_user/features/search/email/presentation/search_email_bindings.dart'; import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; @@ -90,12 +93,12 @@ class ThreadController extends BaseController with EmailActionController { bool canLoadMore = false; bool canSearchMore = false; MailboxId? _currentMemoryMailboxId; - jmap.State? _currentEmailState; final ScrollController listEmailController = ScrollController(); final FocusNode focusNodeKeyBoard = FocusNode(); final latestEmailSelectedOrUnselected = Rxn(); @visibleForTesting bool isListEmailScrollViewJumping = false; + WebSocketQueueHandler? _webSocketQueueHandler; StreamSubscription? _resizeBrowserStreamSubscription; @@ -128,6 +131,7 @@ class ThreadController extends BaseController with EmailActionController { if (PlatformInfo.isWeb) { _registerBrowserResizeListener(); } + _initWebSocketQueueHandler(); super.onInit(); } @@ -140,7 +144,6 @@ class ThreadController extends BaseController with EmailActionController { @override void onClose() { _currentMemoryMailboxId = null; - _currentEmailState = null; listEmailController.dispose(); focusNodeKeyBoard.dispose(); if (PlatformInfo.isWeb) { @@ -154,8 +157,6 @@ class ThreadController extends BaseController with EmailActionController { super.handleSuccessViewState(success); if (success is GetAllEmailSuccess) { _getAllEmailSuccess(success); - } else if (success is RefreshChangesAllEmailSuccess) { - _refreshChangesAllEmailSuccess(success); } else if (success is LoadMoreEmailsSuccess) { _loadMoreEmailsSuccess(success); } else if (success is SearchEmailSuccess) { @@ -231,6 +232,13 @@ class ThreadController extends BaseController with EmailActionController { }); } + void _initWebSocketQueueHandler() { + _webSocketQueueHandler = WebSocketQueueHandler( + processMessageCallback: _handleWebSocketMessage, + onErrorCallback: onError, + ); + } + void _resetLoadingMore() { if (loadingMoreStatus.value == LoadingMoreStatus.running) { loadingMoreStatus.value = LoadingMoreStatus.idle; @@ -383,8 +391,8 @@ class ThreadController extends BaseController with EmailActionController { log('ThreadController::_getAllEmailSuccess: GetAllForMailboxId = ${success.currentMailboxId?.asString} | SELECTED_MAILBOX_ID = ${selectedMailboxId?.asString} | SELECTED_MAILBOX_NAME = ${selectedMailbox?.name?.name}'); return; } - _currentEmailState = success.currentEmailState; - log('ThreadController::_getAllEmailSuccess():COUNT = ${success.emailList.length} | EMAIL_STATE = $_currentEmailState'); + mailboxDashBoardController.setCurrentEmailState(success.currentEmailState); + log('ThreadController::_getAllEmailSuccess():COUNT = ${success.emailList.length} | EMAIL_STATE = ${mailboxDashBoardController.currentEmailState}'); final newListEmail = success.emailList.syncPresentationEmail( mapMailboxById: mailboxDashBoardController.mapMailboxById, selectedMailbox: selectedMailbox, @@ -413,8 +421,7 @@ class ThreadController extends BaseController with EmailActionController { log('ThreadController::_refreshChangesAllEmailSuccess: RefreshedMailboxId = ${success.currentMailboxId?.asString} | SELECTED_MAILBOX_ID = ${selectedMailboxId?.asString} | SELECTED_MAILBOX_NAME = ${selectedMailbox?.name?.name}'); return; } - - _currentEmailState = success.currentEmailState; + mailboxDashBoardController.setCurrentEmailState(success.currentEmailState); log('ThreadController::_refreshChangesAllEmailSuccess: COUNT = ${success.emailList.length}'); final emailsBeforeChanges = mailboxDashBoardController.emailsInCurrentMailbox; final emailsAfterChanges = success.emailList; @@ -515,31 +522,112 @@ class ThreadController extends BaseController with EmailActionController { void _refreshEmailChanges({jmap.State? newState}) { log('ThreadController::_refreshEmailChanges(): newState: $newState'); - if (searchController.isSearchEmailRunning) { - _searchEmail(limit: limitEmailFetched, needRefreshSearchState: true); - } else { - if (_currentEmailState == null || - _currentEmailState == newState || - _session == null || - _accountId == null) { - return; + if (_accountId == null || + _session == null || + mailboxDashBoardController.currentEmailState == null || + newState == null) { + return; + } + + _webSocketQueueHandler?.enqueue(WebSocketMessage(newState: newState)); + } + + Future _handleWebSocketMessage(WebSocketMessage message) async { + try { + if (mailboxDashBoardController.currentEmailState == null || + mailboxDashBoardController.currentEmailState == message.newState) { + log('ThreadController::_handleWebSocketMessage:Skipping redundant state: ${message.newState}'); + return Future.value(); } - consumeState(_refreshChangesEmailsInMailboxInteractor.execute( + + if (searchController.isSearchEmailRunning) { + await _refreshChangeSearchEmail(); + } else { + await _refreshChangeListEmail(); + } + } catch (e, stackTrace) { + logError('ThreadController::_handleWebSocketMessage:Error processing state: $e'); + onError(e, stackTrace); + } finally { + if (mailboxDashBoardController.currentEmailState != null) { + _webSocketQueueHandler?.removeMessagesUpToCurrent( + mailboxDashBoardController.currentEmailState!.value); + } + } + } + + Future _refreshChangeSearchEmail() async { + log('ThreadController::_refreshChangeSearchEmail:'); + if (limitEmailFetched.value > ThreadConstants.maximumEmailQueryLimit && + listEmailController.hasClients) { + listEmailController.jumpTo(0); + } + canSearchMore = false; + searchController.updateFilterEmail( + positionOption: option( + _searchEmailFilter.sortOrderType.isScrollByPosition(), + 0, + ), + beforeOption: const None(), + ); + searchController.activateSimpleSearch(); + + final searchViewState = await _searchEmailInteractor.execute( + _session!, + _accountId!, + limit: limitEmailFetched, + position: _searchEmailFilter.position, + sort: _searchEmailFilter.sortOrderType.getSortOrder().toNullable(), + filter: _searchEmailFilter.mappingToEmailFilterCondition( + moreFilterCondition: _getFilterCondition(), + ), + properties: EmailUtils.getPropertiesForEmailGetMethod( _session!, _accountId!, - _currentEmailState!, - sort: EmailSortOrderType.mostRecent.getSortOrder().toNullable(), - propertiesCreated: EmailUtils.getPropertiesForEmailGetMethod( - _session!, - _accountId!, - ), - propertiesUpdated: ThreadConstants.propertiesUpdatedDefault, - emailFilter: EmailFilter( - filter: _getFilterCondition(mailboxIdSelected: selectedMailboxId), - filterOption: mailboxDashBoardController.filterMessageOption.value, - mailboxId: selectedMailboxId, - ), - )); + ), + needRefreshSearchState: true, + ).last; + + final searchState = searchViewState + .foldSuccessWithResult(); + + if (searchState is SearchEmailSuccess) { + _searchEmailsSuccess(searchState); + } else { + mailboxDashBoardController.updateRefreshAllEmailState( + Left(RefreshAllEmailFailure())); + canSearchMore = false; + mailboxDashBoardController.emailsInCurrentMailbox.clear(); + onDataFailureViewState(searchState); + } + } + + Future _refreshChangeListEmail() async { + log('ThreadController::_refreshChangeListEmail:'); + final refreshViewState = await _refreshChangesEmailsInMailboxInteractor.execute( + _session!, + _accountId!, + mailboxDashBoardController.currentEmailState!, + sort: EmailSortOrderType.mostRecent.getSortOrder().toNullable(), + propertiesCreated: EmailUtils.getPropertiesForEmailGetMethod( + _session!, + _accountId!, + ), + propertiesUpdated: ThreadConstants.propertiesUpdatedDefault, + emailFilter: EmailFilter( + filter: _getFilterCondition(mailboxIdSelected: selectedMailboxId), + filterOption: mailboxDashBoardController.filterMessageOption.value, + mailboxId: selectedMailboxId, + ), + ).last; + + final refreshState = refreshViewState + .foldSuccessWithResult(); + + if (refreshState is RefreshChangesAllEmailSuccess) { + _refreshChangesAllEmailSuccess(refreshState); + } else { + onDataFailureViewState(refreshState); } } @@ -1168,12 +1256,10 @@ class ThreadController extends BaseController with EmailActionController { } void goToCreateEmailRuleView() async { - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; - if (accountId != null && session != null) { + if (_accountId != null && _session != null) { final arguments = RulesFilterCreatorArguments( - accountId, - session, + _accountId!, + _session!, mailboxDestination: selectedMailbox ); @@ -1182,7 +1268,7 @@ class ThreadController extends BaseController with EmailActionController { : await push(AppRoutes.rulesFilterCreator, arguments: arguments); if (newRuleFilterRequest is CreateNewEmailRuleFilterRequest) { - _createNewRuleFilterAction(accountId, newRuleFilterRequest); + _createNewRuleFilterAction(_accountId!, newRuleFilterRequest); } } else { logError('ThreadController::goToCreateEmailRuleView: Account or Session is NULL'); diff --git a/test/features/push_notification/presentation/websocket/web_socket_queue_handler_test.dart b/test/features/push_notification/presentation/websocket/web_socket_queue_handler_test.dart new file mode 100644 index 0000000000..3f95a6ee80 --- /dev/null +++ b/test/features/push_notification/presentation/websocket/web_socket_queue_handler_test.dart @@ -0,0 +1,197 @@ +import 'dart:collection'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/websocket/web_socket_message.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/websocket/web_socket_queue_handler.dart'; + +class MockWebSocketMessage extends WebSocketMessage { + MockWebSocketMessage(String message) + : super( + newState: State(message), + ); +} + +void main() { + group('WebSocketQueueHandler::test', () { + late Queue processedMessages; + + setUp(() { + processedMessages = Queue(); + }); + + WebSocketQueueHandler createHandler({ + required ProcessMessageCallback processMessageCallback, + OnErrorCallback? onErrorCallback, + }) { + return WebSocketQueueHandler( + processMessageCallback: processMessageCallback, + onErrorCallback: onErrorCallback, + ); + } + + group('Basic Operations', () { + late WebSocketQueueHandler handler; + + setUp(() { + handler = createHandler( + processMessageCallback: (message) async { + processedMessages.add(message.id); + }, + ); + }); + + tearDown(() => handler.dispose()); + + test('Should process messages in correct order', () async { + final messages = List.generate(5, (index) => MockWebSocketMessage('$index')); + + for (var message in messages) { + handler.enqueue(message); + } + + await handler.waitForEmpty(); + + expect(processedMessages, containsAllInOrder(['0', '1', '2', '3', '4'])); + }); + + test('Should correctly remove messages up to specified ID', () async { + final messages = List.generate(5, (index) => MockWebSocketMessage('$index')); + + for (var message in messages) { + handler.enqueue(message); + } + + handler.removeMessagesUpToCurrent('2'); + + expect(handler.queueSize, 2); + + await handler.waitForEmpty(); + + expect(processedMessages.length, 2); + expect(processedMessages.first, '3'); + }); + }); + + group('Concurrent Operations', () { + late WebSocketQueueHandler handler; + + setUp(() { + handler = createHandler( + processMessageCallback: (message) async { + processedMessages.add(message.id); + }, + ); + }); + + tearDown(() => handler.dispose()); + + test('Should handle concurrent message enqueueing', () async { + final messages = List.generate(5, (index) => MockWebSocketMessage('$index')); + + await Future.wait(messages.map((message) => Future(() => handler.enqueue(message)))); + + await handler.waitForEmpty(); + + expect(processedMessages, containsAllInOrder(['0', '1', '2', '3', '4'])); + }); + + test('Should maintain order under high concurrency', () async { + final messages = List.generate(10, (index) => MockWebSocketMessage('$index')); + + for (var message in messages) { + handler.enqueue(message); + } + + await handler.waitForEmpty(); + + expect(processedMessages, containsAllInOrder(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'])); + }); + }); + + group('Error Handling', () { + test('Should continue processing after message failure', () async { + final handler = createHandler( + processMessageCallback: (message) async { + if (message.id == '2') throw Exception('Simulated Failure'); + processedMessages.add(message.id); + }, + ); + + handler.enqueue(MockWebSocketMessage('1')); + handler.enqueue(MockWebSocketMessage('2')); + handler.enqueue(MockWebSocketMessage('3')); + + await handler.waitForEmpty(); + + expect(processedMessages, ['1', '3']); + handler.dispose(); + }); + + test('Should handle exception in process callback', () async { + final handler = createHandler( + processMessageCallback: (message) async { + if (message.id == '1') throw Exception('Simulated Failure'); + processedMessages.add(message.id); + }, + onErrorCallback: (error, stackTrace) { + expect(error, isA()); + }, + ); + + handler.enqueue(MockWebSocketMessage('1')); + handler.enqueue(MockWebSocketMessage('2')); + + await handler.waitForEmpty(); + + expect(processedMessages, ['2']); + handler.dispose(); + }); + }); + + group('Stress Testing', () { + late WebSocketQueueHandler handler; + + setUp(() { + handler = createHandler( + processMessageCallback: (message) async { + processedMessages.add(message.id); + }, + ); + }); + + tearDown(() => handler.dispose()); + + test('Should handle large bursts of messages', () async { + handler = createHandler( + processMessageCallback: (message) async { + processedMessages.add(message.id); + }, + ); + + const int burstSize = 1000; + for (int i = 0; i < burstSize; i++) { + await Future.delayed(const Duration(milliseconds: 10)); + handler.enqueue(MockWebSocketMessage('$i')); + } + + await handler.waitForEmpty(); + + expect(processedMessages.length, burstSize); + expect(processedMessages, List.generate(burstSize, (i) => '$i')); + }); + + test('Should handle interleaved slow and fast messages', () async { + handler.enqueue(MockWebSocketMessage('1')); + + Future.delayed(const Duration(milliseconds: 10), () { + handler.enqueue(MockWebSocketMessage('2')); + }); + + await handler.waitForEmpty(); + + expect(processedMessages, ['1', '2']); + }); + }); + }); +} diff --git a/test/features/thread/presentation/controller/thread_controller_test.dart b/test/features/thread/presentation/controller/thread_controller_test.dart index 7f8ad860d8..e582c7db12 100644 --- a/test/features/thread/presentation/controller/thread_controller_test.dart +++ b/test/features/thread/presentation/controller/thread_controller_test.dart @@ -286,6 +286,7 @@ void main() { when(mockMailboxDashBoardController.emailUIAction).thenReturn(Rxn(null)); when(mockMailboxDashBoardController.viewState).thenReturn(Rx(Right(UIState.idle))); when(mockMailboxDashBoardController.filterMessageOption).thenReturn(Rx(FilterMessageOption.all)); + when(mockMailboxDashBoardController.currentEmailState).thenReturn(State('old-state')); when(mockSearchController.searchState).thenReturn(Rx(SearchState.initial())); when(mockSearchController.isAdvancedSearchViewOpen).thenReturn(RxBool(false)); when(mockSearchController.isSearchEmailRunning).thenReturn(true); From 65a52d500167b0a7f3b49223de3a276a959c0e4b Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 23 Dec 2024 15:11:57 +0700 Subject: [PATCH 19/72] TF-3334 Dispose web socket when close controller --- .../presentation/websocket/web_socket_queue_handler.dart | 3 +++ .../search/email/presentation/search_email_controller.dart | 1 + lib/features/thread/presentation/thread_controller.dart | 1 + 3 files changed, 5 insertions(+) diff --git a/lib/features/push_notification/presentation/websocket/web_socket_queue_handler.dart b/lib/features/push_notification/presentation/websocket/web_socket_queue_handler.dart index 155b0deb6b..455473983b 100644 --- a/lib/features/push_notification/presentation/websocket/web_socket_queue_handler.dart +++ b/lib/features/push_notification/presentation/websocket/web_socket_queue_handler.dart @@ -116,6 +116,9 @@ class WebSocketQueueHandler { bool isMessageProcessed(String messageId) => _processedMessageIds.contains(messageId); void dispose() { + _messageQueue.clear(); + _processedMessageIds.clear(); _queueController.close(); + _processingLock = null; } } diff --git a/lib/features/search/email/presentation/search_email_controller.dart b/lib/features/search/email/presentation/search_email_controller.dart index 37c9081eb9..2bed6ef896 100644 --- a/lib/features/search/email/presentation/search_email_controller.dart +++ b/lib/features/search/email/presentation/search_email_controller.dart @@ -1030,6 +1030,7 @@ class SearchEmailController extends BaseController _deBouncerTime.cancel(); emailUIActionWorker.dispose(); dashBoardActionWorker.dispose(); + _webSocketQueueHandler?.dispose(); super.onClose(); } } \ No newline at end of file diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index c23994fcd3..f3d2fa6722 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -149,6 +149,7 @@ class ThreadController extends BaseController with EmailActionController { if (PlatformInfo.isWeb) { _resizeBrowserStreamSubscription?.cancel(); } + _webSocketQueueHandler?.dispose(); super.onClose(); } From d6c3ddcb9a8927392fb875cb5b0a67e3e495dfe3 Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 23 Dec 2024 16:10:36 +0700 Subject: [PATCH 20/72] TF-3334 Skip getting stored state in interactor when performing actions with email and mailbox --- .../state/save_email_as_drafts_state.dart | 12 +---- .../domain/state/send_email_state.dart | 9 +--- .../state/update_email_drafts_state.dart | 12 +---- ...w_and_save_email_to_drafts_interactor.dart | 43 +--------------- .../create_new_and_send_email_interactor.dart | 31 ------------ .../usecases/send_email_interactor.dart | 20 +------- .../presentation/composer_bindings.dart | 2 - .../domain/model/move_to_mailbox_request.dart | 4 ++ .../state/delete_email_permanently_state.dart | 10 +--- ...ete_multiple_emails_permanently_state.dart | 26 +++------- .../state/mark_as_email_read_state.dart | 14 ++---- .../state/mark_as_email_star_state.dart | 15 ++---- .../domain/state/move_to_mailbox_state.dart | 9 +--- .../state/restore_deleted_message_state.dart | 13 ++--- .../store_event_attendance_status_state.dart | 16 ++---- .../domain/state/unsubscribe_email_state.dart | 11 +---- .../delete_email_permanently_interactor.dart | 20 ++------ ...ultiple_emails_permanently_interactor.dart | 26 ++-------- .../mark_as_email_read_interactor.dart | 14 +----- .../mark_as_star_email_interactor.dart | 6 +-- .../usecases/move_to_mailbox_interactor.dart | 19 ++----- .../restore_deleted_message_interactor.dart | 7 +-- ...re_event_attendance_status_interactor.dart | 6 +-- .../unsubscribe_email_interactor.dart | 3 +- .../presentation/bindings/email_bindings.dart | 8 +-- .../state/mark_as_mailbox_read_state.dart | 9 +--- .../domain/state/move_mailbox_state.dart | 9 +--- .../domain/state/rename_mailbox_state.dart | 10 +--- .../mark_as_mailbox_read_interactor.dart | 23 ++------- .../usecases/move_mailbox_interactor.dart | 7 ++- .../usecases/rename_mailbox_interactor.dart | 9 ++-- .../state/remove_email_drafts_state.dart | 11 +---- .../remove_email_drafts_interactor.dart | 19 ++----- .../bindings/mailbox_dashboard_bindings.dart | 49 ++++--------------- .../mailbox_dashboard_controller.dart | 1 - .../sending_email_interactor_bindings.dart | 4 +- .../presentation/search_email_controller.dart | 5 -- .../domain/constants/thread_constants.dart | 1 - .../domain/state/empty_spam_folder_state.dart | 12 ++--- .../state/empty_trash_folder_state.dart | 12 ++--- .../mark_as_multiple_email_read_state.dart | 22 +++------ .../mark_as_star_multiple_email_state.dart | 22 +++------ .../move_multiple_email_to_mailbox_state.dart | 16 ++---- .../empty_spam_folder_interactor.dart | 25 +--------- .../empty_trash_folder_interactor.dart | 28 ++--------- ...ark_as_multiple_email_read_interactor.dart | 21 ++------ ...ark_as_star_multiple_email_interactor.dart | 6 +-- ..._multiple_email_to_mailbox_interactor.dart | 27 +++------- .../presentation/thread_controller.dart | 4 -- ..._save_email_to_drafts_interactor_test.dart | 5 -- ...te_new_and_send_email_interactor_test.dart | 4 -- ...ent_attendance_status_interactor_test.dart | 23 ++------- 52 files changed, 138 insertions(+), 602 deletions(-) diff --git a/lib/features/composer/domain/state/save_email_as_drafts_state.dart b/lib/features/composer/domain/state/save_email_as_drafts_state.dart index ae82f2d468..1eddfdeeae 100644 --- a/lib/features/composer/domain/state/save_email_as_drafts_state.dart +++ b/lib/features/composer/domain/state/save_email_as_drafts_state.dart @@ -1,22 +1,14 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; class SaveEmailAsDraftsLoading extends LoadingState {} -class SaveEmailAsDraftsSuccess extends UIActionState { +class SaveEmailAsDraftsSuccess extends UIState { final EmailId emailId; - SaveEmailAsDraftsSuccess( - this.emailId, - { - jmap.State? currentEmailState, - jmap.State? currentMailboxState, - } - ) : super(currentEmailState, currentMailboxState); + SaveEmailAsDraftsSuccess(this.emailId); @override List get props => [emailId, ...super.props]; diff --git a/lib/features/composer/domain/state/send_email_state.dart b/lib/features/composer/domain/state/send_email_state.dart index 814e6c3cb6..f52f3a6acc 100644 --- a/lib/features/composer/domain/state/send_email_state.dart +++ b/lib/features/composer/domain/state/send_email_state.dart @@ -2,27 +2,22 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; -import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_action_type.dart'; class SendEmailLoading extends LoadingState {} -class SendEmailSuccess extends UIActionState { +class SendEmailSuccess extends UIState { final EmailRequest emailRequest; SendEmailSuccess({ - jmap.State? currentEmailState, - jmap.State? currentMailboxState, required this.emailRequest, - }) : super(currentEmailState, currentMailboxState); + }); @override List get props => [ - ...super.props, emailRequest, ]; } diff --git a/lib/features/composer/domain/state/update_email_drafts_state.dart b/lib/features/composer/domain/state/update_email_drafts_state.dart index 47ca09cca3..b6f8d8621a 100644 --- a/lib/features/composer/domain/state/update_email_drafts_state.dart +++ b/lib/features/composer/domain/state/update_email_drafts_state.dart @@ -1,22 +1,14 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; class UpdatingEmailDrafts extends LoadingState {} -class UpdateEmailDraftsSuccess extends UIActionState { +class UpdateEmailDraftsSuccess extends UIState { final EmailId emailId; - UpdateEmailDraftsSuccess( - this.emailId, - { - jmap.State? currentEmailState, - jmap.State? currentMailboxState, - } - ) : super(currentEmailState, currentMailboxState); + UpdateEmailDraftsSuccess(this.emailId); @override List get props => [emailId, ...super.props]; diff --git a/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart b/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart index dd7649a7d2..ac651fc187 100644 --- a/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart +++ b/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart @@ -3,9 +3,6 @@ import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart' as dartz; import 'package:dio/dio.dart'; -import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/session/session.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/composer/domain/exceptions/compose_email_exception.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; @@ -15,17 +12,14 @@ import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; class CreateNewAndSaveEmailToDraftsInteractor { final EmailRepository _emailRepository; - final MailboxRepository _mailboxRepository; final ComposerRepository _composerRepository; CreateNewAndSaveEmailToDraftsInteractor( this._emailRepository, - this._mailboxRepository, this._composerRepository, ); @@ -36,11 +30,6 @@ class CreateNewAndSaveEmailToDraftsInteractor { try { yield dartz.Right(GenerateEmailLoading()); - final listCurrentState = await _getStoredCurrentState( - session: createEmailRequest.session, - accountId: createEmailRequest.accountId - ); - final emailCreated = await _createEmailObject(createEmailRequest); if (emailCreated != null) { @@ -55,11 +44,7 @@ class CreateNewAndSaveEmailToDraftsInteractor { ); yield dartz.Right( - SaveEmailAsDraftsSuccess( - emailDraftSaved.id!, - currentMailboxState: listCurrentState?.value1, - currentEmailState: listCurrentState?.value2 - ) + SaveEmailAsDraftsSuccess(emailDraftSaved.id!) ); } else { yield dartz.Right(UpdatingEmailDrafts()); @@ -73,11 +58,7 @@ class CreateNewAndSaveEmailToDraftsInteractor { ); yield dartz.Right( - UpdateEmailDraftsSuccess( - emailDraftSaved.id!, - currentMailboxState: listCurrentState?.value1, - currentEmailState: listCurrentState?.value2 - ) + UpdateEmailDraftsSuccess(emailDraftSaved.id!) ); } } else { @@ -112,24 +93,4 @@ class CreateNewAndSaveEmailToDraftsInteractor { return null; } } - - Future?> _getStoredCurrentState({ - required Session session, - required AccountId accountId - }) async { - try { - final listState = await Future.wait([ - _mailboxRepository.getMailboxState(session, accountId), - _emailRepository.getEmailState(session, accountId), - ]); - - final mailboxState = listState.first; - final emailState = listState.last; - - return dartz.Tuple2(mailboxState, emailState); - } catch (e) { - logError('CreateNewAndSaveEmailToDraftsInteractor::_getStoredCurrentState: Exception: $e'); - return null; - } - } } \ No newline at end of file diff --git a/lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart b/lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart index cbff76b4db..ed980974c3 100644 --- a/lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart +++ b/lib/features/composer/domain/usecases/create_new_and_send_email_interactor.dart @@ -5,7 +5,6 @@ import 'package:dartz/dartz.dart' as dartz; import 'package:dio/dio.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/composer/domain/exceptions/compose_email_exception.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart'; @@ -15,18 +14,15 @@ import 'package:tmail_ui_user/features/composer/presentation/extensions/create_e import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; class CreateNewAndSendEmailInteractor { final EmailRepository _emailRepository; - final MailboxRepository _mailboxRepository; final ComposerRepository _composerRepository; CreateNewAndSendEmailInteractor( this._emailRepository, - this._mailboxRepository, this._composerRepository, ); @@ -38,11 +34,6 @@ class CreateNewAndSendEmailInteractor { try { yield dartz.Right(GenerateEmailLoading()); - final listCurrentState = await _getStoredCurrentState( - session: createEmailRequest.session, - accountId: createEmailRequest.accountId - ); - sendingEmailArguments = await _createEmailObject(createEmailRequest); if (sendingEmailArguments != null) { @@ -67,8 +58,6 @@ class CreateNewAndSendEmailInteractor { yield dartz.Right( SendEmailSuccess( - currentMailboxState: listCurrentState?.value1, - currentEmailState: listCurrentState?.value2, emailRequest: sendingEmailArguments.emailRequest ) ); @@ -108,26 +97,6 @@ class CreateNewAndSendEmailInteractor { } } - Future?> _getStoredCurrentState({ - required Session session, - required AccountId accountId - }) async { - try { - final listState = await Future.wait([ - _mailboxRepository.getMailboxState(session, accountId), - _emailRepository.getEmailState(session, accountId), - ]); - - final mailboxState = listState.first; - final emailState = listState.last; - - return dartz.Tuple2(mailboxState, emailState); - } catch (e) { - logError('CreateNewAndSendEmailInteractor::_getStoredCurrentState: Exception: $e'); - return null; - } - } - Future _deleteOldDraftsEmail({ required Session session, required AccountId accountId, diff --git a/lib/features/composer/domain/usecases/send_email_interactor.dart b/lib/features/composer/domain/usecases/send_email_interactor.dart index 83ade78aca..6052d18903 100644 --- a/lib/features/composer/domain/usecases/send_email_interactor.dart +++ b/lib/features/composer/domain/usecases/send_email_interactor.dart @@ -7,16 +7,12 @@ import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart' import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_action_type.dart'; class SendEmailInteractor { final EmailRepository _emailRepository; - final MailboxRepository _mailboxRepository; - SendEmailInteractor( - this._emailRepository, - this._mailboxRepository); + SendEmailInteractor(this._emailRepository); Stream> execute( Session session, @@ -30,14 +26,6 @@ class SendEmailInteractor { try { yield Right(SendEmailLoading()); - final listState = await Future.wait([ - _mailboxRepository.getMailboxState(session, accountId), - _emailRepository.getEmailState(session, accountId), - ], eagerError: true); - - final currentMailboxState = listState.first; - final currentEmailState = listState.last; - await _emailRepository.sendEmail( session, accountId, @@ -50,11 +38,7 @@ class SendEmailInteractor { } yield Right( - SendEmailSuccess( - currentEmailState: currentEmailState, - currentMailboxState: currentMailboxState, - emailRequest: emailRequest - ) + SendEmailSuccess(emailRequest: emailRequest) ); } catch (e) { yield Left(SendEmailFailure( diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index 9c563bce8e..903399479e 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -206,12 +206,10 @@ class ComposerBindings extends BaseBindings { Get.lazyPut(() => GetAlwaysReadReceiptSettingInteractor(Get.find())); Get.lazyPut(() => CreateNewAndSendEmailInteractor( Get.find(), - Get.find(), Get.find(), )); Get.lazyPut(() => CreateNewAndSaveEmailToDraftsInteractor( Get.find(), - Get.find(), Get.find(), )); Get.lazyPut(() => RestoreEmailInlineImagesInteractor( diff --git a/lib/features/email/domain/model/move_to_mailbox_request.dart b/lib/features/email/domain/model/move_to_mailbox_request.dart index e3b16422a9..aa958d274f 100644 --- a/lib/features/email/domain/model/move_to_mailbox_request.dart +++ b/lib/features/email/domain/model/move_to_mailbox_request.dart @@ -21,6 +21,10 @@ class MoveToMailboxRequest with EquatableMixin { this.destinationPath, }); + int get totalEmails => currentMailboxes + .values + .fold(0, (sum, element) => sum + element.length); + @override List get props => [ currentMailboxes, diff --git a/lib/features/email/domain/state/delete_email_permanently_state.dart b/lib/features/email/domain/state/delete_email_permanently_state.dart index 9cd56e1e63..22f2a0a6c4 100644 --- a/lib/features/email/domain/state/delete_email_permanently_state.dart +++ b/lib/features/email/domain/state/delete_email_permanently_state.dart @@ -1,17 +1,9 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; class StartDeleteEmailPermanently extends UIState {} -class DeleteEmailPermanentlySuccess extends UIActionState { - - DeleteEmailPermanentlySuccess({ - jmap.State? currentEmailState, - jmap.State? currentMailboxState, - }) : super(currentEmailState, currentMailboxState); -} +class DeleteEmailPermanentlySuccess extends UIState {} class DeleteEmailPermanentlyFailure extends FeatureFailure { diff --git a/lib/features/email/domain/state/delete_multiple_emails_permanently_state.dart b/lib/features/email/domain/state/delete_multiple_emails_permanently_state.dart index 33b9b84671..07ac090d6c 100644 --- a/lib/features/email/domain/state/delete_multiple_emails_permanently_state.dart +++ b/lib/features/email/domain/state/delete_multiple_emails_permanently_state.dart @@ -1,41 +1,27 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; class LoadingDeleteMultipleEmailsPermanentlyAll extends UIState {} -class DeleteMultipleEmailsPermanentlyAllSuccess extends UIActionState { +class DeleteMultipleEmailsPermanentlyAllSuccess extends UIState { List emailIds; - DeleteMultipleEmailsPermanentlyAllSuccess( - this.emailIds, - { - jmap.State? currentEmailState, - jmap.State? currentMailboxState, - } - ) : super(currentEmailState, currentMailboxState); + DeleteMultipleEmailsPermanentlyAllSuccess(this.emailIds); @override - List get props => [emailIds, ...super.props]; + List get props => [emailIds]; } -class DeleteMultipleEmailsPermanentlyHasSomeEmailFailure extends UIActionState { +class DeleteMultipleEmailsPermanentlyHasSomeEmailFailure extends UIState { List emailIds; - DeleteMultipleEmailsPermanentlyHasSomeEmailFailure( - this.emailIds, - { - jmap.State? currentEmailState, - jmap.State? currentMailboxState, - } - ) : super(currentEmailState, currentMailboxState); + DeleteMultipleEmailsPermanentlyHasSomeEmailFailure(this.emailIds); @override - List get props => [emailIds, ...super.props]; + List get props => [emailIds]; } class DeleteMultipleEmailsPermanentlyAllFailure extends FeatureFailure {} diff --git a/lib/features/email/domain/state/mark_as_email_read_state.dart b/lib/features/email/domain/state/mark_as_email_read_state.dart index 2e7ddbd688..1c327f1da0 100644 --- a/lib/features/email/domain/state/mark_as_email_read_state.dart +++ b/lib/features/email/domain/state/mark_as_email_read_state.dart @@ -1,12 +1,10 @@ import 'package:core/presentation/state/failure.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/email/read_actions.dart'; -import 'package:model/model.dart'; -import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; -class MarkAsEmailReadSuccess extends UIActionState { +class MarkAsEmailReadSuccess extends UIState { final EmailId emailId; final ReadActions readActions; final MarkReadAction markReadAction; @@ -15,14 +13,10 @@ class MarkAsEmailReadSuccess extends UIActionState { this.emailId, this.readActions, this.markReadAction, - { - jmap.State? currentEmailState, - jmap.State? currentMailboxState, - } - ) : super(currentEmailState, currentMailboxState); + ); @override - List get props => [emailId, readActions, markReadAction, ...super.props]; + List get props => [emailId, readActions, markReadAction]; } class MarkAsEmailReadFailure extends FeatureFailure { diff --git a/lib/features/email/domain/state/mark_as_email_star_state.dart b/lib/features/email/domain/state/mark_as_email_star_state.dart index c9d639e726..3313c95253 100644 --- a/lib/features/email/domain/state/mark_as_email_star_state.dart +++ b/lib/features/email/domain/state/mark_as_email_star_state.dart @@ -1,21 +1,14 @@ import 'package:core/presentation/state/failure.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:core/presentation/state/success.dart'; import 'package:model/email/mark_star_action.dart'; -import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; -class MarkAsStarEmailSuccess extends UIActionState { +class MarkAsStarEmailSuccess extends UIState { final MarkStarAction markStarAction; - MarkAsStarEmailSuccess( - this.markStarAction, - { - jmap.State? currentEmailState, - jmap.State? currentMailboxState, - } - ) : super(currentEmailState, currentMailboxState); + MarkAsStarEmailSuccess(this.markStarAction); @override - List get props => [markStarAction, ...super.props]; + List get props => [markStarAction]; } class MarkAsStarEmailFailure extends FeatureFailure { diff --git a/lib/features/email/domain/state/move_to_mailbox_state.dart b/lib/features/email/domain/state/move_to_mailbox_state.dart index 66e1359d49..56809a70dd 100644 --- a/lib/features/email/domain/state/move_to_mailbox_state.dart +++ b/lib/features/email/domain/state/move_to_mailbox_state.dart @@ -1,15 +1,13 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; -import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; class LoadingMoveToMailbox extends UIState {} -class MoveToMailboxSuccess extends UIActionState { +class MoveToMailboxSuccess extends UIState { final EmailId emailId; final MailboxId currentMailboxId; final MailboxId destinationMailboxId; @@ -25,10 +23,8 @@ class MoveToMailboxSuccess extends UIActionState { this.emailActionType, { this.destinationPath, - jmap.State? currentEmailState, - jmap.State? currentMailboxState, } - ) : super(currentEmailState, currentMailboxState); + ); @override List get props => [ @@ -38,7 +34,6 @@ class MoveToMailboxSuccess extends UIActionState { moveAction, emailActionType, destinationPath, - ...super.props ]; } diff --git a/lib/features/email/domain/state/restore_deleted_message_state.dart b/lib/features/email/domain/state/restore_deleted_message_state.dart index c946367aac..c72b51769b 100644 --- a/lib/features/email/domain/state/restore_deleted_message_state.dart +++ b/lib/features/email/domain/state/restore_deleted_message_state.dart @@ -1,23 +1,16 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:email_recovery/email_recovery/email_recovery_action.dart'; -import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; class RestoreDeletedMessageLoading extends LoadingState {} -class RestoreDeletedMessageSuccess extends UIActionState { +class RestoreDeletedMessageSuccess extends UIState { final EmailRecoveryAction emailRecoveryAction; - RestoreDeletedMessageSuccess( - this.emailRecoveryAction, - { - jmap.State? currentMailboxState, - } - ) : super(null, currentMailboxState); + RestoreDeletedMessageSuccess(this.emailRecoveryAction); @override - List get props => [emailRecoveryAction, ...super.props]; + List get props => [emailRecoveryAction]; } class RestoreDeletedMessageFailure extends FeatureFailure { diff --git a/lib/features/email/domain/state/store_event_attendance_status_state.dart b/lib/features/email/domain/state/store_event_attendance_status_state.dart index 1325cf81bd..835f41a651 100644 --- a/lib/features/email/domain/state/store_event_attendance_status_state.dart +++ b/lib/features/email/domain/state/store_event_attendance_status_state.dart @@ -1,27 +1,17 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; -import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; class StoreEventAttendanceStatusLoading extends LoadingState {} -class StoreEventAttendanceStatusSuccess extends UIActionState { +class StoreEventAttendanceStatusSuccess extends UIState { final EventActionType eventActionType; - StoreEventAttendanceStatusSuccess( - this.eventActionType, - { - jmap.State? currentEmailState, - jmap.State? currentMailboxState, - } - ) : super(currentEmailState, currentMailboxState); + StoreEventAttendanceStatusSuccess(this.eventActionType); @override - List get props => [ - eventActionType, - ...super.props]; + List get props => [eventActionType]; } class StoreEventAttendanceStatusFailure extends FeatureFailure { diff --git a/lib/features/email/domain/state/unsubscribe_email_state.dart b/lib/features/email/domain/state/unsubscribe_email_state.dart index 8ec09d478f..362570f7a7 100644 --- a/lib/features/email/domain/state/unsubscribe_email_state.dart +++ b/lib/features/email/domain/state/unsubscribe_email_state.dart @@ -1,18 +1,9 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; -import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; class UnsubscribeEmailLoading extends LoadingState {} -class UnsubscribeEmailSuccess extends UIActionState { - UnsubscribeEmailSuccess( - { - jmap.State? currentEmailState, - jmap.State? currentMailboxState, - } - ) : super(currentEmailState, currentMailboxState); -} +class UnsubscribeEmailSuccess extends UIState {} class UnsubscribeEmailFailure extends FeatureFailure { diff --git a/lib/features/email/domain/usecases/delete_email_permanently_interactor.dart b/lib/features/email/domain/usecases/delete_email_permanently_interactor.dart index 35433b82ca..20260256ee 100644 --- a/lib/features/email/domain/usecases/delete_email_permanently_interactor.dart +++ b/lib/features/email/domain/usecases/delete_email_permanently_interactor.dart @@ -1,35 +1,23 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanently_state.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; class DeleteEmailPermanentlyInteractor { final EmailRepository _emailRepository; - final MailboxRepository _mailboxRepository; - DeleteEmailPermanentlyInteractor(this._emailRepository, this._mailboxRepository); + DeleteEmailPermanentlyInteractor(this._emailRepository); Stream> execute(Session session, AccountId accountId, EmailId emailId) async* { try { yield Right(StartDeleteEmailPermanently()); - - final listState = await Future.wait([ - _mailboxRepository.getMailboxState(session, accountId), - _emailRepository.getEmailState(session, accountId), - ], eagerError: true); - - final currentMailboxState = listState.first; - final currentEmailState = listState.last; - final result = await _emailRepository.deleteEmailPermanently(session, accountId, emailId); if (result) { - yield Right(DeleteEmailPermanentlySuccess( - currentEmailState: currentEmailState, - currentMailboxState: currentMailboxState)); + yield Right(DeleteEmailPermanentlySuccess()); } else { yield Left(DeleteEmailPermanentlyFailure(null)); } diff --git a/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart b/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart index 29befa5b14..685933fd57 100644 --- a/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart +++ b/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart @@ -1,41 +1,25 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails_permanently_state.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; class DeleteMultipleEmailsPermanentlyInteractor { final EmailRepository _emailRepository; - final MailboxRepository _mailboxRepository; - DeleteMultipleEmailsPermanentlyInteractor(this._emailRepository, this._mailboxRepository); + DeleteMultipleEmailsPermanentlyInteractor(this._emailRepository); Stream> execute(Session session, AccountId accountId, List emailIds) async* { try { yield Right(LoadingDeleteMultipleEmailsPermanentlyAll()); - - final listState = await Future.wait([ - _mailboxRepository.getMailboxState(session, accountId), - _emailRepository.getEmailState(session, accountId), - ], eagerError: true); - - final currentMailboxState = listState.first; - final currentEmailState = listState.last; - final listResult = await _emailRepository.deleteMultipleEmailsPermanently(session, accountId, emailIds); if (listResult.length == emailIds.length) { - yield Right(DeleteMultipleEmailsPermanentlyAllSuccess( - listResult, - currentEmailState: currentEmailState, - currentMailboxState: currentMailboxState)); + yield Right(DeleteMultipleEmailsPermanentlyAllSuccess(listResult)); } else if (listResult.isNotEmpty) { - yield Right(DeleteMultipleEmailsPermanentlyHasSomeEmailFailure( - listResult, - currentEmailState: currentEmailState, - currentMailboxState: currentMailboxState)); + yield Right(DeleteMultipleEmailsPermanentlyHasSomeEmailFailure(listResult)); } else { yield Left(DeleteMultipleEmailsPermanentlyAllFailure()); } diff --git a/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart b/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart index 7fe15acbb9..7db473e033 100644 --- a/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart +++ b/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart @@ -8,13 +8,11 @@ import 'package:model/email/read_actions.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; class MarkAsEmailReadInteractor { final EmailRepository _emailRepository; - final MailboxRepository _mailboxRepository; - MarkAsEmailReadInteractor(this._emailRepository, this._mailboxRepository); + MarkAsEmailReadInteractor(this._emailRepository); Stream> execute( Session session, @@ -24,14 +22,6 @@ class MarkAsEmailReadInteractor { MarkReadAction markReadAction, ) async* { try { - final listState = await Future.wait([ - _mailboxRepository.getMailboxState( session,accountId), - _emailRepository.getEmailState(session, accountId), - ], eagerError: true); - - final currentMailboxState = listState.first; - final currentEmailState = listState.last; - final result = await _emailRepository.markAsRead( session, accountId, @@ -43,8 +33,6 @@ class MarkAsEmailReadInteractor { result.first, readAction, markReadAction, - currentEmailState: currentEmailState, - currentMailboxState: currentMailboxState, )); } catch (e) { yield Left(MarkAsEmailReadFailure(readAction, exception: e)); diff --git a/lib/features/email/domain/usecases/mark_as_star_email_interactor.dart b/lib/features/email/domain/usecases/mark_as_star_email_interactor.dart index 6ca420eb75..aa8cc68c7e 100644 --- a/lib/features/email/domain/usecases/mark_as_star_email_interactor.dart +++ b/lib/features/email/domain/usecases/mark_as_star_email_interactor.dart @@ -20,17 +20,13 @@ class MarkAsStarEmailInteractor { MarkStarAction markStarAction, ) async* { try { - final currentEmailState = await emailRepository.getEmailState(session, accountId); await emailRepository.markAsStar( session, accountId, [emailId], markStarAction, ); - yield Right(MarkAsStarEmailSuccess( - markStarAction, - currentEmailState: currentEmailState, - )); + yield Right(MarkAsStarEmailSuccess(markStarAction)); } catch (e) { yield Left(MarkAsStarEmailFailure(markStarAction, exception: e)); } diff --git a/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart b/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart index 6c073dd740..eebdebd2e6 100644 --- a/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart +++ b/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart @@ -1,30 +1,20 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; class MoveToMailboxInteractor { final EmailRepository _emailRepository; - final MailboxRepository _mailboxRepository; - MoveToMailboxInteractor(this._emailRepository, this._mailboxRepository); + MoveToMailboxInteractor(this._emailRepository); Stream> execute(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) async* { try { yield Right(LoadingMoveToMailbox()); - - final listState = await Future.wait([ - _mailboxRepository.getMailboxState(session, accountId), - _emailRepository.getEmailState(session, accountId), - ], eagerError: true); - - final currentMailboxState = listState.first; - final currentEmailState = listState.last; - final result = await _emailRepository.moveToMailbox(session, accountId, moveRequest); if (result.isNotEmpty) { yield Right(MoveToMailboxSuccess( @@ -34,8 +24,7 @@ class MoveToMailboxInteractor { moveRequest.moveAction, moveRequest.emailActionType, destinationPath: moveRequest.destinationPath, - currentMailboxState: currentMailboxState, - currentEmailState: currentEmailState)); + )); } else { yield Left(MoveToMailboxFailure(moveRequest.emailActionType)); } diff --git a/lib/features/email/domain/usecases/restore_deleted_message_interactor.dart b/lib/features/email/domain/usecases/restore_deleted_message_interactor.dart index 4a68bdd841..142d0bc7a4 100644 --- a/lib/features/email/domain/usecases/restore_deleted_message_interactor.dart +++ b/lib/features/email/domain/usecases/restore_deleted_message_interactor.dart @@ -8,13 +8,11 @@ import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:tmail_ui_user/features/email/domain/model/restore_deleted_message_request.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/state/restore_deleted_message_state.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; class RestoredDeletedMessageInteractor { final EmailRepository _emailRepository; - final MailboxRepository _mailboxRepository; - RestoredDeletedMessageInteractor(this._emailRepository, this._mailboxRepository); + RestoredDeletedMessageInteractor(this._emailRepository); Stream> execute( Session session, @@ -23,9 +21,8 @@ class RestoredDeletedMessageInteractor { ) async* { try { yield Right(RestoreDeletedMessageLoading()); - final currentMailboxState = await _mailboxRepository.getMailboxState(session, accountId); final emailRecovery = await _emailRepository.restoreDeletedMessage(newRecoveryRequest); - yield Right(RestoreDeletedMessageSuccess(emailRecovery, currentMailboxState: currentMailboxState)); + yield Right(RestoreDeletedMessageSuccess(emailRecovery)); } catch (e) { yield Left(RestoreDeletedMessageFailure(e)); } diff --git a/lib/features/email/domain/usecases/store_event_attendance_status_interactor.dart b/lib/features/email/domain/usecases/store_event_attendance_status_interactor.dart index d524691aa2..da3fd94a4a 100644 --- a/lib/features/email/domain/usecases/store_event_attendance_status_interactor.dart +++ b/lib/features/email/domain/usecases/store_event_attendance_status_interactor.dart @@ -22,17 +22,13 @@ class StoreEventAttendanceStatusInteractor { try { yield Right(StoreEventAttendanceStatusLoading()); - final currentEmailState = await _emailRepository.getEmailState(session, accountId); - await _emailRepository.storeEventAttendanceStatus( session, accountId, emailId, eventActionType); - yield Right(StoreEventAttendanceStatusSuccess( - eventActionType, - currentEmailState: currentEmailState)); + yield Right(StoreEventAttendanceStatusSuccess(eventActionType)); } catch (e) { yield Left(StoreEventAttendanceStatusFailure(exception: e)); } diff --git a/lib/features/email/domain/usecases/unsubscribe_email_interactor.dart b/lib/features/email/domain/usecases/unsubscribe_email_interactor.dart index 4e1720b989..874022853e 100644 --- a/lib/features/email/domain/usecases/unsubscribe_email_interactor.dart +++ b/lib/features/email/domain/usecases/unsubscribe_email_interactor.dart @@ -15,9 +15,8 @@ class UnsubscribeEmailInteractor { Stream> execute(Session session, AccountId accountId, EmailId emailId) async* { try { yield Right(UnsubscribeEmailLoading()); - final currentEmailState = await emailRepository.getEmailState(session, accountId); await emailRepository.unsubscribeMail(session, accountId, emailId); - yield Right(UnsubscribeEmailSuccess(currentEmailState: currentEmailState)); + yield Right(UnsubscribeEmailSuccess()); } catch (e) { yield Left(UnsubscribeEmailFailure(exception: e)); } diff --git a/lib/features/email/presentation/bindings/email_bindings.dart b/lib/features/email/presentation/bindings/email_bindings.dart index 576532f4f8..071b20d0f8 100644 --- a/lib/features/email/presentation/bindings/email_bindings.dart +++ b/lib/features/email/presentation/bindings/email_bindings.dart @@ -123,9 +123,7 @@ class EmailBindings extends BaseBindings { @override void bindingsInteractor() { Get.lazyPut(() => GetEmailContentInteractor(Get.find())); - Get.lazyPut(() => MarkAsEmailReadInteractor( - Get.find(), - Get.find())); + Get.lazyPut(() => MarkAsEmailReadInteractor(Get.find())); Get.lazyPut(() => DownloadAttachmentsInteractor( Get.find(), Get.find(), @@ -139,9 +137,7 @@ class EmailBindings extends BaseBindings { Get.find(), Get.find(), )); - Get.lazyPut(() => MoveToMailboxInteractor( - Get.find(), - Get.find())); + Get.lazyPut(() => MoveToMailboxInteractor(Get.find())); Get.lazyPut(() => MarkAsStarEmailInteractor(Get.find())); Get.lazyPut(() => DownloadAttachmentForWebInteractor( Get.find(), diff --git a/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart b/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart index 6f16d3e4e9..60a2df0c15 100644 --- a/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart +++ b/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart @@ -39,7 +39,7 @@ class MarkAsMailboxReadAllSuccess extends UIActionState { ]; } -class MarkAsMailboxReadHasSomeEmailFailure extends UIActionState { +class MarkAsMailboxReadHasSomeEmailFailure extends UIState { final String mailboxDisplayName; final int countEmailsRead; @@ -47,17 +47,12 @@ class MarkAsMailboxReadHasSomeEmailFailure extends UIActionState { MarkAsMailboxReadHasSomeEmailFailure( this.mailboxDisplayName, this.countEmailsRead, - { - jmap.State? currentEmailState, - jmap.State? currentMailboxState, - } - ) : super(currentMailboxState, currentEmailState); + ); @override List get props => [ mailboxDisplayName, countEmailsRead, - ...super.props ]; } diff --git a/lib/features/mailbox/domain/state/move_mailbox_state.dart b/lib/features/mailbox/domain/state/move_mailbox_state.dart index 1b9a8a5791..9720a77be8 100644 --- a/lib/features/mailbox/domain/state/move_mailbox_state.dart +++ b/lib/features/mailbox/domain/state/move_mailbox_state.dart @@ -1,13 +1,11 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; class LoadingMoveMailbox extends UIState {} -class MoveMailboxSuccess extends UIActionState { +class MoveMailboxSuccess extends UIState { final MailboxId mailboxIdSelected; final MoveAction moveAction; @@ -22,10 +20,8 @@ class MoveMailboxSuccess extends UIActionState { this.parentId, this.destinationMailboxId, this.destinationMailboxDisplayName, - jmap.State? currentEmailState, - jmap.State? currentMailboxState, } - ) : super(currentEmailState, currentMailboxState); + ); @override List get props => [ @@ -34,7 +30,6 @@ class MoveMailboxSuccess extends UIActionState { parentId, destinationMailboxId, destinationMailboxDisplayName, - ...super.props ]; } diff --git a/lib/features/mailbox/domain/state/rename_mailbox_state.dart b/lib/features/mailbox/domain/state/rename_mailbox_state.dart index 988f8760f6..1861185bc9 100644 --- a/lib/features/mailbox/domain/state/rename_mailbox_state.dart +++ b/lib/features/mailbox/domain/state/rename_mailbox_state.dart @@ -1,17 +1,9 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; class LoadingRenameMailbox extends UIState {} -class RenameMailboxSuccess extends UIActionState { - - RenameMailboxSuccess({ - jmap.State? currentEmailState, - jmap.State? currentMailboxState, - }) : super(currentEmailState, currentMailboxState); -} +class RenameMailboxSuccess extends UIState {} class RenameMailboxFailure extends FeatureFailure { diff --git a/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart b/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart index bd892595a3..074efdd514 100644 --- a/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart +++ b/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart @@ -1,19 +1,18 @@ import 'dart:async'; -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; class MarkAsMailboxReadInteractor { final MailboxRepository _mailboxRepository; - final EmailRepository _emailRepository; - MarkAsMailboxReadInteractor(this._mailboxRepository, this._emailRepository); + MarkAsMailboxReadInteractor(this._mailboxRepository); Stream> execute( Session session, @@ -27,14 +26,6 @@ class MarkAsMailboxReadInteractor { yield Right(MarkAsMailboxReadLoading()); onProgressController.add(Right(MarkAsMailboxReadLoading())); - final listState = await Future.wait([ - _mailboxRepository.getMailboxState(session, accountId), - _emailRepository.getEmailState(session, accountId), - ], eagerError: true); - - final currentMailboxState = listState.first; - final currentEmailState = listState.last; - final listEmails = await _mailboxRepository.markAsMailboxRead( session, accountId, @@ -43,16 +34,12 @@ class MarkAsMailboxReadInteractor { onProgressController); if (totalEmailUnread == listEmails.length) { - yield Right(MarkAsMailboxReadAllSuccess( - mailboxDisplayName, - currentEmailState: currentEmailState, - currentMailboxState: currentMailboxState)); + yield Right(MarkAsMailboxReadAllSuccess(mailboxDisplayName)); } else if (listEmails.isNotEmpty) { yield Right(MarkAsMailboxReadHasSomeEmailFailure( mailboxDisplayName, listEmails.length, - currentEmailState: currentEmailState, - currentMailboxState: currentMailboxState)); + )); } else { yield Left(MarkAsMailboxReadAllFailure(mailboxDisplayName: mailboxDisplayName)); } diff --git a/lib/features/mailbox/domain/usecases/move_mailbox_interactor.dart b/lib/features/mailbox/domain/usecases/move_mailbox_interactor.dart index acf3687af3..23b8363d80 100644 --- a/lib/features/mailbox/domain/usecases/move_mailbox_interactor.dart +++ b/lib/features/mailbox/domain/usecases/move_mailbox_interactor.dart @@ -1,4 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; @@ -14,8 +15,6 @@ class MoveMailboxInteractor { Stream> execute(Session session, AccountId accountId, MoveMailboxRequest request) async* { try { yield Right(LoadingMoveMailbox()); - - final currentMailboxState = await _mailboxRepository.getMailboxState(session, accountId); final result = await _mailboxRepository.moveMailbox(session, accountId, request); if (result) { yield Right(MoveMailboxSuccess( @@ -24,7 +23,7 @@ class MoveMailboxInteractor { parentId: request.parentId, destinationMailboxId: request.destinationMailboxId, destinationMailboxDisplayName: request.destinationMailboxDisplayName, - currentMailboxState: currentMailboxState)); + )); } else { yield Left(MoveMailboxFailure(null)); } diff --git a/lib/features/mailbox/domain/usecases/rename_mailbox_interactor.dart b/lib/features/mailbox/domain/usecases/rename_mailbox_interactor.dart index 893967ec33..8e2ef950da 100644 --- a/lib/features/mailbox/domain/usecases/rename_mailbox_interactor.dart +++ b/lib/features/mailbox/domain/usecases/rename_mailbox_interactor.dart @@ -1,4 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; @@ -15,17 +16,13 @@ class RenameMailboxInteractor { Stream> execute(Session session, AccountId accountId, RenameMailboxRequest request) async* { try { yield Right(LoadingRenameMailbox()); - - final currentMailboxState = await _mailboxRepository.getMailboxState(session, accountId); - log('RenameMailboxInteractor::execute:currentMailboxState: $currentMailboxState'); final result = await _mailboxRepository.renameMailbox(session, accountId, request); if (result) { - yield Right(RenameMailboxSuccess(currentMailboxState: currentMailboxState)); + yield Right(RenameMailboxSuccess()); } else { yield Left(RenameMailboxFailure(null)); } } catch (e) { - logError('RenameMailboxInteractor::execute(): error: $e'); final exception = SetMailboxNameException.detectMailboxNameException(e, request.mailboxId); yield Left(RenameMailboxFailure(exception)); } diff --git a/lib/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart b/lib/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart index 97bf45ddea..5c79d586bd 100644 --- a/lib/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart +++ b/lib/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart @@ -1,14 +1,7 @@ import 'package:core/presentation/state/failure.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; -import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; +import 'package:core/presentation/state/success.dart'; -class RemoveEmailDraftsSuccess extends UIActionState { - - RemoveEmailDraftsSuccess({ - jmap.State? currentEmailState, - jmap.State? currentMailboxState, - }) : super(currentEmailState, currentMailboxState); -} +class RemoveEmailDraftsSuccess extends UIState {} class RemoveEmailDraftsFailure extends FeatureFailure { diff --git a/lib/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart b/lib/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart index 291ef7cd21..a832a29a1b 100644 --- a/lib/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart +++ b/lib/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart @@ -1,33 +1,22 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart'; class RemoveEmailDraftsInteractor { final EmailRepository _emailRepository; - final MailboxRepository _mailboxRepository; - RemoveEmailDraftsInteractor(this._emailRepository, this._mailboxRepository); + RemoveEmailDraftsInteractor(this._emailRepository); Stream> execute(Session session, AccountId accountId, EmailId emailId) async* { try { - final listState = await Future.wait([ - _mailboxRepository.getMailboxState(session, accountId), - _emailRepository.getEmailState(session, accountId), - ], eagerError: true); - - final currentMailboxState = listState.first; - final currentEmailState = listState.last; - final result = await _emailRepository.removeEmailDrafts(session, accountId, emailId); if (result) { - yield Right(RemoveEmailDraftsSuccess( - currentEmailState: currentEmailState, - currentMailboxState: currentMailboxState)); + yield Right(RemoveEmailDraftsSuccess()); } else { yield Left(RemoveEmailDraftsFailure(result)); } diff --git a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart index 22dc4b10be..57dbd600a0 100644 --- a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart +++ b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart @@ -275,53 +275,30 @@ class MailboxDashBoardBindings extends BaseBindings { @override void bindingsInteractor() { - Get.lazyPut(() => RemoveEmailDraftsInteractor( - Get.find(), - Get.find())); - Get.lazyPut(() => MoveToMailboxInteractor( - Get.find(), - Get.find())); - Get.lazyPut(() => DeleteEmailPermanentlyInteractor( - Get.find(), - Get.find())); + Get.lazyPut(() => RemoveEmailDraftsInteractor(Get.find())); + Get.lazyPut(() => MoveToMailboxInteractor(Get.find())); + Get.lazyPut(() => DeleteEmailPermanentlyInteractor(Get.find())); Get.lazyPut(() => SaveRecentSearchInteractor(Get.find())); Get.lazyPut(() => GetAllRecentSearchLatestInteractor(Get.find())); Get.lazyPut(() => SearchEmailInteractor(Get.find())); Get.lazyPut(() => SearchMoreEmailInteractor(Get.find())); Get.lazyPut(() => RefreshChangesSearchEmailInteractor(Get.find())); Get.lazyPut(() => QuickSearchEmailInteractor(Get.find())); - Get.lazyPut(() => MarkAsMailboxReadInteractor( - Get.find(), - Get.find()) - ); + Get.lazyPut(() => MarkAsMailboxReadInteractor(Get.find())); Get.lazyPut(() => GetComposerCacheOnWebInteractor(Get.find())); Get.lazyPut(() => RemoveComposerCacheOnWebInteractor(Get.find())); - Get.lazyPut(() => MarkAsEmailReadInteractor( - Get.find(), - Get.find() - )); + Get.lazyPut(() => MarkAsEmailReadInteractor(Get.find())); Get.lazyPut(() => MarkAsStarEmailInteractor(Get.find())); Get.lazyPut(() => MarkAsMultipleEmailReadInteractor( Get.find(), - Get.find() )); Get.lazyPut(() => MarkAsStarMultipleEmailInteractor(Get.find())); Get.lazyPut(() => MoveMultipleEmailToMailboxInteractor( Get.find(), - Get.find() - )); - Get.lazyPut(() => DeleteMultipleEmailsPermanentlyInteractor( - Get.find(), - Get.find())); - Get.lazyPut(() => EmptyTrashFolderInteractor( - Get.find(), - Get.find(), - Get.find())); - Get.lazyPut(() => EmptySpamFolderInteractor( - Get.find(), - Get.find(), - Get.find() )); + Get.lazyPut(() => DeleteMultipleEmailsPermanentlyInteractor(Get.find())); + Get.lazyPut(() => EmptyTrashFolderInteractor(Get.find())); + Get.lazyPut(() => EmptySpamFolderInteractor(Get.find())); Get.lazyPut(() => GetAppDashboardConfigurationInteractor( Get.find())); Get.lazyPut(() => GetEmailByIdInteractor( @@ -336,17 +313,11 @@ class MailboxDashBoardBindings extends BaseBindings { Get.lazyPut(() => GetSpamReportStateInteractor( Get.find())); Get.lazyPut(() => GetSpamMailboxCachedInteractor(Get.find())); - Get.lazyPut(() => SendEmailInteractor( - Get.find(), - Get.find(), - )); + Get.lazyPut(() => SendEmailInteractor(Get.find())); SendingQueueInteractorBindings().dependencies(); Get.lazyPut(() => StoreSessionInteractor(Get.find())); Get.lazyPut(() => UnsubscribeEmailInteractor(Get.find())); - Get.lazyPut(() => RestoredDeletedMessageInteractor( - Get.find(), - Get.find() - )); + Get.lazyPut(() => RestoredDeletedMessageInteractor(Get.find())); Get.lazyPut(() => GetRestoredDeletedMessageInterator( Get.find(), Get.find() diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index d97e43b1ae..604724c944 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -404,7 +404,6 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo } else if (success is UnsubscribeEmailSuccess) { _handleUnsubscribeMailSuccess(); } else if (success is RestoreDeletedMessageSuccess) { - dispatchMailboxUIAction(RefreshChangeMailboxAction(success.currentMailboxState)); _handleRestoreDeletedMessageSuccess(success.emailRecoveryAction.id!); } else if (success is GetRestoredDeletedMessageSuccess) { _handleGetRestoredDeletedMessageSuccess(success); diff --git a/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart b/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart index dc8e65f8fb..f5983336bb 100644 --- a/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart +++ b/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart @@ -96,9 +96,7 @@ class SendEmailInteractorBindings extends InteractorsBindings { @override void bindingsInteractor() { - Get.lazyPut(() => SendEmailInteractor( - Get.find(), - Get.find())); + Get.lazyPut(() => SendEmailInteractor(Get.find())); } @override diff --git a/lib/features/search/email/presentation/search_email_controller.dart b/lib/features/search/email/presentation/search_email_controller.dart index 2bed6ef896..ccd7b79f7b 100644 --- a/lib/features/search/email/presentation/search_email_controller.dart +++ b/lib/features/search/email/presentation/search_email_controller.dart @@ -294,11 +294,6 @@ class SearchEmailController extends BaseController ? UnsignedInt(listResultSearch.length) : ThreadConstants.defaultLimit; - if (limit.value > ThreadConstants.maximumEmailQueryLimit && - resultSearchScrollController.hasClients) { - resultSearchScrollController.jumpTo(0); - } - _updateSimpleSearchFilter( beforeOption: const None(), positionOption: option(searchEmailFilter.value.sortOrderType.isScrollByPosition(), 0), diff --git a/lib/features/thread/domain/constants/thread_constants.dart b/lib/features/thread/domain/constants/thread_constants.dart index 9a5618b929..2e5791697e 100644 --- a/lib/features/thread/domain/constants/thread_constants.dart +++ b/lib/features/thread/domain/constants/thread_constants.dart @@ -5,7 +5,6 @@ import 'package:model/email/email_property.dart'; class ThreadConstants { static const maxCountEmails = 20; - static const maximumEmailQueryLimit = 256; static final defaultLimit = UnsignedInt(maxCountEmails); static final propertiesDefault = Properties({ EmailProperty.id, diff --git a/lib/features/thread/domain/state/empty_spam_folder_state.dart b/lib/features/thread/domain/state/empty_spam_folder_state.dart index f0bfbad412..fedb07ae34 100644 --- a/lib/features/thread/domain/state/empty_spam_folder_state.dart +++ b/lib/features/thread/domain/state/empty_spam_folder_state.dart @@ -1,23 +1,17 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; class EmptySpamFolderLoading extends LoadingState {} -class EmptySpamFolderSuccess extends UIActionState { +class EmptySpamFolderSuccess extends UIState { final List emailIds; - EmptySpamFolderSuccess( - this.emailIds, { - jmap.State? currentEmailState, - jmap.State? currentMailboxState, - }) : super(currentEmailState, currentMailboxState); + EmptySpamFolderSuccess(this.emailIds); @override - List get props => [emailIds, ...super.props]; + List get props => [emailIds]; } class EmptySpamFolderFailure extends FeatureFailure { diff --git a/lib/features/thread/domain/state/empty_trash_folder_state.dart b/lib/features/thread/domain/state/empty_trash_folder_state.dart index 8f9c6b62fc..577eb6d7cc 100644 --- a/lib/features/thread/domain/state/empty_trash_folder_state.dart +++ b/lib/features/thread/domain/state/empty_trash_folder_state.dart @@ -1,23 +1,17 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; class EmptyTrashFolderLoading extends LoadingState {} -class EmptyTrashFolderSuccess extends UIActionState { +class EmptyTrashFolderSuccess extends UIState { final List emailIds; - EmptyTrashFolderSuccess( - this.emailIds, { - jmap.State? currentEmailState, - jmap.State? currentMailboxState, - }) : super(currentEmailState, currentMailboxState); + EmptyTrashFolderSuccess(this.emailIds); @override - List get props => [emailIds, ...super.props]; + List get props => [emailIds]; } class EmptyTrashFolderFailure extends FeatureFailure { diff --git a/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart b/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart index 1c67a8212e..9134f49b2b 100644 --- a/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart +++ b/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart @@ -1,26 +1,20 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:model/email/read_actions.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; -import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; class LoadingMarkAsMultipleEmailReadAll extends UIState {} -class MarkAsMultipleEmailReadAllSuccess extends UIActionState { +class MarkAsMultipleEmailReadAllSuccess extends UIState { final int countMarkAsReadSuccess; final ReadActions readActions; MarkAsMultipleEmailReadAllSuccess( this.countMarkAsReadSuccess, this.readActions, - { - jmap.State? currentEmailState, - jmap.State? currentMailboxState, - } - ) : super(currentEmailState, currentMailboxState); + ); @override - List get props => [countMarkAsReadSuccess, readActions, ...super.props]; + List get props => [countMarkAsReadSuccess, readActions]; } class MarkAsMultipleEmailReadAllFailure extends FeatureFailure { @@ -32,21 +26,17 @@ class MarkAsMultipleEmailReadAllFailure extends FeatureFailure { List get props => [readActions]; } -class MarkAsMultipleEmailReadHasSomeEmailFailure extends UIActionState { +class MarkAsMultipleEmailReadHasSomeEmailFailure extends UIState { final int countMarkAsReadSuccess; final ReadActions readActions; MarkAsMultipleEmailReadHasSomeEmailFailure( this.countMarkAsReadSuccess, this.readActions, - { - jmap.State? currentEmailState, - jmap.State? currentMailboxState, - } - ) : super(currentEmailState, currentMailboxState); + ); @override - List get props => [countMarkAsReadSuccess, readActions, ...super.props]; + List get props => [countMarkAsReadSuccess, readActions]; } class MarkAsMultipleEmailReadFailure extends FeatureFailure { diff --git a/lib/features/thread/domain/state/mark_as_star_multiple_email_state.dart b/lib/features/thread/domain/state/mark_as_star_multiple_email_state.dart index a284194f2b..b41a891546 100644 --- a/lib/features/thread/domain/state/mark_as_star_multiple_email_state.dart +++ b/lib/features/thread/domain/state/mark_as_star_multiple_email_state.dart @@ -1,26 +1,20 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:model/email/mark_star_action.dart'; -import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; class LoadingMarkAsStarMultipleEmailAll extends UIState {} -class MarkAsStarMultipleEmailAllSuccess extends UIActionState { +class MarkAsStarMultipleEmailAllSuccess extends UIState { final int countMarkStarSuccess; final MarkStarAction markStarAction; MarkAsStarMultipleEmailAllSuccess( this.countMarkStarSuccess, this.markStarAction, - { - jmap.State? currentEmailState, - jmap.State? currentMailboxState, - } - ) : super(currentEmailState, currentMailboxState); + ); @override - List get props => [countMarkStarSuccess, markStarAction, ...super.props]; + List get props => [countMarkStarSuccess, markStarAction]; } class MarkAsStarMultipleEmailAllFailure extends FeatureFailure { @@ -32,21 +26,17 @@ class MarkAsStarMultipleEmailAllFailure extends FeatureFailure { List get props => [markStarAction]; } -class MarkAsStarMultipleEmailHasSomeEmailFailure extends UIActionState { +class MarkAsStarMultipleEmailHasSomeEmailFailure extends UIState { final int countMarkStarSuccess; final MarkStarAction markStarAction; MarkAsStarMultipleEmailHasSomeEmailFailure( this.countMarkStarSuccess, this.markStarAction, - { - jmap.State? currentEmailState, - jmap.State? currentMailboxState, - } - ) : super(currentEmailState, currentMailboxState); + ); @override - List get props => [countMarkStarSuccess, markStarAction, ...super.props]; + List get props => [countMarkStarSuccess, markStarAction]; } class MarkAsStarMultipleEmailFailure extends FeatureFailure { diff --git a/lib/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart b/lib/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart index 283bdc464f..b4f179a334 100644 --- a/lib/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart +++ b/lib/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart @@ -1,15 +1,13 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/email_action_type.dart'; -import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; class LoadingMoveMultipleEmailToMailboxAll extends UIState {} -class MoveMultipleEmailToMailboxAllSuccess extends UIActionState { +class MoveMultipleEmailToMailboxAllSuccess extends UIState { final List movedListEmailId; final MailboxId currentMailboxId; final MailboxId destinationMailboxId; @@ -25,10 +23,8 @@ class MoveMultipleEmailToMailboxAllSuccess extends UIActionState { this.emailActionType, { this.destinationPath, - jmap.State? currentEmailState, - jmap.State? currentMailboxState, } - ) : super(currentEmailState, currentMailboxState); + ); @override List get props => [ @@ -38,7 +34,6 @@ class MoveMultipleEmailToMailboxAllSuccess extends UIActionState { moveAction, emailActionType, destinationPath, - ...super.props ]; } @@ -52,7 +47,7 @@ class MoveMultipleEmailToMailboxAllFailure extends FeatureFailure { List get props => [moveAction, emailActionType]; } -class MoveMultipleEmailToMailboxHasSomeEmailFailure extends UIActionState { +class MoveMultipleEmailToMailboxHasSomeEmailFailure extends UIState { final List movedListEmailId; final MailboxId currentMailboxId; final MailboxId destinationMailboxId; @@ -68,10 +63,8 @@ class MoveMultipleEmailToMailboxHasSomeEmailFailure extends UIActionState { this.emailActionType, { this.destinationPath, - jmap.State? currentEmailState, - jmap.State? currentMailboxState, } - ) : super(currentEmailState, currentMailboxState); + ); @override List get props => [ @@ -81,7 +74,6 @@ class MoveMultipleEmailToMailboxHasSomeEmailFailure extends UIActionState { moveAction, emailActionType, destinationPath, - ...super.props ]; } diff --git a/lib/features/thread/domain/usecases/empty_spam_folder_interactor.dart b/lib/features/thread/domain/usecases/empty_spam_folder_interactor.dart index 38e864a947..601998a4cc 100644 --- a/lib/features/thread/domain/usecases/empty_spam_folder_interactor.dart +++ b/lib/features/thread/domain/usecases/empty_spam_folder_interactor.dart @@ -4,40 +4,19 @@ import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; class EmptySpamFolderInteractor { final ThreadRepository threadRepository; - final MailboxRepository _mailboxRepository; - final EmailRepository _emailRepository; - EmptySpamFolderInteractor( - this.threadRepository, - this._mailboxRepository, - this._emailRepository - ); + EmptySpamFolderInteractor(this.threadRepository); Stream> execute(Session session, AccountId accountId, MailboxId spamMailboxId) async* { try { yield Right(EmptySpamFolderLoading()); - - final listState = await Future.wait([ - _mailboxRepository.getMailboxState(session, accountId), - _emailRepository.getEmailState(session, accountId), - ], eagerError: true); - - final currentMailboxState = listState.first; - final currentEmailState = listState.last; - final emailIdDeleted = await threadRepository.emptySpamFolder(session, accountId, spamMailboxId); - yield Right(EmptySpamFolderSuccess( - emailIdDeleted, - currentMailboxState: currentMailboxState, - currentEmailState: currentEmailState, - )); + yield Right(EmptySpamFolderSuccess(emailIdDeleted)); } catch (e) { yield Left(EmptySpamFolderFailure(e)); } diff --git a/lib/features/thread/domain/usecases/empty_trash_folder_interactor.dart b/lib/features/thread/domain/usecases/empty_trash_folder_interactor.dart index 4a22143ed4..5a540c8b91 100644 --- a/lib/features/thread/domain/usecases/empty_trash_folder_interactor.dart +++ b/lib/features/thread/domain/usecases/empty_trash_folder_interactor.dart @@ -1,42 +1,22 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; class EmptyTrashFolderInteractor { final ThreadRepository threadRepository; - final MailboxRepository _mailboxRepository; - final EmailRepository _emailRepository; - EmptyTrashFolderInteractor( - this.threadRepository, - this._mailboxRepository, - this._emailRepository - ); + EmptyTrashFolderInteractor(this.threadRepository); Stream> execute(Session session, AccountId accountId, MailboxId trashMailboxId) async* { try { yield Right(EmptyTrashFolderLoading()); - - final listState = await Future.wait([ - _mailboxRepository.getMailboxState(session, accountId), - _emailRepository.getEmailState(session, accountId), - ], eagerError: true); - - final currentMailboxState = listState.first; - final currentEmailState = listState.last; - final emailIdDeleted = await threadRepository.emptyTrashFolder(session, accountId, trashMailboxId); - yield Right(EmptyTrashFolderSuccess( - emailIdDeleted, - currentMailboxState: currentMailboxState, - currentEmailState: currentEmailState, - )); + yield Right(EmptyTrashFolderSuccess(emailIdDeleted,)); } catch (e) { yield Left(EmptyTrashFolderFailure(e)); } diff --git a/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart b/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart index 68cae75621..08779f9641 100644 --- a/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart +++ b/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart @@ -1,18 +1,17 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; class MarkAsMultipleEmailReadInteractor { final EmailRepository _emailRepository; - final MailboxRepository _mailboxRepository; - MarkAsMultipleEmailReadInteractor(this._emailRepository, this._mailboxRepository); + MarkAsMultipleEmailReadInteractor(this._emailRepository); Stream> execute( Session session, @@ -23,14 +22,6 @@ class MarkAsMultipleEmailReadInteractor { try { yield Right(LoadingMarkAsMultipleEmailReadAll()); - final listState = await Future.wait([ - _mailboxRepository.getMailboxState(session, accountId), - _emailRepository.getEmailState(session, accountId), - ], eagerError: true); - - final currentMailboxState = listState.first; - final currentEmailState = listState.last; - final result = await _emailRepository.markAsRead( session, accountId, @@ -42,16 +33,14 @@ class MarkAsMultipleEmailReadInteractor { yield Right(MarkAsMultipleEmailReadAllSuccess( result.length, readAction, - currentEmailState: currentEmailState, - currentMailboxState: currentMailboxState)); + )); } else if (result.isEmpty) { yield Left(MarkAsMultipleEmailReadAllFailure(readAction)); } else { yield Right(MarkAsMultipleEmailReadHasSomeEmailFailure( result.length, readAction, - currentEmailState: currentEmailState, - currentMailboxState: currentMailboxState)); + )); } } catch (e) { yield Left(MarkAsMultipleEmailReadFailure(readAction, e)); diff --git a/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart b/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart index 840056fc48..6f828426b6 100644 --- a/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart +++ b/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart @@ -22,22 +22,20 @@ class MarkAsStarMultipleEmailInteractor { try { yield Right(LoadingMarkAsStarMultipleEmailAll()); - final currentEmailState = await _emailRepository.getEmailState(session, accountId); - final result = await _emailRepository.markAsStar(session, accountId, emailIds, markStarAction); if (emailIds.length == result.length) { yield Right(MarkAsStarMultipleEmailAllSuccess( emailIds.length, markStarAction, - currentEmailState: currentEmailState)); + )); } else if (result.isEmpty) { yield Left(MarkAsStarMultipleEmailAllFailure(markStarAction)); } else { yield Right(MarkAsStarMultipleEmailHasSomeEmailFailure( result.length, markStarAction, - currentEmailState: currentEmailState)); + )); } } catch (e) { yield Left(MarkAsStarMultipleEmailFailure(markStarAction, e)); diff --git a/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart b/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart index 0069c1b9ec..19bf07647e 100644 --- a/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart +++ b/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart @@ -1,19 +1,18 @@ import 'dart:async'; -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; class MoveMultipleEmailToMailboxInteractor { final EmailRepository _emailRepository; - final MailboxRepository _mailboxRepository; - MoveMultipleEmailToMailboxInteractor(this._emailRepository, this._mailboxRepository); + MoveMultipleEmailToMailboxInteractor(this._emailRepository); Stream> execute( Session session, @@ -22,20 +21,8 @@ class MoveMultipleEmailToMailboxInteractor { ) async* { try { yield Right(LoadingMoveMultipleEmailToMailboxAll()); - - final listState = await Future.wait([ - _mailboxRepository.getMailboxState(session, accountId), - _emailRepository.getEmailState(session, accountId), - ], eagerError: true); - - final currentMailboxState = listState.first; - final currentEmailState = listState.last; - final result = await _emailRepository.moveToMailbox(session, accountId, moveRequest); - int totalEmail = 0; - for (var element in moveRequest.currentMailboxes.values) { - totalEmail = totalEmail + element.length; - }if (totalEmail == result.length) { + if (moveRequest.totalEmails == result.length) { yield Right(MoveMultipleEmailToMailboxAllSuccess( result, moveRequest.currentMailboxes.keys.first, @@ -43,8 +30,7 @@ class MoveMultipleEmailToMailboxInteractor { moveRequest.moveAction, moveRequest.emailActionType, destinationPath: moveRequest.destinationPath, - currentEmailState: currentEmailState, - currentMailboxState: currentMailboxState)); + )); } else if (result.isEmpty) { yield Left(MoveMultipleEmailToMailboxAllFailure(moveRequest.moveAction, moveRequest.emailActionType)); } else { @@ -55,8 +41,7 @@ class MoveMultipleEmailToMailboxInteractor { moveRequest.moveAction, moveRequest.emailActionType, destinationPath: moveRequest.destinationPath, - currentEmailState: currentEmailState, - currentMailboxState: currentMailboxState)); + )); } } catch (e) { yield Left(MoveMultipleEmailToMailboxFailure(moveRequest.emailActionType, moveRequest.moveAction, e)); diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index f3d2fa6722..278a37dca3 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -559,10 +559,6 @@ class ThreadController extends BaseController with EmailActionController { Future _refreshChangeSearchEmail() async { log('ThreadController::_refreshChangeSearchEmail:'); - if (limitEmailFetched.value > ThreadConstants.maximumEmailQueryLimit && - listEmailController.hasClients) { - listEmailController.jumpTo(0); - } canSearchMore = false; searchController.updateFilterEmail( positionOption: option( diff --git a/test/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor_test.dart b/test/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor_test.dart index 0869982c1e..1b0f1805c0 100644 --- a/test/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor_test.dart +++ b/test/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor_test.dart @@ -8,7 +8,6 @@ import 'package:tmail_ui_user/features/composer/domain/repository/composer_repos import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import '../../../../fixtures/account_fixtures.dart'; import '../../../../fixtures/session_fixtures.dart'; @@ -16,22 +15,18 @@ import 'create_new_and_save_email_to_drafts_interactor_test.mocks.dart'; @GenerateNiceMocks([ MockSpec(), - MockSpec(), MockSpec(), ]) void main() { late MockEmailRepository emailRepository; - late MockMailboxRepository mailboxRepository; late MockComposerRepository composerRepository; late CreateNewAndSaveEmailToDraftsInteractor createNewAndSaveEmailToDraftsInteractor; setUp(() { emailRepository = MockEmailRepository(); - mailboxRepository = MockMailboxRepository(); composerRepository = MockComposerRepository(); createNewAndSaveEmailToDraftsInteractor = CreateNewAndSaveEmailToDraftsInteractor( emailRepository, - mailboxRepository, composerRepository); }); diff --git a/test/features/composer/domain/usecases/create_new_and_send_email_interactor_test.dart b/test/features/composer/domain/usecases/create_new_and_send_email_interactor_test.dart index 72027e6f49..f5bac53621 100644 --- a/test/features/composer/domain/usecases/create_new_and_send_email_interactor_test.dart +++ b/test/features/composer/domain/usecases/create_new_and_send_email_interactor_test.dart @@ -7,7 +7,6 @@ import 'package:tmail_ui_user/features/composer/domain/repository/composer_repos import 'package:tmail_ui_user/features/composer/domain/usecases/create_new_and_send_email_interactor.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/create_email_request.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import '../../../../fixtures/account_fixtures.dart'; import '../../../../fixtures/session_fixtures.dart'; @@ -15,16 +14,13 @@ import 'create_new_and_send_email_interactor_test.mocks.dart'; @GenerateNiceMocks([ MockSpec(), - MockSpec(), MockSpec(), ]) void main() { final emailRepository = MockEmailRepository(); - final mailboxRepository = MockMailboxRepository(); final composerRepository = MockComposerRepository(); final createNewAndSendEmailInteractor = CreateNewAndSendEmailInteractor( emailRepository, - mailboxRepository, composerRepository); group('create new and send email interactor test:', () { test( diff --git a/test/features/email/domain/usecases/store_event_attendance_status_interactor_test.dart b/test/features/email/domain/usecases/store_event_attendance_status_interactor_test.dart index 3deda8c8c6..096c62f918 100644 --- a/test/features/email/domain/usecases/store_event_attendance_status_interactor_test.dart +++ b/test/features/email/domain/usecases/store_event_attendance_status_interactor_test.dart @@ -1,6 +1,5 @@ import 'package:dartz/dartz.dart' hide State; import 'package:flutter_test/flutter_test.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -34,13 +33,8 @@ void main() { const eventActionType = EventActionType.yes; test('SHOULD emit loading and success states when storeEventAttendanceStatus is successful', () async { - final currentEmailState = State('abc123'); final updatedEmail = Email(id: emailIdFixture); - when(mockEmailRepository.getEmailState( - sessionFixture, - accountIdFixture - )).thenAnswer((_) async => currentEmailState); when(mockEmailRepository.storeEventAttendanceStatus( sessionFixture, accountIdFixture, @@ -58,13 +52,10 @@ void main() { result, emitsInOrder([ Right(StoreEventAttendanceStatusLoading()), - Right(StoreEventAttendanceStatusSuccess( - eventActionType, - currentEmailState: currentEmailState)), + Right(StoreEventAttendanceStatusSuccess(eventActionType)), ]), ); - verify(mockEmailRepository.getEmailState(sessionFixture, accountIdFixture)).called(1); verify(mockEmailRepository.storeEventAttendanceStatus( sessionFixture, accountIdFixture, @@ -77,9 +68,11 @@ void main() { test('SHOULD emit loading and failure states when storeEventAttendanceStatus throws an exception', () async { final exception = Exception(); - when(mockEmailRepository.getEmailState( + when(mockEmailRepository.storeEventAttendanceStatus( sessionFixture, - accountIdFixture + accountIdFixture, + emailIdFixture, + eventActionType, )).thenThrow(exception); final result = storeEventAttendanceStatusInteractor.execute( @@ -95,12 +88,6 @@ void main() { Left(StoreEventAttendanceStatusFailure(exception: exception)), ]), ); - - verify(mockEmailRepository.getEmailState( - sessionFixture, - accountIdFixture - )).called(1); - verifyNoMoreInteractions(mockEmailRepository); }); }); } \ No newline at end of file From 420df05711d1b0f47f7528a312098e65a95b64f4 Mon Sep 17 00:00:00 2001 From: Dat PHAM HOANG Date: Mon, 23 Dec 2024 22:41:36 +0700 Subject: [PATCH 21/72] TF-3334 Add more concurrence test cases for WebSocketQueueHandler --- .../mailbox_dashboard_controller.dart | 4 - .../websocket/web_socket_queue_handler.dart | 51 +++- .../presentation/thread_controller.dart | 2 - .../web_socket_queue_handler_test.dart | 276 ++++++++++++++++++ 4 files changed, 311 insertions(+), 22 deletions(-) diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 604724c944..a288c35ad3 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -1465,10 +1465,6 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo void dispatchRoute(DashboardRoutes route) { log('MailboxDashBoardController::dispatchRoute(): $route'); dashboardRoute.value = route; - - if (dashboardRoute.value == DashboardRoutes.searchEmail) { - searchController.activateSimpleSearch(); - } } @override diff --git a/lib/features/push_notification/presentation/websocket/web_socket_queue_handler.dart b/lib/features/push_notification/presentation/websocket/web_socket_queue_handler.dart index 455473983b..34855be4ad 100644 --- a/lib/features/push_notification/presentation/websocket/web_socket_queue_handler.dart +++ b/lib/features/push_notification/presentation/websocket/web_socket_queue_handler.dart @@ -38,11 +38,16 @@ class WebSocketQueueHandler { return; } - if (queueSize >= _maxQueueSize) { - log('WebSocketQueueHandler::enqueue:Queue full, removing oldest message'); - _messageQueue.removeFirst(); + try { + if (queueSize >= _maxQueueSize) { + log('WebSocketQueueHandler::enqueue:Queue full, removing oldest message'); + _messageQueue.removeFirst(); + } + } catch (e) { + logError('WebSocketQueueHandler::enqueue:Exception = $e'); } + log('WebSocketQueueHandler::enqueue(): ${message.id}'); _messageQueue.add(message); _queueController.add(message); } @@ -57,6 +62,7 @@ class WebSocketQueueHandler { try { while (queueSize > 0) { final message = _messageQueue.removeFirst(); + log('WebSocketQueueHandler::_processQueue(): processing message ${message.id}'); try { await processMessageCallback(message); @@ -78,27 +84,40 @@ class WebSocketQueueHandler { } void _addToProcessedMessages(String messageId) { - if (_processedMessageIds.length >= _maxProcessedIdsSize) { - _processedMessageIds.removeFirst(); + log('WebSocketQueueHandler::_addToProcessedMessages(): adding message $messageId to processed messages'); + try { + if (_processedMessageIds.length >= _maxProcessedIdsSize) { + _processedMessageIds.removeFirst(); + } + } catch (e) { + logError('WebSocketQueueHandler::_addToProcessedMessages:Exception = $e'); } + _processedMessageIds.add(messageId); } void removeMessagesUpToCurrent(String messageId) { - final isCurrentStateExist = _messageQueue - .any((message) => message.id == messageId); + try { + log('WebSocketQueueHandler::removeMessagesUpToCurrent(): removing messages up to $messageId'); + final isCurrentStateExist = _messageQueue + .any((message) => message.id == messageId); - if (!isCurrentStateExist) { - log('WebSocketQueueHandler::removeMessagesUpToCurrent:Current state $messageId not found in the queue.'); - return; - } - while (queueSize > 0) { - final removedMessage = _messageQueue.removeFirst(); - if (removedMessage.id == messageId) { - break; + if (!isCurrentStateExist) { + log('WebSocketQueueHandler::removeMessagesUpToCurrent:Current state $messageId not found in the queue.'); + return; + } + + while (queueSize > 0) { + final removedMessage = _messageQueue.removeFirst(); + log('WebSocketQueueHandler::removeMessagesUpToCurrent(): removing message ${removedMessage.id} up to $messageId'); + if (removedMessage.id == messageId) { + break; + } } + log('WebSocketQueueHandler::removeMessagesUpToCurrent:Updated Queue: $queueSize'); + } catch (e) { + logError('WebSocketQueueHandler::removeMessagesUpToCurrent:Exception = $e'); } - log('WebSocketQueueHandler::removeMessagesUpToCurrent:Updated Queue: $queueSize'); } @visibleForTesting diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 278a37dca3..fd0e310265 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -567,8 +567,6 @@ class ThreadController extends BaseController with EmailActionController { ), beforeOption: const None(), ); - searchController.activateSimpleSearch(); - final searchViewState = await _searchEmailInteractor.execute( _session!, _accountId!, diff --git a/test/features/push_notification/presentation/websocket/web_socket_queue_handler_test.dart b/test/features/push_notification/presentation/websocket/web_socket_queue_handler_test.dart index 3f95a6ee80..4a72deb950 100644 --- a/test/features/push_notification/presentation/websocket/web_socket_queue_handler_test.dart +++ b/test/features/push_notification/presentation/websocket/web_socket_queue_handler_test.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:collection'; import 'package:flutter_test/flutter_test.dart'; @@ -32,6 +33,15 @@ void main() { group('Basic Operations', () { late WebSocketQueueHandler handler; + late List processedMessages; + + setUp(() { + processedMessages = []; + }); + + tearDown(() { + handler.dispose(); + }); setUp(() { handler = createHandler( @@ -55,6 +65,30 @@ void main() { expect(processedMessages, containsAllInOrder(['0', '1', '2', '3', '4'])); }); + test('Duplicate messages should be skipped', () async { + final message = MockWebSocketMessage('duplicate_msg'); + + handler.enqueue(message); + await handler.waitForEmpty(); + + handler.enqueue(message); + await handler.waitForEmpty(); + + expect(processedMessages.length, equals(1)); + expect(processedMessages, equals(['duplicate_msg'])); + }); + + test('Queue size should not exceed maximum size', () async { + // Enqueue more messages than the max queue size + final messages = List.generate(130, (i) => MockWebSocketMessage('msg_$i')); + + for (var message in messages) { + handler.enqueue(message); + } + + expect(handler.queueSize, lessThanOrEqualTo(128)); + }); + test('Should correctly remove messages up to specified ID', () async { final messages = List.generate(5, (index) => MockWebSocketMessage('$index')); @@ -73,8 +107,86 @@ void main() { }); }); + group('Queue Size Management Tests', () { + test('Queue should drop oldest message when full', () async { + late List errors = []; + + final handler = WebSocketQueueHandler( + processMessageCallback: (message) async { + await Future.delayed(const Duration(milliseconds: 10)); // Simulate processing time + processedMessages.add(message.id); + }, + onErrorCallback: (error, stackTrace) { + errors.add(error); + }, + ); + + // Fill the queue to maximum capacity (128) + for (var i = 0; i < 128; i++) { + handler.enqueue(MockWebSocketMessage('msg_$i')); + } + + expect(handler.queueSize, equals(128)); + + // Add one more message + handler.enqueue(MockWebSocketMessage('msg_128')); + + // Queue size should still be 128 + expect(handler.queueSize, equals(128)); + + // Process all messages + await handler.waitForEmpty(); + + // Verify that msg_0 (the oldest) was dropped and msg_128 (newest) was processed + expect(processedMessages.contains('msg_0'), isFalse); + expect(processedMessages.contains('msg_1'), isTrue); + expect(processedMessages.contains('msg_127'), isTrue); + expect(processedMessages.contains('msg_128'), isTrue); + }); + + test('Queue should maintain size limit during message removal', () async { + final processedIds = []; + late List errors = []; + + final handler = WebSocketQueueHandler( + processMessageCallback: (message) async { + await Future.delayed(const Duration(milliseconds: 10)); + processedIds.add(message.id); + }, + onErrorCallback: (error, stackTrace) { + errors.add(error); + }, + ); + + // Fill queue to capacity + for (var i = 0; i < 128; i++) { + handler.enqueue(MockWebSocketMessage('msg_$i')); + } + + // Remove messages up to msg_64 + handler.removeMessagesUpToCurrent('msg_64'); + + // Add new messages to fill the queue again + for (var i = 128; i < 192; i++) { + handler.enqueue(MockWebSocketMessage('msg_$i')); + } + + expect(handler.queueSize, lessThanOrEqualTo(128), + reason: 'Queue size should not exceed maximum after removal and refill'); + + await handler.waitForEmpty(); + + // Verify that earlier messages were properly removed + for (var i = 0; i <= 64; i++) { + expect(processedMessages.contains('msg_$i'), isFalse, + reason: 'Message msg_$i should have been removed'); + } + }); + }); + group('Concurrent Operations', () { late WebSocketQueueHandler handler; + late List errors; setUp(() { handler = createHandler( @@ -107,6 +219,169 @@ void main() { expect(processedMessages, containsAllInOrder(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'])); }); + + test('Should handle concurrent enqueueing while processing is blocked', () async { + final processingCompleter = Completer(); + final processedIds = []; + errors = []; + var processingStarted = Completer(); + + // Create handler with a processing delay to simulate long-running task + handler = WebSocketQueueHandler( + processMessageCallback: (message) async { + if (!processingStarted.isCompleted) { + processingStarted.complete(); + } + await processingCompleter.future; // Block processing + processedIds.add(message.id); + }, + onErrorCallback: (error, stackTrace) { + errors.add(error); + }, + ); + + // Enqueue first message to start processing + handler.enqueue(MockWebSocketMessage('initial_msg')); + + // Wait for processing to start + await processingStarted.future; + + // Concurrently enqueue messages while first message is still processing + await Future.wait( + List.generate(150, (i) => Future(() { + handler.enqueue(MockWebSocketMessage('concurrent_$i')); + })) + ); + + // Verify queue size is capped at max while processing is blocked + expect(handler.queueSize, lessThanOrEqualTo(128)); + + // Allow processing to continue + processingCompleter.complete(); + + await handler.waitForEmpty(); + // Verify process order and dropped messages + expect(processedIds[0], equals('initial_msg')); + expect(processedIds.length, lessThanOrEqualTo(129)); + expect(handler.isMessageProcessed('concurrent_149'), isTrue); + }); + + test('Should handle rapid enqueueing during active processing', () async { + final processedIds = []; + final processingStarted = Completer(); + final batchProcessing = Completer(); + errors = []; + + // Create handler with controlled processing delays + handler = WebSocketQueueHandler( + processMessageCallback: (message) async { + if (!processingStarted.isCompleted) { + processingStarted.complete(); + await batchProcessing.future; + } + processedIds.add(message.id); + }, + onErrorCallback: (error, stackTrace) { + errors.add(error); + }, + ); + + // Start with initial batch + for (var i = 0; i < 50; i++) { + handler.enqueue(MockWebSocketMessage('batch1_$i')); + } + + // Wait for the first message to start processing + await processingStarted.future; + + // Add second batch while first batch is blocked + for (var i = 0; i < 50; i++) { + handler.enqueue(MockWebSocketMessage('batch2_$i')); + } + + // Add third batch immediately + for (var i = 0; i < 50; i++) { + handler.enqueue(MockWebSocketMessage('batch3_$i')); + } + + // Allow processing to continue + batchProcessing.complete(); + + // Wait for queue to be empty + await handler.waitForEmpty(); + + // Verify results + expect(processedIds.length, lessThanOrEqualTo(129), + reason: 'Total processed messages should not exceed queue capacity'); + + // Check if we have messages from the latest batch + final lastBatchCount = processedIds + .where((id) => id.startsWith('batch3_')) + .length; + expect(lastBatchCount, greaterThan(0), + reason: 'Should have processed some messages from the latest batch'); + + // Verify that some early messages were dropped + final firstBatchCount = processedIds + .where((id) => id.startsWith('batch1_')) + .length; + expect(firstBatchCount, lessThan(50), + reason: 'Some messages from first batch should have been dropped'); + }); + + test('Should handle concurrent removeMessagesUpToCurrent during processing', () async { + final processedIds = []; + final processingDelay = Completer(); + errors = []; + + handler = WebSocketQueueHandler( + processMessageCallback: (message) async { + await processingDelay.future; + processedIds.add(message.id); + }, + onErrorCallback: (error, stackTrace) { + errors.add(error); + }, + ); + + // Fill queue + for (var i = 0; i < 128; i++) { + handler.enqueue(MockWebSocketMessage('msg_$i')); + } + + // Start concurrent operations + final futures = []; + + // Add new messages + futures.add(Future(() async { + for (var i = 128; i < 256; i++) { + handler.enqueue(MockWebSocketMessage('msg_$i')); + await Future.delayed(const Duration(microseconds: 100)); + } + })); + + // Concurrently remove messages + futures.add(Future(() async { + await Future.delayed(const Duration(milliseconds: 10)); + handler.removeMessagesUpToCurrent('msg_64'); + })); + + // Allow processing to continue after concurrent operations + await Future.delayed(const Duration(milliseconds: 50)); + processingDelay.complete(); + + await Future.wait(futures); + await handler.waitForEmpty(); + + expect(processedIds.length, lessThanOrEqualTo(192)); + + // Verify that messages after removal point were processed + for (var id in processedIds.skip(1)) { + final messageNumber = int.parse(id.split('_')[1]); + expect(messageNumber, greaterThan(64), + reason: 'Only messages after msg_64 should be processed'); + } + }); }); group('Error Handling', () { @@ -179,6 +454,7 @@ void main() { expect(processedMessages.length, burstSize); expect(processedMessages, List.generate(burstSize, (i) => '$i')); + expect(handler.queueSize, equals(0)); }); test('Should handle interleaved slow and fast messages', () async { From f2dd7071fd5e2297b04ff9d1ac3044b7511a66c5 Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 19 Dec 2024 16:51:30 +0700 Subject: [PATCH 22/72] TF-3333 Remove `Mailbox/query` & `Mailbox/get` when performing display spam banner --- .../spam_report_datasource_impl.dart | 73 ------------------- .../data/network/spam_report_api.dart | 60 --------------- .../spam_report_repository_impl.dart | 20 ----- .../repository/spam_report_repository.dart | 13 ---- ...et_number_of_unread_spam_emails_state.dart | 21 ------ .../state/get_spam_mailbox_cached_state.dart | 4 +- .../get_spam_mailbox_cached_interactor.dart | 1 - .../get_unread_spam_mailbox_interactor.dart | 56 -------------- .../bindings/mailbox_dashboard_bindings.dart | 13 ---- .../mailbox_dashboard_controller.dart | 20 +++-- .../controller/spam_report_controller.dart | 28 ++----- .../bindings/network/network_bindings.dart | 2 - 12 files changed, 25 insertions(+), 286 deletions(-) delete mode 100644 lib/features/mailbox_dashboard/data/datasource_impl/spam_report_datasource_impl.dart delete mode 100644 lib/features/mailbox_dashboard/data/network/spam_report_api.dart delete mode 100644 lib/features/mailbox_dashboard/domain/state/get_number_of_unread_spam_emails_state.dart delete mode 100644 lib/features/mailbox_dashboard/domain/usecases/get_unread_spam_mailbox_interactor.dart diff --git a/lib/features/mailbox_dashboard/data/datasource_impl/spam_report_datasource_impl.dart b/lib/features/mailbox_dashboard/data/datasource_impl/spam_report_datasource_impl.dart deleted file mode 100644 index d57d9799da..0000000000 --- a/lib/features/mailbox_dashboard/data/datasource_impl/spam_report_datasource_impl.dart +++ /dev/null @@ -1,73 +0,0 @@ - -import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/session/session.dart'; -import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; -import 'package:jmap_dart_client/jmap/core/user_name.dart'; -import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox_filter_condition.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource/spam_report_datasource.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/data/network/spam_report_api.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/spam_report_state.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/unread_spam_emails_response.dart'; -import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; - -class SpamReportDataSourceImpl extends SpamReportDataSource { - final SpamReportApi _spamReportApi; - final ExceptionThrower _exceptionThrower; - - SpamReportDataSourceImpl(this._spamReportApi, this._exceptionThrower); - - @override - Future deleteLastTimeDismissedSpamReported() { - throw UnimplementedError(); - } - - @override - Future findNumberOfUnreadSpamEmails( - Session session, - AccountId accountId, - { - MailboxFilterCondition? mailboxFilterCondition, - UnsignedInt? limit - } - ) { - return Future.sync(() async { - final unreadSpamEmailsResponse = await _spamReportApi.getUnreadSpamEmailbox( - session, - accountId, - mailboxFilterCondition: mailboxFilterCondition, - limit: limit); - return unreadSpamEmailsResponse; - }).catchError(_exceptionThrower.throwException); - } - - @override - Future getLastTimeDismissedSpamReported() { - throw UnimplementedError(); - } - - @override - Future storeLastTimeDismissedSpamReported(DateTime lastTimeDismissedSpamReported) { - throw UnimplementedError(); - } - - @override - Future deleteSpamReportState() { - throw UnimplementedError(); - } - - @override - Future getSpamReportState() { - throw UnimplementedError(); - } - - @override - Future storeSpamReportState(SpamReportState spamReportState) { - throw UnimplementedError(); - } - - @override - Future getSpamMailboxCached(AccountId accountId, UserName userName) { - throw UnimplementedError(); - } -} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/data/network/spam_report_api.dart b/lib/features/mailbox_dashboard/data/network/spam_report_api.dart deleted file mode 100644 index 06caecadb1..0000000000 --- a/lib/features/mailbox_dashboard/data/network/spam_report_api.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:jmap_dart_client/http/http_client.dart'; -import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/request/reference_path.dart'; -import 'package:jmap_dart_client/jmap/core/session/session.dart'; -import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; -import 'package:jmap_dart_client/jmap/jmap_request.dart'; -import 'package:jmap_dart_client/jmap/mail/mailbox/get/get_mailbox_method.dart'; -import 'package:jmap_dart_client/jmap/mail/mailbox/get/get_mailbox_response.dart'; -import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox_filter_condition.dart'; -import 'package:jmap_dart_client/jmap/mail/mailbox/query/query_mailbox_method.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/exceptions/spam_report_exception.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/unread_spam_emails_response.dart'; -import 'package:tmail_ui_user/main/error/capability_validator.dart'; - -class SpamReportApi { - final HttpClient _httpClient; - static const int _deafaultLimit = 1; - - SpamReportApi(this._httpClient); - - Future getUnreadSpamEmailbox( - Session session, - AccountId accountId, - { - MailboxFilterCondition? mailboxFilterCondition, - UnsignedInt? limit - } - ) async { - final processingInvocation = ProcessingInvocation(); - final requestBuilder = JmapRequestBuilder(_httpClient, processingInvocation); - final spamReportQueryMethod = QueryMailboxMethod(accountId)..addLimit(limit ?? UnsignedInt(_deafaultLimit)); - - if(mailboxFilterCondition != null) spamReportQueryMethod.addFilters(mailboxFilterCondition); - - final spamReportQueryMethodInvocation = requestBuilder.invocation(spamReportQueryMethod); - final getMailBoxMethod = GetMailboxMethod(accountId) - ..addReferenceIds(processingInvocation.createResultReference( - spamReportQueryMethodInvocation.methodCallId, - ReferencePath.idsPath, - )); - final getMailboxInvocation = requestBuilder.invocation(getMailBoxMethod); - - final capabilities = getMailBoxMethod.requiredCapabilities - .toCapabilitiesSupportTeamMailboxes(session, accountId); - - final result = await (requestBuilder - ..usings(capabilities)) - .build() - .execute(); - - final mailboxResponse = result - .parse(getMailboxInvocation.methodCallId, GetMailboxResponse.deserialize); - - if (mailboxResponse?.list.isNotEmpty == true) { - return UnreadSpamEmailsResponse(unreadSpamMailbox: mailboxResponse!.list.first); - } else { - throw NotFoundSpamMailboxException(); - } - } -} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/data/repository/spam_report_repository_impl.dart b/lib/features/mailbox_dashboard/data/repository/spam_report_repository_impl.dart index d1732e7e41..fe13707c02 100644 --- a/lib/features/mailbox_dashboard/data/repository/spam_report_repository_impl.dart +++ b/lib/features/mailbox_dashboard/data/repository/spam_report_repository_impl.dart @@ -1,13 +1,9 @@ import 'package:core/data/model/source_type/data_source_type.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/session/session.dart'; -import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox_filter_condition.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource/spam_report_datasource.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/spam_report_state.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/unread_spam_emails_response.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/spam_report_repository.dart'; class SpamReportRepositoryImpl extends SpamReportRepository { @@ -30,22 +26,6 @@ class SpamReportRepositoryImpl extends SpamReportRepository { return mapDataSource[DataSourceType.local]!.deleteLastTimeDismissedSpamReported(); } - @override - Future getUnreadSpamMailbox( - Session session, - AccountId accountId, - { - MailboxFilterCondition? mailboxFilterCondition, - UnsignedInt? limit - } - ) { - return mapDataSource[DataSourceType.network]!.findNumberOfUnreadSpamEmails( - session, - accountId, - mailboxFilterCondition: mailboxFilterCondition, - limit: limit); - } - @override Future getSpamReportState() async { return await mapDataSource[DataSourceType.local]!.getSpamReportState(); diff --git a/lib/features/mailbox_dashboard/domain/repository/spam_report_repository.dart b/lib/features/mailbox_dashboard/domain/repository/spam_report_repository.dart index b1e17290b4..edabee51e0 100644 --- a/lib/features/mailbox_dashboard/domain/repository/spam_report_repository.dart +++ b/lib/features/mailbox_dashboard/domain/repository/spam_report_repository.dart @@ -1,11 +1,7 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/session/session.dart'; -import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox_filter_condition.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/spam_report_state.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/unread_spam_emails_response.dart'; abstract class SpamReportRepository { Future storeLastTimeDismissedSpamReported(DateTime lastTimeDismissedSpamReported); @@ -14,15 +10,6 @@ abstract class SpamReportRepository { Future deleteLastTimeDismissedSpamReported(); - Future getUnreadSpamMailbox( - Session session, - AccountId accountId, - { - MailboxFilterCondition? mailboxFilterCondition, - UnsignedInt? limit - } - ); - Future getSpamReportState(); Future storeSpamReportState(SpamReportState spamReportState); diff --git a/lib/features/mailbox_dashboard/domain/state/get_number_of_unread_spam_emails_state.dart b/lib/features/mailbox_dashboard/domain/state/get_number_of_unread_spam_emails_state.dart deleted file mode 100644 index 0dceb71418..0000000000 --- a/lib/features/mailbox_dashboard/domain/state/get_number_of_unread_spam_emails_state.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:core/presentation/state/failure.dart'; -import 'package:core/presentation/state/success.dart'; -import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; - -class GetUnreadSpamMailboxLoading extends UIState {} - -class GetUnreadSpamMailboxSuccess extends UIState { - final Mailbox unreadSpamMailbox; - - GetUnreadSpamMailboxSuccess(this.unreadSpamMailbox); - - @override - List get props => [unreadSpamMailbox]; -} - -class InvalidSpamReportCondition extends FeatureFailure {} - -class GetUnreadSpamMailboxFailure extends FeatureFailure { - - GetUnreadSpamMailboxFailure(dynamic exception) : super(exception: exception); -} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/state/get_spam_mailbox_cached_state.dart b/lib/features/mailbox_dashboard/domain/state/get_spam_mailbox_cached_state.dart index d03094bf1a..f256506a19 100644 --- a/lib/features/mailbox_dashboard/domain/state/get_spam_mailbox_cached_state.dart +++ b/lib/features/mailbox_dashboard/domain/state/get_spam_mailbox_cached_state.dart @@ -17,4 +17,6 @@ class GetSpamMailboxCachedSuccess extends UIState { class GetSpamMailboxCachedFailure extends FeatureFailure { GetSpamMailboxCachedFailure(exception) : super(exception: exception); -} \ No newline at end of file +} + +class InvalidSpamReportCondition extends FeatureFailure {} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor.dart b/lib/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor.dart index c180b6211d..89fb998578 100644 --- a/lib/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor.dart +++ b/lib/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor.dart @@ -6,7 +6,6 @@ import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/spam_report_repository.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_number_of_unread_spam_emails_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_spam_mailbox_cached_state.dart'; class GetSpamMailboxCachedInteractor { diff --git a/lib/features/mailbox_dashboard/domain/usecases/get_unread_spam_mailbox_interactor.dart b/lib/features/mailbox_dashboard/domain/usecases/get_unread_spam_mailbox_interactor.dart deleted file mode 100644 index 33776105dd..0000000000 --- a/lib/features/mailbox_dashboard/domain/usecases/get_unread_spam_mailbox_interactor.dart +++ /dev/null @@ -1,56 +0,0 @@ - -import 'package:core/presentation/state/failure.dart'; -import 'package:core/presentation/state/success.dart'; -import 'package:core/utils/app_logger.dart'; -import 'package:dartz/dartz.dart'; -import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/session/session.dart'; -import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; -import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox_filter_condition.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/spam_report_repository.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_number_of_unread_spam_emails_state.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor.dart'; - -class GetUnreadSpamMailboxInteractor { - final SpamReportRepository _spamReportRepository; - - GetUnreadSpamMailboxInteractor(this._spamReportRepository); - - Stream> execute( - Session session, - AccountId accountId, - { - UnsignedInt? limit, - MailboxFilterCondition? mailboxFilterCondition, - } - ) async* { - try { - yield Right(GetUnreadSpamMailboxLoading()); - if (await _validateIntervalToShowBanner()) { - final response = await _spamReportRepository.getUnreadSpamMailbox( - session, - accountId, - mailboxFilterCondition: mailboxFilterCondition, - limit: limit); - final unreadSpamMailbox = response.unreadSpamMailbox; - - if (unreadSpamMailbox!.unreadEmails!.value.value > 0) { - yield Right(GetUnreadSpamMailboxSuccess(unreadSpamMailbox)); - } else { - yield Left(InvalidSpamReportCondition()); - } - } else { - yield Left(InvalidSpamReportCondition()); - } - } catch (e) { - yield Left(GetUnreadSpamMailboxFailure(e)); - } - } - - Future _validateIntervalToShowBanner() async { - final lastTimeDismissedSpamReported = await _spamReportRepository.getLastTimeDismissedSpamReported(); - final currentTime = DateTime.now().difference(lastTimeDismissedSpamReported); - log('GetUnreadSpamMailboxInteractor::_compareSpamReportTime:lastTimeDismissedSpamReported: $lastTimeDismissedSpamReported | currentTime: $currentTime'); - return currentTime.inHours > GetSpamMailboxCachedInteractor.spamReportBannerDisplayIntervalInHour; - } -} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart index 57dbd600a0..1f7c0a782e 100644 --- a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart +++ b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart @@ -52,14 +52,11 @@ import 'package:tmail_ui_user/features/mailbox/domain/usecases/mark_as_mailbox_r import 'package:tmail_ui_user/features/mailbox/presentation/mailbox_bindings.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource/search_datasource.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource/session_storage_composer_datasource.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource/spam_report_datasource.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource_impl/hive_spam_report_datasource_impl.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource_impl/local_spam_report_datasource_impl.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource_impl/search_datasource_impl.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource_impl/session_storage_composer_datasoure_impl.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource_impl/spam_report_datasource_impl.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/local/local_spam_report_manager.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/data/network/spam_report_api.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/repository/composer_cache_repository_impl.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/repository/search_repository_impl.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/repository/spam_report_repository_impl.dart'; @@ -71,7 +68,6 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_app import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_spam_report_state_interactor.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_unread_spam_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/quick_search_email_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart'; @@ -153,7 +149,6 @@ class MailboxDashBoardBindings extends BaseBindings { Get.put(SpamReportController( Get.find(), - Get.find(), Get.find(), Get.find(), Get.find())); @@ -198,7 +193,6 @@ class MailboxDashBoardBindings extends BaseBindings { Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); Get.lazyPut( @@ -244,10 +238,6 @@ class MailboxDashBoardBindings extends BaseBindings { Get.lazyPut(() => SessionStorageComposerDatasourceImpl( Get.find(), Get.find())); - Get.lazyPut(() => SpamReportDataSourceImpl( - Get.find(), - Get.find(), - )); Get.lazyPut(() => LocalSpamReportDataSourceImpl( Get.find(), Get.find(), @@ -306,8 +296,6 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find())); Get.lazyPut(() => StoreSpamReportInteractor( Get.find())); - Get.lazyPut(() => GetUnreadSpamMailboxInteractor( - Get.find())); Get.lazyPut(() => StoreSpamReportStateInteractor( Get.find())); Get.lazyPut(() => GetSpamReportStateInteractor( @@ -376,7 +364,6 @@ class MailboxDashBoardBindings extends BaseBindings { Get.lazyPut(() => ComposerCacheRepositoryImpl(Get.find())); Get.lazyPut(() => SpamReportRepositoryImpl( { - DataSourceType.network: Get.find(), DataSourceType.local: Get.find(), DataSourceType.hiveCache: Get.find() }, diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index a288c35ad3..4af181a921 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -2112,15 +2112,25 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo bool get enableSpamReport => spamReportController.enableSpamReport; void getSpamReportBanner() { - if (spamReportController.enableSpamReport && - sessionCurrent != null && - accountId.value != null) { - spamReportController.getSpamMailboxAction(sessionCurrent!, accountId.value!); + if (enableSpamReport) { + final spamId = spamMailboxId; + if (spamId == null) { + spamReportController.setSpamPresentationMailbox(null); + return; + } + + final spamMailbox = mapMailboxById[spamId]; + final unreadEmails = spamMailbox?.unreadEmails?.value.value ?? 0; + if (unreadEmails > 0) { + spamReportController.setSpamPresentationMailbox(spamMailbox); + } else { + spamReportController.setSpamPresentationMailbox(null); + } } } void refreshSpamReportBanner() { - if (spamReportController.enableSpamReport && sessionCurrent != null && accountId.value != null) { + if (enableSpamReport && sessionCurrent != null && accountId.value != null) { spamReportController.getSpamMailboxCached(accountId.value!, sessionCurrent!.username); } } diff --git a/lib/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart index 418a882698..7f8283cc83 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart @@ -3,31 +3,25 @@ import 'package:core/presentation/state/success.dart'; import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; -import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox_filter_condition.dart'; import 'package:model/extensions/mailbox_extension.dart'; import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/spam_report_state.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_number_of_unread_spam_emails_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_spam_mailbox_cached_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_spam_report_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/store_last_time_dismissed_spam_reported_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/store_spam_report_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_spam_report_state_interactor.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_unread_spam_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/store_last_time_dismissed_spam_reported_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/store_spam_report_state_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; class SpamReportController extends BaseController { final StoreSpamReportInteractor _storeSpamReportInteractor; - final GetUnreadSpamMailboxInteractor _getNumberOfUnreadSpamEmailsInteractor; final StoreSpamReportStateInteractor _storeSpamReportStateInteractor; final GetSpamReportStateInteractor _getSpamReportStateInteractor; final GetSpamMailboxCachedInteractor _getSpamMailboxCachedInteractor; @@ -37,7 +31,6 @@ class SpamReportController extends BaseController { SpamReportController( this._storeSpamReportInteractor, - this._getNumberOfUnreadSpamEmailsInteractor, this._storeSpamReportStateInteractor, this._getSpamReportStateInteractor, this._getSpamMailboxCachedInteractor @@ -46,9 +39,7 @@ class SpamReportController extends BaseController { @override void handleSuccessViewState(Success success) { super.handleSuccessViewState(success); - if (success is GetUnreadSpamMailboxSuccess){ - presentationSpamMailbox.value = success.unreadSpamMailbox.toPresentationMailbox(); - } else if (success is StoreLastTimeDismissedSpamReportSuccess) { + if (success is StoreLastTimeDismissedSpamReportSuccess) { presentationSpamMailbox.value = null; } else if (success is GetSpamReportStateSuccess) { spamReportState.value = success.spamReportState; @@ -62,9 +53,7 @@ class SpamReportController extends BaseController { @override void handleFailureViewState(Failure failure) { super.handleFailureViewState(failure); - if (failure is GetUnreadSpamMailboxFailure || - failure is GetSpamMailboxCachedFailure || - failure is InvalidSpamReportCondition) { + if (failure is GetSpamMailboxCachedFailure) { presentationSpamMailbox.value = null; } } @@ -91,13 +80,6 @@ class SpamReportController extends BaseController { } } - void getSpamMailboxAction(Session session, AccountId accountId) { - consumeState(_getNumberOfUnreadSpamEmailsInteractor.execute( - session, - accountId, - mailboxFilterCondition: MailboxFilterCondition(role: Role('Spam')))); - } - void getSpamMailboxCached(AccountId accountId, UserName userName) { consumeState(_getSpamMailboxCachedInteractor.execute(accountId, userName)); } @@ -121,6 +103,10 @@ class SpamReportController extends BaseController { } void getSpamReportStateAction() { - consumeState(_getSpamReportStateInteractor.execute()); + consumeState(_getSpamReportStateInteractor.execute()); + } + + void setSpamPresentationMailbox(PresentationMailbox? spamMailbox) { + presentationSpamMailbox.value = spamMailbox; } } diff --git a/lib/main/bindings/network/network_bindings.dart b/lib/main/bindings/network/network_bindings.dart index ce115afe95..10a8bdf1eb 100644 --- a/lib/main/bindings/network/network_bindings.dart +++ b/lib/main/bindings/network/network_bindings.dart @@ -26,7 +26,6 @@ import 'package:tmail_ui_user/features/login/data/utils/library_platform/app_aut import 'package:tmail_ui_user/features/mailbox/data/local/mailbox_cache_manager.dart'; import 'package:tmail_ui_user/features/mailbox/data/local/state_cache_manager.dart'; import 'package:tmail_ui_user/features/mailbox/data/network/mailbox_api.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/data/network/spam_report_api.dart'; import 'package:tmail_ui_user/features/manage_account/data/network/forwarding_api.dart'; import 'package:tmail_ui_user/features/manage_account/data/network/identity_api.dart'; import 'package:tmail_ui_user/features/manage_account/data/network/rule_filter_api.dart'; @@ -123,7 +122,6 @@ class NetworkBindings extends Bindings { Get.put(ForwardingAPI(Get.find())); Get.put(QuotasAPI(Get.find())); Get.put(FcmApi(Get.find())); - Get.put(SpamReportApi(Get.find())); Get.put(ServerSettingsAPI(Get.find())); Get.put(WebSocketApi(Get.find())); } From ee82f217ac24f7beb77123ecbbf9ddcdf7408924 Mon Sep 17 00:00:00 2001 From: DatDang Date: Fri, 20 Dec 2024 10:47:39 +0700 Subject: [PATCH 23/72] TF-3332 Prevent refresh when switching mailbox --- .../thread/data/repository/thread_repository_impl.dart | 3 ++- lib/features/thread/domain/repository/thread_repository.dart | 1 + .../domain/usecases/get_emails_in_mailbox_interactor.dart | 4 +++- lib/features/thread/presentation/thread_controller.dart | 5 +++-- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/features/thread/data/repository/thread_repository_impl.dart b/lib/features/thread/data/repository/thread_repository_impl.dart index 3541a07f63..7334f919c6 100644 --- a/lib/features/thread/data/repository/thread_repository_impl.dart +++ b/lib/features/thread/data/repository/thread_repository_impl.dart @@ -45,6 +45,7 @@ class ThreadRepositoryImpl extends ThreadRepository { EmailFilter? emailFilter, Properties? propertiesCreated, Properties? propertiesUpdated, + bool getLatestChanges = true, } ) async* { log('ThreadRepositoryImpl::getAllEmail(): filter = ${emailFilter?.mailboxId}'); @@ -92,7 +93,7 @@ class ThreadRepositoryImpl extends ThreadRepository { await _updateEmailCache(accountId, session.username, newCreated: networkEmailResponse.emailList); } - if (localEmailResponse.hasState()) { + if (localEmailResponse.hasState() && getLatestChanges) { log('ThreadRepositoryImpl::getAllEmail(): filter = ${emailFilter?.mailboxId} local has state: ${localEmailResponse.state}'); await _synchronizeCacheWithChanges( session, diff --git a/lib/features/thread/domain/repository/thread_repository.dart b/lib/features/thread/domain/repository/thread_repository.dart index 87e6ea37bb..e794b893c3 100644 --- a/lib/features/thread/domain/repository/thread_repository.dart +++ b/lib/features/thread/domain/repository/thread_repository.dart @@ -25,6 +25,7 @@ abstract class ThreadRepository { EmailFilter? emailFilter, Properties? propertiesCreated, Properties? propertiesUpdated, + bool getLatestChanges = true, } ); diff --git a/lib/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart b/lib/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart index 7bb5fa1e75..0eccaae504 100644 --- a/lib/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart +++ b/lib/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart @@ -26,6 +26,7 @@ class GetEmailsInMailboxInteractor { EmailFilter? emailFilter, Properties? propertiesCreated, Properties? propertiesUpdated, + bool getLatestChanges = true, } ) async* { try { @@ -39,7 +40,8 @@ class GetEmailsInMailboxInteractor { sort: sort, emailFilter: emailFilter, propertiesCreated: propertiesCreated, - propertiesUpdated: propertiesUpdated) + propertiesUpdated: propertiesUpdated, + getLatestChanges: getLatestChanges) .map((emailResponse) => _toGetEmailState( emailResponse: emailResponse, currentMailboxId: emailFilter?.mailboxId diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index fd0e310265..36d944d581 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -254,7 +254,7 @@ class ThreadController extends BaseController with EmailActionController { _currentMemoryMailboxId = mailbox.id; consumeState(Stream.value(Right(GetAllEmailLoading()))); _resetToOriginalValue(); - _getAllEmailAction(); + _getAllEmailAction(getLatestChanges: false); } else if (mailbox == null) { // disable current mailbox when search active _currentMemoryMailboxId = null; _resetToOriginalValue(); @@ -446,7 +446,7 @@ class ThreadController extends BaseController with EmailActionController { } } - void _getAllEmailAction() { + void _getAllEmailAction({bool getLatestChanges = true}) { log('ThreadController::_getAllEmailAction:'); if (_session != null &&_accountId != null) { consumeState(_getEmailsInMailboxInteractor.execute( @@ -461,6 +461,7 @@ class ThreadController extends BaseController with EmailActionController { ), propertiesCreated: EmailUtils.getPropertiesForEmailGetMethod(_session!, _accountId!), propertiesUpdated: ThreadConstants.propertiesUpdatedDefault, + getLatestChanges: getLatestChanges, )); } else { consumeState(Stream.value(Left(GetAllEmailFailure(NotFoundSessionException())))); From b42ad94027fcb78e74c5d4a093b7abdce7148b88 Mon Sep 17 00:00:00 2001 From: DatDang Date: Fri, 20 Dec 2024 11:27:50 +0700 Subject: [PATCH 24/72] fixup! TF-3332 Prevent refresh when switching mailbox --- .../controller/mailbox_dashboard_controller_test.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart index a6045ae75a..474d493b2e 100644 --- a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart +++ b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart @@ -414,6 +414,7 @@ void main() { limit: anyNamed('limit'), sort: anyNamed('sort'), emailFilter: anyNamed('emailFilter'), + getLatestChanges: anyNamed('getLatestChanges'), propertiesCreated: anyNamed('propertiesCreated'), propertiesUpdated: anyNamed('propertiesUpdated'))); expect(searchController.sortOrderFiltered, EmailSortOrderType.mostRecent); @@ -426,6 +427,7 @@ void main() { filter: EmailFilterCondition(inMailbox: testMailboxId), filterOption: FilterMessageOption.all, mailboxId: testMailboxId), + getLatestChanges: false, propertiesCreated: ThreadConstants.propertiesDefault, propertiesUpdated: ThreadConstants.propertiesUpdatedDefault)); }); @@ -471,6 +473,7 @@ void main() { limit: anyNamed('limit'), sort: anyNamed('sort'), emailFilter: anyNamed('emailFilter'), + getLatestChanges: anyNamed('getLatestChanges'), propertiesCreated: anyNamed('propertiesCreated'), propertiesUpdated: anyNamed('propertiesUpdated'))); expect(searchController.sortOrderFiltered, EmailSortOrderType.mostRecent); @@ -483,6 +486,7 @@ void main() { filter: EmailFilterCondition(inMailbox: testMailboxId), filterOption: FilterMessageOption.all, mailboxId: testMailboxId), + getLatestChanges: false, propertiesCreated: ThreadConstants.propertiesDefault, propertiesUpdated: ThreadConstants.propertiesUpdatedDefault )).called(1); From 39b8f66d21cad034b4a040b888d91670b2f83fc3 Mon Sep 17 00:00:00 2001 From: DatDang Date: Mon, 23 Dec 2024 11:40:57 +0700 Subject: [PATCH 25/72] fixup! TF-3332 Prevent refresh when switching mailbox --- .../thread_repository_impl_test.dart | 129 ++++++++++++++++++ .../controller/thread_controller_test.dart | 51 +++++++ 2 files changed, 180 insertions(+) create mode 100644 test/features/thread/data/repository/thread_repository_impl_test.dart diff --git a/test/features/thread/data/repository/thread_repository_impl_test.dart b/test/features/thread/data/repository/thread_repository_impl_test.dart new file mode 100644 index 0000000000..2ed0fd5381 --- /dev/null +++ b/test/features/thread/data/repository/thread_repository_impl_test.dart @@ -0,0 +1,129 @@ +import 'package:core/data/model/source_type/data_source_type.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart'; +import 'package:tmail_ui_user/features/thread/data/datasource/thread_datasource.dart'; +import 'package:tmail_ui_user/features/thread/data/model/email_change_response.dart'; +import 'package:tmail_ui_user/features/thread/data/repository/thread_repository_impl.dart'; + +import '../../../../fixtures/account_fixtures.dart'; +import '../../../../fixtures/session_fixtures.dart'; +import 'thread_repository_impl_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), +]) +void main() { + final threadDataSource = MockThreadDataSource(); + final stateDataSource = MockStateDataSource(); + final threadRepository = ThreadRepositoryImpl( + { + DataSourceType.network: threadDataSource, + DataSourceType.local: threadDataSource, + }, + stateDataSource, + ); + + group('thread repository impl test:', () { + test( + 'should not call threadDatasource.getChanges ' + 'when getAllEmail is called ' + 'and getLatestChanges is false', + () async { + // arrange + when(threadDataSource.getAllEmailCache( + any, + any, + filterOption: anyNamed('filterOption'), + inMailboxId: anyNamed('inMailboxId'), + limit: anyNamed('limit'), + sort: anyNamed('sort'), + )).thenAnswer( + (_) => Future.value(List.generate(30, (index) => Email(id: EmailId(Id('$index'))))), + ); + when(stateDataSource.getState( + any, + any, + any, + )).thenAnswer((_) => Future.value(State('some-state'))); + when(threadDataSource.getChanges( + any, + any, + any, + propertiesCreated: anyNamed('propertiesCreated'), + propertiesUpdated: anyNamed('propertiesUpdated'), + )).thenAnswer( + (_) => Future.value(EmailChangeResponse(hasMoreChanges: false)), + ); + + // act + await threadRepository.getAllEmail( + SessionFixtures.aliceSession, + AccountFixtures.aliceAccountId, + getLatestChanges: false, + ).last; + + // assert + verifyNever(threadDataSource.getChanges( + any, + any, + any, + propertiesCreated: anyNamed('propertiesCreated'), + propertiesUpdated: anyNamed('propertiesUpdated'), + )); + }); + + test( + 'should call threadDatasource.getChanges ' + 'when getAllEmail is called ' + 'and getLatestChanges is true', + () async { + // arrange + when(threadDataSource.getAllEmailCache( + any, + any, + filterOption: anyNamed('filterOption'), + inMailboxId: anyNamed('inMailboxId'), + limit: anyNamed('limit'), + sort: anyNamed('sort'), + )).thenAnswer( + (_) => Future.value(List.generate(30, (index) => Email(id: EmailId(Id('$index'))))), + ); + when(stateDataSource.getState( + any, + any, + any, + )).thenAnswer((_) => Future.value(State('some-state'))); + when(threadDataSource.getChanges( + any, + any, + any, + propertiesCreated: anyNamed('propertiesCreated'), + propertiesUpdated: anyNamed('propertiesUpdated'), + )).thenAnswer( + (_) => Future.value(EmailChangeResponse(hasMoreChanges: false)), + ); + + // act + await threadRepository.getAllEmail( + SessionFixtures.aliceSession, + AccountFixtures.aliceAccountId, + getLatestChanges: true, + ).last; + + // assert + verify(threadDataSource.getChanges( + any, + any, + any, + propertiesCreated: anyNamed('propertiesCreated'), + propertiesUpdated: anyNamed('propertiesUpdated'), + )); + }); + }); +} \ No newline at end of file diff --git a/test/features/thread/presentation/controller/thread_controller_test.dart b/test/features/thread/presentation/controller/thread_controller_test.dart index e582c7db12..8bf8bf57e2 100644 --- a/test/features/thread/presentation/controller/thread_controller_test.dart +++ b/test/features/thread/presentation/controller/thread_controller_test.dart @@ -43,6 +43,7 @@ import 'package:tmail_ui_user/features/thread/domain/usecases/refresh_changes_em import 'package:tmail_ui_user/features/thread/domain/usecases/search_email_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/search_more_email_interactor.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/search_state.dart'; +import 'package:tmail_ui_user/features/thread/presentation/model/search_status.dart'; import 'package:tmail_ui_user/features/thread/presentation/thread_controller.dart'; import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; import 'package:tmail_ui_user/main/utils/toast_manager.dart'; @@ -413,5 +414,55 @@ void main() { expect(mockMailboxDashBoardController.emailsInCurrentMailbox.length, equals(0)); }); }); + + group('_registerObxStreamListener test:', () { + test( + 'should call _getEmailsInMailboxInteractor.execute with getLatestChanges is false ' + 'when mailboxDashBoardController.selectedMailbox updated', + () async { + // arrange + final mailboxBefore = PresentationMailbox(MailboxId(Id('mailbox-before-id'))); + final mailboxAfter = PresentationMailbox(MailboxId(Id('mailbox-after-id'))); + final selectedMailbox = Rxn(mailboxBefore); + when(mockMailboxDashBoardController.sessionCurrent).thenReturn(SessionFixtures.aliceSession); + when(mockMailboxDashBoardController.accountId).thenReturn(Rxn(AccountFixtures.aliceAccountId)); + when(mockMailboxDashBoardController.selectedMailbox).thenReturn(selectedMailbox); + when(mockMailboxDashBoardController.searchController).thenReturn(mockSearchController); + when(mockMailboxDashBoardController.dashBoardAction).thenReturn(Rxn()); + when(mockMailboxDashBoardController.emailUIAction).thenReturn(Rxn()); + when(mockMailboxDashBoardController.viewState).thenReturn(Rx(Right(UIState.idle))); + when(mockMailboxDashBoardController.emailsInCurrentMailbox).thenReturn(RxList()); + when(mockMailboxDashBoardController.listEmailSelected).thenReturn(RxList()); + when(mockMailboxDashBoardController.currentSelectMode).thenReturn(Rx(SelectMode.INACTIVE)); + when(mockMailboxDashBoardController.filterMessageOption).thenReturn(Rx(FilterMessageOption.all)); + when(mockSearchController.searchState).thenReturn(SearchState(SearchStatus.INACTIVE).obs); + + // act + threadController.onInit(); + mockMailboxDashBoardController.selectedMailbox.value = mailboxAfter; + await untilCalled(mockGetEmailsInMailboxInteractor.execute( + any, + any, + limit: anyNamed('limit'), + sort: anyNamed('sort'), + emailFilter: anyNamed('emailFilter'), + propertiesCreated: anyNamed('propertiesCreated'), + propertiesUpdated: anyNamed('propertiesUpdated'), + getLatestChanges: anyNamed('getLatestChanges'), + )); + + // assert + verify(mockGetEmailsInMailboxInteractor.execute( + any, + any, + limit: anyNamed('limit'), + sort: anyNamed('sort'), + emailFilter: anyNamed('emailFilter'), + propertiesCreated: anyNamed('propertiesCreated'), + propertiesUpdated: anyNamed('propertiesUpdated'), + getLatestChanges: false, + )); + }); + }); }); } \ No newline at end of file From 63b1b48195d1089eadfb7be5d029e59cc95bf781 Mon Sep 17 00:00:00 2001 From: DatDang Date: Tue, 24 Dec 2024 10:58:47 +0700 Subject: [PATCH 26/72] fixup! TF-3332 Prevent refresh when switching mailbox --- .../thread/presentation/controller/thread_controller_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/features/thread/presentation/controller/thread_controller_test.dart b/test/features/thread/presentation/controller/thread_controller_test.dart index 8bf8bf57e2..571219a5fd 100644 --- a/test/features/thread/presentation/controller/thread_controller_test.dart +++ b/test/features/thread/presentation/controller/thread_controller_test.dart @@ -448,7 +448,7 @@ void main() { emailFilter: anyNamed('emailFilter'), propertiesCreated: anyNamed('propertiesCreated'), propertiesUpdated: anyNamed('propertiesUpdated'), - getLatestChanges: anyNamed('getLatestChanges'), + getLatestChanges: false, )); // assert From 36a0f779a8a7e862a471c21e7a7503529bd7c8e6 Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 6 Dec 2024 00:39:59 +0700 Subject: [PATCH 27/72] TF-3181 Display contact support button on web app (cherry picked from commit cfc027522962fbf7092c8a15876d6c61f0aca6fc) --- assets/images/ic_help.svg | 3 ++ .../presentation/resources/image_paths.dart | 1 + .../domain/extensions/session_extensions.dart | 16 +++++++ .../mailbox_dashboard_view_web.dart | 3 ++ .../app_grid_dashboard_icon.dart | 10 ++-- .../navigation_bar/navigation_bar_widget.dart | 48 +++++++++++++++---- .../manage_account_dashboard_view.dart | 3 ++ lib/l10n/intl_messages.arb | 6 +++ lib/main/localizations/app_localizations.dart | 7 +++ model/lib/model.dart | 4 +- .../support/contact_support_capability.dart | 24 ++++++++++ 11 files changed, 111 insertions(+), 14 deletions(-) create mode 100644 assets/images/ic_help.svg create mode 100644 model/lib/support/contact_support_capability.dart diff --git a/assets/images/ic_help.svg b/assets/images/ic_help.svg new file mode 100644 index 0000000000..07b88fa8f8 --- /dev/null +++ b/assets/images/ic_help.svg @@ -0,0 +1,3 @@ + + + diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index 52b6013b14..51206955ed 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -221,6 +221,7 @@ class ImagePaths { String get icBadSignature => _getImagePath('ic_bad_signature.svg'); String get icDeleteSelection => _getImagePath('ic_delete_selection.svg'); String get icLogoTwakeWelcome => _getImagePath('ic_logo_twake_welcome.svg'); + String get icHelp => _getImagePath('ic_help.svg'); String _getImagePath(String imageName) { return AssetsPaths.images + imageName; diff --git a/lib/features/home/domain/extensions/session_extensions.dart b/lib/features/home/domain/extensions/session_extensions.dart index cb01440fb0..98e0b88a74 100644 --- a/lib/features/home/domain/extensions/session_extensions.dart +++ b/lib/features/home/domain/extensions/session_extensions.dart @@ -9,6 +9,7 @@ import 'package:get/get.dart'; import 'package:jmap_dart_client/http/converter/state_converter.dart'; import 'package:jmap_dart_client/http/converter/user_name_converter.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/session/session.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:model/model.dart'; @@ -18,6 +19,7 @@ import 'package:tmail_ui_user/features/home/domain/converter/session_capabilitie import 'package:tmail_ui_user/features/home/domain/converter/session_primary_account_converter.dart'; extension SessionExtensions on Session { + static final CapabilityIdentifier linagoraContactSupportCapability = CapabilityIdentifier(Uri.parse('com:linagora:params:jmap:contact:support')); Map toJson() { final val = {}; @@ -79,4 +81,18 @@ extension SessionExtensions on Session { return null; } } + + ContactSupportCapability? getContactSupportCapability(AccountId accountId) { + try { + final contactSupportCapability = getCapabilityProperties( + accountId, + linagoraContactSupportCapability, + ); + log('SessionExtensions::getContactSupport:contactSupportCapability = $contactSupportCapability'); + return contactSupportCapability; + } catch (e) { + logError('SessionExtensions::getContactSupportCapability():[Exception] $e'); + return null; + } + } } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart index 9b3e329e20..c214085f21 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -11,6 +11,7 @@ import 'package:tmail_ui_user/features/base/widget/scrollbar_list_view.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_view_web.dart'; import 'package:tmail_ui_user/features/email/presentation/email_view.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; +import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/mailbox_view_web.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/base_mailbox_dashboard_view.dart'; @@ -66,7 +67,9 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { return const SizedBox.shrink(); } else { return NavigationBarWidget( + imagePaths: controller.imagePaths, avatarUserName: controller.sessionCurrent?.username.firstCharacter ?? '', + contactSupportCapability: controller.sessionCurrent?.getContactSupportCapability(accountId), searchForm: SearchInputFormWidget(), appGridController: controller.appGridDashboardController, onShowAppDashboardAction: controller.showAppDashboardAction, diff --git a/lib/features/mailbox_dashboard/presentation/widgets/app_dashboard/app_grid_dashboard_icon.dart b/lib/features/mailbox_dashboard/presentation/widgets/app_dashboard/app_grid_dashboard_icon.dart index 5d393bbedf..885fdfbae5 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/app_dashboard/app_grid_dashboard_icon.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/app_dashboard/app_grid_dashboard_icon.dart @@ -9,15 +9,15 @@ import 'package:tmail_ui_user/main/utils/app_utils.dart'; class AppGridDashboardIcon extends StatelessWidget { - final ImagePaths _imagePaths = Get.find(); - + final ImagePaths imagePaths; final AppGridDashboardController appGridController; final VoidCallback? onShowAppDashboardAction; - AppGridDashboardIcon({ + const AppGridDashboardIcon({ super.key, + required this.imagePaths, required this.appGridController, - this.onShowAppDashboardAction + this.onShowAppDashboardAction, }); @override @@ -48,7 +48,7 @@ class AppGridDashboardIcon extends StatelessWidget { }), visible: isAppGridOpen, child: TMailButtonWidget.fromIcon( - icon: _imagePaths.icAppDashboard, + icon: imagePaths.icAppDashboard, backgroundColor: Colors.transparent, iconSize: 30, padding: const EdgeInsets.all(6), diff --git a/lib/features/mailbox_dashboard/presentation/widgets/navigation_bar/navigation_bar_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/navigation_bar/navigation_bar_widget.dart index 21d4ba7c65..2c0207d60c 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/navigation_bar/navigation_bar_widget.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/navigation_bar/navigation_bar_widget.dart @@ -1,16 +1,22 @@ import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:core/presentation/views/image/avatar_builder.dart'; import 'package:flutter/material.dart'; +import 'package:model/support/contact_support_capability.dart'; import 'package:tmail_ui_user/features/base/widget/application_logo_with_text_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/app_grid_dashboard_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/navigation_bar_style.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/app_dashboard/app_grid_dashboard_icon.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; class NavigationBarWidget extends StatelessWidget { + final ImagePaths imagePaths; final String avatarUserName; + final ContactSupportCapability? contactSupportCapability; final Widget? searchForm; final AppGridDashboardController? appGridController; final VoidCallback? onTapApplicationLogoAction; @@ -19,7 +25,9 @@ class NavigationBarWidget extends StatelessWidget { const NavigationBarWidget({ super.key, + required this.imagePaths, required this.avatarUserName, + this.contactSupportCapability, this.searchForm, this.appGridController, this.onShowAppDashboardAction, @@ -54,12 +62,24 @@ class NavigationBarWidget extends StatelessWidget { child: searchForm ), const Spacer(), + if (contactSupportCapability != null) + TMailButtonWidget.fromIcon( + icon: imagePaths.icHelp, + iconColor: AppColor.messageDialogColor, + backgroundColor: Colors.transparent, + margin: const EdgeInsetsDirectional.only(end: 8), + tooltipMessage: AppLocalizations.of(context).getHelpOrReportABug, + onTapActionCallback: () {}, + ), if (AppConfig.appGridDashboardAvailable && appGridController != null) - AppGridDashboardIcon( - appGridController: appGridController!, - onShowAppDashboardAction: onShowAppDashboardAction, + Padding( + padding: const EdgeInsetsDirectional.only(end: 16), + child: AppGridDashboardIcon( + imagePaths: imagePaths, + appGridController: appGridController!, + onShowAppDashboardAction: onShowAppDashboardAction, + ), ), - const SizedBox(width: 16), (AvatarBuilder() ..text(avatarUserName) ..backgroundColor(Colors.white) @@ -82,12 +102,24 @@ class NavigationBarWidget extends StatelessWidget { else ...[ const Spacer(), + if (contactSupportCapability != null) + TMailButtonWidget.fromIcon( + icon: imagePaths.icHelp, + iconColor: AppColor.messageDialogColor, + backgroundColor: Colors.transparent, + margin: const EdgeInsetsDirectional.only(end: 8), + tooltipMessage: AppLocalizations.of(context).getHelpOrReportABug, + onTapActionCallback: () {}, + ), if (AppConfig.appGridDashboardAvailable && appGridController != null) - AppGridDashboardIcon( - appGridController: appGridController!, - onShowAppDashboardAction: onShowAppDashboardAction, + Padding( + padding: const EdgeInsetsDirectional.only(end: 16), + child: AppGridDashboardIcon( + imagePaths: imagePaths, + appGridController: appGridController!, + onShowAppDashboardAction: onShowAppDashboardAction, + ), ), - const SizedBox(width: 16), (AvatarBuilder() ..text(avatarUserName) ..backgroundColor(Colors.white) diff --git a/lib/features/manage_account/presentation/manage_account_dashboard_view.dart b/lib/features/manage_account/presentation/manage_account_dashboard_view.dart index bfe29c01ab..f1e3e74a17 100644 --- a/lib/features/manage_account/presentation/manage_account_dashboard_view.dart +++ b/lib/features/manage_account/presentation/manage_account_dashboard_view.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/state/banner_state.dart'; +import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/navigation_bar/navigation_bar_widget.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/always_read_receipt/always_read_receipt_view.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/email_rules/email_rules_view.dart'; @@ -41,7 +42,9 @@ class ManageAccountDashBoardView extends GetWidget controller.backToMailboxDashBoard(context: context), onTapAvatarAction: (position) => controller.handleClickAvatarAction(context, position), ); diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index d4d794eb14..ababfd56d1 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -4071,5 +4071,11 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "getHelpOrReportABug": "Get help or report a bug", + "@getHelpOrReportABug": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 9dac836f6b..01ff076bd1 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4273,4 +4273,11 @@ class AppLocalizations { name: 'createTwakeIdFailed', ); } + + String get getHelpOrReportABug { + return Intl.message( + 'Get help or report a bug', + name: 'getHelpOrReportABug', + ); + } } diff --git a/model/lib/model.dart b/model/lib/model.dart index 2835ec4dfa..1cfbf702bd 100644 --- a/model/lib/model.dart +++ b/model/lib/model.dart @@ -79,4 +79,6 @@ export 'oidc/token_id.dart'; export 'oidc/token_oidc.dart'; // Upload export 'upload/file_info.dart'; -export 'upload/upload_response.dart'; \ No newline at end of file +export 'upload/upload_response.dart'; +// Support +export 'support/contact_support_capability.dart'; \ No newline at end of file diff --git a/model/lib/support/contact_support_capability.dart b/model/lib/support/contact_support_capability.dart new file mode 100644 index 0000000000..2142436616 --- /dev/null +++ b/model/lib/support/contact_support_capability.dart @@ -0,0 +1,24 @@ + +import 'package:jmap_dart_client/jmap/core/capability/capability_properties.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'contact_support_capability.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ContactSupportCapability extends CapabilityProperties { + final String? httpLink; + final String? supportMailAddress; + + ContactSupportCapability({this.httpLink, this.supportMailAddress}); + + factory ContactSupportCapability.fromJson(Map json) => _$ContactSupportCapabilityFromJson(json); + + Map toJson() => _$ContactSupportCapabilityToJson(this); + + static ContactSupportCapability deserialize(Map json) { + return ContactSupportCapability.fromJson(json); + } + + @override + List get props => [httpLink, supportMailAddress]; +} \ No newline at end of file From 23a3474ee83fbb877af34095c38829900804efe0 Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 6 Dec 2024 01:45:38 +0700 Subject: [PATCH 28/72] TF-3181 Display contact support button on mobile app (cherry picked from commit 01d2a9b71d937930be6ae247996a3ee1e21a5dc0) --- .../views/button/tmail_button_widget.dart | 16 ++-- .../domain/extensions/session_extensions.dart | 2 +- .../presentation/mailbox_controller.dart | 10 +++ .../mailbox/presentation/mailbox_view.dart | 50 ++++++++++++- .../presentation/mailbox_view_web.dart | 75 +++++++++++++++---- .../manage_account_dashboard_view.dart | 2 - .../quotas/presentation/quotas_view.dart | 10 +-- 7 files changed, 133 insertions(+), 32 deletions(-) diff --git a/core/lib/presentation/views/button/tmail_button_widget.dart b/core/lib/presentation/views/button/tmail_button_widget.dart index b6d06720fb..85927c9235 100644 --- a/core/lib/presentation/views/button/tmail_button_widget.dart +++ b/core/lib/presentation/views/button/tmail_button_widget.dart @@ -40,6 +40,7 @@ class TMailButtonWidget extends StatelessWidget { final MainAxisSize mainAxisSize; final bool isLoading; final Color? hoverColor; + final TextOverflow? textOverflow; const TMailButtonWidget({ super.key, @@ -74,6 +75,7 @@ class TMailButtonWidget extends StatelessWidget { this.mainAxisSize = MainAxisSize.max, this.isLoading = false, this.hoverColor, + this.textOverflow, }); factory TMailButtonWidget.fromIcon({ @@ -149,6 +151,7 @@ class TMailButtonWidget extends StatelessWidget { BoxBorder? border, int? maxLines, Color? hoverColor, + TextOverflow? textOverflow, }) { return TMailButtonWidget( key: key, @@ -172,6 +175,7 @@ class TMailButtonWidget extends StatelessWidget { border: border, maxLines: maxLines, hoverColor: hoverColor, + textOverflow: textOverflow, ); } @@ -200,7 +204,7 @@ class TMailButtonWidget extends StatelessWidget { color: AppColor.colorTextButtonHeaderThread ), maxLines: maxLines, - overflow: maxLines == 1 ? CommonTextStyle.defaultTextOverFlow : null, + overflow: textOverflow ?? (maxLines == 1 ? CommonTextStyle.defaultTextOverFlow : null), softWrap: maxLines == 1 ? CommonTextStyle.defaultSoftWrap : null, ), if (trailingIcon != null) @@ -240,7 +244,7 @@ class TMailButtonWidget extends StatelessWidget { color: AppColor.colorTextButtonHeaderThread ), maxLines: maxLines, - overflow: maxLines == 1 ? CommonTextStyle.defaultTextOverFlow : null, + overflow: textOverflow ?? (maxLines == 1 ? CommonTextStyle.defaultTextOverFlow : null), softWrap: maxLines == 1 ? CommonTextStyle.defaultSoftWrap : null, ), ) @@ -253,7 +257,7 @@ class TMailButtonWidget extends StatelessWidget { color: AppColor.colorTextButtonHeaderThread ), maxLines: maxLines, - overflow: maxLines == 1 ? CommonTextStyle.defaultTextOverFlow : null, + overflow: textOverflow ?? (maxLines == 1 ? CommonTextStyle.defaultTextOverFlow : null), softWrap: maxLines == 1 ? CommonTextStyle.defaultSoftWrap : null, ), if (trailingIcon != null) @@ -284,7 +288,7 @@ class TMailButtonWidget extends StatelessWidget { color: AppColor.colorTextButtonHeaderThread ), maxLines: maxLines, - overflow: maxLines == 1 ? CommonTextStyle.defaultTextOverFlow : null, + overflow: textOverflow ?? (maxLines == 1 ? CommonTextStyle.defaultTextOverFlow : null), softWrap: maxLines == 1 ? CommonTextStyle.defaultSoftWrap : null, ), ) @@ -297,7 +301,7 @@ class TMailButtonWidget extends StatelessWidget { color: AppColor.colorTextButtonHeaderThread ), maxLines: maxLines, - overflow: maxLines == 1 ? CommonTextStyle.defaultTextOverFlow : null, + overflow: textOverflow ?? (maxLines == 1 ? CommonTextStyle.defaultTextOverFlow : null), softWrap: maxLines == 1 ? CommonTextStyle.defaultSoftWrap : null, ), SizedBox(width: iconSpace), @@ -339,7 +343,7 @@ class TMailButtonWidget extends StatelessWidget { color: AppColor.colorTextButtonHeaderThread ), maxLines: maxLines, - overflow: maxLines == 1 ? CommonTextStyle.defaultTextOverFlow : null, + overflow: textOverflow ?? (maxLines == 1 ? CommonTextStyle.defaultTextOverFlow : null), softWrap: maxLines == 1 ? CommonTextStyle.defaultSoftWrap : null, ); } diff --git a/lib/features/home/domain/extensions/session_extensions.dart b/lib/features/home/domain/extensions/session_extensions.dart index 98e0b88a74..f0bab07f99 100644 --- a/lib/features/home/domain/extensions/session_extensions.dart +++ b/lib/features/home/domain/extensions/session_extensions.dart @@ -88,7 +88,7 @@ extension SessionExtensions on Session { accountId, linagoraContactSupportCapability, ); - log('SessionExtensions::getContactSupport:contactSupportCapability = $contactSupportCapability'); + log('SessionExtensions::getContactSupportCapability:contactSupportCapability = $contactSupportCapability'); return contactSupportCapability; } catch (e) { logError('SessionExtensions::getContactSupportCapability():[Exception] $e'); diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index f117eae647..8308ccf599 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -21,6 +21,7 @@ import 'package:tmail_ui_user/features/base/mixin/mailbox_action_handler_mixin.d import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; +import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart'; import 'package:tmail_ui_user/features/mailbox/domain/constants/mailbox_constants.dart'; import 'package:tmail_ui_user/features/mailbox/domain/exceptions/set_mailbox_name_exception.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; @@ -1361,4 +1362,13 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM mailboxDashBoardController.emptySpamFolderAction(spamFolderId: presentationMailbox.id); } } + + ContactSupportCapability? get contactSupportCapability { + final accountId = mailboxDashBoardController.accountId.value; + final session = mailboxDashBoardController.sessionCurrent; + + if (accountId == null || session == null) return null; + + return session.getContactSupportCapability(accountId); + } } \ No newline at end of file diff --git a/lib/features/mailbox/presentation/mailbox_view.dart b/lib/features/mailbox/presentation/mailbox_view.dart index c0fbf84c4b..545604983e 100644 --- a/lib/features/mailbox/presentation/mailbox_view.dart +++ b/lib/features/mailbox/presentation/mailbox_view.dart @@ -80,10 +80,52 @@ class MailboxView extends BaseMailboxView { }), ]), ), - Obx(() => !controller.isSelectionEnabled() - ? const QuotasView() - : const SizedBox.shrink(), - ), + Obx(() { + if (controller.isSelectionEnabled()) { + return const SizedBox.shrink(); + } + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Divider(color: AppColor.colorDividerHorizontal), + if (controller.contactSupportCapability == null) + const QuotasView() + else + Row( + children: [ + const Expanded(child: QuotasView()), + Expanded( + child: TMailButtonWidget( + text: AppLocalizations.of(context).getHelpOrReportABug, + icon: controller.imagePaths.icHelp, + verticalDirection: true, + backgroundColor: Colors.transparent, + maxLines: 2, + flexibleText: true, + mainAxisSize: MainAxisSize.min, + margin: const EdgeInsetsDirectional.only( + end: 12, + start: 4, + top: 6, + bottom: 6, + ), + borderRadius: 10, + textOverflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: 13, + fontWeight: FontWeight.w500, + color: AppColor.primaryColor, + ), + onTapActionCallback: () {}, + ), + ), + ], + ), + ], + ); + }), Obx(() { if (!controller.isSelectionEnabled() && controller.responsiveUtils.isPortraitMobile(context)) { return Container( diff --git a/lib/features/mailbox/presentation/mailbox_view_web.dart b/lib/features/mailbox/presentation/mailbox_view_web.dart index 60c54f8b99..c3b36186b2 100644 --- a/lib/features/mailbox/presentation/mailbox_view_web.dart +++ b/lib/features/mailbox/presentation/mailbox_view_web.dart @@ -58,12 +58,54 @@ class MailboxView extends BaseMailboxView { child: _buildListMailbox(context), ), )), - const QuotasView( - padding: EdgeInsetsDirectional.only( - start: QuotasViewStyles.padding, - top: QuotasViewStyles.padding, + const Divider(color: AppColor.colorDividerHorizontal), + if (controller.responsiveUtils.isWebDesktop(context) || + controller.contactSupportCapability == null) + const QuotasView( + padding: EdgeInsetsDirectional.only( + start: QuotasViewStyles.padding, + top: QuotasViewStyles.padding, + ), + ) + else + Row( + children: [ + const Expanded( + child: QuotasView( + padding: EdgeInsetsDirectional.only( + start: QuotasViewStyles.padding, + top: QuotasViewStyles.padding, + ), + ), + ), + Expanded( + child: TMailButtonWidget( + text: AppLocalizations.of(context).getHelpOrReportABug, + icon: controller.imagePaths.icHelp, + verticalDirection: true, + backgroundColor: Colors.transparent, + maxLines: 2, + flexibleText: true, + mainAxisSize: MainAxisSize.min, + margin: const EdgeInsetsDirectional.only( + end: 12, + start: 4, + top: 6, + bottom: 6, + ), + borderRadius: 10, + textOverflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: 13, + fontWeight: FontWeight.w500, + color: AppColor.primaryColor, + ), + onTapActionCallback: () {}, + ), + ), + ], ), - ), Container( color: AppColor.colorBgMailbox, width: double.infinity, @@ -130,14 +172,21 @@ class MailboxView extends BaseMailboxView { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded(child: Text( - AppLocalizations.of(context).folders, - style: const TextStyle( - fontSize: 17, - color: Colors.black, - fontWeight: FontWeight.bold - ) - )), + Expanded( + child: Padding( + padding: EdgeInsetsDirectional.only( + start: controller.responsiveUtils.isWebDesktop(context) ? 0 : 12, + ), + child: Text( + AppLocalizations.of(context).folders, + style: const TextStyle( + fontSize: 17, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + ), Padding( padding: EdgeInsetsDirectional.only(end: controller.responsiveUtils.isDesktop(context) ? 0 : 12), child: Row( diff --git a/lib/features/manage_account/presentation/manage_account_dashboard_view.dart b/lib/features/manage_account/presentation/manage_account_dashboard_view.dart index f1e3e74a17..906c656cc1 100644 --- a/lib/features/manage_account/presentation/manage_account_dashboard_view.dart +++ b/lib/features/manage_account/presentation/manage_account_dashboard_view.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/state/banner_state.dart'; -import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/navigation_bar/navigation_bar_widget.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/always_read_receipt/always_read_receipt_view.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/email_rules/email_rules_view.dart'; @@ -44,7 +43,6 @@ class ManageAccountDashBoardView extends GetWidget controller.backToMailboxDashBoard(context: context), onTapAvatarAction: (position) => controller.handleClickAvatarAction(context, position), ); diff --git a/lib/features/quotas/presentation/quotas_view.dart b/lib/features/quotas/presentation/quotas_view.dart index ed054d7bf3..9320c83c4d 100644 --- a/lib/features/quotas/presentation/quotas_view.dart +++ b/lib/features/quotas/presentation/quotas_view.dart @@ -31,12 +31,6 @@ class QuotasView extends GetWidget { color: controller.responsiveUtils.isWebDesktop(context) ? QuotasViewStyles.webBackgroundColor : QuotasViewStyles.mobileBackgroundColor, - border: const Border( - top: BorderSide( - color: QuotasViewStyles.topLineColor, - width: QuotasViewStyles.topLineSize, - ) - ), ), alignment: AlignmentDirectional.centerStart, child: Column( @@ -59,6 +53,8 @@ class QuotasView extends GetWidget { fontWeight: QuotasViewStyles.labelFontWeight, color: QuotasViewStyles.labelTextColor ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ) ], ), @@ -80,6 +76,8 @@ class QuotasView extends GetWidget { fontWeight: QuotasViewStyles.progressStateFontWeight, color: octetQuota.getQuotasStateTitleColor() ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ) ], ), From 1a9cb88a07f7c5bd3cb8767c8468e18e5983ee0a Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 6 Dec 2024 02:28:59 +0700 Subject: [PATCH 29/72] TF-3181 Handle on click contact support (cherry picked from commit 7a6a02f58f5194c7f8c8ad3c047a5b8938ba598e) --- .../base/mixin/contact_support_mixin.dart | 38 +++++++ .../presentation/mailbox_controller.dart | 4 +- .../mailbox/presentation/mailbox_view.dart | 6 +- .../presentation/mailbox_view_web.dart | 6 +- .../mailbox_dashboard_controller.dart | 4 +- .../mailbox_dashboard_view_web.dart | 5 + .../navigation_bar/navigation_bar_widget.dart | 7 +- .../contact_support_capability_extension.dart | 11 ++ model/lib/model.dart | 1 + .../session/session_extensions_test.dart | 100 ++++++++++++++++++ 10 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 lib/features/base/mixin/contact_support_mixin.dart create mode 100644 model/lib/extensions/contact_support_capability_extension.dart diff --git a/lib/features/base/mixin/contact_support_mixin.dart b/lib/features/base/mixin/contact_support_mixin.dart new file mode 100644 index 0000000000..535984d45b --- /dev/null +++ b/lib/features/base/mixin/contact_support_mixin.dart @@ -0,0 +1,38 @@ + +import 'package:core/utils/app_logger.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/extensions/contact_support_capability_extension.dart'; +import 'package:model/support/contact_support_capability.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; + +typedef OnTapContactSupportAction = Function(ContactSupportCapability contactSupport); + +mixin ContactSupportMixin { + + void onGetHelpOrReportBug( + ContactSupportCapability contactSupport, + MailboxDashBoardController mailboxDashBoardController, + ) { + log('ContactSupportMixin::onGetHelpOrReportBug:contactSupport = $contactSupport'); + if (contactSupport.isMailAddressSupported) { + _handleMailAddress(contactSupport.supportMailAddress!, mailboxDashBoardController); + } else if (contactSupport.isHttpLinkSupported) { + _handleHttpLink(contactSupport.httpLink!); + } + } + + void _handleMailAddress( + String mailAddress, + MailboxDashBoardController mailboxDashBoardController, + ) { + mailboxDashBoardController.goToComposer( + ComposerArguments.fromEmailAddress(EmailAddress(null, mailAddress)), + ); + } + + void _handleHttpLink(String httpLink) { + AppUtils.launchLink(httpLink); + } +} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 8308ccf599..524f7d1a04 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -17,6 +17,7 @@ import 'package:model/model.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:rxdart/transformers.dart'; import 'package:tmail_ui_user/features/base/base_mailbox_controller.dart'; +import 'package:tmail_ui_user/features/base/mixin/contact_support_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/mailbox_action_handler_mixin.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; @@ -77,7 +78,8 @@ import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_utils.dart'; import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart'; -class MailboxController extends BaseMailboxController with MailboxActionHandlerMixin { +class MailboxController extends BaseMailboxController + with MailboxActionHandlerMixin, ContactSupportMixin { final mailboxDashBoardController = Get.find(); final isMailboxListScrollable = false.obs; diff --git a/lib/features/mailbox/presentation/mailbox_view.dart b/lib/features/mailbox/presentation/mailbox_view.dart index 545604983e..6117a92acd 100644 --- a/lib/features/mailbox/presentation/mailbox_view.dart +++ b/lib/features/mailbox/presentation/mailbox_view.dart @@ -118,7 +118,11 @@ class MailboxView extends BaseMailboxView { fontWeight: FontWeight.w500, color: AppColor.primaryColor, ), - onTapActionCallback: () {}, + onTapActionCallback: () => + controller.onGetHelpOrReportBug( + controller.contactSupportCapability!, + controller.mailboxDashBoardController, + ), ), ), ], diff --git a/lib/features/mailbox/presentation/mailbox_view_web.dart b/lib/features/mailbox/presentation/mailbox_view_web.dart index c3b36186b2..0be90fdbf6 100644 --- a/lib/features/mailbox/presentation/mailbox_view_web.dart +++ b/lib/features/mailbox/presentation/mailbox_view_web.dart @@ -101,7 +101,11 @@ class MailboxView extends BaseMailboxView { fontWeight: FontWeight.w500, color: AppColor.primaryColor, ), - onTapActionCallback: () {}, + onTapActionCallback: () => + controller.onGetHelpOrReportBug( + controller.contactSupportCapability!, + controller.mailboxDashBoardController, + ), ), ), ], diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 4af181a921..7966608a71 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -28,6 +28,7 @@ import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:rxdart/transformers.dart'; import 'package:tmail_ui_user/features/base/action/ui_action.dart'; +import 'package:tmail_ui_user/features/base/mixin/contact_support_mixin.dart'; import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart'; import 'package:tmail_ui_user/features/composer/domain/exceptions/set_method_exception.dart'; import 'package:tmail_ui_user/features/composer/domain/extensions/email_request_extension.dart'; @@ -166,7 +167,8 @@ import 'package:tmail_ui_user/main/utils/email_receive_manager.dart'; import 'package:tmail_ui_user/main/utils/ios_notification_manager.dart'; import 'package:uuid/uuid.dart'; -class MailboxDashBoardController extends ReloadableController with UserSettingPopupMenuMixin { +class MailboxDashBoardController extends ReloadableController + with UserSettingPopupMenuMixin, ContactSupportMixin { final RemoveEmailDraftsInteractor _removeEmailDraftsInteractor = Get.find(); final EmailReceiveManager _emailReceiveManager = Get.find(); diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart index c214085f21..bef9b16a6a 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -75,6 +75,11 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { onShowAppDashboardAction: controller.showAppDashboardAction, onTapApplicationLogoAction: controller.redirectToInboxAction, onTapAvatarAction: (position) => controller.handleClickAvatarAction(context, position), + onTapContactSupportAction: (contactSupport) => + controller.onGetHelpOrReportBug( + contactSupport, + controller, + ), ); } }), diff --git a/lib/features/mailbox_dashboard/presentation/widgets/navigation_bar/navigation_bar_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/navigation_bar/navigation_bar_widget.dart index 2c0207d60c..1061d3e8c0 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/navigation_bar/navigation_bar_widget.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/navigation_bar/navigation_bar_widget.dart @@ -5,6 +5,7 @@ import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:core/presentation/views/image/avatar_builder.dart'; import 'package:flutter/material.dart'; import 'package:model/support/contact_support_capability.dart'; +import 'package:tmail_ui_user/features/base/mixin/contact_support_mixin.dart'; import 'package:tmail_ui_user/features/base/widget/application_logo_with_text_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/app_grid_dashboard_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/navigation_bar_style.dart'; @@ -22,6 +23,7 @@ class NavigationBarWidget extends StatelessWidget { final VoidCallback? onTapApplicationLogoAction; final VoidCallback? onShowAppDashboardAction; final OnTapAvatarActionWithPositionClick? onTapAvatarAction; + final OnTapContactSupportAction? onTapContactSupportAction; const NavigationBarWidget({ super.key, @@ -33,6 +35,7 @@ class NavigationBarWidget extends StatelessWidget { this.onShowAppDashboardAction, this.onTapApplicationLogoAction, this.onTapAvatarAction, + this.onTapContactSupportAction, }); @override @@ -69,7 +72,7 @@ class NavigationBarWidget extends StatelessWidget { backgroundColor: Colors.transparent, margin: const EdgeInsetsDirectional.only(end: 8), tooltipMessage: AppLocalizations.of(context).getHelpOrReportABug, - onTapActionCallback: () {}, + onTapActionCallback: () => onTapContactSupportAction?.call(contactSupportCapability!), ), if (AppConfig.appGridDashboardAvailable && appGridController != null) Padding( @@ -109,7 +112,7 @@ class NavigationBarWidget extends StatelessWidget { backgroundColor: Colors.transparent, margin: const EdgeInsetsDirectional.only(end: 8), tooltipMessage: AppLocalizations.of(context).getHelpOrReportABug, - onTapActionCallback: () {}, + onTapActionCallback: () => onTapContactSupportAction?.call(contactSupportCapability!), ), if (AppConfig.appGridDashboardAvailable && appGridController != null) Padding( diff --git a/model/lib/extensions/contact_support_capability_extension.dart b/model/lib/extensions/contact_support_capability_extension.dart new file mode 100644 index 0000000000..54fec1ba91 --- /dev/null +++ b/model/lib/extensions/contact_support_capability_extension.dart @@ -0,0 +1,11 @@ + +import 'package:model/support/contact_support_capability.dart'; + +extension ContactSupportCapabilityExtension on ContactSupportCapability { + + bool get isMailAddressSupported => + supportMailAddress?.trim().isNotEmpty == true; + + bool get isHttpLinkSupported => + httpLink?.trim().isNotEmpty == true; +} \ No newline at end of file diff --git a/model/lib/model.dart b/model/lib/model.dart index 1cfbf702bd..29e3c97330 100644 --- a/model/lib/model.dart +++ b/model/lib/model.dart @@ -59,6 +59,7 @@ export 'extensions/properties_extension.dart'; export 'extensions/session_extension.dart'; export 'extensions/username_extension.dart'; export 'extensions/utc_date_extension.dart'; +export 'extensions/contact_support_capability_extension.dart'; // Identity export 'identity/identity_request_dto.dart'; export 'mailbox/expand_mode.dart'; diff --git a/test/features/session/session_extensions_test.dart b/test/features/session/session_extensions_test.dart index 221914a42c..abacfc00c0 100644 --- a/test/features/session/session_extensions_test.dart +++ b/test/features/session/session_extensions_test.dart @@ -6,6 +6,7 @@ import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/support/contact_support_capability.dart'; import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart'; import '../../fixtures/account_fixtures.dart'; @@ -102,4 +103,103 @@ void main() { expect(result, isNull); }); }); + + group('getContactSupportCapability::test', () { + test('SHOULD return ContactSupportCapability WHEN ContactSupportCapability is available', () { + // Arrange + final contactSupportCapability = ContactSupportCapability( + supportMailAddress: 'contact.support@example.com', + httpLink: 'https://contact.support', + ); + final session = Session( + { + SessionExtensions.linagoraContactSupportCapability: contactSupportCapability + }, + { + AccountFixtures.aliceAccountId: Account( + AccountName('Alice'), + true, + false, + { + SessionExtensions.linagoraContactSupportCapability: contactSupportCapability + }, + ) + }, + {}, + UserName(''), + Uri(), + Uri(), + Uri(), + Uri(), + State(''), + ); + + // Act + final result = session.getContactSupportCapability(AccountFixtures.aliceAccountId); + + // Assert + expect(result?.supportMailAddress, equals(contactSupportCapability.supportMailAddress)); + expect(result?.httpLink, equals(contactSupportCapability.httpLink)); + }); + + test('SHOULD return null WHEN ContactSupportCapability is not available', () { + // Arrange + final session = Session( + { + SessionExtensions.linagoraContactSupportCapability: EmptyCapability() + }, + { + AccountFixtures.aliceAccountId: Account( + AccountName('Alice'), + true, + false, + { + SessionExtensions.linagoraContactSupportCapability: EmptyCapability() + }, + ) + }, + {}, + UserName(''), + Uri(), + Uri(), + Uri(), + Uri(), + State(''), + ); + + // Act + final result = session.getContactSupportCapability(AccountFixtures.aliceAccountId); + + // Assert + expect(result, isNull); + }); + + test('SHOULD return null WHEN ContactSupportCapability is not supported', () { + // Arrange + final session = Session( + {}, + { + AccountFixtures.aliceAccountId: Account( + AccountName('Alice'), + true, + false, + {}, + ) + }, + {}, + UserName(''), + Uri(), + Uri(), + Uri(), + Uri(), + State(''), + ); + + // Act + final result = session.getContactSupportCapability(AccountFixtures.aliceAccountId); + + // Assert + expect(result, isNull); + }); + }); } From 7a5ebd4c6163ec3a53ec1185859e1bd8e2ccdbea Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 24 Dec 2024 10:12:27 +0700 Subject: [PATCH 30/72] TF-3372 Fix [MU] Emptying trash: Many unnecessary `Email/query + Email/get` requests --- lib/features/thread/presentation/thread_controller.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 36d944d581..c279b89826 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -439,9 +439,7 @@ class ThreadController extends BaseController with EmailActionController { } canLoadMore = newListEmail.length >= ThreadConstants.maxCountEmails; - if (mailboxDashBoardController.emailsInCurrentMailbox.isEmpty) { - refreshAllEmail(); - } else if (PlatformInfo.isWeb) { + if (PlatformInfo.isWeb) { _validateBrowserHeight(); } } From 54454526a23afbe23ea15ed495e5d480271daff4 Mon Sep 17 00:00:00 2001 From: Dat Dang Date: Thu, 26 Dec 2024 12:39:47 +0700 Subject: [PATCH 31/72] TF-3369 Fix cid image without disposition (#3373) --- ...r-displaying-attachment-types-in-emails.md | 5 +- integration_test/robots/thread_robot.dart | 11 + .../no_disposition_inline_scenario.dart | 43 + .../no_disposition_inline_test.dart | 23 + model/lib/email/attachment.dart | 2 + .../extensions/list_attachment_extension.dart | 2 +- .../no_disposition_inline.eml | 5670 +++++++++++++++++ provisioning/integration_test/provisioning.sh | 9 +- 8 files changed, 5761 insertions(+), 4 deletions(-) create mode 100644 integration_test/scenarios/no_disposition_inline_scenario.dart create mode 100644 integration_test/tests/attachments/no_disposition_inline_test.dart create mode 100644 provisioning/integration_test/eml/no_disposition_inline/no_disposition_inline.eml diff --git a/docs/adr/0038-logic-for-displaying-attachment-types-in-emails.md b/docs/adr/0038-logic-for-displaying-attachment-types-in-emails.md index 54010a4417..f31c9a79fd 100644 --- a/docs/adr/0038-logic-for-displaying-attachment-types-in-emails.md +++ b/docs/adr/0038-logic-for-displaying-attachment-types-in-emails.md @@ -24,8 +24,11 @@ Brief the logic flows to make it easier to track changes during `attachment` dis 2. Inline attachments: Displayed within the email body - Display is only allowed when the following conditions are met: - - `cid is not NULL` AND `disposition = 'inline'` + - `cid is not NULL` AND `disposition = 'inline' || disposition = NULL` ## Consequences - Any changes to attachment display while reading emails should be updated in this ADR. + +## References +- [rfc2392](https://datatracker.ietf.org/doc/html/rfc2392) \ No newline at end of file diff --git a/integration_test/robots/thread_robot.dart b/integration_test/robots/thread_robot.dart index 8789a7af76..f2c4afdf93 100644 --- a/integration_test/robots/thread_robot.dart +++ b/integration_test/robots/thread_robot.dart @@ -19,4 +19,15 @@ class ThreadRobot extends CoreRobot { Future tapOnSearchField() async { await $(ThreadView).$(SearchBarView).tap(); } + + Future openMailbox(String mailboxName) async { + await $(#mobile_mailbox_menu_button).tap(); + await $.scrollUntilVisible(finder: $(mailboxName)); + await $(mailboxName).tap(); + } + + Future openEmailWithSubject(String subject) async { + await $.scrollUntilVisible(finder: $(subject)); + await $(subject).tap(); + } } \ No newline at end of file diff --git a/integration_test/scenarios/no_disposition_inline_scenario.dart b/integration_test/scenarios/no_disposition_inline_scenario.dart new file mode 100644 index 0000000000..0ff2cd5ed3 --- /dev/null +++ b/integration_test/scenarios/no_disposition_inline_scenario.dart @@ -0,0 +1,43 @@ +import 'package:core/presentation/views/html_viewer/html_content_viewer_widget.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tmail_ui_user/features/email/presentation/email_view.dart'; + +import '../base/base_scenario.dart'; +import '../robots/search_robot.dart'; +import '../robots/thread_robot.dart'; +import 'login_with_basic_auth_scenario.dart'; + +class NoDispositionInlineScenario extends BaseScenario { + NoDispositionInlineScenario( + super.$, { + required this.loginWithBasicAuthScenario, + }); + + final LoginWithBasicAuthScenario loginWithBasicAuthScenario; + + @override + Future execute() async { + final threadRobot = ThreadRobot($); + final searchRobot = SearchRobot($); + + await loginWithBasicAuthScenario.execute(); + + await threadRobot.openSearchView(); + await searchRobot.enterQueryString('Greeting Card'); + await $.pumpAndTrySettle(); + await threadRobot.openEmailWithSubject('Greeting'); + await $.pumpAndTrySettle(); + _expectEmailViewWithBase64Image(_base64); + } + + void _expectEmailViewWithBase64Image(String base64) { + expect( + $(EmailView) + .$(HtmlContentViewer) + .which((view) => view.contentHtml.contains(base64)), + findsOneWidget, + ); + } + + static const _base64 = '''/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/bAEMAEAsMDgwKEA4NDhIREBM'''; +} \ No newline at end of file diff --git a/integration_test/tests/attachments/no_disposition_inline_test.dart b/integration_test/tests/attachments/no_disposition_inline_test.dart new file mode 100644 index 0000000000..d119690896 --- /dev/null +++ b/integration_test/tests/attachments/no_disposition_inline_test.dart @@ -0,0 +1,23 @@ +import '../../base/test_base.dart'; +import '../../scenarios/login_with_basic_auth_scenario.dart'; +import '../../scenarios/no_disposition_inline_scenario.dart'; + +void main() { + TestBase().runPatrolTest( + description: 'Should see base64 inline image when attachment has no disposition but has cid', + test: ($) async { + final loginWithBasicAuthScenario = LoginWithBasicAuthScenario($, + username: const String.fromEnvironment('USERNAME'), + hostUrl: const String.fromEnvironment('BASIC_AUTH_URL'), + email: const String.fromEnvironment('BASIC_AUTH_EMAIL'), + password: const String.fromEnvironment('PASSWORD'), + ); + + final noDispositionInlineScenario = NoDispositionInlineScenario($, + loginWithBasicAuthScenario: loginWithBasicAuthScenario, + ); + + await noDispositionInlineScenario.execute(); + } + ); +} \ No newline at end of file diff --git a/model/lib/email/attachment.dart b/model/lib/email/attachment.dart index 90b727089a..bccb38d0fb 100644 --- a/model/lib/email/attachment.dart +++ b/model/lib/email/attachment.dart @@ -39,6 +39,8 @@ class Attachment with EquatableMixin { bool isDispositionInlined() => disposition == ContentDisposition.inline; + bool isDispositionUndefined() => disposition == null; + bool isApplicationRTFInlined() => type?.mimeType == applicationRTFType && isDispositionInlined(); String getDownloadUrl(String baseDownloadUrl, AccountId accountId) { diff --git a/model/lib/extensions/list_attachment_extension.dart b/model/lib/extensions/list_attachment_extension.dart index d67cc6a67f..79e93187b3 100644 --- a/model/lib/extensions/list_attachment_extension.dart +++ b/model/lib/extensions/list_attachment_extension.dart @@ -25,7 +25,7 @@ extension ListAttachmentExtension on List { } List get listAttachmentsDisplayedInContent => - where((attachment) => attachment.hasCid() && attachment.isDispositionInlined()) + where((attachment) => attachment.hasCid() && (attachment.isDispositionInlined() || attachment.isDispositionUndefined())) .toList(); Map toMapCidImageDownloadUrl({ diff --git a/provisioning/integration_test/eml/no_disposition_inline/no_disposition_inline.eml b/provisioning/integration_test/eml/no_disposition_inline/no_disposition_inline.eml new file mode 100644 index 0000000000..9fb416637e --- /dev/null +++ b/provisioning/integration_test/eml/no_disposition_inline/no_disposition_inline.eml @@ -0,0 +1,5670 @@ +Return-Path: +Delivered-To: paul.nationaladmin@govmu.org +Received: from 192.168.43.3 (EHLO POSTPC01) ([192.168.43.3]) + (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384) + by smtp.govmu.org (JAMES SMTP Server ) with ESMTPSA ID acf95670 + for ; + Wed, 18 Dec 2024 09:57:38 +0000 (UTC) +From: "Email Administrator" +To: +Subject: Greeting Card +Date: Wed, 18 Dec 2024 14:00:06 +0400 +Message-ID: <013901db5133$9def28c0$d9cd7a40$@govmu.org> +MIME-Version: 1.0 +Content-Type: multipart/related; + boundary="----=_NextPart_000_013A_01DB5155.2500C8C0" +X-Mailer: Microsoft Outlook 15.0 +Thread-Index: AdtRM5uRGVEe0tg0T62NYF5vvNhyew== +Content-Language: en-us + +This is a multipart message in MIME format. + +------=_NextPart_000_013A_01DB5155.2500C8C0 +Content-Type: multipart/alternative; + boundary="----=_NextPart_001_013B_01DB5155.2500C8C0" + + +------=_NextPart_001_013B_01DB5155.2500C8C0 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: 7bit + +Dear all, + + + + + + + +Regards + + + +Government Email Administrator + + + + +------=_NextPart_001_013B_01DB5155.2500C8C0 +Content-Type: text/html; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +

Dear all,

 

 

Regards

 

Government Email = +Administrator

 

+------=_NextPart_001_013B_01DB5155.2500C8C0-- + +------=_NextPart_000_013A_01DB5155.2500C8C0 +Content-Type: image/png; + name="image001.png" +Content-Transfer-Encoding: base64 +Content-ID: + +iVBORw0KGgoAAAANSUhEUgAAAl4AAAGqCAIAAABVqN1DAAAAAXNSR0IArs4c6QAA/8pJREFUeF7s +vQdgG1X2Pawy6s2SuxOn916BBEICIYTee++9s8AuvSwssPSy1KWX0DsECIH0kN57cZzYcbdl9RmN +9J1730iW7SRyIOzu//cxOxtkaTSaefPePbece6/+WZ1Z9+f2f3EE9DoddmxJ3tNbUrz7X9oyfz2h +SyaSdGl6fdtrMmResbiFbJed1Bna3FOST97xrf2v7O0ZOvhbWS9Ln3G34iWupPUItB2ONpfa5qHv +8sLS45U+uNXT0ev0SW0KtUye1kPaoeeS9cElW83PDo7h3h6Gybbnr7T/2ND67mlBZXtyWWcpz/kE +r0oeF70OSwCv0yONM6R/JXPk0kOdoAOwYFoNa/unj8OybKlHmb4n/HTL/eHrmHF6nRGr00A/hzek +jF+lyYPjDUlFVnHxbqcDwxUKh+MJ/KnJHNyaqtNLkmSxWGx2i6rEo6FwQo3r1aSkM5r1Ek5il4zx +pNoYj/UZN/KEC88ONQe/feWdmjWbXQaT3mCUk0oyHjfrcRVGxaCPJpIRXSKQUGVdQmW5gTPQ2LUT +IG3uXcXNZBsPnOm1RDx9VFtpkv3rfx7x5wjs6xHA5AYWZu5Y2Jl7Vomzr6/ov34+kn5iF2tfvBB7 +IruI7tD1Q0KLXYjdNoOMx5FVunboZ8Sp97B36Cz74KC0yN7lizY/IG4fe3paZsXFjlwinZCP0zTX +lMBOD3VHfgVj2WaB/Ib1kl5wuKKEnvZWSzAJpOM5B1ykjScjvSaQxE6qmsGgMBJ6PW6L2RyKhIGF +klEyGPiEPGnxl8NpM1skJRKNhSO6uGpMAG4JZQ16/EZSSaiNaqzPfkOOOO1EJSZ///7HVas3FJrs +UlIXVEJmi0UyWXQGk2zQh5OJ5qTqTygKgzZ+3Uj/GPRGwx8xxf6Exo7M5z+P+a+PACmoe973LCzw +aQfUxra3KaTCnrffMDQtKJeGu9YvMlGKXgvDkXeyOPbRlh6x3Wke7UVwkuRZyw7xtg+uRRhNe9x/ +/6/sGRfTn7b6Ib4kbJCSYs/64LIOB5+BjCCgjAAdcdr/wpZCOTxQsREWttkYCzHpUh+m7TMaMDUe +1yeSLrsNIBUIBlU1gXfjCaBdUlFVJZEAgFlsZskkwfKLRWNqLA60lHSSSWeUdAA2nC0ZSsaK+3Ud +d8pRAMFfPvikevFKu04XjkcSetUpmRQ5pugSUYMukEz6E2owGY+RHm2Q9EYzwaMec1jFWbIOnzbe +YtRp39XDbHWWP6Ex66D+ecAfOwKauEmJoTY/tmuZ9cde0X/77BpSafZi2t5Kud1SUPm7L7O9rv27 +T9n+BB1Bk/8ONHTwZjNvoINf2fNh4m41VSDj0I4/147AfLZLJZ+9NqOS6WvhKScsRL5E/i9BkPBd +8gt2rfI3sFmtNrPZEgwGZVkhoIKzGDv5UQH+BrMFVp8pHpejsWhCVYFIRoJGGHoGI/6vx8FKTp53 +0mkn2Drl//zDT1vmL/XFDTajOapPKAYdPKoJoK/REE7Eg4l4KKnGyOiW8EMwPWEqAphxikQiLlzB +e9izjcYuPtf/X4o1ZsX5jgRgfsMg/oav7NlDKCIPexYYHbqXlLXxG65wH35lD2Ee3IVYaNjaBLrS +FyBUwjahtt9wea1CKbv5/h8UXMz8NdaV6cGkn6CItWVqvlkdyCJGm7m1+Ts9f1p+JTWf0idPK9vi +u+3nmwrxl3Wgs10rD2nWuZzlZ7L9SKuva2G21s8y+41oa448iTgdnHUispaen3A4JuHMztgYP1r+ +5p/gQeXAsPj9dkHi1FxOzwEh1vnPXTzWzPOnPB84eM+3w4C25yHFJKQYITtRtdvAVzJMWDImYdgZ +DDqEBIX1mFQTBqMRWKSqhH0WK7ydplgkKsdiuAc2LQ3xZEJJwp9qsFltVotFScSScjwpK/guJr7F +YLQm9SYyKJNRHJ7rOuj040aP3X/BN9/P/+J7UyyuQ3DSYJQS2OkGwsZEszHZJMdxcogRjioSXouR +4LlNL/B7bZdDB2Zu61VJY/Fq8s9YY9YF/wcfkNV9tE8U6Q7B5x98p1kW6H/11/93frwDgrvtxbbR +kTt+L+kvpr+SVQ/r+Ml3c+Q+1+mzX9Hejw++wYHXlLUkIrDiHS3gu0c3nGYO7urSdnf/2W/jjzoi +84oy0J3wXAtAG8guY68lO4HpXyPAW1ETcbxnskjAv1hMluMKm4gU+FOTsODwdb3JZMaOF3IMn8dx +UpiKJvw/CXerYjKZFIPBL+mHHzZuyKgRS+fMXzp9djKIeKIOsUo4Zo0GCfamYpL8CC7KisqPgYCX +VA9WJFKqpAh9/hGDlNXQ+iN+9I86557X32+QPn/UhXbgvP+X7qUDt/v/o0OEABbkl7aqboqt8vuH +Yw+/kqZb/P5f+T92BohYEvDEdW6RtmmOGN1salkKq00zKPn1/5oamlWAkEuS7oDNLwIcYGKakoUR +AB4lOKyMN2HKKTo9EFHYiwnJbJAkQ1xVIrEYLEBgFlF0DHpBBJVMRpvFgpFUY7IOsUKVvA8AUtBp +zElQUq0RXbLZkBhy0KiDDj+sdnP5nC9+8Nc2mc36mJzANXmcHr1ZakooNWq0NhkPCzDc1/7SNlMX +NxlvDYb//4LG/xF0FEr6nvess+F/5F7+nxaO5Oxqvf3HbmeXnLp9K2Hb/ER7R0X72287Gh2ZZL+b +QpOVXpjNN8jItPePss03mOgFuAAYkCXCrxOGBIQ1JVyIPU20JSAhNGF3KXY4CMWeNjH3ceSr1cTU +fngPz09c3J73DNYw4zybyzpEBGG0JeBHJS2BxkFkipBAAjDCcAPdVDIZEknkYpD7EYoEhgYO01gC +nxpAujGbzbAP1bisxCIgy5g4viipOpOaBDrispCq0XPogBNPP7W5rv7HDz4L7qjxmM1xFWkeRqfD +KdmtkWSyRonUJpQYfh5Xw47TPe17//TbaqXtvNn/p6AxTSTb3Ys/xPD+TdI0KzT+P3Qvv2kA/ie+ +1J6R95+5rDYE1DQAaUr7f+YiIAyzbR3yVGVFtmy3kxVbO0BAzPYbu/q87d0Lfx2HGGG7GGln6gkT +SsWOP8VO2X68MzeFNkF8JI1XDMieVN/fcrWZ39kVl7T13WRcavqa27xorYfR1NPYNjwERoovaiFG +FpscHU/qgF6IL+LQGPykCUQAQYpBYiKIqSoUB4NZMlstRkkC9UaORpDCaEbihd5oZH8rgate16DG +CnuVHnfayXpF/ebtD+s2lOVZrCCigtea5/O53O6qxsY6ORzCwUaD2QiYlTJHfpe3sy8WMqFr5iD/ +n4LG3zvj/lPfb7NuNM9M68X0n7qWP3/nvzYCmgjl3xf+z324pfMu2ht+aa1rH/7cf/1UrUwKtiH2 +9pKwKkUoiwxHznZnK4pyhtL2VRvRqQUXU7HIvf3F/+7xbQaIbhfuUxiLlChoMAkOKUxHykLkKZOk +JEVEEHG7MSAZsjSYAwOTUQUlFZqBicinRgnfTsqyrCKFEU5YHEB8Hj4vcjwSirsk74izTrH43J9/ +/On2VRvcgFqcRU2U5OeCuFPZUO+XI+CjykiPRIZGPCkp+3Zl7HrUxdNvpX/8HoaqYAe1CYRSEJYp +vhQ41cw04vkKDTQjZNr+0bS+stSXdzeBQNsVwVlNpSGlhgex/Rfb1X34D0/KNuU+MlgPdPVEuWq3 +kjNjy+mRysJr5aEQZ+pggZH07wq1i7++dzIlFWfZ04h25IxiJqW3Nl8hb042t1q2z3cV29vLm+3I +tGmhffKz3dWTgMTlAE/qdO3HPOuIZT0A5xZEzfaekvQ7iBrteWNOYLYt+xFZzrCLB5eNf9v+jJnF +X3Z5RekCQ6mf06ijzHjkDHIh/ulfWgqcRq4dmx4xDkRmzlIkKmT8ieicxrambHaxgc+ZIvNo74jV +Lr4nLiJdDUe8mWilJqVXZUsAtD1DNY0e6cHM+lhaGLLETOUb1ycJCQFtxE0Vd8A3yIoCgA0bjorH +Vbbx2JfMBFeYjIBREFaBp3T9cUWJKXAvm8BiRT6+ZMDhgEC8nfA4DjvvtIFjhv/0yZdrvp3hjSTl +ZFzW6wry8lxu35aqHTWhsKIHMVUX1+vgg7UQ7ZWcvJlPvP16yQqeWeWhkGNvULRV236X1ZgS35pW +xWI5rQqTM0J4HShAaySjWtjVVC2B3dh7u7MGk/EtVmiEX0N4NVJ/tF04YiJmnSvZZMA++xxX0lKF +ZDfh5fTdZPHNtLJAU1pKe3G4m2vPOmM6cs/p4d3di6wnaZlAv+NJ/f7LyHqdHTwgQ/XBNzg1TNuF +7zD7bNwn99L6MjIFunYB6Wo4u3uRnf/wxyyq9refdeQzv7L7g9OrJXUIq4PpdH56nUHIFN5UlnLa +3mq1ae+22JREaxFwwU9duP7a+F3FGVql+adkU8tYZrqYW2FC5tRpuRa2cLWtI7NLHJrShIU2kCFc +6bIp3koxRBBriIPDWYREL9UBF+NwpHLsFcYi+KgJgCgYpdgg4QEBwL+YArObUFSUvJHjPpMZX4vb +LaOPPGTwqOErf5m37LsZ7mjSZNDL+mSXXl3tOTkbtm+vDykxvSFiMJANylURhPMjLTB3N1H3anrs +cnG1P8PvgkYa35SJCB8EDx/tlP1DygTlolA6il6Fb54892mfPQ1bm+Jg+EqWXTtb6jD8EMx/nBMP +hMoSoThRxpZ1sP6TB6RAO413bbFrV0EfGh+MqhhSUcWJazWl9l0EHMSM1yzRjtyglrmVzUDvyKn+ +PEaMQNqT2X5AOi62/hzMP2gE2liomvaQ9m6JFynbMBMI09fTBl3brGRhdBLEtiuz16Ys8O++QWHN +CQOSUFVMvI5v8LGlzC/6noAi1gzoTCTX2XYnAxrsGpiRYJyqlGDPOf3CZARsklWJZAyLxYzj4zEF +WYwQxqaEzoIUDiKtJr06s0vVhXWJ/oeOPfiYIzcvXbnwo298YRW5HIGk2qlLZ4i5rVVVNUo4bFAj +KBuQAgf8usCPP0YByzJUvzflnwUxKUuM8OIPTTqnE07wdkIryKeJbpELm3lp7R03QsTvbqP0W/4f +/SJ5LNg1oTGZWs4snv3eOgk7Pr06eGSbe+G5LKbdbmEM2lb65GJIyfFD39He3t1NZd5yGyfkLty2 +GQ/hNztUs5qee7VidzmkQgzsebTb/8p//bmz8cBVQ8S1szThR95m8rdd+9kdRB0QgllvX82mGItg +0n9+a3Pl9PSz3W/762x3+5wpzpvgWwpPEzR2DigynrEXkXg3dKgmgIRMS30xtfoyPKL8YLX3KZVd +e9TaCgd4iM/SJxEXplGuNOdqqgANH4ny2xljTg+Jj2p5WjSjMo7QxictGTr2wISvn+6dcY/+pSgh +DQJDJH4jTvaiBo4YEoOC1PtEMg6A5DlN6R0YQMmI7H58VQWXJhoDdRUXakZVcQpV4syqw2hpVKMl +Y0ccceEZgZrGqS++Fd9W7bJY6mOhbj27myzWNZu31sSiqhHFxBPEuSFDlA0lKiOAkxG4ZN5T1ond +fgCyFpcXX3mDipannvVviDWy11QLTaG6q1BeaGLRa9rYt5mK+fFrIkSnPtol5u1CarcZjozb1c4l +Fi4rMCRxRJl35jXTzGOv8f8INKZHRlskIoOVBzE9YG0eZ5uSnzTcNGf5BsVS3E31/Yxbbjuou4PG +3zNKaZjfw3rMLl7b3G27c/02aMwqIn7DGst+zlZHsMhrCXVr0NjmdttfRlZozHoZNPmzwUlL5Y/d +nO6PGJ/2P9X+OttD477QrhgaU7KDiTYihZFlP2wTLl2mOUJT5agE0KXRLm35pdG6zdzGaWBGaW9y +V43M4r0ivJcVGtVMaOSQX1to5PAkSTlNqu6ijE62hw9xzUKb5DbyI4yMjtpoCNGE7H78LhBTaHgQ +qEjvFzYjGyNcSleixhomyYSPFcCbokpUOtyAKCPiZqDl6JLxxoRaPLzPiZefr1MSHz3/un/dVofB +qEj6kq6dHU7Hhk1bm0KRYDIZw29JhM6g5GjPhdCX8IW8t79v6wA00pi+kVEN57dYjWloFKYMRpHe +Yb2DJhvVFtKq0gpExCFIk0kjYgehcc9DQVZjGgLpFZdwihNvAFsaF3+P0P99z6LVtzl83QrbUyOj +aRjtZVBm0rHQRAS+ipA+j2rbyZ95s0JhaCtiWr+TCnzR+dof3MHb7wg0Zj1V+qZ2d+T/i9DIcpAl +CD8Gfmqpt9q6TNoqD79XEvA4/gmNmdOJ64Gml4zgoHKSBlMGOU+DJBmOIA8XGU+tviBO1R6zMwdZ +CyQTvAoHFgv11sacYNIKVNMUe3rRympEZe2WK28HjfiWiFRl3l26wpy4nsxb3d2aMpG9rJk0xCql +8B6BnZAt+K9KOEEWJK4YlcOR04h8f7LoqF4OCy6ikFIuB/BSlmOqolBLDUZEBBGBs6Ce6lXZ1KPk +8EvOKi4unvrKlPVzFnh0JgBqQdfOnnzfkvXr6/xBGYcRxYd4Org5KjtOj4rbCVBUCWOYlTGWRcZ0 +DBr1byRxAdr2W6CRda9UpIttXZLjwgbCOAqmDKJ/HMrV4FOk/AggpVc8ydgIYsGMoUg7vlNXRlWD +Mm6Y7SyhxKV8Ftz6jOrZ0hSkeDCyRmkguSMaz0ANLVK8o+zmS1Yp3uEDNKWAjxfELXbViP+SwiUc +NgIbxXWKD8VFGmmipnw6aYeqMMHFNWio2jJIQiHgU9DkpRHJHMGUb1kgoRhG7TUPn/hDGPsdNxf2 +HTS2RvrWHnb+lSx68G8wLDp+m+nH3iJcxQPVhjv9mr1U2gMSb2rQKJ5Z6pNWMzF9Gel3f9tEbYOF +WU+SVd78hvFJD1THX7S/7D0jUEfO3P7eRX8GFlbsxgJPBNomQ6MGhwQDTJlILS1NztB6SK/NTKWF +PhcUGG22sokgNCHmqfDkSKmzPIdbjhXsXzqW7DP+Ymrm0FJsmWf0ipFPe0tgfJp6I07bMoVS0Ngy +21qNl5Az9D9JYj2AvKmkdUNsEyYJLZl6Zohrp8MZF6mfBv7Ga/5FA7WeAqCaJXwHteBQEI5crjqd +FW8bJXzBYZBkRZby3IefedLA0SNnfPntr59M9SJT0WjILcw1uVyrtpbVoh8HfgA/D1ZuAtFHFGjF +g0qvErpAupzWmU27sApa65rtZ0gHoJGe05t7ZTVqUTGh8LJaJcwX8VqDPiHoGQZJ6JMGQhOMzEf+ +iOxG0gPIrU/cI23SkPTn2QIlCNVnW81njLtgRmvwxg9JTFZSYxJkiJEPFRUayGJE8T7VwBWJ2OBP +8HRLQS5hBCmJbeG3I4ts748R0M8GtLaRVUTaAs9JrEeey/DUi2fMg8WRBNYW2FnK7mlWXel+xWTh +AdeGnTVHIVNa4hykGfAbvLJoGHBUSj9I6RNpe5LxEMlGNJg0ZhTRF/pm6isUYhdgnILVXQ5GVusk +6xCmyevpI3+DUG4fOfsNJ8l6qWmXGiExCdI0FqYlV1ZISotSfnwZP/kbjMVWVksbZTLbhbQvZt3m +9n//k806nm1/UQDJb3hyrStuC0BqmU7wKLGyDOkvoU4nMQKpdJkGjVruBh4ndYcgBg0vLg11KLVB +6Dh44OnAoVioAtK0KuzCOyCACh+yys4cS36TwpApmwxfTPfxZmjk4zVNlX6mzcVnykaOjQqI1+aP +GLH0n9oLzZDQ3tcUXhhgtKBZhJsQZUQbC4AcrjKBBoqwHSG+kY+IaKJIUMQPQS7DVlTiOjSHShhh +SqIoOQ7Gp6iiKmE4okpEVlXgog2piyoMvQRyH2GRWpO6kKQOOvvYCeMmrP1pzk8ff26UFaPFXJzv +M9tta8sqaiIxBf2MSUa1mqm7evgZz5Ixp820wVXteaZ1RPLjtG/9hlhjWiKnrUCW1YyFeDCYaAII ++apRDUEYjpQ2yrkbdPda0hBmZSokybNPUFkx59pUk2dDXkNOzQqik1MjE54RbPpDpgMOxSwjxQbl +bXmaMY9Kg0a8+I9DIw+EKM3L/OWU9Sxao4kZzdObZkWKGJYSlBxI0NYGuRUYHUnJYEHM+CqgLzMQ +oi0TsTYpMkBYp+mtwiJkwCNtIxWFpT9xNh4oGmrxRdYnxMhreuwfAI3tzIKOeICyiFlyxGRsQgXY +W9G8d8dnQGOLnq/pIHs8k1A1Mq+29Z8dv4w/oVEbq2zQCLWR8YSSClDBRUJVT5JJVEObsIJAE34u +KDwi44K2dJhQLDhybTEtJW3ApR4iVUoTiCguRkBdCg7/W9DYiuwlgJ6uiymTFNIzkclI/k+WpLD+ +SLoyKNK9crdFkgdohgFohEsOqf3gm/LomGAYwv4DasbjUTghVdWaMFjJ/gSUxi1ULc7UnIgOPvTA +Yy88a9XCJV+//r7SEHCZjcVdOpud9nVlZTubIvgllRp74Im0Xbkdn//agGe1GjsgC/YeGlNSK2Xi +iPJBGkQSIrLZw8xfJFIwGRUsI8kAi4SDu5QrClVCuC2olzTTv9iARDdoli4839pqAil9PCWmSVci +ONB0M9LBKLgYR51DqlMEqzGuk8lgZIYxLH8h6xkP6DWuqy387u0T6Njx3KKT1pkYJ2HtserAhqOw +p3XI3WHnpTAIhVEu8JIJvTRIwqYU/6XRNXINRw0aNf01ZSjQF2E+p26ZoFGorRhGVhe0oRCjIVYJ +vUngLMjYaWhkPjYdoF1P2nDfDdK0sS3a63RtXJ3tQas9+7T9dGjzK7uY6imVSzyl/wA0sgNJMxFa +jJQOQGM7zaA1TLZfCe0nXjsrga6kjdWY7TxZrcZMDOjY3P+9Rwlr77coNdmgkTxTbGoBCUAV4Tpw +HGgUVd9IjSXLTThU6TZYoxVYxwmLPKkymh+JWZ0WR/xahAzZRqR/had0n0OjsIi1p5seMW3aa2+T +1G6Zibh3TRmj+wTpRoIsMpGKQA5VBnEyHSEI1CRoNRDcVPiNnEnI3+dAI4f7FJTgpsR0IzyfSGVM +KCrKpMb0CRRHdSQMFr0xqlMtqOyWSPqVWKeR/c+54fL6Tds//ffbTTtq8szmopJiye3YsLNia60f +Mgj4yuUQUgO+p7mTRcfN6m75Y6xGwjaW0cK3JlzDmqVDFiE9I+ouCcucvfc89jzkVBxIbFxAj5+m +aIMm5L1wOvBJGTzamNVi4mn/Cq1HJNQA+TQ/BeszeBJEKKbCRRRkhIsV/4Mik7aTBDYwH/m3Kud7 +s+TZVhYOUR46MW7CqMasI12NWF8azrEJLShfYoXSwVyrib/JvlWxclntoEUq8nHF86CFp21kNwsT +kE1D+q5GS6Iho4vhX8GU18YUwwHvCKOmcKgSarL6oMlueldb/+mHsQsp3VoEt4fGNvNaqCuZ52lf +3aM9NLYLJbZdLenoS/rMHdAU9+a5pse55UsZDlWRXyYedbazpi9MzEdeA62+llZKdnumjkBjtsvo +QAAm661k+429/PyPg0axboCC7FAV5iOnDXOqhkhoZ+KHVkk1JZn4LeHxp4WScmMyMV/EGtL+TA7k +0Ck1B6lw34hVuC8dqmKu7MahmnpimmBNyQaBpYLMh+R8iGGJooRcvk1gN0tWHA6DEEtYgaXB8iCO +7ot0P9DLUXc8iW+yp5W7PSowSBToHBhPSxJfM8bJmyoF5Whxz64nXH2h3ip98+wbFWs3+SyWkqLi +hNm4qnx7fUxGR2LmUhC/lUcmK7T9T0IjIZxm/TC6CYNcbBrwwYPKfxIFmDQw/Kt9Qh5rCcfLJhpZ +KGtci4jFPOs0oroBci6EQyItJVget/ofU71oLYMlRUBC0p9eM2kKXlVUslWNaJxCtBy0hqYqDaTF +CbgUXlYCY2FH/bEbpo5g64obJStQOFRJRyCSElf1QQoPRxQ0+GPrkFOKSLyKGC1vAmlIHUEFekJN +Wm8YRyTVImTSqj4WGYqk92HHWPF/aQFD6cMIMSWOgZCMBc1MhA5BkU/6Xmp5QKvItBrFg0lrKLsa +ufZWY3t0bPO9NqDV3ppvi50dSGduYSGlfizrZfyGeZCePnzXKauRZmvqeWWDxpbxTGNtWpyJF6nI +Vkcub3cOVbG4smyZVc468GSznW4ffP7HQSP3V6J6oFh6kMcmUsU1aKTlxOtQxBQ1h2pGuFE4zlhP +bQEkDlpoSh7zRFjAG4zIN2CQIeTETjGefQyNHYk1sgUrJhP/S75OEkHANohkiniRpk3sG46UckSK +Lt+AKCExYuCCQ+oGIlSqqH1DGRkg3ZC1SN2msFHb4STlL0KSkaJP3jCH0RiJK/bivKMuOLO0b4+P +X39768zFHslYVFhgc7t+3bwlFo+HQW81oikjCSFISuGcypw6WTXa9gdk1Ub/EKuRoDEdIUsxQUj3 +YK4N2UZGnRFlD2iTYDhSyAcjD5PbxONPVhIlujBMwJVBg4/BFWoUcUlElgwBHv6ipAx2QULzIoFJ +NFVB0IH3g7lLwBTi+JJuhth6UpXjMTxHtJZGE01FxtGEnTAiyWokaCRwZOfqfxIaMYNorWkeVS6w +KyKvNLFoxODNQUwb1wYlDCNEy4gZX3Eur0TanMamoe+x45Pc1Mg/ouxcAi80eoGmwfa2iL2SBS4m +AGY0tDdoeHou7ovBAEryZ8LJTCY1nZIsSIqp84caKRt/InYrorfcbCalT9JPdNih2haT2tge7fQT +tnVbCdbfAI145JmnENrIPpDWrU+xS2jkJ7AX0Jjpycxc1VrV0z+hMat0bPdc21a34BWROacghEky +UY4/BbfwL2cGcBUYLXjBL2jpcdSNnVl0BvqDn27K+aehjbY0SEnXIhfkl4QN0MpqFJ4csgH+ozSc +TGik65VI8EDWABoJGTkfP44BIV8W5AIp0kBKqvemqMjCgMWIInA6NBEGNEJgkFQyGM02EuMsgeNU +JVVvADRaKblCHyd3IXVkhAQ79MwTxxxz+DeffLr46x8tQaVXly42h2XJhk2NqipDoQAAkJlDwiye +RJNEMiP2AI1i7DPdSEKvb7Ep6Mlk0QP3DTSmBZcIlpFGRf54Tr0knYqde8RnQnoGrG+jDvxdo96u +l6zwHhvMskkvQ96bDRYzdALMExVlhFwmezShREyqajfEpISsj4PUhGYldtXkiBqcKixxNRKL65M2 +xYwWXegejZQYg2JSTJinAdVo0YdsiUaz2pQMBXWhZjUQN+ktMVNB2FoQNkohvd6PY3XokIlHSnYk ++VcJGpnLCmlP2ICHwfezp03YrXve0kUpUuRsNm8ziLsi6wIUWuxkSwPUJIS82c8Mc1pibdOYsNtM +ajBsSZisJocKx7/JDFd+FGhmk+JohK1ToKJxZQl8i5QMYt/qkmajyYjOZ0ZjzKKPqrIVrKa4KhnN +pN8l9LKq8IOCNySGn6J4AWY50FGPMr4wzo0G+D8wpxlEcDpjQgrrkacbB3lMFL8AJJvioPPpZLJS +QRLmCoo8ejQNeHQ0dVSIHsZiYdmyj7zF2BXjk55C6VElKBYsKc0YpWJ/xITYvW1KB7f2t9MKzTge +rwVxeg9b+yBgOwnclg3UVp8lUyzFkEr9UruTZJ1B2WZYxuftgVN82ML62e1l7FaV6fjPt9NYsn+1 +/QrLWqUo63ixSdeKtMnrueV7whAXM4/lFOXd4W/BSsWXsQbtSfTRJctGxIIYBVl9ZG4O/4uj6WKF +BclaOp8zpbVx/IJTP0RWIv8e8RpagqP4Ap0Yogd8aea0caSfi8qQe4xDOmKItNBHarnQMSnZxKtD +2wTA078pNhyptZoR2/I4xDmFFctPTbtOBnnI0wTaCwu9Gq0WyXCEiEaoMB43m5BogfpuqtlsQooi +/KgyyU62KFDhG/YICVViiuBrFrMFTRjRoBiyRrQmJssF4j+RsJN9rN8p6UYffciJZ52+btb8n958 +TxeMlvbsnrRZF63f4I/JECXg/XAKKY8LBCF5ZlNUezGxMwS0ECDChwU3OPze7GSDczCB4uUQihQZ +NZgh79nk3dM8yib4tZHMwlDNhEZhHdKEo0nF8wg3gjEGNMKawfAADA0YVBMQMY44rEWyGSR7QmcG +VcmYjDsl2WOqMYa3Sf7qaKA5ihJ5+gDYvjDREQim5FC9x2QsdTv7mD294nn2ZpuSjCZMCVWWJXL1 +6VWTRTEbK+MVm+TGSr3caAwE0abEYkDsl8omRHT5kqFv3NN/h6Ok0hDUKxgyAY1AB1otFDzbC2hk +oZNlSz+7NDTSbBRefJ73tNKAZAyNZAqiliBjpM5kUM24I1C5rLY4YC8muQwhm9xoi/v1qsXkcAR1 +rojObbbIMurzWkmdMoNZJMPUdJjtdGUmU8yYDANSjXLQFAkkArIaxsR12Rz2hDVXdrnDkjGIQKwR +34nKEZ0aw8IgG50aj5LPmXysWCqwv4m4FBcaow55L3Fd1KiL4lmrqjmOSQtNEXiKOce50UL2pCyj +NusWyKkhowj802jwPBHDQbOnrVYozCaxislgJomSJiTvQqbvE2hs/1yzQ2M7QzmddZU+25/QmDmw +fwQ0iiB8a6HZ6sG0gUZa80yioWnG38QadBA0sg+LWd5aOiODn/CginALz1yesy2lZlLznT8Uyp/Q +oXnnejcaJhEOt0AjYxpjlYBGytfiRaFJfNIR9xE0aquJr4cfh6D8sUXGq9dEXabYbwXXHkxIWDXw +D8HvpgAgFaPJzJn7UKQTgEYKwaR6M9Plk8VIG26DjpepqwZkP05rN5tgQpoTEHFSbTw6YPyYE84/ +s6GmbsrzL0Wra7sWFdndOVsqK8samiishl4c+HlRrUD4qTRobJlB7aGRcAKHq4rVpLMY9XI8GVP1 +yMzHNSlAaOby4OM9S+19A4384MmrKZIxhMRnjKRYNc0ioBpyYYj+S4Ntgc1i0kcR/JKsFpPFbjBY +TMm4O1ltja1LNGyK+esSCdlk8OS5ioq7FRZ2KcovdTm9GHxUaK+vrdq6cfXmDRvlWGB0Ud5412Cb +XzLHMXyQ3GHZoS/TR5cEdm7wb5fyPL0GjDzgoIP6dx6YbynWJZAQE9tWv/67Xz5b8v33E9Sibtv1 +iTCoOByAhKmkhSRbQWM21GunjO3xCy38FzYT09DIRePJjQ+yFzd3oYArrUYMmokA0oB4tMESdsXq +8mOrk1WbUcreq8+1OruGrIOCrvyQHcYc7Gfcg0kvoZMn4btRr9gNNfpIhS64NVZXqUabjJakU291 +QNnEdE7KdbruusQwq7e3wWdqUA0yPjElos0hfSJKCaIwFhNWCIR4UkZzbiwWMgXhmA6Df22A3wQ4 +CM0MvwvTEV4U6BRJBbipMd6F8qYZiKkgRgrvaPGniVpCdU2DYiuNMGMBtDb40lzA3RmOQnFuI4Lb +WI0dnP17fKTtrMZdQGPbE7SBxqxhj6yTkA5I/W6motYqrtx6NHaB+poyk0Vi7Pnjdqif/dr3CTS2 ++d32VqOI3qW3TGjkNxkagYHMaCf3qV5v43bzzICjSckxNqJlckoxU1F4o+sX0JjmhhC4ah+mYo6a +tzadm5hO3gA0Uq24FAoKchvOyowKrWicZm6yWihgld0xLVajtoL4N3dpNQobJfNhpEdMW3xsHDOr +XSPFgzrDzFP4+WC4UZ4TZCxWdDgcBivH4XBCRY5EYxCgCpUMJ7oCR7voLAgNUl8NtC+OxuhjypcD +NFKNAIyxW2+CmNouB7sMHXDSJeehkfFn/36zbMnKbp0LzTbrlvKdgZgMEw86ZRhGqmSGaE9zB9nH +3PpG2lmN1Cs5mbRb1KICg8spwTaqb0z4Q3DGSrEElHt2rLZbp21mageFQ0fzGgWVRgTMCCap0wUb +kdA94DqVYDXSmCGiDSPSLNmtRjtUj4hd8XuVpXLl8khdzJnTp+/I0YMOmjzuuNKibnaLizz5qVA2 +i2cyXgLNDS989sRXn7yyv8M7oWCotZHs0LAjuKhpy9z67QG3b/KhJ5957EW9Ow3mmBsXXcN0Igqq +sr1+xT8evsG/aNEwxeHdKUky8v5T0EiMkrbQmE2ktJ5xexQFu4PGBDXhJG8DQBC+TjiFExYaOfxj +RQQW3mK7Lphr2GhpXpWsN5Z0Hj7kkCEjRq9ZOm/Zt18cIhV3jvgk1Zq0RODQTEZVyWqW3cYdxvCq +cPWa5pqkJ6e0Z6+hgw8cNujAriW9c305MJDrGuu2bN/49ddvbF02p7vOMMrWOb/RYYyZgjoZSp4h +nrDG9WY4SVQUKoQhybHXqIKSiPBM4F34R2Rd3KTqrTJiBYBSPYxWDC/6iFIFYQqyp3IoM6YgaeUc +AWCeOG1itMRiFn9qMZvWExcftFiNDAOp+vD8h2ZPtrLeWflv9TDaO1Szzv6s/oD2fqr2S4407dbb +n9CYOR5/BDSyG62VO7xNHDHl1GBsoxkIeOIpSCWqqWsgxDqiYlSuRYh6FkPEaSOsE6xUdpVqQUcx +e4VzlWd0Chq56izbPOIDnslpDipPX0I4cqhq4QbNahSmJC2ZVOm4tEN130KjuD5CPub1Id4lcuiA +i5DWhJQiR4D4enCewkRM2J0uq8USCkcIGpNQtAkXGaqJgE0BSJic+E5CjYZjyFs0wFYjkc9maTJh +M5hRPTynZ5cTr7kkvyj/u7enrPhhZr+Szhji8tq6+lAUvKQoRA8/EDjAcL/tQxvpKdTeakT6qdOS +6NnDOGig2+c1NzRG160PbS6L+0PGKEVB2SzOtrazCgdxAR2FRhpIcu3C/iHyDKUkkIrFgw1oNIEF +zNAomZMwyB2mhFPXaInucETmVm2L5vpGHzzpiANP2K/fgXnOzuST1/l31lY3+huocYmasFudJZ1K +JRM4TWbgbVSnvP7mQ1++/uj5o8fnJC0Bo/xLxbLVDc1Dx0w858xrx/SdGMGDSzbGw4rD7rHY3VTU +IpYIB4LOPO+nM96c8vzfu9QGem7T2yMwgSDRyVtIhnYchJT/tEM1wb4LCxWRN6pmQCP8ojR6JnZl +wBJsKFKXWup2JKXRk486+dgrRnbfX6ezffXt0689ff8kd6dixZGMSDmSPaKEFWcy4Eysje5cUl8V +droPnDj5sHHHDe45qsDdLTWZsFq1Wjq1ge1vfvT41PdfHW3PHWPqKjeEohbVKKsSxlunBvRKvaTU +GGLNsDH1SbSMcctSfq3Z22xAOlKEKWqSqhhQJdjIZYXjegkxYARsWGsWiS80v1PKgwi30CIUcXIR +QSGHk3DBt7zZFjw41kiCiBRzFjQiGJwJjK3RlKGxFSi1h8asscb2y2cXDtXWv9IeGlnottraHZNt +mbbF1j39/WesURudNmmLrSk2mdBIs4hzfslNCmIlZQiALQKMQGSRHIzsUGUffiqLkbOsxCoixiQd +RPOZSGis+aUpPginiWgBbaIgAPtsNYcqk0sIGhks+VMxvfkpClMSjj+RY7hvY41CpRSpdExAozKd +TKshVyg5+EyUW0d0B6LrUYobcU9hxqGblMUGbkYoEoGfCv4k5IqTRkEZd1pBbDN8rWDQREAXQRyI +iPGgThCFB1LCaAgC9vLdp1x+fo9BvWd/+e3KH2bY47ocX962mpq6YDhCo0OCgjyfyaSManCcGyrQ +UbAuWzkA2lmNZp1akqsbM9YxarTH7U0E/erixc1zf41VVOliIP9QDkiKiLj7lbRvoDF9FrYUKbJI +hdk5aMZWI8bbCKsRHFTK0jAZLUYrxi7mS+x0BVYm6pcHQyMOPvasU64e2mO0x5qjS8TWb1n2+fev +rd++ZntVbRRNufzRqD+x37DB1119S/++w41Jq4Tom862Zuu8ay+ffObg0T6LdfrG5bMbGy69/G9n +TL4s1124s3Lrx1+8klsoTTzkpDxvL3jOwZDy19eH5bCtKOfp1+5f8/m7IyKRkvIcc8wCCgrFGimf +A/yTvYNGWmPZJFd7Gg6vlwyHKtePpzEDXxcKBE9QjBYCsBGnMegyrjY1brQlrr7q7sMOPcdl8+nU +2je/fPHzHz6Uy2vO9vb1NcH1irWsD9rlnY7gLzs274iFD5hw9LmnXt+v63CnxVdfWzFnwYzFq3/e +7t9WF4l1K+5y7lEXj+wzJhqNBQ21d999eeOKxceUDsqpilhjCfCZqkzyel3zumRDeTyqc+p8Xpfd +Yg4GA6GmaKnBOGCnuVulTQqbUfACTBz4X9mfCqA0KCigTxEBog2l67ln6nc0VrwU09AoHDA0bTKh +USvJyFFIliP0aDgsQkuDHAAUks9ExzaZGPsGGlsvwjQSZzzwlJ2QeutPaMy6HNosl31iNbZfghQG +zHi3jZ60O2jEpEWehlkHaAStQWVLUfhUacqKmp8MoqIXlTAd0/WntJ/MmDUIcYlwZIoXwzQcFhqa +KkeM7xQKMjpqIXfysiI+wTGxPwQaU7qpFv9CLgDbM5TCSORUyq9jFiABCYQnoM6QSCDy53A6Qaxp +DgQRt0uA0scWt2gJBfMHg8I9io1wpUYCISoklCT7G+IJn6FQXESVm0z6wy889eBjD182Y+avH3xm +D8WcDveamvoaBbwbKnIGLAYcChIpP0Q2IGlJ01CzhtHybNtbjeak2qOTbtLhjmH7WSR3RBc0rFih +TpsW3bgFPjGDajDIiThlnOxx+13QmJb4bKBiDNhLjRQWmIgGvQXP1WSIgwBjtKCliAHBV5saM4I8 +YjODfFPoXOZumuffETa6rrr8thMmX241e6CczN8y519vPrho3hyrauje09EsN5dvDw3ovd/V5/5t +eL8DvJ5cEKbQ5yQab/p21gdTv34nuG3NYQeP+HnBigq/cuP1D592zGUIi33y9bv/eumx66+49shJ +ZyCJJuBvNhltOT7fmvJfz7vmxE5ddEpF4+AGa+9qcH4QWk7oiGVFxRsg15DgCPuRaJZEcIL4TRGZ +Us+C5Hir1a+R0NJiWoxpK5smw7mT9sCQ/SSOJDMbcxL0WyNcOeRBNZpCZp3HipIR4UCJebkjvjIW +uf2uJydPOEeO6xui5f985OoFM37s4/MN9PTurbqcyEAxqc3u6PxQ2byaCrOv+83X/P3Q/U40S9a6 +htVvff36v9973aQ2ulxJL4oVhs1+RbfDr3vyiacnjT4HhK1Ppr787EPXXjl4vKMyqXeaVitV8xvL +avWGISPHnnzS+QN7DrUb7cmECbHsyor19z17rVq5fljAVLohHxZu2BJQDM2uiAV6JWjGCdWBeCQG +FMAlUfIpab6gh8GCwwqBKEHFSdw2NG2hX2skbPqENCr8KxI62QNDdB6kkXCgh1oAcV14KvRHPDWO +uZCUYQKAME/FmHPaDaVri4Y56fm/KxGchcAt+IpCY2UHSKp4Zfr5shGQeWaC62xhjGyqVNvP0/Db +Rg/LXN1Z68HyAO3tL7c9vr082ec32+Yn0+HSzGsXdS4y32lTi0CAzR7vltkmPDtgmYGZjYkJjx9o +7lKCXakwGTkSwyn/wptKk5QsTJbTTFXVvC+s7+16Q5lmMTlRME3YseRQZbK0uD5ENqFGCq8pzzao +mZrQEUEETHfKB+QQm/gW333mGdgNnLoCzQeb8WB4TYmJSfasqOuWzlxCTJHcp2T1qeDWoGIbN2Mk +2xfOLCxbKpAqYyHGURk8J8cTCoajoSjx3GEsUlImTDHUYtaBjYo3LDiFXlJC0UQ4CroLEfpIEUY6 +o4mICzpDXTI65OSJx551wvYVq2a+8aHaHEEb48ZwrD4ihzl3DDRSdjhp/JX2w5rWKnY94igmYEiU +FugOOsiy34Eee75O8esXLgj+Miu0ZRsyL9mkJ3BlCu/uN9ZYMrZWBxP0CeX73YTcImHS/RpbQSP7 +UTXSDT7gGC6xUWngwXFKSg6rnEg4rAiLugLu2BpP/c8NFd0GHnDPrQ/17DoGc6/KX/bilGe/+nqK +J9Q4prTn8JFDVmxZPXPpqoMnnXLJRbeXFvXHOAMTa+sqiotza+s2XXLj+f76iiMOGLp8w+aqkPmv +tz101Jizg6H6m++7dtHqeY/d/cwh+x/TFKp8+t8Pf/ndJ126Fx9yxOSNq5at+HnaQJ/bVRcfIBda +dyCrkZCQoJFc5qQf8UZBNloDPBPTK0wbyDRGpoYkRdjWZntaUKbRkXQezQ/YsoRY4tKxXFEDrmYT +lC6VoBFxU4viMFsAE1a1Ii85M1Y76pgz7v7LU3rFKifDj/7z5hlfv31Qj259nIW5qscYkG0uW4NV +mbpjxYqG5v0nHHPVlX/pUTIsFKv7edYnTz3/eFVj/aGHjZObmnZsWHlMv4H99HmNCeWTNYst3Qc8 +8+hnJsmybPX3f736pLN7HZLjsiwMr5mzrbz7kLEXX3T9qL4HOyxF4i4jEVUvxUAZWzD/25eeuq2g +uqlfVa7UHFMNYUQmLbID2oRsSJgQfySsoIVliMeNsO0MBkCjxiMw6mKcSGyBqsweKPxLHlKmb1Ee +pliSBj0s15gSQ+kHs8HEtYtV6A14PmQaEi4m8YdWr47LE4iFJBYUq974wbbQ2F6IZ81tolwtAY3C +1k1RJFJ+Mf5v6/X1B0Fjevlp8jQtB1MfUHy39dbmfv+PQWMbMEoDoXgcaT1pD7KPnHOpJU3UGwGN +4HSDPEmWIq9LIGULNNKbpDEzNIqyOKww7WlLOzeAfyITgxmqPHnpe1xFgKGRkY+uHCEJ0tI5eQN/ +7xNoFEPCnpVUKorGI2cjj+umQ+9D+hxyNjihkcxbvETGM3g0eAOr0On1IG0rEAhCX4UflXI2qAi7 +DuQGJtbokcMIhI0rKrAzEQM1Hml6RjAccXobzKVEsjEZ73nQyNMuP9dfVzPvs69rV2+CJdMcTzRF +5FgiEYPqTF0b9xRZ1Fb6HsccxRlyncmBAwxDhjq8XkNzQLdqdXjZKqW6AUNK9g585biF3wqNLbK8 +HTTS/KEt3VKA9C+RhMcEVVKDEE00WWAUS7AdzCpY/gj42QxIzHPETIntpcoH9cv6jTzsvhueLc3t +C61sXfmSJ16+e+b0b8b3635k90G9Crp+t2XR14sWHzL59Csv/nthfld0iw7LtS++/fgPM34YvV+f ++vqarStXHnXo8FWrd67csPO++587YvxZdbU77vr77QuWTXvlpVdH9TtmR/3Wh569Ye4P3wzsWaIq +zZU7/XlGwyhvd2+j0REymgKqLhxTJcpgxEPGxtAo8udY2hJPGP9poSCKdSQEsVh/QnkQ6l9aJqag +URNN/B8KQ5B8TcUg6E0RoxJaFfypJiT7wAxLcnwRXl6TDYPqMy23B8pzTDfe9/zQvofZDIbZC768 +65aLDiwtPLCkl6UBppzZ6DZv0zVOLVuzSY2ecur1F5x4vdeTV1m74aUpD3019WMkJ9567b1nHHd1 +Te22y645s1QNnz/4gJCkvLViXiCv9LUnvraZLD/Pe/+hBy4+pe/YulDwi80rJk8876rL7igt6Fnr +L5s5/4fyiq0+b8FBYyb1LBoUkyMrN8955K4rOteH+9Q4bH5axdAtDKpFMciASaShUhDQaILnAJom +VEo4DRCiZ8ua3CFwUUGIU+9R0rk55sj6AVcDQsCdlHQEPQKYYhgT1lo4/MPFizF8zJNiscFFz8mM +FHULNL2E1WJ6FswapBFPLyI2Lltt3NhuT5uARrpGDt6TICM+UoYSLqhdYjoIZN57kzGba0c7e+aF +pgEyfUtZT9LGwZjlznf7cbsxbHu/WfxUe/u7GtS1HlbB5Wp5shlSThuZDjyIVAhQuEYRXwQZ3IjG +EKCEc1k4SDI2HGlOwr8neKiahwCWDe4b9iV7Jva0cVFKVtc4pqjNTy3zl+KITAESKf9s2VFuJb3P +WhdBJftUScdMTzDuhbFHq5H4oi3TnccwFX7gdSEKcdISJMHDhd44F9rEMEkhHRxPhVAUjIkcjSKi +mOvzQR2tr2/CepdMFtg5+BOBQFyt1YkUUNIaHBaLGpEjobCCGAsM8WQSUJtQFJfVihyPaFzJH9Lv +qMvP8ZmtM97/qHr5WqtkrQw210SiEb5Y8qaygrIH0k2GxN3tsBuTYC8m8/LU4mKjw66PRvU7a9Sd +NYkIeBHEXaUUOar2tcdNsxpbravMWcdaki75bkZTKuNRKUUpPTcpCM2OMhE0ovR+g4lpNwlkBoFs +abQii85mSUpyTmJbYeAH/9bSEQc/9rdXSty99Bbdr1tm3Pb3y8qXzLlwxP6n9Btl0RuW12z9aPav +Iw8++pab/5mfA1yETZX4ctprz734kMse2751bfn2zadMHF+5o3rh1h1XXXH/aUdeosSab37gqm+m +ffXeK2+MGnScrAQefObmJTM/O6X3yIMtXXr77Qe4uwwxFeeHHVJD0qWzxaJhow3DRGqbyJaBccu6 +Ey8UdmyLUhAUj+cSr2yHCBoXV3wVfxInV9BxmZpMlWCZViKGQuwU7dbia+ITfsDCGOEx40Zm8PAj +dYhq21OzT6sZGplTWqcLOLp1vfDCv+j1jljc/8q/Hw7UbDm09yB7wGRCuobbXJ2jfLxh4XolfMVV +d1x86o1Oq3d92dKHnvjr1J++9OUmunTqdvFZt9mkTh5PweK1v1TULBs0vNvCynU/LNtwyilnHzho +klGyfPTZW5tr1uXm5cxcsuLgI0+98/on8r1dquu33v+vW97+6Jl1q2YumD1t/vxfmuCnz3e8+ulT +1etX9lU9Xr/RYjChFgBWAMsN8hjhjhyy3gZ5YCI+DurFJyQpTtYwXlDggjl/7FTgLGLmwjHPG7eN +Tqk4ElkqktGnMyMyb4F9SYsVw8M8CBo7THuMETUDT9mdQncXe6qjmYh5a4apQMn0AdqzSz/EPb7Q +Ho82vdmJJx6fpgClnHK/ARFbFmbH0CQTlAj+M76VFRf3FpB+x/Edu5kO/8Bubq1FSJGEyhyKDp9Z +GFLsEqVsDYAfjEUYHNR0iVypJO3JzQ87klQ3eupa+j9/iq8ITOX0/93u7DES+i958Zj0wpI/FVAQ +7Bv2pPITZjJPKu1J6N5iqqcwWGBaq43V9/R7HOpsE40TCrpw1IjsATJnmIYKSU3/IqEOeRKoA443 +2T0C9ACqgbuPC7JYLTabvaGhCahnhOCBvksMJMrFN1ktkhmJzSoWKm4hGo7GwqjRokfUDPFaYyJu +h8/QYNipxAp7dz/iojPyuxQvmDqtesV6YyRR0+SvkeUAYjDMDOSuSzxAe/EQd3EoniSoP+FIoqk5 +UVevq65NNgWQ3YhQKhWH5zI3/DCyYWOG+sUPX1PI0i/o4Zx87z3p03AEWuzsaucQtej2jBujGYWZ +wxQvDKAOA5Y0WCAmLWZjzBWvzJFnRsvsvfvced2jee5SxLuXbZ7117suj+5cf/EhB48vGahr1FXE +lbcWz+3S/4Cbr/67x1pUV92Esa0Pbf7huw9H+twvnn3V04df8MLJVzoU+4+LNk069rzTT76kvrni +rkeu/3LGV/f/445RQyeiWt8rHzz1w1cfHVzcd4CpwFSb9MR9tqBL12yKIyjns/otctwjRaAicntp +Zs9KMHDgv6MwqdmIzBLeKaOH7kZkvfKuN8Pzqe1wI9Brs1nPu3gBGIeTgnajtovKeTxUgo2mlcYQ +jSqJum2gEkx4dvBcqJIehXuwlGB9xY2S7EQCSw7jcyIUqF2/al0nd16ewW1JWqxFedvs0XcWzqrV +Ox964KWLjrnFonctWPbjnfdevnretHMnjBnduW+wMRRD8oVJhr8TWUhhVf/q9J/e+nnFuIlHnnLE +ebjHbeUrFq2ZGTbFfi5f3XO/8bdd8w+nM29b3epbH7lmxrefT8jpfn6ng04p6m+q2PrWm3+5/qax +m2ZM7R6zFUTMTiCWTZHNoaQtmrRGDGYoRA6D3ms2eUCSMibNVgTsDch+MrsTFmfSBEcxhgRtvI0W +MJQN2E3YLViQICsbjVaMv9FgM+ptxrhdV+8KNpaq2zvHVnnrN+Q21xQqAY+ctMctloTdrLNhjM1I +dsFXxI5CR9B1BdWLHyVKIiEltM2OT+nJtuw6Mwoz7Wmnp0/5uEy6FpJS0IUE7gqNMA2THbBUdrkY +Bb9gz7umdwpzIa2GstKlpRcIxTRja/9bWX8l6wEp6SCQaZf775Nqu/l2u5vT3hDPIq1rZpN2bc9O +qhbvZuFKZdsRDFWWYpANokQcSzTUUKU6AMTEocLOLGVZycqyCTlKl0nWZ0q48pdo5rCPQ7hOect8 +jb+IgiMgUzivNIUs6xin7JQ2M0K8LVLsqJkGuP7gQ4LagLAK6BhYRsR2gBkLF5oC2gVU/VhENdts +3ty82gZ/KCpDYkCsRZHzD8ykMAdKmCGrHqsWS1mKBsOxMHppUDcS0nopv5BWTiAWdeZ5Djj+8B69 +eqz7Ze62pasVsEWUeG08HuKyNJxIKqIqgtn+uzbcAOzDMPy3EX1t0FgfNoTBkyW/FX4KLlvi2u7N +D6QpC+kX/OxIW+G0wNS2CwcCLxEacjqGXlGxN0wHRTKhYptRb7EbDWFjc2OeulEfDCQ8f73q0Z75 +QwLx0OraJY88c4dtx45Lh43vay4KJKPNPuOP61c0S54bb32wuLjPysWr6qt2YsxWbFo1f8Xy4UP2 +14WMXouvKRh59avvR48/7urTbtPL0dc/f/qDH945+/gzzjziGoQkF6z88qXXHpnYu8941xArmpnY +EgF7IK5vcullVzxii0ckJWyH1w8LgY1CsmZIUWQlCsUIIKMhrC2kSkGcY+JgR6oOdphz8HaSLKdi +EJhWkO4AQiGIqcg8/wtjmTi6iEFjxwuE+KE3UUMtoUKIfym1n5GSEwH5fdL2uJgMrUwXsZdMcDWY +rQ7iTqv6KGreJMK5LqcxHnfnOLYrNd+tXxh22++5/fHxA0/G0l60+ue7/3HDzoqlF04cd3SX4d11 +eRXl9evK16B4kxJulhKWhhAy+7tcevn9t97wr9y8vtG4/80Pn6jeuTzXgnRR9/U3POpzdQ8lwu9+ +9e8Vi6ae0KfXRGN+jx36fkrOUV37nOAsOabJd0zMt1/UkxOhFNUoPAJWl8ng0JltOgtQDmmY8so+ +wU9H1H3br3pFn2BVJznoihnNMAURG4dyCtXBSjoExtZKKEi7XcJusMJJI1lNkhtH2aV13XQfO6s/ +8lXOHJKY3i00y9O0o7PUCDaxy6xaLQaLhbQQPCbeJQuWOD0yemrw4OBP3sWn6R2VE/DAM3f2B+xp +F49Pw8IULvIj42fHAJmWVr/LcOzYSv0fNhY7dgP79KjdGosdMzrEoyNnPpk/Bqjw1K9YK5gm8vrJ +lco7VUOkuSCabxBAUl1ncpewz2kPu6ibQyabmCtkrYlCrJpKow0JuezZ2KOtFQGkBREz3RV7OZKp +r7JrhY0z0vbRIxGgaAG3QYLCb5F0ZqIXxRFXTCgy0VIVNcfjcbq89f5gcwQlZSAnUV6TUjWg2EL6 +2W02qtulJmwmM/Ke5eYIKk+CyoQN1FwZA2cyBGF3mqUjjzlq4JhR65ev3DZnUaLB3xho3hH2+1FO +i47l9D6cnEMke3lnuzicinXpZKRpwNJAhj8uG/oFyhIAyxF9IRKSqCWXZdNsv9RRmbMqbUS2OgW+ +oHkPhM3ODg0S6+RJJ5oXxc2SkklG0xEjfGkJpKw7LLaExVgvxdfV1p584sX7DRwPTy4k1RuvP7Z1 +2azj9h8yyN7VFrKZ3PbZtWvmbd5xx/UPj+hz0I6KDZ989WJ+sTOpi2xYu9aVNA7rPkA1KTs8gX8t +/NGSW3D7FbeV+Dr/vPCzf7/9fI9ORX+56D670VPVvOnpp/8+0Og82NfVqkRh+uMB2SkJleJ4wCfU +4LEYbZgh0JhoesCRiRgy2X/w6cGZAjsBUAgIJEQ0WjBlSODCrCHLBtI/tXOdGsqvQC0fGC7AVFif +lKFKX8XOZ+CINn5DW3lcFYhLfpN7kZiXnFxMdYMoh4mCDmyGY76ZFRtUr3BxMl6zrZwUVJ1sdXm9 +hZ1rGpv1bqnGEfhmy/J1zcm7b3953IhjAQVry5fec/eNxqrKaw8/qU+Xft8vW/TBnIXDh+8/uMdw +sx5l4/JuueWx157/8V8PfXHDJXd1KezSmKj825M3fjvrk+JCX6TScMd59w7uNFynRH7+8e3v33jp +uC79D3L0dMq4a6oSl1Pn6d9QOirWrZuS79HZoXMqKPlLeoPVbIZ1iEo6OpNbV9EpMcPc5B/QOXfc +IStzk1sKQ0FfLO4yKy674jTrHEar1WSzQK+g2g8wFfUWV8ICWDVY8BKDmWOrLTYv9gQWxmpLh4y6 +98Y3nr71m6fv/nrkuOMqI01WF0hwRrMVEQpUF+TVjAvAv1R20AQFmFQZPGIioZOOoofiQvYp1a3n +Msh4qkwzIEOfzX086dROvDEyOKlUE1VronCAwaaT7EBys0WUt+VwDH2BUJZSv7Tq0qQQpuyAbOtt +F5+nja/Mz9p46DRPAwe2WatmgcnFJMXOJSxb76mDxVeE4/F37rswK4WPbk97O+sym3WbaUKnrzjz +Zin9mMJpLXesRZsz/pMpydKu9ZaRoDGDRkrKDx4mPU/6ASr0zRl+HAqgOpzcP4IztYEmrPwSgjKI +chNHDoXsYSfRKOp5grnP3jZqA0D5kUy/EW2tqIK5wOlUBJD5aCQi2EHBXB0KSIo2kCkkFgMr0Dbz +2YtwEBMoeZeoOwOlhpFrE38AAq1YR5LVhqmN6A05WaxmSZVj1FAKtA4lClUfA4IPnQ5HqDkQ8gc4 +xZMCdYhBUlYeFpDDqkj6cFIxOexgyoUDIWQ42pD3QoOIUjYJlGt2WWyNuuSI4yeNOHZi7aay9T/M +DZTVNvtjDTElCllrkFAqFCY7rpX6HgtnqnCNcIaG2NvMa7HYCHoyGQZiVfD78AQDDOEeR64IXG8o +/qVH7jpin4jemEwg1moaSno288JLzR2KCnONgcxd678plJeMhdUOGsUbmnsbrj/Nu82sYtZL2Aef +hPoAtzR+I6bGTTnOnYHG/NJuxx97OqAbUmvh4p8X/vj9gT269y3srMiKPc/TnFS+nb7opOPPPG7y +6XEl8slnb4Oi6Ssogqm0fMniHgUlhU4XYO2XRQs27qi/69a7uxV1q9i5/sV/Pw0f5NUX3lJS3F1J +hj764q2ytSsm9BngjBuR4k9Zu4CfBAhUVgXjYjJjXujNiHtyYFAUQSJHO9AR4pAEJKX2pHeAFHay +QjR5KTxyEI4kPoVgZdkLm4ni11SkCKuHw2ocjqAlRRYjPXGRGZyKPmhREjERONmAHy4T56haNyLw +cbXQZq+q2bJk3QJkLnqceeOOOGpuReUvTRu+2rp4ZU3wsQefPnjoRMzsQGDj/bedaVXKzzztkFAy +/O9vP3x//ryBh47/298e7l40dM36Nb+uXGxzuPv2HODL8zYEtn47583rbzxrzvSPB/Yqra8LDBg8 ++pCJJ0BUlNdu+fjT14vslgEFPVXQdUxW2S6pQHncrUWKmeIyKDYwlM1UbthGRdCpMi7uVDHraty6 +1Xa5T9/hr98+5em/vXf2mdeGVL0NKSPsKcFYYbaCOARMslpcZrvD5DRLDoPJbrDZHahsqMuzNuQq +S0w7F0Tqzjr5lqfuePfog84d1fOg0X0ndu3d148ixjajajWiljp82hKpumb8izgHdjw+/Gmi18A/ +wCTb+iKEIh4lrlf8yz5wWP/8gogHYmdYJeVVOADoXzw5ZC5z5Uj8zSQ8es5EE9DWMMNThpuro/6u +dvjYEjoTq7S13ZNakHsKj2hSJHPVZn6tA0rybwB1DaL36Gb9DacldErtGejfAo6ChJy5tfqVXd5s +6yHlAWenOJCD2k5xYgEV5uUyaUy1YhcoPWAtPidEm7ggptRoK7mjmoE4nv2r3MRDiy2KK+Gib0Lk +C3BgxYtJsPR5hpwWXxT0sNQmNJ8Mu1P7Ic10YV8uR4qQf0EhDOiWoETaoafiNdRBCCw0b+BqcIoM +bg317IPllevLVRSlscGPtEar1QYjEyo6dEyLBfU9sSDgsDFZbTb8SjAQjEWi8AzRcsA4gYBDGViJ +2khoyNgRoyZNaPQ3rZmzoLZseyACXIwGFCWGn1AUEZnjWxR3xOIwbXS1hSKa5iIRU7Rs5IZXQjng +vC62C8kMNVoNOjPYQHiDQqC4HhIPYL7QWmaPQNYt/bjaeyHS9n1raBSIqAVLmZikFX7V3OHQiKgW +NelWqPwJCqXBKFvRBCNeHgkdNPGIooLuUdBNY/433n/WrMiH9hvtbHYAUYIedcovM7y5PS48/VqT +zrZ204L3Pnl74sSj8FOxSKRy24auXXOtXvuKsq0//LLwqEmnHjj6OERWP/vm9VWr1h029pQJB5yK +61ixbs4nU94e2q1n74IuxpjwBbPyxdOdWbTcbZpSS7iYPASimQUoQJEtP5iJKEhAr62YNSRzeSfs +1HYwQ3jHMXQwfINoeEGuPJgo5NyjblrM+KZcBOGjEY2aOS6VWrY83OxQ5ZVB/wV+mynhH1ornq6K +19aYzYfQnRp6//1X5Bj859LZx1x4xsVXfrqsbPqailtuvP3w/U8OBlHXPvH3x/6+tXFLOCfx7xk/ +Pv7l1JA977rr73rojhf7dh62euOP9z587rmXj77ypiNuve+s6+49/YzLj77/rqtqV688eb/9u1p8 +cpXl0nP+YrPkB5KRr2Z8um7F0rF9BnthymHoyNKihiikfqEyqwmuEUIfmG96GyGTyYxEJjN0Tr3D +GreZMOknTjq9IH+IzuTpPXT/OtCyJb3iMRtcBrNNMlmtBovNaHFLFp/B5lXtloQVTgWMYo5ky4u6 +bdt0zWsbG8+/9NYrzrszEIq/88Uz/3r/nhe+uHvW3Gled45RshltDtJ1cVHQcm1g8cIJi8dEj8xo +Mxt4l+xsU9JDhAHIjnH8a8NjQlgR4qElogwJkbnDT6szY9RNqKwMtwf2uFWKOyQozwTEDKVw0VCu +FifACd2WV0NK5glga71nXYI4XhiFYhMzRJxE+NvbU1HaG0lZf2WfHPBbjM7fMCDtb6/1Oy3LaG/u +Ks1MESFiqshFef1AIErqI1cD22rkLCWaADl8AE4iu5G8oIJhIV4Q2IiHs9ugawoF2XFKM0TQdniy +pP2bacwjZn+6+yMd9juUmbRvlsQIR9eoKwA7RbCYzbxuYPJZHBaTHZKM+j3AbiaqUTgUIi8XVruq +K8jPiymJ2tp6eFwBOOFIFE4TcgrazKCkWu02yFLw2+0WaxQl44IIGpK5xi4zkxVjG0+GEoq1b5dJ +553qcjpX/jR7+/J19Q0NNbFgCCkgrA5QkdjUnaaFY5vl0/5PardLflGKboGyFKNWP1o7Ze1s6Kuc +UFEuFQ8M8RacGffusONGqcYl9xrLGN2W1UVKS8Z63pu5xccyCZ/95ZzaI3qqkColpo/QdqjOEFGE +yZ7CSCedxp1qIOJ0jBpzaDKOkJJl9pIfVy2bf/jIwUU6DwpWI799QeXGRdsqr7j2jq7dBqlq6K1P +XoiokT49B9EcUeMBf50n1xW1Jr+bv8DXud/ll9xqN+cuWz3nrQ9e7NWt9PTjLvI4igPhmk8+eckU +aZw4eFjCH7EiM5CgUFBgNDapoCOStUfxRdKj4LyEDQRuJKQ+x6XIp4bQl8bEYXoIqVupqJUIazEW +ijcJXGF3ktmBN8kVChylXaOE4LSiqoRGZxXU1gyDgwNX3P4K0UjyQFP9CH3CnDB4EvZc2TrCVzh/ +2tfPvvYgsued5sIbLrj33tufvuLSO0446ox4pNlkjj3x0r2ffz/F5PCa7d36Dzj6gbuef+bxDy44 ++w6nNf/bnz++7c4b6mrXHzF+qNJYsW7+z1Wrfi1NqqcMHXXzSceO7tFz7aK1Ew48dMjAcQlZrqve +9P0P7w0oKhqY29kQUOBuodshfCGvJHMV6A28BVI2ACNpJQWCdAgbCGzwd5ptqt6FYSdVN7x4+o+e +hCHX6TbgOaAYBqkOVrvdSS3cTKpE8wK4CocNSOEGo9fVbDEu2FR19NEXXHjaLVX+7Xc+cvlDT1z/ +2r/u//DJB6Ir1g5ylOQZPA4LfLIopA5qF/zYNP4arSbjGSUJ/zgGiQsjD6z4F3oPPymh4qAcE8AV +1if83ryL9wHzwvTkjzA/EqAdIFQOFQfl+xXQ3FBpA+PAqW2kqwomDvNi2OBv5QjSTLisbkzhNUrt +rV6n6m22w5e2EiNLyCtTFuxpyWeVS3stL/b2C4KXsmcM3ttzkqqRcr4JPQarjeh38AvSDu4JNeTl +VA3mg5ArRGCkeJ8ZJaIFB1WPo9ZrKaTMcI2mfaSZL7Rqq5qrgenUmutUMxMFqV+ki6SMPc2Xx5Cc +1r725q6F/45+jDoJM/2dojRQT21QK6HuYu1hUVPGOeXZI0tDicaiIViDVIAnkfR6c1HqZudONHqg +QB0OQn04ulWciu0HyvMA5QLlnYOhsD+AcCOtSCqll4zKMbfFhiwuZ773yDNOyCnIWzFnwda5S5U6 +fziGqpNxmSJJZJ8Q9VywdwU/l1kXu4oMtFI3wYaFO53CpbgGprCjrLPoncIqB3m5ObgHEWVCiBGW +o9vphMRFAViAFdI2iOmTnmRiQuxi35vxFhrPcyh0I7QgRn6yH4VkJ88kbGzySsHRBg8lOiMlbQ7V +YUh45EXGpp0FBS/96ysp5rK7pTseu3zpt5/cdMSJ3qDV4nbWOEJPfD6lc7+Dnv7ne3azd+bcjy67 +7az9Rk545oF3HGZnc3T7oYcPuv7Ks8yq8ckX37nmlgcvPO3mWDx0053nfP3D1zdcetPVl9ynN1gX +rfzilmvOPWpw30O7Dm4ub3aY7LIcoYGhrFWqgEs+AnieqSAc2hhrrex52YgOvPz/llqdHNQRt0pu +Qy4AgDOk3Nxkw9MR9B/xWlQa5LR0LgdFra+p3zIy36kvCLX/RICYXnPJFo0lReFnikGSzYlQFjlm +4fGFPmawWewO2ZmUiw0zwpumVzUde9yZV537ly4FA9glgy5oMar/bUrOWvSz1+0qyi2x2/LtDjdE +u5psnLt01kefvfvrL9O6uqynHzqpq68kEojEExHkR7glNHg0KlbdDxuWfjJr8RPPvj92v2PVZOT5 +N+5757VHLzt48gBLkVwThPIXiUVQOBEhbfBliZOGXlRUXRw3gpLCceTdY6pR5VkEUozJepsyL14t +53tPOfuSYG3TlNee6m8wj7R00oeNDm4YKYPkjYqKBtCz0AKEum3F8TXctMNUa419vX6puUefp596 +N9fZ6dGnb/r+m9fH9+9VCudpSLLpHJIs2eIGOR5Hy5C4EoYuQ1F1KupP44yBpvXFGzdZpamPp8qq +KL3WHhCpdkJ7F9EM9oemN3yNe7CSuYaTUI5mJGHRxdAaLBLXh2QjfpvKLCMtM6mnsr50vIF7mYky +dWnbZq9WVRs6Sas/hZXRzn/TnvKT1chob3ru1UWKg9vUN+jIGdpcKmvmrTxUbQ7ATyCdfM+3k/Vm +xZpNH0aPOhWhJdFJMeOkHT4RFCQDPZWzYEXzejYpyC1IrAn6k1V9cvuwDw7PnqOD1GyJRSBm854G +AfdCJWN46EjKaK2m+HnS1MOblBqMi6N/CSCoxy9mNOUYsJsVU4yK4+AQLrwoHAmcKJl6FvQtLMqW +WcxyXsxsjhmRHQApAb0QyMcOVYQTKL4IKwolT8jphGa4aL9KmYjGWEwpLCjGz5Vvr8LUttksYKY6 +7FZgIboIAIrAvYFmCYUBp0nElNodlWHU7SZ+L1cRIsOILGR4V488+9TRRx66csHiWR99Ha2owWVX +RCMhfjBUfRwBQFpLmmZAAViOL4pK72I18dynp9NqkFNP02wxO+2ojWKura0jScs0YF7i5DKFSgBx +C0Gf782zW6019XXRWAyqAoqTcEmDtL2qPcF03VvxWx2ZYzjsHYiE1CZyTzJ0H3qZyuAT+gkFYsGG +QDvGpGqhCnpmq7k5KvfsO8xjKbTbHTvqNi9atmBkl77eZI7OZIl49fM3rQzFTJdedC1wsSm0/d/v +PA0dpXevvg5HDnST8p1lxNOwSu99PXXA/mOPPApsTMMHX7z6y4LvB/bvetIx5yApIRxrePnNZ4ud +lv2K+0r1MsyTEOx1It/Dtywhqg59B/oFtd8kY4i4Foh3gicJAxEeOYS/KEEHjkEKJVIXSZiAEsK4 +sC04pgWlCck9dAw5Ek1mvICXDb49vInbg6+QXuE1kUyIE00vmNcKu5/JPliQQoEjP02KcS6CC6yW +YmLKSYNCmgbq/hgTQUskaEbFCb09ZJmY3+/UXkU/fDHl4quPf+q5O0NBfyyOYUW/5xyzyTthzLGD +BhzozMkPRqtWbPj+hffuuvj2E25/4vyl6z4ZNyTvL6eeMtheoK+o9YSUwoTZh+I0NcFYUIkmDT+v +XD5y4sRBI8ag7EFVoPKzj9+a2H/A0KIexojObHHEwQki1RJRTjgtrQgQOiweirhpmS3kdobhSIm+ +dhuCwMV6+0C7L1G19blHbnzl+b8Xu0w980vserfdnAMCkey1bvFEpyY2v+5f+WV021Zbs+xIoKuk +apeSBca1/rKaZOKy6+/JdZZu27b0s6/f6JLnGOotHWjs0cXYyWvKtbs8CHmi45ZFgnXsc9pcFrsd +0RJUWTLZ8WhsdjvocjYLrtNmw+qlj+A2olLIFgte2OFoNeNfPEQUR7Za7RaHzYRgqR270Sh2RDGt +Br3NoLMi9wg6p76mRD+nyP+Ns2JpUbCpk1lxQESyoShSujiCLNbFb44yZq75TN+pMJv+3PbhCKQB +l+0TRJyIrUw8ccE+1XCRQIqYOEA0FrWCbKVRrlLGIqMPCXEUB9jlzjQLyvQQcbCU8ccO2wxBr00b +zdRoMRBb+ek7KKTTI6V1QSYvFfHkUC6SZJMZqwTOT5sV0oqkGt0+9UcHfMSScpTvNxmOKnl5uXCc +VFfX4xog+ULoOMAGZiQWw9mQoG5GFj1LUsiyUCAYCYYpcIQER2L4kIyFMGtMKqOOOXTE5PFl6zct ++f6XQPlORYlXI5ec+EhM/aWCCVTKI/P5EvCnNI00Lu5qAhBRBEsRYVHko0FNLyzIg10GjYo6HkJW +SyAkJMxAXhWkQWd+nrMp0BiTo1zGheqbUz0EGnPBQaXlm/Kjitfp9/du9omkVQZywS0RMCmy4PEm +OQ+pOTw0MwtVUoXQt8YN9uaokl9YKCejUGJ2Vu6oq67sUlSqi6kut7XMv+2bpcsPOemUkUMnJhOx +r757b0XZchg/OS43wjvQl8rLyx0uy8q1q3bWhi+/6IoiZ+fy7as+++x1UzJxzORTO5f0dzgcS5bP +XDp3zrghI7ySCwYB4n2Y/HEU/0zqLUqyUHLm2ZBfZwiY4yCXGF3Ij7PJ9hzJbEcczYI0PFNIZwmh +ho/X7smz5ToM8KXb4Cy1kcyUQLSSnWBc6d1Oh8tky/W5rG6DzaFzG21u9NWyG+MFhuY8fSjXGXd5 +9RYHVgDKfXN7Sp1k0TW7lfqcSMATQ0VuCYRM8kpwm3p2qdBOi4dcN/yYyF/DJRPRnJpq+kKl89Rb +J5gGn7n/yJodWytrNptNcQPathiic5dOffC5m2946OKrH7j4ir+ecvZ1k2667+S3pjy2cvnscGOz +LCdX7Kx/Y9ZPy0I7dMXoeQwFIu6PJ62+fDSznLVhZVVEPu7Yk92SD36N777/EIV/u5QU+uVGg1dv +IvaMbEKmJXpFuixGjxU4HbcmfDa312Jzu20Oh9HttCCEh4iFwWHUu01Ou62vPu9Uz6gzbQMvLu1/ +jKtn13pzccLhcruT+a4mrzSnqayxW+6AU08Odeo0tbwy4DM7Swp9pSXNJv2vm8uPPPacUQNRMjD+ +7gevVoVCVWrwp42rm7xJpVAfcIVkT0Ly4EddPpR+8lqirnjSmfC6bTkWm9PpNuY6w25d2Kkz5jhN +DqfVbLOD3wN12WWKuYwBTCOzy2lwOySnJdel5BnDXjVsk5PgAQElJbvL6HIiURPywgbbHcFio84u ++fOTq2yBnSW+MeddbB09dI0tUJ0rqyhzQBQdpJ8SdwOMP+FE1YKFu0okTjtrdrfOSCKIVNdd2Ict +X9qDr/M3GHO7vJhMIZy2p1tdfzYx3f4i2/xQCibaBnXY96I1oMgcil3edcckFl0481s0bBIGKyXE +slxi2ie5RtnDmQocM/VGxIVEiS/NV87XwVFmTY6SxzXlCdR+IA2BWsVwwSXWkLDFuiRLIqUas4En +HLliRNLed5HklUqBTrtbhTOYd/ZfCWzROEHM2KGqUqKPMPk8wcM3QJ0ke5ECC9APsWYlK4ppwKBE +W4x4HJwCYAWKcCajyc75+fDpbd9WheI1+CE4jBA9AXsFniEzYvxW4KqViIochYiFI6GGRhS7wpCS +ug8xidhZXG5QYv3GjZ102mm1tQ3zv/2xdtNWNLprSMabGe4oS479jeySo1sgX0vqH25ekgq0pyy3 +tnNAmM9kUutDEbmhoRGhkk5FucgCA+Q5bfp8n6k41+iwqoVeS1Ghq6GxtjEYxIlxjXAdc9IcT7WW +FSdUl/TWzkuzqwnHc6YVtBuPg3AQM4S4emSy44vcaorYwWQqUztGDCAYwojXmFXJHLCZ1kRqB48/ +eGjvkQjlrdmwaPpPHx8+YFg+kihs+s+XzapMWm694+kie+fG5i33P3xLTq4UiwUnHXzSgF77x+TQ +jDnfb9q6eMOmupNOPPnUEy6Ej+LJl+9dtmxGaVHp1Zfd5XN2lhOBp5/+mzXaNGngCEvMEI2jVT1q +O5mhRpjNMZvXtDPR9Evtqun+9XNC5SuDFVGr6rbn2BTk5oRjBebFyYZpjWs36OsDuaZAvm1pc3m1 +Lao4wU/UWzyWWruywlg7M7BlrVLld0ajhe4FgfKV6s5ovtGc5064LI2OxAp1+8xg+Qq5IWgPOTxm +5DskZZTH0we9iY2uxlnuxk25kbAPTBZJHzWYZehXyHWE+wJxCxIIzFejOqpaMgcV+CWuC3UQJUsJ +I4sS3znLGncE7JY7Hni8wN0D/syGwPZ7H7pu+o+fhAPbd5avdSTCnTvlhKLIr9V1ze80dsRhh44/ +TecqWrt946Lly7oUOYvceXIMZQ0d0FqaJfnLJbM6DRx68dk3yAE1JNc/9MRNjdGadVXVczetKw9U +ur1Onw1XbADqbJCrvt26YI5/w1qlQsmX1uvq18art8n1oIw68twRq1ptCc9v2jSvYVuzFC52lXa3 +Feckdah/5HB5oh5dmS+8LFC2Jd7QaEhed8V9Vx7/t56lXb/8+VPJa6yLR5ZUbphXuyXk8V5z3T2l +vm6r1i189e1XH7jrAU9u59nLFsed6vzNK34tX785WJHwWk3Fjlk7Fv/UtGZ6oGxNuBLoKBW4N6h1 +M/wbFiW3L1eqdhqbFauU44MxzT3aUchDsjs9OUmHMZErlZsbFipb58YrV1mqNxpry5VmNcdkczts +eNRwb+tV2Z4wFLobHbodjvA2T2i7Gjv39OtvOOvR/JKSBSt+lWNBT8RskWEPgLEOVhSdn92rXFcw +5ZQXEi69aFIir5V/T5OvfGRmRpu2BjWhl0IlTXJkrN00XqVEKf1cxpvtX7f3wbZd7+xgblnrQu63 +VqqFUNrDlg06NTGeGh4SjMILxbiI8JYWA8rQ4nfxay1cz93cssAzQSblJFSchMwVBHnQFRU7XH+o +2s12IQMhxwDFP0y9oYx+sky0ECDZ8IKAQ5xVzStP/h2NpZq6Rg06mV6nFckWopJFuVazRkupoKHg ++oOMk5qY5giN1jySz6DxVLQDBC6L3EtsggUkIjycx08nolpvnDhGERrEBOEjQ9IwUbiJfYMoITqK +GFRFp8TisTB61SoRmSqdysk8d4E3J3fbth3RmAyLUI4hXgIPqgW/QnRFeF68sAVsEFuwPdFBoLGi +JtwYAMri3DQAEC0K4oiJgiH9jrjobPzenE+/2TZvkRyJNCYSSP6g8YJBzkMmKp/QzWpeUwZ4Efho +cWbuWqskoiKFo8RwYhUm0OSjKC/H5wbLIZqXE+/aScr3JbweY1GxG5/uqPQD43kExYQg9jkjY1at +NYsOhu+fdO9d6YOMx1DouqUMB10fsz2pWSPXIuEoNnefkgww1BSbXnEaVvtrRx929IDuw3Fdy9fM +nTPj28k9hhV6vfMr1322ZPEF59581JhToWy98tETc3764fRJk1au33z4xNN7dhmKBTN74bdLVs53 +OnPu/Oujec5es5f+8Oyr95rsxkPGnXjcpMswmRcs/f7tN589bPCg7o5O8WBcMaIfsMkQi9pNSr2x +cWrdujfKly9HGkLfbjm9+li7dVlds7Wxpq5f/9JVlrK3tiybEZVzxowxlHSetWHNosatZd5kWU58 +ZmVNtcHfUJj8qXbLyli08IARrkFdv167dmFDRYPbEPa455btrLX5dzoDv1RsrEjoC4cM9A4u2Bje +1iyHPA4vsn7qHepMZcdqt77zmAnenr3rwn5J1hn9UTd83YhkYWmCo0UPC9DI+qkwvLmMBI0nV1Yj +thD+cJqa7MqXKxYde8bFkw483YDJbDL88P2H0z9754pJh5w1ZOSxA4aMGjSgrGzr9vLg8cdf9Jfr +HznykPNHD5582P7HjBq835zZP5fvqBw1dKAR3SuhFjt0q5t2wFI/64xr9hs4Ac/r9Xefnv7LrEPG +T+4/5EBXfuHqjdvXrdswsH8/V773u2XzPlyyoMlj7jyof6Mu8dPmleti9TulZKU+vrh6h98lb4xW +T1u5MZRjs/UrXhat2xzcWVTodOrcisu1xdz8+Y5fP9u8qVofrolGd9SEDh19TN9eI0oKu/6yZOrP +q5aUxxpr9fqtDZETjj//uENOBcHhuVeedOX4rrngnlHDRn3wxbsLV693FBV3HTayyqBO27BsUfWG +NU2xgsGDcwcMVHMdszavWdywZZ3cJBfm5fTq5eraBXUN1m3fYHQophwUdXAWGnMdUGpdukWJTZ/V +LFusNsR89vwe/RzFPaSikkZHclmgcrtcV1SY60TNAo9tp1P+PrD+5+COTUk5aNDvqFAnHnh4n97D +cn3FS5f/WrN5Y37cYoVmQ+uSCkeLwpQUVxahrAxkSGPMLmxB/izNNMmy/nb5cStttRUS/5azpb7T +JgrY5kdw1L4wTxm0UogrxCCPnYa6/DptDu3mbtpfWZsDxRkEjmiFu4gwTj2SqEQL2VxQT0VjDQAg +81QJbShbkQvFkdOGi7OJKCO/4IpfbHqyLcnaCCOTdhhjJmsTDHCMoCyAhVdVYJjASI2hlUY2GlsN +HUXXx9RZhEUlTiPGjesxi2Cl5o8kSCFTkatUUiI/WSRaSRKy74CLFph8CAcBKYGL8Sg8qISL2qw1 +IM4Itml+Xl7Z9spoFPlZUjCEqDoNDfLlkY+J2ITV47R4nKhERclXZkuguqG+ooq6TZnhTVKAnx6D +GY0YC4YOnHT52cVdimd/9sWKqdMTYaVRVYO4dmp0RcOWdX5m1a543FvyXTHMsHFDoXCnAveAXu7O +nZTSztEePaSuXVwIJldUNjb7QRvAIHLVNm4sxipf1t/JeqV0lkxo1L+A7iLsQ+VScWwyEihSWhh8 +0NSDF+BIlU9sIMMnbXrZafY7jJ9u33jW7Q+cffSlsKY/+uq5x//xl4ePv8hsNzzz0+eJTt2fue+D +oryuK7fOveC6E48ePnBocffHP/7y6WfeG97/MEDpnY+e++Gn79143V3gLoJGddVfTy+vW4AEt6fv ++WRwzwkJY/Sex65Y/POHN0861Ru0JaAMobFvPGGy6BrNgS9WLVtQ4x9xyLEnH3/2+KHjzWg/qFd+ +mP/2/Q/cUJDn3Fq+c8Tgg84649qJY49KxIMvv/BMXX3dGeee6/C5l62f/8wrj69bs3HyhCNPPO6q +g8ccFg00Pv/cAx639bBJR+UXFM5f/Mszrz64s6l65IhxF51809gRk+Cvf/Xth7587ZUJAwZG/Q0r +tm8xde1+zmV/PWL/k0D2f/Kp6xd9+mlvv7G4PmkKE+EjTp5TjK8K4xGBSCJ/wkakJEs4eamppQHl +YRCytNmieYb5oa2rY4nHnnm/d+lYlPWNm4IXX3Zsoqb85hNOtTXGTG7Hu8unfb947VXX333aaVeY +JU88FsVBqAJscxi+++nt++658sYTThmMThrhiFJsfmH21C1Rw2evzUmG4bQNv/PJqweOPWjkoPGC +vrx2y+y/3HKewxE2Sdat22onHXnSBede171wcG192d13XXvwhIPHHnwo+Jyf/fDu8y8+U5KXe8xh +p5984mVdCnvPWvPjnfdddYDbeWrpwetqtn+7fkHM6zzq1POHDRwJKuqq9et7lgwZ2n9/OV5/8iWH +OLyuC864fFCPMeHmeGFuJ+jyerP67kf/Hjx61JA+o8rq1l1/24WHjz/k9BMuKs0fsLli3Y23XdSp +sOCCs68eNvgQkM8D4Z13Pnj9+vL1Z5xy8eEHn1iS0x1DWde0/V/P3/nrzx8d0XtEl3iJ0RKvNzdN +37JqTaixz6ixh4w7btywiX1LhsDWAydPNjZNnffJy0/8fajZNSqve33Q/3PlmgafY9LRJ40ZPr7A +U1y5fbvD5uvXdyQab/31trOi61f1brD56iwSin6g9VscnVqZr0PcKi4QzZQgsaR2KQPEctQEZ2rp +ZY8pMrpmLuX2zJ10rf/dLejsViNbIm2+3kZ+ZOv01BF5o0k2bZRY5uMfbtWktfkVqW572LKPWFry +8Q3huVCSKpVIJZcXomESFbhRRDdBmJKMdpSNJ7yp7PQTeEnfF1x/shpJjxVESHLKiefYisnFFy36 +EhO7T8tIZNaMKC/OO7KWuYkBPRNWjsk8pJx3MoZAGKVIGE5OiReYTwSB9H/2uhMMgkgi4JMKsvEj +A41UQCNJXipkQjo1/KhwOAEjCSmtSPKFMzOhR4m2cDABzxIoeRHFanOEoqhvY+zaudv2sh1NzWHY +NrAa0bwJLDMq5IwaGgg55LiduV7VLCUkPQrfyM3BqjVlkcZmt8kCKQO71KY3BmKhnL69J19ydu8h +fRd8/d2cD74I1PhDOl2THpVxUByFaaPt3A4dmZa7mAmsFtB4cr0EKteQTHTyJA8ekz94oN7lbLDa +dNGYfUtZfM780LYduqagPorWDbQoBflx30AjTtSKhiPqZ9MU4kQgCptxmizHGNmc5BLbFIqhOhHE +WAXN3mk11Owsg2VEPUzsSMdPoJ/SypptW+objz7iHOCiHGv+cMoLUqD58DH7l5eVW01Onzc/JoP/ +k9i0oWzE0P1POvocPP03Pnxyw9b5EEujhk4a1HskuojtqN3w0w9fje43IN/m0sEVEE+aEwAYfZ1V +fm3R4mX+2FnnXPePmx6eNGRcGEUA0Y5ekcYM2M/m8K6t8p94zHUP3v76xLGn+kNwceadfuGtN9/5 +r+Lu4xy2IYfuf3YX18Azjrjw/puenjzmaKU+6JJybrziwWuvfqJ/n8Mshj7HTLzG7uh9+EGnP/yX +1w8ecUKwQUaSaZ9OfRLoT6GPrGwoM5Z2uuLae4886GwlYQtjetA8Q/UcsGUjJp1sS8hWVTETK0y4 +frRNuObEv/SC506zIbm0tmHIfuN6dB2Ofp/IJ1q4eu7GrStGjxgMv4cfBdn91dO2rj7ktNNOOfVy +4GJdfeXm7etCyTqjHanykSHDhppcutqGBrTeQmywPFC/ZN3GE487w6K3WeBf1Vkuv/j6kYMmYLVG +5Ug4oPTrMebYUy9eWFG9LhC97KZ7b7322e7e0fEoag70uv+B98487a4S3wFFjhGH7XfRiD5H3nrD +s9dd/niRa3A8ah3ZdVSxtSgZt8BlPXP74pDPdtNt/7zk1AdGDTihX9GkUw6/ZvigcXIiOm3et1u3 +bTt+0slHjT3P4+lc1KkrvJqI3tmcjgvOu3L/wePrG3Z+/+XX5x1/yfUX3dfZPUwOWrv4Bt1y2d// +cecL+484AhSkhmDcZC05+9SbH/3b6xccf22xsxhsguZgfU5O0TEnXGKwlyphQ0mebaNU/mbZr2ut +yUuuvPeJ2z+45KibO5cM3BGAh6UxIod0UftJ487r331AQyQY8ZqWBqpjuUU3Xf/Izec9Mm7wyT27 +jJ1w4OmD+x3qdvjmzfuhqbw8V2e3Qg5oVH9yr7W3o1hp5E2zKNL/FW5DUd0h43NRBSJtQvKLXeTv +Z1dh9/ERmujhS9PKGWbX+DtyDUTDTCMuhxOoSQT34yT/WBuiYEfO2P4YOi3n7ZPJyJxS2DcMigx7 +wvLTchlJu+fcDJEKTsYFFcEB1hBSIpWXziTK0pD3ld2Woow9mQainI1WT06UkQOjhyrMsa0p/hXs +m1SNSGquKMKc4qln2TQXQ0vBAzGxtDQ04Uelas2cqw0spNRF7AgL2i02B7DPZnciXR/hVXQcl1UU +RQUHgTrfSIDucCiG6Fv3rj39/lAD+toiwhSVIdipbgbwFbBqtVjdTpvHZXYiI5lqX0G6hqrr5UAQ +dSzB2EcVcrxXG49ZSwonnn9qj4F910ybOX/KV9G6YFSvb5QM5JwFA4VY5NTtTzgE9rBnGw8GNtYd +UHSHbAui7ybAm8TvREJ+yRDxuEGio1VE1+6CxUGees2cpwgzuKzU064DP7RH/Yx1oMwjKBNFpOhp +AMlISGVlBAGTympz+igS9IhFa0EFGofR5HWYNq5frOqgRujdOYUJm3lJ9YaZ65YPG3nQ5AOPxQxe +vmr6jGlfnjh6v0KrB75HX25ujtObjCOXtKGpLnrGiZcVenqv27Tgs69eLexkUqPmEw4722BAAnji +u+8/kMKhYcW9ZH8kDl0BaZIWa6Mr/t3mZdvk+CWX3XHFBbcicPavNx5755OXdgS3oSBLeV21HIhf +dfKVd9z4j6KiXmhaDbKjMQ56h8Wka2qq32jQNcajwavOvuyuG/+Rn9slGKpCXR4kzKH9V1Cprw9s +M5qaa2q3XHrcpQ/d9JzbVKgEI3aUTdKFpi+fbupk2OTf0oT0/ItuHTf6hPqGylU7Zj7/yu2/fv+x +yx/MiepcEqiZkmwyoj9XVFJiJmAd2FUtm3CtcHdvXRyhLEnnR/EIg2H8gZPNaKkGlVen+/Gnb3wm +4wBPrqneb7folm9ZZbYVnH7iZVaT94fpH190/WnHnXvMrfdeI0dRfw+T2oU5hOITcUQY7Mqslctz +87ocM+5oSk+y6T05rvqayidf/Out919UXrESKUnIjujs7VRq6vbM31654OhbqMWHEtKjK4ZFys/1 +NYa2N0er4vpYia/0nhsePuKgkxJozYZ6BObkys3za7ZvKcnLL4/VbpSDR5x6yZiRx2yv3rR955qw +UiXHAtFY44xfP3vy+XuxqlAYECS4QCTUHGpClzeg42c/fPTO56/e/cgN9/z9tgE9B5xz6lWGeA4K +0irGWFJSxx18dG1j85MvP1Eb3KGzEal9v2EHDBkwYNbsLx564sbZiz5HekdDuD4/v8BiNftd4SVS ++UdLl9jcPZ66+83LT/procs77ef3br71hBtvOmnuvC+aIjsjuqaFm2dtq91qAlXHENkRahpy0IQJ +Y4+r31m/fuWCpvC2cLQ+mqyZOuftKe88IzX6CyJWVwQZnTrqwIXqU+gAgmeTXbhpTzYtFUhlTcuG +Xa6+tBNWvPhvbC3otU9/nbQJEm4i7UnbUrYEdxPfFz/HNjahrVA0yHEq6nBQZFFkIhIhM4VqrN2z +/5MOS7/P/FV2unKdcebkEODSv1wZh8pK8ldSZcpT5CJ+vKmCEMz3IetYqzYmjD0h4Dt2u2lPrOaM +0OKTqcwSQBicpsSUZ6K8zWxzWJwumw28ODs5UqmGOJJElJAaCaLDFCU0UJ1URGqQQmDoXto1FIxu +Lq/EADSH0A2PSt4AxED1phxtu8WZk2PNcaLqGkjqOWa70tDcXFVP9de4J7JbsjYEmyWfa/LFZw4a +NWjdnDkzP/iyqbYRfMs61NaBUY4OhczVZQdB9r0Dz59wjsoYEXWRmMa4jhi6QgJqkkZ/AJWmgZZ4 +MpYoEs4IOUXpnFSzL7H2OrxsO3A92iEcq+b/kzbFsxxMdyhX8AxwT0wqBEppsWzzcuas0aI3u822 +7VvWV9WW4RulnXrkdeo0Z+OKbX7/KSefn+cqCIUrX3nreaRRDOjeA61PAnHE8nIov9uYrKqqGtR7 ++ORDjvdHa555+TGDIQLX+LABo3v1GFZfX98Y2jH1588H9+5Zai3Sh3SYGKpFCtr0cyrXz91aedpp +l1xw8uXwer302msfv/fV6L779fB1i8Uapnz1cV1D8+ghw8qq1k+d/W0oEgRwgLWxbseCK+86bfIZ +Q2+476TbHryySa6v3Llt8Zz5ks5lk3xoZb1yy+JLbjj+4BO63/CP4//62LWFPTqV11VM+faNQCwI +5ld5RfW0WTPiOYbV9f5jz7x08qHnVO3c/sLz9191+eRp773YORgbbi2wN+kTEaOCOtzonIIBQzUY +FRKWlEryvmhrWquBIrwNkBf14Sajxzxo2NBYLIokzTq5ZvXWtZ0Ki1yq2a4aI0p49dYtk8ccMaTH +8GVrfnr06ZtD0TUHT3AtW/FNo78ckzIajDqSkhuTxabbEalduG7doQcfVZrfKxIJ6vSRWcumnnXJ +MR998+znX35cU1uVkKKKMbB2w/JuXXv0691/zsLpKzYulU2KIiGYHfvwpxeOvuCAYy4ZfeGdpz77 +0cNmrzx7znfolAmNFerYnPnTLTqlR7cuOxsb9AbPxLEnGmOOLz95d8LRw069esRFfxl/1Pn9b/rr +pbkWXe/ivKlffN4cqCx05BY68qWYsaKy7MFHbnvi6bu+n/ZGTXBTr0Fd5s6bPn3Gj4jpxfURsymx +eNF3V1564eJffu7m83og5qg0Vd1jz991098vefG9Fz/89FO7ocCl91VWbarXVW21V/176czuAw9+ +8s53xw44qqm55t4nrvzL4xd+M/PbHl27jxt5ZOf8vt/98skVV57ZXFs1vO/gWASdz6QJh07CKvr6 +q3fOOfvAcy7qf/HNB156w/j777s8Xreth9XhhNJF+ZOoskE1luFw4jYMvAjaeVAzoSWNdB1fZukj +9wlUdPB3MxGZDK4UemUSaDt4qt0dliEcacIzgolq22ITkbvdbh1UFdic1zImtDIk7ObCrwvOC71I +dXvgP9OF31KgxZnoAsMEagrzTqtuIuKY/EarXZB6ROYs73xL9F1e4OmAYeprbNem98xzafm4YiRo +EmhETu7Tw6YwO+ZASODeUiDaEJWUMsyQrARMdOH/yGQCXqC4AQAeJSuiOhkuJHQcB+tdHwrFUKat +a5fuwWBk3ebyHFSGQi1Niykmx9HGAMollTFB0haMTrcD+AoghBEGi6uhqkYOoQMvxzwhjhUZ9B40 +QOo/4YCVv/4679OvqrdXxCVzHRRsIAK164XvmpmG/GD3gdbHwVbheKGBJtopandJcHfV1EXgUorF +LTHFqKim5uZ4Xb0Sh4nIPCxu8SrIrVk89h2b5My8y9gwzKA+UXM9kRCEZ46GwIhYQQvBNYTNsmJC +7JZcDDED9Gti/4OM2dNXotY3fPndu3E10qOw99EHnrJsVf1hh5110H5gvJp/mP3p2s2/5HQ1PPb1 +W49/8eq2hu0Olx21AGw2lxxVLz7vcsQtv5nx3uI1PyEQqdRZR/QfXZRb6nI7Vi+bu71qW98+vewy +uBc2swxPuG1btHr6stWHH3HaZWfdAqLonLnfbdq46NWX39r/gEm1/qp/T3n466kvF5bEHn3m9vvv +v0ZSwy67A56BNWXzbrvzyq0LlxzStUfT+vWL5/3w3Cv33P/UDRFzs8VhBbV59bYZ191xcu3WleO7 +d2tYvXHVkulPPH3NXX8732U2uajPp/zVl6/rw7WhBmVQ34lnnXwdahW99Pbfv/7x5QPy3OOt+b2a +nI5q9G2yob49MAYNlI0JhfQHGiHwVblJMhWfR3I9nrUeHFdjDCn2akSNNsuxPF+px1YM/yeoTjUV +G+u2bS525yBE3oSsWyQned0L1iydMu3xp167Uwo3XXvIUYN1+Xkw2e1WzIllK36Frz3f7sYyWrx1 +k2p2HXHEacmEAyyyn3/9+C8PXKR4alFA6bSTTx85+CA4k7bv2LxgybTV5TOvvPO0N959okteqSnh +TsZjH33z/AMPXl8kqcPdzvplC77/+t9X/+3MuUt+yi3IQ7htxfrFn3/07n59+nV2FZlUS77V5nFI +wWTwhNPOGz56v7ra6niwtovTe8n4iVcddMJRw0esXDHjxbcfkw0hCeWq7NLUGR87bJHrTph8y1En +qP7q2+47//GXbuvas0gOJtySd+GK6Tc/fMVOdft1t99i1LngEUqY5Qf+ec+nU98q6GTo1jPnqiuu +JGPM2PTBp08H1YatOxq7lwx85O6nUVCpumbrP5+9+vsfXi9y6ft1Kjjn9EudjnxZF5n56+clutil +gw8YpMu3UsxVtcI+TtpPOOPi/mPH1oWj4aYKZ33dJHfRmGin0ia3DbFbKcGiBdoMVD5VoWLFJLOg +EDL/XmMSkgeSzBMSh1yqi5vTkvaq7S3FbzgM1VIsQLjVMzys4jzpMJX2Ucr9rrE0OtBIoMXTm/b4 +thYA7Kgg4p/YU/2h+XJ4FxZdG9GWVdKlO02LXyPLjIt4izooGDPo0NA3qGeQqFnVOkq7i6EQGnmW +TSQrIikdejnq31JvPC27nM09Ci9Svw2umZp2gGstqFJ+USF/hW1GSYqs9otK31qDHO67nSqMQ95T +Lqcjdii+1PE1Pe6CP8q2pHZaERyEEYH1C/ONvbKwWWG1UXMI0UeWYBY5z1x+h/CFrFj0+7NxIS9k +oVCdRBRCRY4xsqzxyo68Qxucn5ATFqsO/iSHLm4NBZIBvxIIG6KqHcUSY3AAIaPbNmBAv3A4tH5D +mduoj4QjGNU4nJ5wTlLhTNS2MVh9TmdRrjXHgeJQsCZB66yvqvbX1iP9w4baVmwIRuymMSceOfKw +8VvXbpj+1sd1m8pjRn0DamvpEJekMK+sV9DgCkRSfB1b22I3qcwloX61VC7IeL5tpi7rHWR6wfQF +0R9ahxk57wlzLATuqbO52bV8eWj1quimTfCR5ZqSrkiEvwD3G76VRudsDtXWIY42EQ+h5miqT/pK +UctEjUvU68OaTNiSEqSJK2HPU82FIN4kVCceVkxvV+DcgklElXvphsOhUodtYJ53+ucfb61cBVrT ++adcf/qRV5x85AV2S07Z9mWvvvlMVVO8qtFwwOTjGyTbjnCoIK8QP4nQdO8effsMGLCpZsO7b705 +rKBLscsXjutHjjo4HgePXlq0fHFJzDDImpOM+UPOQJOlJmoPzVixJL9L/ysv/4vdmlNXi+yWIXfc ++nj3Hj1WbF/wz5f++ua/nx9R2nncAQdsr6n2OosPHXdkLBmqiZQ99MzdzTu2njNg1LmeARd3Gn3K +kANWr93SrcvAsaPHIye2Jlb+8EO3OxqbLh550JndxlxzwKSzDhhdtmxdl4Kuxx91phSXN22e//VP +U1zIPY3bbr3mLzlO9y8LPpn2/ZSDijoPr83tX5NbHHBbweRKGBGTVHi2k2Tg/A0Wmxr7n+vrI+ZO +oolaNuqTwE8kHeXnFWM9xyC1TFJzQ4OuMVhscxPvI656gvrJ3foGQ6uffPKuHYvmnzp6f69OWrhg +Ta8+B+Q58lGzbfbsqarX7uhcUNFQs2DB5gkHHjm43wgIjI3lcx949C6vj55TrrvH5ZfcZLPl1tRt +f+Txe2sbNw8e0mPx7GUXnH5JQW6+2Rqft+qH55/+xwR38YXd9juty8hrJo3PQ2maQPKUE86HZLZa +1M8/es5tj3lLzd8s/nL+pkXVzfXr169FyAJKzDmnnC/FrKOK+1w/4aiJRQO8cWtP2Lx5rsUL5iZl +QIwlHG6c+/NPXd32MV37Dy8eMKh3jxXLth5z5Fm9eg6yOKXNFYvvfeSmmtqKIw+fNLD30GhEBq/g +2Zfu/2XBGy6fWlUuX3z6Nf167hfXN7709sOzF8+EWLDIvpuufKJT0SDM4Nfeeuq777/s178wFIgd +PfmUft2HY0rOnfvjzO/nTRg3ylRgmbZjztzGldXRuvqdW52o4WRz33D5LbkN1uGx3ENNiMNY8/2G +HKQ0gvQuhCH764ygOHL1nD+33zACHYqz/Ybz8lcIY5C+xeE9qBmsxovIn3jN4UAAIfionJgv6pdy +P0VRH5XgUOP6C2OPTU2OITEJlcuACedZ2529r+x35bTy1hudJQPY03+S9BeeWiFy+d80oYN4jZRK +wnFOGHfUeQ0mAMpyIJMf0USr0+Wyu5x2D5ISXVanw+qgCh0Iv5GjJxyQg41yKARSDjg6KIgVTajB +mDpw5LBAKLxu43aX0wIXJGQNVEPKNqAuQ/Cj2qwel7cgD+cExRIlA7CFwdKpqjPEEEWAMxYZ9aA9 +yCMOGzfp/NOryrfP/te7jeXVzYlkEAYqOIZUJ4vHqSV4IHzMv3PD+IkqCxQjRo0/OG+Yu6RKqH9u +8a1cHViyXF6xVjdzbt2q9c3OnKIcnwfLFLFOfIHbIkjQq/8Qh6qaRG32OKiPsHJgtjZaQ+gStbM0 +tNVdFbCGLEikNtop3AiDG9kykB9oWJu0GmqjQ/JKgjvLn3/xXn+0zJOTe+8Dj/Xrv7+aiL799ltV +ZZFzj734n3e/9/D1b5x+zEW6qKk4vwvMl2BEBu8YoZ13Pnq1etPK40ccUL+1zp7TacjgsUCLnU07 +Zy6Z299bWGi2VhuDH5XN+dfSL77ctgB0xJNOu7TE2zcalkuK8oYMH6Qzxl6b8sjf7j57wbT3j+7V +4/Q+Y9VNfqcx58br7lWSsK0S77/39JaFc44YMqyrwxttbkRHiPL6qq59+px1xqVGgxucsede/Efl ++kVHDB3QVZ9rDaCzlWF77U53aeFF190WCetALfl82ntNyaq6hthFp/1lQM+JwUj4k3fe6CxbhhqL +bXEHEglikjmKCkEo9QtOVxxF0mB8UFQaFQWBf5QOQKoiqetkRpCDnC1IYJ8+GYrLKBsHQpOKQp7w +gVIgXUGZq1BzEHS7HEUaaiy8qd+xlxWMv/HAM7p2Kv50yewNcXniiadZTK6Fi6fO/mnawT16J43K +3M3rgwnzmWdeAl7UzvqNf7v3WkmqjTY2O5Ndnnnw313yh2wqX/Low3/ZuOyXg4b3rdxae+pxZx54 +wGR4lcrq1j711P0+KTJxyGhf0gN9aOvOar8cu/DC6zoXDkb9o/kLv/n+p6/seY4P5y//YXu1sVcf +Y07Be1M+Qj1flPk9eNzRI8dMmrt8ZV1tXSDcGLclps5dUB2SL778WrvNi0pVoLCh9hA0gmhcrqqr +rthRNXbkQacffwmUz0C0+p5HbvZHan3ewrNOuNhidIHH+81P70356mVfsbmqMnHh6decfdKN6LLy +wScvv/f+iwWdXVG/fMHJV40acih8GlOnvffV92/sN7ZbXU2gU/7A4w6/yCQ5w7Hal/71wNh+BRGj ++uC0aW9X1ezIzcnt0v2bTz6TG2rigfCQwQcdeNzZSyrrkZKFfC1Qb9DJByiOBqTwEeE5QYog2MiF +4FttrOO02rR0rVZp4SI5PL3/TmHxP/T1Nvf+G/mHv/uGAG8wrVABDv2HsXMskLstUtUblqyMnVyQ +hQqFiwbFopI4BxRFp0bNj8rp/5qlp/nxtKASoWSmF5SNS3qHmiugGBfPjrSznSmt/E5mhrlI4OfU +EcF45SO0TSsNQG4lbhGJgBXoHGiNaqd8DFR0srrtcHi6clwur8eV6/Hketw+F8DRhiJg8ag+1KwL +N+siISpnmEjG4goIqEEluf/B44KhyNIV6x12WxxkfpMFfTBQABI3D6eq2e2weF32fK81Lwf1+qmk +OHiqYDxGYv76JotOZ0W1ESWG6nA9x+8/6ayTG6prvn713Zo1G+SYiva4UdwCZaBRAW0tqKo90FQo +9vc9XxG21MxqGikJdndEJ+eVFFU2RBavbt683bS5wlRWbVy8pnHjtjpvXr7LYyf/DZfuxBRA1eys +l9B+Jmed2wY7yqsTgcusmKRGj1zRRz+rZ+Drns3fdA6s6WGotilBqtAGBg5KKSg6A+xLvR55eGGz +V+86ZNTABXO+ve+hm5oaauEDAE8KjY4vOufymVN/vfu2l/YffNiWLevL1q/DCbp37wOZROwiQ2zG +/G+mfjzluP1GdfXmb1xXNnDAaJTdtJrt1XUVW8rW9+nVRbYav1238peqarVv31/Ktpf0HnTUhBNB +jTKZlPWbF7791cNX3HH08+/+o6phk9MhdeteWutvnrlkzREnnlNU3DuRkH5Z/MuH7719aO/+/U3e +eHVD2JzYEK2Zu37tsZNP7tl9EGjOc+Z9N++bT48cNHCAswv6XKGWYHm4ecbiLRMmnt6721C3x7Zh +84Lvf/w86FfGHXDsccddgqk9e9aPG5YuHtG5izUAH4kexNmYWY5JEQWFOMEJg/EIegknvtKCgFKT +GnhhPXLFVWqNRjVCEeQ0IM4cxsO1ciNOT26uLse9I9qs2I3hhFwfblQikcKg8aCS3sg+enfenB+3 +7zzm3IuPOvTkqtpNL772eLcS/aHdO2/Zuu6bRYvPufTigf32D0Zqn37pwarGap1q8poHPHrn031L +99+8c+ljL962cNa3px82FlHMQI16yomXJ/U20J7f+eClxi0bjho41KO3gNpdr1O+X702r/vgIw47 +IR6JK3LTK28+4+5k2OwPDRt+xJMPffri01NfefqzIm/nJUsW4D495twzT71ge01toxy1dvb8unP1 +j0vWHn3MJeP3PyEajWF4HPbcIQNGVddFA6b4/LKVW7fXnXPmFVazDyX4Pvr61dUb58VjylETTxox +YFwEfJ76Tc+9cldeob6+0nDTlQ9dc8lDEGIvvPf4s0/+fWivrsaEKz+ny6knnYU8n/rGLa+//6Q9 +J6Eo5p0V1huv+Wf3LiMhC3785Zv65vLOxc5Zs+aP7L/fJ4998POzv376/M/d+g77ZuZXFhdUS+vk +yaclfL4aZHnYTAitBNHITmJeB1Ms08yRrGvszwP+8yNA9gFAEf+KnodU0493LofGjRgFj5RzlchG +ZEqOKP8pUvC1DEXNhtPQSxhzwsuaYiGK1xoZJ2V9MuuVg5QpFNQin/yOQMcUGUeT9HwAwWPKhKWY +JVce4MuhepcIPcE1J6HgmwVxRPBOXQ6H24nMCofX7chBVShYjxaXxWBPylIsqAs0K8FmVFrmwuio +U2OAl6e+ITBu3FhZjv86f4nDbFZiMtgLkahCvRiBZzA1XSjW6bbmepyFuShwhdIjCGDCVo4GQo1V +1TFkPJnMQVUGHa3rwaMPveRMDNR3r7xbtnA5SHpBhB7hliePN6xcoo8KulVGRPn3W40YO1iJFKAg +ryqZppFkUs4v8AQVeXNFQ0xnrgvDapIjqlHRGypqG3dUVIIj6c1xoMUA2XNUIx2K0L64ktYzm5qB +GEBtQsqYzRDM0ZeblS5jDrnmpid6Dj5kQ71fzjcpdowO+Z3woEHHMSYVo6QYnLqoRd3pb5JM7kQU +fbOIzCqrzeV1G+1o1BdpWrVh7lsfPnj9XWdPm/cxmKd9hgzDTaOs0erNiz9678WiZGx8r9611bWB +cGTY4BFgHiV0SqCx2mqU43nGLzcsmb2t4vSTr3j09tf79xyz/4ADve5iCLBFy3++6e4znn319rKd +q9Cxa9Ihx+d3G/Dh0vkfbVzq6N/rhBPPg2ciEqt76+3nOuWY+ri9jiZ4rKW4ZFy0flOX3v1POOws +9Pisa9r62uuPd7LaRuT1szTQ6IJTumjj2q7d+5x74sWsgEQ++OLfNdXVA0qH33z5Q6gB0xzaOfXH +d4uQI2u0JmOqWUmaFVVCLW2tGSm1TKXHy9oimlFjJ2lLLHayFLkwOXARyXcJScYlJZx6Q/WOsoga +RsVXgGX/rj379uq7YM3K8kCtCgNWUlWHusVZ/2N06aM/fbS4vOac066/4dzbQ/7a199/bMXaXzv1 +LiqLNH2/ePmA0Qcdd9xZeDAfffHqjLmfOzx6h73bw/e9OHDg+Dr/2oefvmnxwmknHX5gqa/0p+9X +Tj7qrIED9scUXLz6py8/m3JQ3369TZ11jRFU61+zY2vAaDjjjMudBo/drv/o29dnLf0lIivF3q5/ +vfLO0f0OBnj2LOp7+mmnf/rNh5FICNO4W2FP+Gc2N1QvKN845adpw0ZPvOLCv1gMWJ6J9RuX63Qx +K7iBaqK8sean1cvGTjjpoP2PUeTIhrJfP/j4lVyfxW0vOuvEqxJxi91peuHtJwLRmmggeeFpN5x9 +4hVxNfT6+4++8d7TBwzuPqH/sOptNaefeqkk5dgcxnVbFmypXJVT6F20ZOslF920/8gjoXA0R2pf +fuM5BEE3N6NAZN4Fl98+sM9hsuqz2ztdcv6Vv86Z2VC3E0+gV9ceeV5fMOCPRyOovAyXEzrcwORn +fhmpNSZ0SNVKxLcsEeFly9x2EaZoG8f4z2PHH/WLbe5d+Ab/8xsFoAgROeMCJpew+5hcz3XCiMuv +OUZFHgDBYwrxGLX4K0QBIJI4aURYrOiFS/os7dSajMOMqTbHqe4+IsdDcF85DzLlGmhjPqbfZzAW +hXhSpmgrk5FY/4gjwkGKMsFgYAD/HC4HLEVHjsvpdbl8bnee2+bNsXgcVjsKaMVNCkCxIdFUrwsF +E7ICjwxiizCPY0j4jsUPHHugqhpnTZ8D8wYtYS3cBxx9SKmIDjK5wPDIcdvycmx5XrPHBZORqq8i +MhaLochktDkE2dSkxJI5jqGTxh12+gnI/f/p/U/LFq8AF6TeoDbBKGPLmFA/Hk+qiB2RyMOSgWHH +ewfCxFmmi5b3orV9IiCOu5Ba4nCUV9aFlQRISHgL9q8/GqWsDp2h0Q/WgIxCQGDsogwkSB6JBHIC +s1xJ+5mcdW5TjxK+RX3MGG80qbairhefccdJo6+768qnfN37z0/urHfHFQlxGTMS5qCMJKRYyOHf +qG77qWzJ3LKqicefd/fdz/vySspqNpx55SmnXnbECRcccdXtZ11xyxlP/vvOpHWHJ9fRrXs/p6uI +Op7Eg6998MqyX+efNmGsKSzXNvqTRnNRfhFmHnyODY31oVjk27WL3/x1yegjj7vm3DtKc4bY1Nw8 +ezGejmLSrdiwatXKLSCi2OPuW8574L6r377orBv9cWlbc+iEyWf1yh8AfP3mxynr5/46uV8vTxin +NKCX1cawf2Oz/5iTzivO64s2Hd989saOTQtHD+jlQGE0GQ08E82G4MLN2yYed0pJSf9ILDl/xeL3 +v/0mr7j/PXc83amgLyzhFRvnLZk3fUB+iQfZd3qgG7IaqU4hEhxMIG+pKIVL2iqcqLIRkWS0WNHy +GFm90tLIRJavhNYaSjJXb5araxr9NVBEZTlqs7gvvvaaQJHnpRWzX9k46/0di17fMPvFpbNeXrEs +3Lnk2cffvPWi+5wW1zvfv/Tql287igzzNm154pdZO1T9LTf+pSi3+9x53z/3/CPIe0HPqLvueqJ/ +v7ENTbWPv3jvzFm/9Ckt7VbS85eZy4zO3JPPuADEt2Bk59uvPe4xh/sUlRpCFnCkg4n4yrJtpb1G +HjD6WFjBNXWV0+dN+/s/HrAnfAf1Hte1uNu22k2ySa4L1PXo033VmvXhaAQemZiiogbbB8tnvjzt +R3en3vfe8yDKVyUSsS+/+fDzHz8CNMbkWvRknLdiSXUkfunlt+j01qQh9uprT+r1DU2N4ZOPO71b +54EQOz/+/Nkvcz5Fl8oTj77swjNvBn31jfcee+rFf+QUmPr36CZFZDUc7td7mMHgwgiv2bQaAmHT +5tqTTjzrwjOvCUXg7Em8O+XVLj3y9ztw7IqNO0cOHD+y9+idFeWBYAgj73H5NmzetL58i6xX7agy +bzWE9Qp8ZPBRoCqvPg7CWZK8IcSSYjrFLqip/3kg+PMXW40AMyQoW0MzywidU35vkUtI3BYtnie+ +KdL36f8cGhN4zrAueguJxE7hD6W0e7KLUl7RtENV2IK0ayovF4ITp2odYmzzwFLqA7mLRIKH2Bku +KWER8ARotIMs6nA4EVN0OOywF9EF0YPd7XC7oTki7oh+R5S5GPargXqEGPWo2kY9EVE4E605dI1N +sf59B6I3wuzpM41whSDOpZdQ9RuGdERRkAluQlF+8HdcDpvLiUAjXHaQWEj8gI4exepATngwhJ4+ +Hm/OsEnjDj335OLioiXf/LTg659Q8gKl2AKywo3AqdQ4teBrKRXOIVy6n30QmOexEu0QTZRcA7e5 +yZjnzamubgB8o6FrMBamBBxkpZmcVJ8ANdIN1np/2N8cQqTWbCVESqBixz4A6bbrDiCNVuvwIqP6 +m4QsBPCYclw+KCAleX1uvPexcq/zx2T5GmdTE2xLnTuaNK41NL0X2fBSfflmX7e7Hnz7zqse8zm8 +W6uX33j/OQ3lM/b3JAeYquKNq1Vztd6ub/BHqjdHTx13WY7Rk9AHXv3ywbnfv3XK8P3yI4WxBhO0 +HkVS3PkuVMpFBcQexT36djk4Fuhyx5X3PX7js2j+9/Jbj8xbOK1LtzyUQTLpTONHH3Hi5CP3G3XC +M89+c9ZpN8mR0KwF0xtD4aG9B15wBgoIJLfXb/34k9eGdHMXq57ckD2pxGvcoS+2rei6/4QTjjgf +qmFdsPyzb94dVNi1r7lY3xxWkCBilNZXB0wFxYeMOyKia05K9W+/8/zoQaNee+Ltob3GhSMyeNFv +vvFCV7urp9lnCqPZi4JGKDroKZhBiB0irT4JZipKxVEdcYAxGgACTTXyH5uTqFpgUBQkyKLftqIk +jEE0OEZrJuWdd1+QMQklkz8ije5x+DsPTkEZhObcLhsseTulzn2HHnTfDY988fzP40efVNFQdfez +17715lMlNtuIvgedcvzlI4cc+cw/P0Z1wxVLl0z/9bNDjhtd36D89cIHR/Y4WIk3vfLWP+b+svjw +MRN21iuvLP7il+aNx551weDOo1DI4pclPyxePPeQzsMLovZooEGvxLfHKstj/ktOOqfI5YpbAtsr +yh+45KnTDr5x0LCDdQnY0OZvvpjy3bfvg/X26cevJyM1RGZGxdemOl0oeuQhR5931rUvPPFFl4IR +sJS//P7tH6d9ddYxp9T5ty0o+7XRlViwfuPl593UNb87enf9unL2zKXfRROxnt1HnHjUJWif0xTe +MuWTp1QltN/QcRef+xcoT599+eoLL7+w36ixufq892b8ONO/ErWTXTbUPEKHxYTb0cVrGXzTxY/f +ed0L6C5gNxmrKjfn2gueuP3tWy5+0oZWk06EcCJzF3z70ZTn/c3ls3+d2hxuKCgoQA2iqoZqf6gZ +osSatMT8sh5oCcobuArEMEABVVRVQlsdqFMK1GIqZkJBYYpXIXxlgtbKPExNwLZeRJlGI2m4WjsP +rfgipxi0bFoyXOszZFVgSX603qB8ZRJQiYOakcuf2fSjQ0ibaQ1pCQ6cKE+2lVZPTmAGtT9MlQkl +jGL5D9UCOwaPOpoRDAjnmKis2UqAZsZkiQbMO94UVTQx4oj4xmkx0XAjdGHRowAXtRYlogVnUeCa +EGukPBsVaq0BoUc8GvJbijapRIrVaKRaHQBRVZzyGCnBn//lzA+iipI8NhH3Esw1+hdUCqY9ajsZ +muy2JXYse25R5ANETToD5VRSi1j01Qa5FLVpBEUISxvVntGbjfiyDMpQlFEnBSWzIdcdqERDrX4k +M1rbuCUzys84rQ5vjjMvz5afby0qsuaX6B1e5GmYUdMt5NfXViaqd8b9zQr8jKiVid+Ixs2hZFV9 +uNuQwc6cgpkz5uGHQOJRQaKJR/GvopOpPoDT4vQ63V6vxwsmex5+Q0LNVKT6g/4ZDuvCoUQk4HQY +vKWFI46aPOaEE51e3w+ffPHjx5/j1v3w/iHow3FWKk5KPesQ2xTahebJFnfH5NK93jInMuaPglro +5LJJxHQKKEIWhx1Zc9EoumAhWTNGLGKkbcbBio3AwafqwctQ8LvoSOVvCqlxBFvhZce4tASJ0xO+ +lTenA8HGtirOv/J8VhQRV+KKI7Glk25Fjm7i+Vdef/ZdwSZQZow/LvvuiRfvqq1bT/MPUW2klyCR +sXjgoWMmHzP+jH7dB2ESr1wz+6ln/r5p5YLjhw/rlVu8oGLlUn/4xHMvz5WKAw2VlqTz2GMusOQ4 +P/npzScfu310nu+IroPtdXh+1tW6xjdX/Hr7Y69NGnlSJCK7beaEHE3G4+irAPfAR9++8M9/3WMw +xa+5+M5zjrnVH5ThfDNRpifFFCrrywCcH3/7Xk5OzgM3PnHI2JMQ4Hv366deeeLu4/oP7aU47MFk +wBRZEt6xNBS69f5nx406Flf/rykPfvDa4yf3HtXHUmCMUIavzWd7b+PCWm/Oy09/kIg7Mb/Kt63v +P3CATXJHw4pkNS7bMuuOG84carUc6OqRqIokogDDqAp6CdgmyBgCh0mGq4FaP8L9wDFGDBRXdUQM +HPm23D+LnNbUAwL1LShnKZinW2Gq2ZrjPPOyW08+5DxEugGbqPSAmYeZjSo/kC/gxOJRRXWheStn +v/HmU6uXzO5eAvdLyS03Pz6gJzpIw2qjLp7bN1UXl/pWrJ3+1LOPvf/GV0qMyuWs2bzel1fUq1Pp +B9Ofu+fhO0YPHPvQX5/rXDC4Od5w7Q2nhbesPrbnsE6yXReKR53J6f7VO6zmF56aajT60MIYaazo +Ev7pD6/9/dF7jp94xH1/e2bFml/f+fAFiIzt22pOPv6io486B3fx8D//ajYpf735eaQIKCo0DF1F +bfni1Uv79wK1aSAcv+99+XhDtHpwr/EP3/aqD5XQ4813/OPqnxd87fHl33zlI8cdfAH0iC++e+Wh +Z2/SW5W7rnn26EPPA9xu3LAaEdn9x4xZs3HRPU/dWuVfq4s6Xn74u/7dRiXUCIRTvb+2wFcYUyOQ +2Wo0idwvyMgdDeXPvv7IrJ+njBw87rlH31qzecO7H70ZiTaVbyk79eiLzzjlar1BnvLpM6+/8MB+ +nryefru5Bv1TDSEseCWmR4WIOJKmKbtGlSES8DQpwM9qKGYa55+xccFGB2VEtPHdZNL0qHZzJimD +V9s+Ya+0OQmnzWbZsh/QgtqZxxLQiOpuSEVi3nVLjC3zJ0WUjautibL6u8h4x7myVmrloqKtasmA +U4OBFEXgRAlLqjsiCqISl5FQkyuJU7k4YuVQ3ww4SzmZhPoUca1GUSWOnghX/BIscr43/lcrJkfy +XiOfChZWxqAImnnLHKAujHGoTojAUdiNzEhCDr5HLgtH+erwOWICkSxgVy81ZqL6yVRJDHQJPRg1 +DgN64SFzGuVtTBaH5HLq0M0N+RN2pFSgSrUihRrlxrpIbbWuOaCTY4gaRBV0utdFI3H0am9oipX0 +6t2jV68fvv2JOs7S7+gVdHulrCPEF1F7w2Fyw5XqcOTnWXN8JrdTstvRoRWUT1Rbbaqsaq5viAUj +yJ3MQ0HhCRNyfd7pUz6Z89W3ppAcSCT8+Dl+sCK2mrntE49662lCpfo4vQXjGUcpaeoBggo+9DcE +HN4U9aUy7XR6WjTqKBYIyxqJDwoefQsnKH3JmQuzg2vwPRg7qc14lA3dP6DWUDkQlA4PWJQN1Vs9 +Rbn9egwwJez9ugwcP3rS8IEHlHYd3L3vqPGHHH/yMRdfctLNh44+vjiva3nDxre+efbFV/4e3br5 +hAHDeqkFsYbY/PKtfcYdfMPFjw7pPXbYkEMGDz0waUt88v1LTz96f3+n85BOPaVqGHRGiKU4ysXW +1yEl8JADD7WaPciBMAKNzbYmufrDb1946sX7S4sMVlXfVCMfesiRcA1QmWDuyL500+yHn7tp1uLP +LK7I+IMOv/Dk69CXD26Ahx+6tFMyMdzSze3XReLRenN0adWO/cafdNZpNxr1ztrqTY8/e19JMn64 +r18igAVtNIM+Y0xU6IJlwaYJYycV+rojl6ikuLSirFLiNi9mq/61dx6rXL34oJKe1rqYGUoMudxl +KrYJwQoGCFzwPDu5eryILeI50tPBfBV9vjTytqhFxU1ozHLCZ7KXhRvmbVhmc5p6d+lqd+ZiASmy +zhSKo3oT+lPiJDvqNr772bMvv3hfePvK8f172JOGyiZlv4OO7VzQnZrwQq2WJF9+zqayBU8+96A9 +x3H0EaeYJJfZ4ulU1M3nzoc8aa6tnfPDrCvOvW708KNwHUuW/vTZ688e0qNHJ8VkBcYj4GK1rUZB +NbcXpWrMih0FOIKxujenPPHKG/8wmJsbQvWjhx8+qPf4kUPGdcrve+zR54894DDklP348zvvTnmy +e/eicWMno/AAko3RfhPOhr49+hTm2D/+/PlX3n5E72yCyn3mSdeMHXIUxMLSpT++/trTeV7LwD77 +X33hXSadtbm54ukn71FiNYiNTJ5wetfS4ZLFWdoZPuA+CF2jgeacX79TZVAPIl2Kew0aODKWRDNz +u9PmXbliFbpTorIEehCgMc/qzQsee/Gu2XO+6F1cWLWjqUfv0SMHT+zff0RpXq8Tjrhgwvjj4RBa +um7RU8/+rSSh65lwW5tVfRixbT3KKCNVhnpiE0eKClxyyVSIOM0aIh2HMw1poQqQY3Rsu2W8I9x3 +vz8il8GQ1ziRrLW37NlhT1zqHncBZrynX4g/2fCjFyT8hbtRbO2Eo5bbp50n4+daBiHLcIiPmUDK +2Rlp25QBT9iBTDHlunNc/o2lAHNQuWOg9mWRisp1L0XdASK5ifRHrbQbCVrOP+Db0DIjU7mQGpk0 +Y8w4A5Tvn5GbHYkijUNMA3LEsi4sjmB1guxo6hBJoora3SLwB7sS1YtNKJ6MjGqzywbQslJ8MceW +k2vLLdDnuAxeu8mDRopgZ4LD3ShXbtY11hvDYECi/gxmJMrZgJqqmAz2yprgwBFD+vbrO33qT3I4 +RjVZVMxikDWIA4mmbGYP6gR7LT6XGcZofq7V5zFCmKDvjwXdjvXUeSoUAfXG5sstGjK417j9Xd6c +2Z9+Pf+Tb8yhWFSN+xEV0rwD1KytjQL2m6Cx/RRsNZVhe9M0JKYpKS1xpPTzcIsRF5jHUyR1Hp6L +5J9myqOgdKS85toiFXM16xppt5J1J997dws0HmlEriceIeA3aY2pDl0yHAbFY1qzPtyptMRstuf7 +Snp3HTJmyIQJwyeN7n1gn5JBMNgrmsu+/Pnt1157ePbUzwpDumN6DvD5JUuQapPvkGtlmzRo6GCP +LQ/2RFnVwtc+fPLNVx/ub7cfUdTLV6N6DK5QlD2TMorHmBauXyY74vl5uYoa9YerZq/85pU3/v7V +e28PdVnG5PcqTnqWLF24qWlDp34liXiiqmn7W1/865UX7guuXXVQaWeXaoxGk54i38aKDXOW/DRn ++tdjCrt1arLbmzCdFL8lVib7EayMe8zbA1unLfxi+a/TxhX16BJxyTFuRqxGVZMSdxmXl62raKx1 +Fbjqw+VvffDsrBmzx48/FLUKN1UsevG5R/vD1DAVJOuiFGhFNQb0gsJkhJmIEvEKoJIJqbST0klP +WUStGAj56bGMFXF52iiTDiiEOdzYUDVn8YzlW1aDqIbyT5EYWGj+pmhtWe3K5z/+53Ov3rvkx6kD +JdvEnN4DdPn2mHlzZUW5v9bbBb4YJRD1VzdVffHtO089c/vO+rWbdmyx+KBV1K1ct3Bb5ZKaxg3r +ts97/8vXN+3cVNy7OGBsLKtd+/nHryplm/cvKDU3oQgPJ4DFI/WG4IZgVVHPrh6PY83WBf/69wPf +TXm1l9tz6LChm9dunrd43sDhvXyewu7dBnpz8uoay76e+tKrrz2EMhqVtTsrmqp8hW7ceFiRdzbX +LFw364U3Hv78w5d757lGdu0brgwU5xTKidCyisWfffuGv35rSSF4yI253Yp2NG2fteCzn6d/NGZY +/0BVU31TIx7EjqptWys2r9z066LVPwGey1cvG5BXpASUpVsXUzqTuyAQ2DLzlw//+di9NU2VXQZ2 +bozXfPrdyy8/++COpctPGTFmVF7p9rINCzctNeU4+3bv16tb79xcb12g5od5n/3zsev1tdUHIOG5 +0ZkMwutECj6sROYLEzZS52nAJD1LrVC0WI3pAhlpU/A/B42tF+5vWOe7uNT2wmAX76RlmYi2tYA9 +y6o2QEfvMX4K5Mw4nSBoUrHsPVOXBOy2fJObYxCqaV01CNs4VUO8w+xTHE3lwgkataKm5OdkUASa +8sHcN5HfYYWa8VLY9Ozo5ArRXLoz7RXkZUvXkcpu5AvTkC9tuQhiFl8ycyTEvYuv0mAQdKI0AHrA +o5MU4yKy91Hbw2ZBEgbql6I5KrraenLMOV7Jm2vI8SbtaG+Pttw6qxLR1dXEKsrVhhpDLIy61JD7 +MaQVxnX+ZhVwur0mMGTkyE49e0796sdEJAaZEYmCGsNlB2Bv4TwIUuZ4wEc15+bYcr1oRWtyOeDM +xbUhsxHlppr8zXBV5xQU5nXvWTiwf4E3b8Hn38756Mtoc9Bgte4EbRveLnhokeBE6kc7f/hv4WEJ +ey5zF49b7DSaEKB4gXYMuF9EntiITD8HMTkyE0VoVfJU1M7MMElOjt+/ZUKj/hlHDqYMuloCuy0w +h+zJhhLD+oL4BjVgLizuN2ps355DC92FdrQB1hnC0UCVf8eyDQs2bFrj376tULGNLOjeO+Fx1EeQ +k6MgfGQ3bLIEf6nYVDwcvMshNYHgxo3z6zfXHFxQONxeUtBsdESlsAFpK6QjhGPhYL51ZmDrBrXe +lVsMnyNCww2VVegvP6awS19LJ1M0GTSqy2PlS8IVITcoXbn+hhpjKDzI7jvIWVpg9qxr3Lk4XFFl +DDZgZOOmMZ7CAWp+j5hXaooYnclae2izFJjbWFVrUlBv3Gu1DLLkHWjvhjAk17HH81AMbnvAGF9S +u+2nbRsSyJYxGZv88XtveebE486H/vTOe499+NLjk3sMKfSbnWFjNBiUYbDEZDjfkH6gILUWr1Ht +hsI+LQ5VsU4oKIPuJfQCExcaHRyqVEEfWaoo/0RUHbOqltqWJKrXobhFTLZ4PAmHvRkxtWQk0RR0 +RnV9XL6BjtyuiUJ7Myx6OepWV5nrZ9Rtj/jQVgbxDKPcEJFC+m453q4l7trmhhXlDXq7LoAuzhZ9 +UySJEEix14DWwOFotK5BtiR0nfTmUZ7OvY15HsUaCcZRkspgDFT71Gn15bWw47y5zbV1tkjsgNzu +I4v6xIPyTsU/o3ltpT46bOABJSUDkZg8a9a3TZWbOnvtQ/r33FDWsKZip2JPOHNz4mgMjcZP9Q15 +MevYnE4j8waABvBr9YrF/vU1hoTJaPYoiTF9ekJjnrtyY30y0hBMdvKaSmw5I7r2rt1ZN7+mpsrY +hGKJKA2IYBOCG8VOU3cpd3SnAbKqTN+ypCypugo7ydEalEs32Z1ow2Nx5+CwaEXDfvldRrr7lOpz +E3JwfWzLnIZNdRZn916DSoq7GC2GVWuWlK/Z1NdpGWL2FVQ77c2miByV4zHo2hT4ZdMfvTbAz0Jk +mt5kSnHKodqy3v7DDlVyDraBxtbeynaf70IsZEVTyprWttYO1XQdMI1Vv1uHKmn7mm1NCryoKyvO +lfbFdcShyt/QrgFfJAck5S9Sc2mu0AUHBPWcStVN5QqobLWJuCMjojANibhKUIqTkMXJ7wsI5Do3 +jGAQvuIdTkhIHSAM5TRICxexZjWnHOvccIMdqhTcpTZQwuFOjlNWAkA2IFoQrX3MenS9lXRWq95m +lZx21J4A8xK+TaSqIadCh76iNhsMONyaKRlKBmuT9TvV6qpobT3OTen3sB4oaKOLAK/01tq6SM9e +3bv06PHz/CXRmlqbHlwNymvEs4okVZQFIGj0OM2+HAusxlyf3Ztjcjp0JjN1/cX39YaGuroQyojn +eAtKSnwFhQ6bY/Oc+V+88qY+HIFFuyUcUhH3SSTNSNZDyrUBXY2ZUZix/SarsW36Y1vfJjUxFm42 +cqtCUSXST6tpqYFo+k1qIUf6C30L1H+CVUpkbTuVf6dDVf+q2yfLEVRM0MGbhoqlJrPfFAwWRAOO +WJ05tt2WqEFDSaQYcIk/u6xzxnQFOmueztnJ5ChIOHyqXQpgCppUK+I0YG4aQlZjhSWwpHlrPdXa +s/QyWve355dGXHo8QAgeSdeEouQ4Fco+JaWAJdnkVnfG66oVcnOjFkKp3dfT5HP7VQcmhE4XNutC +Dn19UinzN6CcnkdnKbDZ8gxuSwMVfpedhmZLJBhuBM8CpbpzdR4jgpVRPGIJchNjB0Bossf8hghu +zB0z5RhtFsmZhLNUnyBdwGSMGmxenSVpV9d56mqi1QvWby3ot/8/H/wYrsqEXHfp+QcXxoMjPZ2t +NXoTao+BNYV4VERBQA/QiCgjKiip8HQTKQIuVtEEl0Ib9Jw5QsK9VKkdNAq1U2FSgkY0l0E43+A2 +GeKSGvUkm1xqXTLcbIwHKYBhcqq6/Li5SOctMNlU2NcqjHm0HoXJGpXNEbDKqpVwYyyCsyKGUGzz +FqPZFXy8pnhtIhhQEFGwwA+A3Em9onolK2LYwVg0qpep5auKUuh2fTBplyxYfCAGgdVgdFurdHJ5 +IlSXCLqt5h52X4nitcVs4EglC/Vb4/U7IkpdU1WDHAFPJd+Z29OZ29/jc6Coj85cI1dXxhsaUP0v +qrObzDkOVyd3oTtgtIcQDIgFbZE6I6paISNJzVWtxeZCJBEHzaGaRB1+165YfTqfO+lC+9WQPb4t +VqM6EJSX7dC0UYTWaPVKbow5erXWGaKbwjV+JWhKGku8+R6nra65CfQrFFfKd+eUOny2oN4UoZhE +3BmpTDZuCUfLAvUBNJ826Nw6ay+3LzdmMtcnbeByQQfXI3Ezqo/FDTHYjMBheMepeAgoOfjTwGk2 +BI0s3tOOmj+hsT0Ya65XFm3kYxTQmNrES8ySDADeBX5rmJgCfiKcknVIViNRbDhMiMwK7nQhUvs5 +ji/S+Qks2aGaiiyypQhxhOO1WKPI9GffqWClajkYDJkafmsEV/LXkvNYWLDkyRQJr9xDnl2nhIf0 +J0cWuayrFocEwGKG4E6RqgwKDpW7gQMTgXCEzlxOdMSRXIj/OQGNeid0SYfksRpR9BGRwSRatCtq +U2VT+bpY1RYIM0xLlIdCDwyo3Gg5FQtiZhpr/XLnnr1LuxbPn7s43hw06YyotU3jbjShnBpMUpCt +JYfV4nNbEbTI8zpyC+ywR81mapKFUQLmgZ5X34C0/aKuXd2FhU6rfe2Ps6a+OwULFGKqkgpYJrjA +N2Vl4Fs0DMyQ+n3QiKHMAo0pZz7GlRtA4hstP0p+aZpEqShV6mKgKtFpua4BTboMNG359u+Fxpdc +TigGVB2QCgRCciMxBvnrrPuYpLhZH4EdYYkinoZ5itq26NtgUx32pB2p4yALIwcPmQuKBUUD2G9I +TkUE09BbQaGgORqqgUiWhJRG1BRsTZ5jFLhHYQtKgKWJxxV1weIi9RPQyc244zKyVtlwNhrxTfwL +HRJyC5iDjzCXwKnR+BDQMnBp1IkWVDAiuYFLgekPhyfXdcSt4buwEmHWUP4aoAn1IKAV4R8Uogga +JcwSsxqTvIZlavUny5fffM/zkyddrCTUD6c+98Y/bzy6uF+XiC8ZQD5CiKKMlEUDfAQ04jXaYONi +wJ3kUnCU089MHEZEUdqRNFj8F9VXqN8XHNfII8W4oO8M2FhY12DIIqRJqh+F6ymjmYrUkvKLPFHU +OaVeLCYMiwm186GmxhGpNqGqoYIbpqbfqGpBEgmPjVYpNbenp0CEIKERI6CrwDpk8jUxhSiwDUtJ +KPe8uqkiBxYOV2MAHYviC0RLwbfNRgSDUV4XVaX8ckRHXgPAK1q6wdyF5wVcWz0yaqjbKjqPaGlm +lLmC/nCsI5ACDTcPnGDUDhsDAycuKAPAeMTXoUtQjFZIJ/ADsc4VA/g/3G8GJyW/ClHUqJ83indQ +vXtkWoDui/VDH+GctIRwuXgEcGvDG0MEGcwCqmujixHkgWWaMIA9LMeNeF6KiusC0Y7oduC4o9UA +knDwXVwKjuUHh+GBvqXZAiKcoYXi2CXYOtCYov+TkSkwoCOWnHbCbIdmataZ0klcRkd+TkjwvdpS +JhCdXiAWpbhoLsZdnwkaO4WGMrb2fIdslyECesL5SaWrBfKRMEp1wKA36SOSnRQGoA5MzJ9kaUrv +pPyr3MqYi8ZpfTmoYo7GuGFnKd0PPyn6OaxTQbRh/qX4kAUxm4JcDoKxEoqmiMWSVITqi0lOqMmD +Q4dCNOEjMnJNKhLRERoBFiF/0OGQgIIut8HlMnpy9CjS5nbpHA69zSYj7V6KOwyKPViXrNgULl8X +rq/GkkBZMiWSiIfx45bmMJH84E1t8sv9BgzNy/XMmvkrKnLAzIzB2cEDQtISqSBuJ7K3DV6XJd9n +K85zFxeZ3DlxCEWSNEBqyWIwQQRElLjd43S7nYV53iU/TP/5xXdQWkRvtjQpcX8cBB8IZIw9MdBE +yplob5llywBPDlGzh5ngTbP8mES1V1srbNQmu7YQtfOwJibeyuoZ2auf1mXScPQvOD0E1WzesJ8e +wpLc+FTjj0YKUsmK7EOebZx4C4lCUzUBdMIOvhZmIVjUmaOoZbym4J8lh7gVEffXHPRipgqVTdSi +w0vRNAtTkOgQXGeA+SzkV2ZhLsKudB7BgydYYoGBWQp6oQj3EZDzimQ1BDeFyhPI/pDguYCYttOM +gr8GWGVOSDbcooJy3T7po21LLZ37PfPoFJerc21ox01/O0Ndt2Syt39Og02lyRgi4YpcDUrCiCNY +Co+ckMucs8iCVbTAZZo4mYzsrOGiwgyWpH9gE+PKL0mdY9OEVyUBqRgEUnEB8DQ2NAs0KU0jQWcX +sotqO4rJy8PEecaCKq9NUPL7MPmPFzVjISu/QINUtR6GawHkfHla6UfRgIfODT2FQiqSUWU1GxCo +Qz4LT0jIC5S54553kFR0+6R1I98TO/OSmHdIMyQCXry4ZgEh/DD5QgjC8TaX+NIqNYu5jKevSTL+ +EsATaze9kSLMRHzizZCQgsaOPldi9hABX2QVUDsNVCCkZ4QKfUnkYpJ2QOQppGxQP0HE/CkHlQnG +1OyVRR7mlRgo8XMsE1ugMb1wxZvC/6rdV8sF7ulVG4jd5aH/FWhMz5z0JYl43h5uZh9BIyXlQ3mE +1kOullSehqDhsHVIeSAZVd8YGoVDlXEUDzwdXBS9FQmn2KwUTBzhbhVOVDETacGw6iyURHL/8uLS +nr4mo9ihms6C5OJkZCCyDOL8DxI8pPjCDWRBpATJBxYjuo1bbQmbDUAIODS6wYjxSB6UprbpnTYQ +3+LU+Va1wI3WWBnduiZeuVUFLsoydHY5kkAKfjJuqqv3xxQdvKmxeKKouJPXl7906dpwKOIwWcnR +yqn3tHDhXXE4zAhh+ry6HJetuNBRXGDz5aClHOY4qdpUdgfaqxlHm1BkwGYrcjhX/DLjs3+/bUFn +YrQsTiTDWCVkQhPOkwqdul8apKzQs++hcRfTrQ1Gt1e/Orbysh/VChr/5cgRPEooC7A9OBjGREoM +KktrkYpAuaqUvcMQx758MtXxhIkjhhnSitPexiWNmYoWX4Ltx/qI8OCLBuoCPARzjDdNYdUirXwE +Q42YvuzuEpJLkNmE0wOXRZcLXUq4Pkju0rWSvYLrZdo0aFqymbwdzoRJMeljVmKPocEWWmXHXIkt +xsDUbRsuuOq+s46/FtbTJ9+98uw/bppc0Kl/LNfWaERFJcQ9YS8mIjBT4ESNYypD0AIa0eKBlhYn +AAng1jKrGHMI5wj1yQykjmjkbOHyGyQJMMa0iWFIb9pr1J8jnYOD7NTzhMt48JNCshHiKygpSVAk +uM1MieO2YrxumVYH2c8aPQZejDePmxguIc65ax1FZ7gdDlqG4OKE5izCNgQxXAuEAZsz2tKCUuBt +KpeZWk3Rw0GdXSplSZfNQQOiszPGiBWUKqbFmjvrE0JKoXIbSrQLCr34CcAb/6EpAsBaSMMWoKLr +o0mjcSyFes+p2wRvfLM8FtRwVYcgIlmo5P8m5o2ARnpoVNeWc/AYGkXVKZ5me4BG+jRlQdJ85GOF +IULSlT/d8/b/DDRSuO4/AY2c8c3cGUofFMoTJSRwO0ZS2qlLLyUd0gRhVipfGBcZ52+Juar1cRT1 +xHEeeDR4nTFwikgknZk1LjG32W1OdAANK1NPTtNEsbK5mpXgo/JDJ02KSiex2xVEPhhjEDEW9IoA +exSgaKXXTpcOgXC3V5/jMbhdFo8bXBjUTCblElXa0IYYKerBarV2e/OmlUrlNl00CE4NvCLw9ccA +iFE1EMLvGkJhORLTeXy5ZqujorImRLwbUxQt5uAcJdd/QrKYESACLppRW64gX/LluDsVg3qjWkC9 +h+YMvQJjZ4QPT0X2Hxy5SrzY5dky+9cvXnnbEqHaF81RmXARQw4aJtW3JCdxShcUizQbnIgjeNKz +9i0WaXoNMJ33d2//JWi0uzWjheYXyz+4GjFT4YckjQ1vIG4FQ4AsRW77QvXRAZPwi8HThz8o60Nj +M+96DBhMSQAJ+UGluVjoiX/FdyhCIAYg5TJKfyqQkYLcms2YQkbBoAI2prBFmKkk/2mBcdCP0B0G +IuUUEhfGgMroBqvehDI4UdS/Rsl7pJkiwS9X/03ZWn1x138+85HH1bVR3nHbbWfJ61cfntfHUaFa +Imj/EgH9Bk45CnnH4K4DNxXzE2EySoNjs1Xb2D5jE5gVC6FJQHkja08U24chxpWpBDCSusYYoAEB +3S19l4xm4XpncUDzldw5GniQToCwgAAtDUEEzVwDLB5uimnTgJAiKKw0hhIxfTVytIbINFBsNQpg +Fk+GlgZ5WajfD5PGKHOa647yKcRpsXFrV5I0TKOGq4fNKaGl0H/oFsVD4sPpLwahFBKSXaqtKh4N +un1apdrI0BByu+3UCejkKTVXW7t8SQR3Av2FHqZpK5ShQUkazEWlkDAjJf0+zEiSt2zxY8TFsJCn +ljdxcylJodmOgl0i4E08ds1qFEe3kgupt1r/978Fjek7Sl9OevXt7kI5X2LXH4p3U1Zji/hr/yt7 +dqiKh8t5FzC9CN6wkZrESEZxCJEkQeFGKqAqrEBm6ND77MciHxBjJ1uHBI0EsdQNikCXnpIWehTQ +yAuLVhA1QdJSO1JD0UqOY7Kwv4W1U3KXsfbDOhlFi8zI8DLpEdlHzW67zeCw6R2OhMWm9+ZIPp/e +6za6QYRxS25PEliFmAPFFwxUJjmwLbB5tbx9o1y5xaKCUmEABMZihmhzHNCIxHfUIYZ0AbHB6vQi +4lJX3xiOxEB0RQESIiShGTgCQFbU1UcKI3g3HlRJNft88KM68/MTZnSM5WAnaeJQeSU9KuCY4aAy +Fud4186c+9WLb7lDitUoVaM7HpR73BLIN6RSc4SRxWZqmYqZ3tGNCVm0fFPf4bW8FyfY/XrZm8vo +6OXu6rhWVuPzNqeQjmSYEXWERlR4ILjrNctpYnWgagHGjiQoyDU82SA3YYij1QTiSqxW7WYjWUAe +QiG5afRpyPh/rBWSjKHqChln0I5IyWiCRtbtWeaxFGWrgIBWqPoMQO392lpiIYVQ4fSA91JPJYrh +XiVNFd5M3GBSydFtMgemVVeecuFfLjr7Zpzwq19ef+0ffxvrLOoatknIgFQQ+4QvlfyomLM6BKw5 +Z4NMEgo08p2lhSkrAJobSkAj7oIjqRTzY02OIFPzYaZ0ViFVCTk1C5I8hWwmAiLIQUSF/8mgwV0K +85HL8mr4IXAsZTEL6NJQknBIwBLGitQIwcNj+U+nojOItuYkjFiBJtWRxQGCm6RVEJjzR4QHnN8t +zs4uU2YHwlgEx4cc3ZCD8PYI16Zo6gZ7moKEPDvEMmFQ0cCLnho9XD41W8hCS2B8E+o9v0+Ofkbn +jJVGnwtNgG6PYoak+ZJnguKI2iNh2BP2IIKdWhYjgWQaGtn0464bfKqUSy31Sy3QyACfnuVarDED +RHlWazblHtbnfwsaf4PIEIi1h+33O1QxYogfs5eCoFEkerAsIPapCEugFhgpk+S7TxNwCDiFoUlu +RcAkfDGp2CQzVwkaEdQWjgoGVK7koimXLH14Aot36NnxfGKbVXuHXC9CsxM7/SI71UD+RsTPYkEH +RZQtTjgcSeRIuN1Gp9PoyTf5csw+p84NSqozaXXErWg+S5doVsNSGAVuNjdvWhQq22SJhOxoeRRD +SUVQRNHoD0mHKow43Chq7keCUTQdxqpqagoIskUER6KrKbe/QJMoM0g3OS5Tbo61wGfK9drzcz0F +RSaHE64RSDb0Y0SEAFXOEEhCQ16LpC/x5Kyfs+Dzf7+l1jWi0EB9LAr6BFy7JF0wdrQ2UsKEoRFj +xdXEO4hsYuzYxOYlkN5+PzS2OSGLuQ5e1V5P+dbQaEFHXBEVI0nIkpHxkSQidwdl/Z+ru2himxV+ +svFImokBFAS+1Nbm0ulrZInyPdHwsRQnMcOST4hLUbu39SYwm5CG/Kns32ehycOvSXzNVEnx0ISW +wieCYUaCnf7CfxAeN0r2hEW1WIKOhBlFNckaMkZtumpneEbDZmPfAf/4+5QSZ1dkBF5/6xmOiorx +ts6W/4+9/wCQJT2rg+HKVZ0nz9y4d3NWQAEQAiFAIphgjJFsbBlsA5/Bhs/YBP98NrYxYGMbE21M +EiZYxkZIyAglhKVVWoWVtDnd3bs33zu5Y3Xl+s953uqe7pm503PDrnbl7Z2dO9NTXeGtt97zhPOc +Z5Wa4RD11QMQGUFqCdMY5EZwq+kiwbWRAF0RTB5edRGIUMpRxSgRbIQSJ1ld5vXEhVQJs8HY8dFX +oVdcACarwifx5ahkpiaqBJv4pnLCJQypkJHjI66jSsnyaZeBEGuCvxUx7EFiHO9BIl0hoiwfYqWo +kCAPySPQejAQmpIjC2oX48/DcVshD9FxUvgj7q1IySjetwQJhhmrAhqVNydnRssWy5tQaAQEB6sS +p4oyLPifhNG20l6jE2X4HLI6SlkoEkpVF86xEOtJRoWpR2omMw/DI0bINfKK5Eyk1p/Df6mA6p7Q +OJz/vON7PpIvGGikFfysQyOfTkT9xCMUmRs1hZF0lJsuwSQCGwNUzL0o/0/lGvnFJVz8SAl6yX6k +AlK2kZbBMicR55cjqPWGS4rY/mq14FxjIELFaWjE8lHlNBLvlT5D8ZhBsAZmtQ5tTTqLHuimSCim +8AunpoyZWasxZU8BGhFBdTIPkjwuaKOghqMEo2J0ap3T8fH7WscfAFXURSIGjO9U70LmGxJbHYAi +qPWG308grhX1AVRY8UzoSwLqoIOjklA4H6TIMR5QFLVRzDbTcBdnvQOzztxsFQHVcsVEv13QHqEw +Dg4dWI/s+uxARWWuVFq5/+F3/dffiVY2wJZFOUBP1/0EqR74soz3cOHnYyxsT4lvC9SxKfOec1me +z+Gyrx6lAhqHNJyx8o8993bJP36BAqqARnnRdleRrwIB5S1euMxMxcdSoCSavFxaBuEWySEWkCQz +avtFyiI32GLg4AwXPsHInZ8aTGP5sKz1vHMFQAgi85DiCMmajVMEJcZMaGhmYEUjkIpOW7bjVPCI +xH0U/iNYz3iaZ1fyCnst1vS1g9H9wcr96+0f+We//O1v/DvdbPO3/+hn//SXfvUbF2++Karq7TiC +UBN40qDeIEXFWnEQcGSFxeKrKJ/iggxxSCbwyHSRiaLagxdPmgw2oFHGfAsa1XOqNsM3Va+jkEFN +TwFC9a/8WlgScvFiacg5FLOIQEAxYuVAFqhdnNjIbFd6aOp9MZ/lAOpOq/9EK1E4WsWBB4UNYurI +mZDWpgju6nfWBopXqXAaPvP41FZwVVyFglvV4W7UKSuuhwuVRBUEGLfM+VHTfjjZCoKcJBoFouWH +Iq4wxEn0TpMYKN5ARLy4DPld5tPAnCj2qlxk+YuM/zDAOES4wpgYWLJFicDwpHb8sPdiU2wu2Ycx +C2Cwn+Jkdjxi248zCaG3bV/MJnXT1WQrsszbn+bBhOKdxV1Epmp0V9vMYvxp14Aqnwlhmcp842SW +uIUolDIXTsK7qq8QnMMSrsr5i5J/Yd8UDFXlcQ5VcuRTrGuk/KYy3bh7McK4jhWeIplwCDoOgFNS +/ogE0UPjCsZZzRAOFW3Y4InMADSfj0sqrYhSxTKop1m1mtTq5tKSuTgPB86uNfIK2haUMjhtFqRl +eXxUUpVRsLZyf+fJe+LTjxudrpGgS6gRdpLIT8J+3u+GJL/7KX9mrgb5K7QpQDkjnEby8wGQZGsj +b5VDaBR5LLM+DT2VqdLCvLk06y3MOtModKqb4P6gIgAuIyjnCLNAcc2C1+rNl0vnPvP5v/jtt/VP +nat7lYtB0LbytvgoGFhZGxSdoHh8CnNUvA9Fihw8CqP3WSHfODSOTaktW/HSj4J64HY+IdsfkX09 +MnsfZn9/HafhuLUCgoqUHn00tSKrOS9ElvGLVggpa0HhnYy/M4oN6pNMX+/YyfC4uw6Q5BdHXzic +SiapWzJyY8QNon/FiKuFYgUQ/dGIAu1qEfWgCeTG69Xw0anwotetes5N1ux17UbFt6OK/ni5/aHl +M9/+/T/6fX/nJ5EWuOdzf/yT//p7vlSfv61fK23EqDBJfGjfkLghpH/ldqAoghU1dBm5tg5Ci4OT +3Xb5uPfMKcpfxQXcgp/iGgdGB/6lEycb7FxQto/qiAjJtqFVv3LKD/CqWLCKkPbuN3T0dhTHUtQe +FVMfucW7zofhXaG+zAAnimmv0sEjr+0LaBHQHZ0koz8rm2Es5L5zjqnpOLh2cf0A0pIDVQuiMLZk +xipGkqBmQd/f+uCwv4KM4XCPO6Bx9B5xnRnsQZhHV/vaCTBDRNwvNF7mKWwliLhG8gEj5XhAyxq7 +dzKG4udLhd9EaBy97zIhVBgTQuGs1se+SLCReKk0ShTmtUik8pZLUp6dhNU0lCAq4qWI9AurXVX0 +M/WoNihY9oRGGEB0KMVVZAISVVs0HYtADDMboj6q4lEotVAZB74tiR6UH8NbI2+Olfu6AQJqyUuq +5bxUziuIl9at6Wk0iTDmZvSlOXN61io1bLcWYvqUqoZXZZlX2gGNwWmfj86izPvedONpVLuZsRt2 +9Rix014Wd4GO6IMBjl/WR21wgHIwpMPBuGNtEeTaYYQrGQXFKgyR2DGzcr0KBbjywlxpacFamLVR +4D8941Zr1MZHxBXCHDraT5WRXYBNujRbO/P5+z/4e2/bePx01TAizVhPszbuLeLCKrpz6ZeY26PQ +OApiBTSOZBguc8INNt8JjTsn/xXu+vI/Ng6NdkUtfBgDJsMI48pxKYxHRZcZPQr/pOKig+/q78P1 +d+cpKarC8MVjjeABV5kd68mOtU8Z9gU2yvFGbRaF5bkn9YqBi9oSlEEaNdML8n5rNn3mQPb4vL5w +5+3N1ebyQ08vpsmxuQOtfnCi3fq6b/3u7//+f3Vw+sgTT3z83/zkDxib519ZW3LP+43MYf0ibDZh +3BAd2VyAXpHUmhIa6ZgwY7EVDKY7PW4HKWjkijBAPjnTAgLFWZRrHUCmugXMJo6/tk9jiVruPbV5 +44qcY2HHbJt2EjAaOApb7sGW4y9J4qKbgbrFOzFp+3kO7Jfh+2NoU8yU8fmgmu+NbXfZ0DicgQoI ++at4jWIW819Fo9gBjYWHJCPD2zB+4K2Jy8+O01KG5suzCo0FFqrRHPiCu9jal78QjD3Ugxui1oJn +AxpJpZNDFlgFwJO4qMSkJFMo5g9TyshSi7QbLpfFwORjK96V5BqlhyLdRKlrVLlGtpKVYLgUoAko +AvnEocSnJPQhGSGEWHmFqBXhdUpaiFODNWucgKgikvwmhKtA7MQPbJnhZZ5lVtBTuKJV5/RGVZ9p +6NBgmz+Ajj0AxQjF+8AjoS0a5TLaQ6Cmt2bFlv9MuvJA/+n7gnOPG93NkmGGftZvJVpgB72430mC +Thz6qOtHwQZoOFkACg6LjBDPYs2RKFFIGEg8PHKsgdBlu4bixcU5Z2GuvLjAXOPUlFtrgK0TQJQa +DFiyKgy0j3ErpQPV6pnP3P/e3/2D5smzJcvys5TS4QznYERQjLmjPen2BWewwmzNtqFPInORD9RO +p+/yJuLzGRodZZNz2iiQlCSZ8l7IUVRIOPLinNryGAdJQ7XoXgLxxXwf38U4NBZCxpce1cEZ0YBS +T7HiXg9jgIqIXUZfrcwInTxAWxdEMyynW4nWlvSnq+GrvuVb/8H3/5tuK/jQJ979p+99+9kLp6fq +9W//5r/2HX/le6er1z9+8v6f+U//tPvg57+uctBb8SvgSGd6AMkY0UplMT9C/rDEgIZSyMjqcNBN +WB4nJ8GRKs6GM3nkQlS4VwYNDy6NVBrjMujypdZt5agrgJd7MVYpyt3tGNqJzgnhZvROqQTf+Itx +I7X3Uczb2k4MJOU9DCMFe8/9AcZsbbXt6VGkmNGdqO60g5VT/WVkxkgiaFhEfKmjiwOgxknmpIwo +LBcx98TRUWpeUgq5NZ6Snx1MXt6m5xU0qsWJp6usz4GV+hxBI6P+g8j9YNwVmjAyIIwtWV+37vDO +BQBIobB2uJEE6OHYSW0Gw5ckSCpo5EOshN8Up4ZUBh3BH0fCm3Qr+T5WCxDIpN2UAkiGZwmN+DhJ +rcJbKMQBitQ5UROfU/FVOJ3YnyjJ0Veka8Z6Yx5eihRJtNE9NwOtvVIyILdWLVv1qlFrZI15Z3bW +W5o1ZmasOtTApiMUGnq2hvOzInAV4dUBmCtx19k82Tv1qd6Zz+WbJ12UWgQGxK5YKtHN827S78S9 +Drg2aeCjGQa5faD5SSEYu38gdiqMMQSnlANAo4JqHp4Fxan6wmz58EHnwII3NwtlOKtSs8sVUvLA +LnRLyEyyzY9nzlTLy/c9DDHi5lMnoX7Vy5O1LG7jXpGnj7IqVSq116tIk43h4rg3ck0CJCOBmQkn +9Oz/eZyGAy9LvUYCfWrJVv9h0LeZBsUqP1gpOQsZ5yhW+F3Pf5vXWABvgQKyhBWpw0teveCL8CoH +x0FwE3NaxSTlmWI4RtifJHpj/jsgTRtmqxaePagfn9b/7o/+1Gte/dcdzHHPPb92utXaqJUrhw8c +Ab/2Q5/6wH/7/f90/IGPf9OBG448o1UCkmTavs/alCgi/qFGHHICsXCcob2pFnc+U6ohT2Foq0Vi +W98iNbUU8HABUBevJqYsO2IUSpZdGcbEoGL70eG4hNWxx3wpFlL54O6fVtwXOW05tJy/CruqDyhI +HECjsrAnQPLOI20PDnMv4ztRegVjvf1GoVEuRCJLw6stkqujV89suQoCFXvn1sNIKcPxkv4cCagW +NsqW+cA59nyAxuHzMmpmXXM4HB28Zy+gqqCREU61jijWeyFhKrdd1UgPRMBJVWWYRVxAruIU9gRT +S8Kt1IQT4JRsYrEflXdkelLykQOvUcoZZTNJN8qcVmFb1CJKx0YyVtXMYukz2sYhzAg3EQJvJRfC +p5A51aFlMzWlT087M9P4QVs64EwtuI05DX0/LR29WRMoIbvAoophVHUUVKUrVnAiW3k0OHlfdOG4 +EbTMHLFVLe7kcTPR/FTr5mEz7nVTfPX7eQhWX6iEQ5BllLIniS2hpkKl7MVUgE1O7US2mpqtleam +G9cfKx1Y8lDmX/FQmw1lcKpqsUGbiU/VqpUZ07j44EPv+i+/1z99EU1+WkmwkRMXY5oDKGFmyEvJ +qOy1fGw9pMNHb+wZ5OO19y728dfnr9f4n9F5Y/Ai+2mYcRygIx358YWMVtmIMyIzkqLpshDtPlaj +Hok6mtpSvc8ZPsmEkWVRHZfLs+xBmNUseBNqGUtPrERy7ahfRXEPeFoeymud3un59PGD7nVf/1f+ +yQ/83GL5uqAfeiVXncZjZ+/76IPv/ZP/+RvGU+e/efq6+tmkmlbCXh/qd31ENRBhCUKItKEuAPgI +/TRqRoGHo9JX+IbgqsIVdX6yz53QyI3lSoe+YPHr2OJUeO2j4zNpau09asW92OPWDO+W1NAUqC3I +NeD1iP0+zPvuen+3vVl8fOTUJ3rA4j+MQ+M2542u3Tg07phpisasppYaF/4wMh9x94bQqHhAzAEP +rlQ++7yDxtGY7aTJcFV/fy6gETYW/MPhS6YWI4Ys9GXgVFKDQrRhDlLJJVI0lcRHpOCU5Jt8oUCQ +EVHFz5JwAZknhc441ycp7ZCqJ9Z1cDoIwZUnAFwUvQ35IAs6eEgw2HPPARxCaNuqlMxaTUM9Bmg1 +MzPa7Jwxv+jMz4PtkpdZkqG5ZTSLImijXxE6lcNThH8JbTA/NNoPRifft3n2k164WUmczDeiIA57 +YdZFK3c9XA/8Fkg3Wg+9hPs5xG4i9B+iCgXWGvD8OANxOiQ3UVdKkergHnPVcRvl2tJMaR6VIfXy +kSPe0oI31UBfOgwGpODgwLjQMUd9mm2VSqXTH733gT9+54Xjp6p2CQnLTfqLeSimABFRSplGDc1d +p87gMRr94xBN1Q+Dh+cqpt7zGBoNb2xpG7gRyoeQ6beje6t4FhyY4h/6asNlePdR2uG9FwcduCzb +gpCXuFXFejcoSmAhDq0sFSWhXo+UXgIaoVNtmfiyjcw142Y9eWZRf7RhfcU3velL7n41usLgk612 +69yZkw9/9i8f+9znb6g4N+e1pfWqs5EiHw/9MLh3aNEAs8DCj6TeSJEIJiDbgLNgHKciJQvi7Q1u +r1qRt+VNh4mogQUgW42s7JQkl8d3GNMUlNjuF+00L/ae3MO87OA27TaoOP/CWVQ2ShE8GIQYiykw +PPQwoDe6r+3W0A783JnuH99E4gESEhjZ7QTO904bbBA9FsdXGV4ju8PvUm0pwyqWFf6Xetlhcnjg +ZA8+JRG44qXuMLcYOcfhUy23uNhYZTTVa4edWED27s/IyLtK8GH046MrCF2LqzfXx0+iSAeo8It6 +upXExCCWsHVJcmJ4AnBbmVYb7Ec9ykVZjkym4uGGpwbqpNi0XOblbfpzRYUvgpk4jiqUltwhbVCY +piy3FbtXxJoEFwvPUtxBQqbKKSpoFJ1x/kzncgiTokIu4STZoeh/sLBZZhyAEzVdAicmmniX7axq +67Wq0QDFZtaYRtn+rD67qM8t6XPzeqMORRukJFHxAEmtHHFXFqR5WuqyaUC+kvUe6m98Jj77Sav9 +lJl2zUjL2k7WMjNETTf9aCPMu1bUhvRN4vdjH1jpx6hoxMVTmklxF9QNR4WGrA6qCazYhBTcqS3N +1Q4vuPPTEBAHYJcWFtxyGQpwHBYEg7HUoW0BJAUM89Qjj330D/+4d/yZku30tLyZJS0khVTYTWQf +eXNhPkwO/4wZo+PPU/Fs7RK8mTS5t8/cHeb9pfyrSTu+Bn8fC6j+ml74T1e8423jt8/9bLv+iZT3 +nbsdumLqYaNFCeVV6YeKpir4jVUcllbCkuukzansxFLypBN0Shn0shGgQPGQFeg3mPXrgjJ6TtV6 +NgqBxEVGKpxKm/ATRdgdAVfpUSzzVzRfClwbXsLl3kvO+5EJMVwvByYHH9q9VUj2M8g8yjUIeOzn +UBO2YWuSvV5qBu0d3ilCBXvsZhe0GH/qVKuEvV8qIKFeo/doCwWHZtA4Rg4n8KinSjgYQc3BNW63 +A7cBMA89gvyjADw8t8lXMulKR/+u9qYY/KNjNlDT3HIp6cuo5vWMpBAX1ahuBeNlv8OZLJYI4oxF +EYXgFrdlaFSgESahkhhUBYicByy6YOJQpJxZYCiQRkE4flCgjrFWSraxqkKRehzmGoVsI/YOFn58 +sZZZ1BpZOqk8UWkFSR05oolteyXTK2su3ETogJfNqpvOzGSzc+biInguLvoMV6fyagPNFBLESrUc +bYjpcMJZtHK0o8PPDs6qfT5tPZiufsRfu1dLNhm56jta20o3U62VJuth3IqDzSjsZD7awfQ00G38 +OAUvFewFYBu4flBZxLWDmoqRJAeH+gSYiWT5sZ4XrNNGo3H0YGVxAUKpKPDXpqar8wsYSs8BQchC +hz+o3gDl65m28eBjH/mDP147cRLdk7t52tbSTpb2B1J5xVKjxKomGJ/bEsSXM59Gtn1eOYUTr+Ea +Q+PE4+26wfYo3A7bYeJud0Aj8upoAEK6hiM5vQSlRXQc0asMvei1XllrV/VWSe+jP0SW1zV7Lnet +XkI7EIGJxHQh7MO6W4hOiJwYFOBUNpEWMsVbye1GDLXQ6RfP6hIB5Anrr6yYe7++uKBx4s2caF9N +hsadx9gxx1TYdo/X2FGuEBq30pviKY1BY3Hobe7sTjAePfNnDxq34atyyxSwqe+ohlJELjkHuUck +ddLSiYWJw/wFiiIYoFDZd/A/ZXPxTLgXxjOl5QyFlaimzz2JiKGKuAxCexynAhqVzM2e0EjLlw6o +QKNwVql9I/CpHCNKBEArjqolhEZoRCJVh0IMEXZC8NHUXaiPwhGsoNYirVaNet2ennfnFoz5OX1+ +jkURcw20WkSnG0RLoYyJYBKZsHYJRAO5HKiGJFp8IfMf6K9+INt42A1aTmLF7STrpFkvy5tpuhFl +TfQmjcJ26oN000+h9+b3ch+pGkxE7NkwqccMhAQyMnbEgcQvAO0EzPgM1AbN9lASUkF+sbq0WDm4 +gKZHieeW52ZK03Om56KWMVdd7gxjsVa/8ODD7/2tt15EZz3H66VZN8eJJH3pjlIE2OTO4kACjRPW +oHHu1MRHePcNXsDQ+KuQ/n4evCZCxa4L3zBVyQeXjwA8RRicVPpESSIeCjYrQuspDcYdC3xZfgHn +sITnBW2e2OkJ5llkG5GJzDQIGAjqg9ojUjfs2ycV84KLqucUKxoH1erDtNYVDB7XjEnQONG/2c9x +rwy597PnkW1G4467f3RbVfhuG+0DGic5Sjv//gWGRuICV3sxoMbu9zBOUADIeOxUAcZwlJ49aJSY +YvFSptio1yi3RB4BrqRFHZtK7CMKj7gLS5nonYmIoxSz4iPsQsxgqRTv45ETKqg6hrpBopKhykkF +FoWwLWwvkQSWeMn+oJGoKAFVSpOzZkscdkZWmWvUUdFMMBReK3g95NmwzQDeNbWqk1dL0P426lPm +7Lw5M2vTU1yyFhaN+oxWq8E9NMsVQBcUbaA9Q5FC2sVR6lHsFP1d0XYv6z4SrH8k2bzP6J2yop6N +lQMF+c0sbcZxqw9cTNfQiFHrraX9Xg4JZnRY9MO4D847rAmnhDNCT+1OuyeUO8jyS5mG/IfYpyLl +oEaxPFVzauzyWFmcrx0+AEkBcOfRo7i+MOdNz8SmAxez7JZmLKt/9sK7f+e/nfrsAwsGmtAY62ip +AUNfnHs5hCjdKYMFw1QkMvdahtSMuKzVYGd89ZqsY5d1Dlez8bX3Gq/g+q8gSL3tmrd5jZJ5QCkj +KxppMIoUOlMSsGqpaUHARFcIBlyY12flh3DZhCnO9BN5WypwKs9s0cNPSasIE1VWLK4ChbU7XMJ2 +ywJezQ3iZ69gSHce8rkKqE44WTyo+xiOy/Dndt3bZPt0H6dxDQKq4jYpH0v9JAJ646uM/D7MX6rL +GTv/kTzQswSNnPmimV2Mu+KSFadS/CyLtVLTVWfMbpeSE2QdExxHbMf4JFt+ktJCdqjkJxnSlBce +mD4CiIpAJw8WdkMwU7Fb9b9EU1WdBoGUWlvgtOwVUKXqr0R2le48eTeiTiH642gczidaGqFKh1Qy +TxkFhQdmeS58QZJOZ2aM6TlndtFdOuIsHHAWFrOpWlL1zFI1B7edWqmWCW6OYQF7eGZMVaMFCHJ7 +XS15JO5+Ku18zuydhuCpHdhaaES9XrfZtJu51kzDDT9cC9LNLG3rvc0UNNQoRftVqw8ZZpgCDjpj +5EEv6PdDRmmp7k0bRE0RDBEbfvAyTPiLtTmI7FRT1/ZmZ8pzcxq65aIibaZeW5h3a1N9FHQ4pbpj +R2cvfPAP/+j4vfc1ULhhmCthsAF1aBrzai6qaPTAii2gcUxDY+czdU2WoMlP5T6Whudsk2cBGi/P +tpCF4IpCkaNjtAs08vGmULliWwpJmzKQbAsFs89CHSLmlVkyIY+uh9C3IXGUk1CeRhjC9BVV3ZsC +RwmlSrRIaij5riKmDk5e/fBsQOPVzwYuytcGYa/2XPYHjXsfRTyMPV8TAw9K4HWvffBWb03lKwyo +jhxFeY30k4q9DkyEATSqs9nlzJ8TaKSVuMM1GI3k49kpOHhSZa8EAfHIkEVJ4Vsm8ZCwI9pJhww8 +CBE6iaPViTxHqimOMoIVDkoOrWDiCKSpciV2vVYEVOIri0/ZkW2PXGMBjaI2TiRGQJXtwAUO4bYK +4waIiKbCqNy3IHWDLt8VGwlFu1LTq3VjbtFaPOAtHbbnltz5A3pjJqtXc8dActJCe1rSVhGMBecU +SwSbXEN/G4rirHJrryW9B9qtv0iD+2tm4CE41cpzVEV086QVhxuJvpKm63FvIww3whhZvkDze3GY +6JHhpFY5hnco1RnAxaADRRwodyF6RVwsoFHs8AgdqjzHrXleA3UYDaNUShAzZXOruu6V0W3Dmmuw +qLFSBdRPlavBheWP/c+3P/CXH5mmHJ69FgYtPe9IClj1pGQrc+WSFgCMW4lOMy9C4/bF4IsTGsns +woTOoT5F7wCJRhaSQ0lDyI9CDM9ZJ6y6Q1J9mLFWPr4CpPjLwCdUPxRiYoXXKDavQnTp71essFcM +jZOig5OTkWNWgvyyzT55/kPj8IT3YZ9OhsaJ0nrweSZ544WKsxrbK4PGrTJgiY0JNCIQrzBZSlCY +vhurVhqFRv7MjJMoBipIGaCL+pN6FWXAuxhlw5k15MzKMKuCTv47cNakUTD9B/5XYLScmWyuDo4E +W+HaUTkBj02pBIzgiZCpzcp0NPaCljBEo9CWhqV4Sj9CnaDYGfC7BictoUKFksV/ciAVPiVBRgot +4A6idngAjTwPxEjJQRWnX+imrLZgVUahvMrPQs2N31mdCHdPqjsgs+15puvYnmO6Xtaom1MzbmMa +1FNrcdFaWnQXF9FqWKvXoK4W0kusuFYNpFU2G8WekXmBWBs9XHSB8jN/UwufSIJ3RN3jZtqHGrMe +Jkk3hEp3vO4b60m+mqerWr6qJRtZtxX2m3HQB/ax0otRTSibepXUdGKw36MQ48WGptAdD6g+SfKg +St7gBefX0Z2y59arbq2MdowQ5QH9was3zFodwq1TS4v20pwzP2vbaL9RNlrdT/3Zez75R386o0FU +3N6Io5ZubKJ1XtHzUoJn4jmOQCNvAxTX946XDrffDiCX8/sL2GucmGucaIxfzkA9u9sqih2PIfJm +nNZSt8Qoj2pPhbkglVXDh79YGYogfwF+jGmo3IlyIvmwF3/iByeRnp/dixzsXSG6cmSvGKGvxamO +Fw/u3CMSuMIZLNZ09TiqFP9gzSTfaZKDO9E1HxCHt9MLhkw7aa+wFwoPqtL3ioEM3crR54L7Hc4Q +uS+CXoOxkIUP80m13Zam0yIPONig+I3URKVmB+le6pmx7BIbqwKhYtgG9tmAT6SujqFNeUestsEJ +EN0V6BXavERmpS3By5BKCn6x4l6cL3YyR2gUlBYb+IL2EjaLhfEn1h4i1YBbCZ8myYI4RuYMOXl4 +HoT9AsW3aNXDS5N7LaQ4GRHJcahQquoHLG/z4ZSGoGC4iGQvW7EJnVfV+FsZ3Dl25qV5SwwDPKOE +GUrJlJ5hyBRyyTrO1oZ/iL6GrotGrAhHOhCyQQ9FHZ5iva7PTplz89Wlg97CYX1mLkPZYtlNgbem +hVJAC1w9OJ4uDkH1OkaLpFexmffzCIUZD8atz8T+wyV3Rc9CsPbQjTgD46aVojYiW4nytThbD5Ff +7KxmUUsLESztRr046+MK3LLqfIXGw7kJ8yG03UwvJXES95u537J7PSsmFRd8nADNG61yudYoIyFK +lo3rIUEKy6Mfx1VGgKedA0szN9/kzC2YDbiMeS2JH3rX+z7+3981DXFWTd/U802dzipuOmyLkWex +KPmVd4rQxT7s0cnLwzZ0GPoMkz/5vNxizGv8IoPGoVVaxH2UzIZEbgpEHFrEyh5XIZ8iWDqImdJ+ +E3HUATTK6lNsdvXc0WsyK5QjMuq/TgSPa3LcXbBvAqqx2+dOtBkL3IlvvvfpTby6YfSY6DI43hAX +eYsnBVSHgi17nMn+obEwBbBTMKDFhqEcT9HMUowzhVqycKpbyYIDmWXIjQ1zf4X0knK0ip2CY824 +pPgYioWPttwkfxZhZwbL4PEhUSC8ar6L9BU3kKYWbEJEfTVXcnAWPSv6W3CubKbdhSxKt7UPXU6S +0rBqpyE8IJwcWswX8p5UvAZkSrKyOLFhSnkLGnk57DC1BzTi40XbdEqnkmUqaCqN8KS6A52TUNFP +aGR+EfV7pJ07aWZnoI6C6QnqK3ARtRhos+PaLsr2XZTtW6zZr+Y1VDugDGPeOLCkzyy49Tm7MZVX +SqnnxvD8nDJxEdwc9LrIfICQZdQMzeUlGW0tOZP2Hw47D2XBE7Z2zsp9I3Eg8q0Fmu7n6WaM2Gm6 +nqQrUbYWJ2th3Az8Thb6GtpooCtBmEPL1MqdMkK6wqONTCctT/tTBzOriuqxtHsh2ziVr62ghJq2 +CStELRQwVp06W8VD1ZW3HJUdYYyRry7NW0tL9ZtvmL39Fn1uBgA3ZzrH//Ij73vr22q9GD1lO3qK +/GIH6WCGxoqgw2AmvwiN+1r8vmihUSzQLa9RfEhpdSoMODFYB9H2geM4dLkUOkosS5YxGrvSvUiF +odQ7ikRwTcytSXdqErOan3+e2GgTx2Pvrm8Yz0G8cdKg7Pl35RAJBI5ttxVhHFnEd92Tqobb8yBb +enUTvcZRaFSkfIEotnUu+o0NoFHAYOuw+DFiMb2gyeBahlo/xXYsalftpbkNkA+ROgnXyjsorsAX +yw24xoKSibhJyXXguEAc1EWGClhomCUhpEn4lkwh5AjjGC0sUTgA6TL8o4Wo9GXBOYVOKXpBFWE4 +jtBqIXKqNJZg+QAaB1cxvAM0TZjo2ILG4iEd8xoF8OggowKSp6o6CErslMJvRAg6r3LNUrCPNhgu +2mXkqL5i216AYrnsERdLZbNctqh6WtEaVWN21pib85aWKvMLoJ6a5RpK+w2nlNsO9FFzMHMgKi5p +FxwX2Ra4zzZ01/K+Fp7Ow4f63U9l0aOmtmFBoBR4mVhaO8x6cQ6J8FYSr0bZeoKveCVO1qN4M4o6 +YddPqHETooudmThOats58py2i9ZSutmpzerTd6TTd2mlWVv39eZT0fnPhWefzLtdDCOKMF3LrTqV +kubg04wwEBR7gRYj9eiVrjvg3nB05qV3Tt1yY+xaB2tTJz9871/+198Lz6/WDK+dJ+tGuklulPSA +UxriW69rA40vdALqxPXlixkaxVoq0BHPaaGdLQmTAf9csj3qURUW8/b/Bg1xucEwvzjiNY5HKiaO +9hVuMDGOvU29j7B/1cymKzvXbZUJO3dCRBgs/rsgD52dKzvytk9tcWhGvcYhRMlSvtN9HcEkAt8E +ss9QynX/0MicWKEgI1QWEL5UiHTkpQKfbAQhL/ZpUhYaJ7EKbxRjRHdRUpgybILlglJsFqGzUQRg +Dwu+DQioA/8MAIcH+ACWIFrKgl20x0UxHfglWYI2SOiUm6RYgVF3jvegc03NINqEQwU9WWcFGhFN +NWyHAvvsCg+gFPGZkQvZ1WucCI24Opw6gZABVal8RPaNFBuhvCqblu1fkPpEkQaIP4ZrGkAbXJGL +yyu5EIsBJwUKaVa1AafQmGqY01Pu/IKFCsXZGXNqGlm6HJYAwsRwM+EZ6yUMDEoTER4GLQE5Sk1v +yG0K8uRUGnwu7n0mD560jSbktOi9oatwP4k7cWkjz1tRthFma1G8GmbrabKZ9TfCCEjZSUMfQdQ8 +RKU0KH+gxCOC6sIMksoUIzC93oEbndmvNN1X5+V51+jq3cfSMx/xTz+QdjfhA7uO5cFhBZEmTX3y +neCdB2HS7SN0hTL/xl231F9y68LL764eXKzYzsXPPnzP7/zRysNPHnSq7RT1lVGL2pZ81GypYxtv +q/EiNO5rhfmihUZJ/Be4WCyIChSFPMcIkcrZDJKNA76AipRK4S2jXgXLhtGq4ZI9kmt8bqBx4p2U +pogF2U9d0BcIGifgjdggW8/pGPLIvSpC2pPQcWJAleu3CjLv5jXiLBmk23NYd352N5gfoNfIUXbN +NQ4huVCgF+0YYiTj9Exl8drF0VMCS/hdFfbRaxRc5IwUZCROsOYcDpN0iMC67sIZQY7NsiCHAgfQ +gupnCX8BBDqIK5KlCdBDbXkCzyMLwRrNAgiUhTFGCUQQ1UgmYLkS0ZUFvTwuQ62SUJRGDyoNKeLb +dNkGRYgQiQIuckeqppBZwuJ1ZQFVaRKA+K9pwmsUWjnLOQARJMDSa6QajknNccFF00IOjo4inCzU +B5adShnap3qtYVZrzvSsOzdnzcxaU9AanTEBk6VK7nga1JRByUHHexbaY8xFSVweeJPeNYoLgzQ5 +G/ceyaJHtPhJK1vGcJtaRUsrWmRoYMt021oz1C9q+UaSoDCD0BilzSzspL22MG5iGBbANCSMUeAB +pW8gOTw4VrswvYw2stX+odut+a+x3C/TnFlL6xrhU9q5D/fPPph1NrwMDrBhJ/0k9MM0xB2Qs4PZ +EsUpTIGjS4tf9rLFL3nJwu23AEGT0xf+7D//9vn7H59x3FYQts18HUFvXg0pqZLZHITWB3dmZHF4 +Mdd4yVXgixYaUYA8XEAH8CfkrCFnYfDT1l8lolqY6ireVaQfabC/IKBxeC0FY2giqF7jDSZDIxsJ +DIBEjfV2ENpHRHUiNDL6vRs0Dik/XyhopLGlXDwBQuUPSkBSiiEY2xD7oWgQIZ6Yh/5/QjhxEAVF +pNDxPI+BOfxDRwnyJ+CjKC4TBWgYi4xDPYkRy0uDKE/gvERRHwVKGfTIYvSOj1mynkIPGF4SCTW8 +JQi+EadZziuFPpIzoPha8Tyw8JyMbllviaHYwERvBxStI/YoRRhDzo98ZPi8DPvoKJNt71zjABrZ +JYP8GsW3kaZRgC9AJn5HdhGQD8yH/2uXMA5IEHI83GrFgZZNueLNLFi1KW9+wVtYQJGiVoa4N9KI +VR22g1NCQJKtk1HxqFekYVXI3scR4qglLQvz+DE//nSafMLKjqPk0EDCMUV+E/QxsGZ8o9c3NhLt +Qqqt6dmqiRQjoDFejxLkF7saWkp10HwxgfuJIGru61AbxxWUAPBINaJ2H5ClpTY0R/Jqd/4m/eBX +u/Uvt4xpI0WbqJP2hY+H5x7NO5vlTKRXepvtzmYb40u5BLJwxXmserXbrz/42i9Zeslds4eOpu3w +L371N09+5FPQqIv1fF1LlsHTQRkkVA7QkAqH5KRSgsFbRsuL0LifZe+FAY07fSC1um45cipGN5Km +2ZYFxJM7bBkxeNgn+Q2qnHHP17XJNU4Kfha5pv3cz2dzmy3qihA+Bi9xv3cDuZHFcWtraUJ3VS8e +bzePbxBzVBXTWExkQogxpLLIEjWQd1X1wtawy6ItdX0ABdWRgwu90luXEB7hbECQKQ4EtuQI9U+l +pkUnYjAYDEpA3J5EEnFTGaUTh4w4Qp0yauCzxAEURPp9UPjC+ozlHyjouQ5ig3AETbMMtRQ4fwBB +F0R++Epgm8Cxk1Sc4GCaBUmCrvBh5vfTPlJbSRYCAuPIj2OmCeG8kEfGNn6pkGNRPyDCFcx0CsuM +seOI0UoZpiJ2q1g8KlYvnJciY0V5cAFyYCqiqRxX+HhyXeLobmXGlds7gEZCKmVN1U0RmpNonhJE +lRnDwzEVymIM7BFXB3eQxRmERmZJ0XWJwjo6HD92sPdKXrlRsVzXKKHCr+FMoZfvjFmvmYvz+LnU +mNJLpRylGl4Z1FDLq0KBVHpTIAqrQtTMMkqJYqinF7PwwaR/rxE/qadNI0cbdHBjpdovTDTAHb53 +Em0z0dezbDWGuk22FqJ+MdiIIAiXQA0V2m9BAkSMEHAGI6kQiKUOF55fTj6xhljcooeGF04v2Ysv +Medf6ZSWKr1+un4qbR232mes3gYmoQVTZvPiJo7LxkG2m8BUSMEWMkqH52dffdfiV335wrFjKI18 +36/81hMf+HhJs7pmBlyEUGo/JUdJhrQwtsQCG31gRh7esQd5r6dyYk5HJsmkaM9VPffP9YdfqNA4 +Ok6jt23ocuyExm2uxkTnRK2nzwU0jh9j50H3Q8N5DibOZUHjKICNItnVQ+OlrnR4v8THFx6s9F5Q +S3sxqkM+QrGWc2eSxGNAQYEf1kPKkqim84RG9Y968mUzxcZilg/rvOQAxf8THGARhKz4KqvEsgt+ +o+AEF3h2SCCREvkxFBcQ+vA/dKWhzGJCmQXo6DgUbgEEIDcoZfBMiSHjR0ePHhpyhFglk7Cf9YMs +QNPbMIRSdRQmcAnDkCqcCI+i4iPGRsKXQTWMyDpR/lsyBNIsRjTeGDuVkWHRvjT9HYZJitqPYr3D +25Ln4/Wj9l85+kNohPsoQ7MdGlU8e3D3C2iU1KjaEwnjGEWWaqj7gJdI2sBHZHgTA8YoKuSyGTtF +fLjkuPD7MHIwHTBibrXs1etmpWyBetqYcadnvMZ0XquGjZJTbZQqM5ZTM2zVnokxWYX5rKx30dTQ +A07q+UYePZUEj/U6D2rZGU1bdgzfhBOue8BoaIbD+yMuduOkGWobSb6e6ut5BFDcAN0mT5BTbEMd +HMRRDaWc4CUBF6m0Tt8aCMVoNKUSVLgW9BuEr2mJJLkV1Wat6UNW7bBpzzoBBHG6VnjRCTbMzgYE +5MIoCP2Wb/PJp8QIhj8y9NqxA8de92okGudvva1mee9/6x/e/+4PeAH+boJB2wYuouYHXZ2lcvtF +aLzKJfH5CI07LZRdvEYJSV1yleTSNXlkrt7ImXyQibyUkRhv4W7sgOTnCzQOyyou4TXy/NVCuGNk +hwOF4NvkGzO+xc7w6bb5sDO9SnZKgWhFCF0B5+CD9FXYzJNOkmr5paBRAg/4LtWHQBNZuomLVNqV +a1AM58KTlJaDooZGX4YXJrIudEuoSAbvBoEw9NFmCQGqIrBGe9UKkA/+H1Jk8BQtA1kldr8FhwS4 +iE2RFZTWE0oXBbkpwGGQR2hjREVqJLpyP4h7vTQMgYvwDhO0S5MaSay64PXDR2SMNCO7R+kfIoRJ +XJRL57rMSxbh0oFAEkOj4gbymgd5dImPqpBvcUNVfaE0FZGYKqERR6GXzIJHGUy5b1uzlYeiwr8M +HMeP8ViOsdDg2AZDsqrqfVGVg8eDvJ8IqQIRMYok1GaIhkIAHKHTkgdhGMfCT1V0tC+79YY7NWXV +61Z92mnMmo1ps1yFp5hVKhA7de0qTBGwc5CXhA4OPLEUWIVqQtH/yKOm3j+tIaEYPJhGJ2w7oGdP +Owb8HvZIRNfh3E/yXgqujdbKUKeYb2Q5yvnXM0RQQ3Bt2nnUB30JZfs6KEvMv0APAN/BcOJVsW0t +nn6YHEzgCnuXajrSpxLBbDQ8rtdKXsXW0faYss5e3DNby73mWqcf9mHioCgHhhP4yaAJgRXVr3sH +v/a1B1//FXM33zxn1x743x/4y7f+vtXr6ra7kSa9FC5jlhSjK4aP3A1W59CqG32irshr3LaPy32G +X4Dbv1Chce+hxrSgpfY8eO0n+VbQhQZnO7KCPw8uYOQUlFCeIN/uAVWVN9z7pK8A5re7+4Wft/04 +o5sxBTawOXYCp3yS3QBlxeb1iGQnvSZBTOKm6kBG4BuAoeK8sCEeX+LwgPOPH+H3AePABGWAlEKd +xDyiIzrmmQbSgl7JgVKJ6+rIMsEpxNYkkCBWqppbyDkgrcUa/FSL0Qa+j4K4NPBBc0TWsNvaBARm +vT7kUhAsRZk9HESRoSDMZTEyW2aqWbg38FpE/V5j8RsWRXHMlEibKnxkuFiWSZQlCv2nuFn4E52k +EX7ZcDAHfiROsrh1SuoPmMbiDdXanPLDxcd3hUaxMOijFS4jvGgMMVxNMRylKRWL9QmJ0GVRMqdw +nmlDsFJRYskuXwiNVlwdiILwaaPuTk+7U7MWpGGqdbPa0CF5CnKN6VTsOv0mFwFZxJ3B5oEDjoQf +VMdxV8E73Ux6n8n8z2XJM0a+bNIfA4vXJjICo8F66md6gGqVKOvEyCbmGymkULONBF/pZpK3tLiV +hL3YR50nEJSDrsNRB4axYoZRWOWzMQGL+YQ5xCwyPUjmmal5bqGXQQrURkAYl5V7aKMFi8AJutHq +2dV+y6c4DdtSEqlDg7SpUNcXX/2y2970bVMvuWu6NvPU+z/6nl/+r8bKasmxljPUaZA4BJ0d3BQa +HPQai6CY0nl4ERqvYCV9nkLjtvV1p9dII36HQzbqkEwMjk8MqO5nNK8+16hs89HXzovdz5k8B9tM +DKgq74Pr5vjZjF/jZE974rXsOmJDChKXfrRrLWCnCKGqBtHyJl0+aoTwX3YykqIDBsCkc3zRLYJJ +MCzQiMPBeWErB/FrJMZHp5DrN6VTmAYjBQSAR5lOBP7YbwmLuiP9XWw4i1jdXeg8Ew4YXQVbkcib +Q1w0wTIc6LmfxUEaoNF7V2e+EE3fe1GvG3bbMZJXoG5A7RfeWUgJT/F0QZmBrBg9SmAedhNx3aWe +dsGggUvC7n4SV0GEDbvgoqmQkt8GxbpiFGzdDQG8ItUoPp4KARR4KeUMuwVUxXGmUy0uoHIsi9fQ +a1TZ2gJSi3gz6aZKcE4GVSwFwBiIqUinEgwtl8BBzhEECOxSxSlX7HLFrFed6QZ6SNny5dTqdqVq +OGXdcXUbXFCEqtGQPcXga15Ftz0AOUbbNSBgeir3j+f+E0nvGbJozJZlo+7e0/Iq6ggjUGzSwIAY +nI+Gw6bRTo1ulDeTFEHUTcrchOuoykD5REp2DTioYDjBC+Rkp/ikQCRC1yJqTlCE28/AKV1Jcn7g +qErXHg44Lg+N8jC7NLsKcdQyJh4sGZTQdDa6rZUNPdRYYgnpARCLdbB/k7Ukq9129JVv/mvXv/Y1 +ler0qU9+/i9+6debTz+NT69lqBlJI0M6rKuIdjHvB3fuWkHjPp7a5+3CNXE92XWD5yM0KsLePtFC +ZuJ2XsbYAzrY0XbnY4Szc6mxm3izrwk0bhP+3ul27WNaXtndv7xP7Q8at2oz1E3c4UherTt/KWOi +CHIK/CWg5gkmFt/xgwRCxTHhDyg1ozvImnHGPqX5tdSSS8trbocgJ2RhsNSiNBCAxxo4+BbAxSJN +CM8GXgyTilzLqaImOS1ETYGI6rNFCwpgKDbD6gelbKCcie+JH/udCFgIRxDcUfgg3U7Sbef9ftzv +ZQFYpPBImCak24GFD4nDiGst4rOojUP+KwTdVDqHogcuOlNQfg1xEraMQQQN/osU4QrYx9gXmdbs +BjVwASXnRuNggGHEUOYLVVJ2ON+G0MgKT4FGvIa5xkTRcPaGRlGxwYDTMEFSUWLUkoBlNb8YG/TB +Ma6ImuJ2UN8NfrZQT+FUwVFESQY0tSkZ2pi2GzVJIlbtKgOnqNOwAJZQ7gFpFVFTAgpvFriasGRK +LM4AO3c59Z8K/cfS6AldX7atlmFEeV6Dp2YacPMseGR5gOaKiR4Ymm+lvh76Xb0Tm5spvEO6iZtZ +hoBqN427UYTwKeUANPKbMN6iyAO0Qz4XoVThGNH1ZDkLHEMjAlimuoNcIFETDqQkbBEdZvdIACBA +v+Ri+14HYnJBc7OtBbGHWkTcTrTGQw8PcKtwYkcXXvqW7zz81a9xytPBk2fe/4v/uX3/AyXLugh/ +0bY70gWZLjmD5Oppk6yxmCp8Zrc/gVcUUN3HGjRxtby85eYLvfUYNP6K/lz0a9yWbJIbJ0/lPkZ/ +1+HaNXu1M6C6Y/dDQbJt4cHhQcTC5qv46EgZefER6eR6Va8J8cer2vfWh5UBoV6j0DJ2dD5F3HBw +vXJnmIpT7+CSd8Dc+OlxbyOjfIlLG47YoKZKbvwg5kPRE8VsZMBvMCtkUR6GhQr3ZAh+RQh6kARk +8HAo0InFlyE17guOnZTekRED4GMzJLp+kl4iOvIXEj/YvQgQWHiHNtZsB4lAAp4iyDAvaCEiiOaf +YHeQY8MWvopmY6GQIpM2vVRuQbiTHYBJAkVGKshj34x9eIdozZe2O3G3m7f6WbcPPS9waJBeAgTi +OkXPDRI2QpMhvZSS5AiBJliAxVNBhRsACvCniijgMqr4GZnMQi/Cus21Um46+bpyYyRxqG6YyNQy +ESZrpwRgEHiVzbdumnpfAqacGEVnDLB45GbgWHBjGUJUVsfgcwVvST01QmotWMGqll/i04wj09Jg +4JSdp+SFzhhQPDVLAAxUWHhOueR6FRgfpeqUhXhprZFXas5Uw6mBa4pbA2cLvji9ScOWMCLdRWH3 +ol05akGji2n/ySQ8noUn8ng5T3sYKpGP8zQNtfvI4ZLWBG9LD2I9AIrlWtfU2nAZ87gTZiCjgnfa +Bt0mzKGPGhh9H44iTA60H4ElwssGyUmNDeOlA9qxpBjJnKHALLxBzmS8I5ldVHBg1lGIwdUoxMfU +KebG+sZ6e6OV9hMrZCNoxBvQWzHVk5JmoGmHduTAa//e36q//GVTR492nnzynv/ym+c+8ZkSpHhy +82KUBnRY1Z1VMXN5ZMTwGdxq/KTUI7YMnv0sKl9kOLefS962zRcYGgvHQgVtrhQad73sfeS0hr0X +htC4zaHBWRUyXmpi7YRGIscL4TWExm1h5HFoHEXBAkMvFxr3cRO3QeMQieVpHpDccfgRMZqikZPq +wCqkRuUiimSYvMdHn07SQCAXGwhNla3k4e0JZRSrEnCSnyLsSRcFNlxBdJSdGuDwMVLKYCmr5ABy +1NVGTpDQ6HDlBTRiOWfUlHX1RASWFiivk2UU8mL2TwpikalE1rCrBT66FGb9ftbrZP1u0kVsDoQa +dG8Ikj6KDmHCAZW46IJNQ5oGBGMAfvQxheiPH0QTAC4agqiU8pUsKMIVkufjlMxQeId1V3oNc9zw +J+xIFU2MyN9LzLMYbTJVBzZQkZuQPNjoS93KQqgdpRRitKgN+GAYWgTPliZT8aZiLDFuKkjJ8Clv +CF98coCGDEGrjCL5osBCDDDugs2kImo2gYP4H+CFvhfQsqlYYNOUSuX6lFWuQ9dN80purYGULVxC +p+wYgBUqpla0HHlCp8SoboCqey06m/Q+lYVnw/6ynrcsMyIGGw58OrkEhJrh+VFFDeFTiMAiTadB +ZMZHbQZUwrMMRE8orYGS2g0zsE+RbvTxCRKOaKyw+SPgjN4fop3KJgOnSVrWifYpL5tvYntx/wpa +F2PomI0upMsts4wwewkXglvba3Vam6tx27cj+CUOajZbUR+gj0B/EiWlgwde+pY3ey+5s37kiLbZ +vu93//DBP30Xuug1LW0th2fL20PhoAIai6iAqsYevoQ2tfWU7XO5ehEav5DQOFxGR4M4+/Gi9oOh ++4DGbT7QcPYM47nklw1mkoD3vsuA9jn/nrPNBNjHjI/hOA9/oAe85SDuCo3iklz6VUDbhKsaQqMq +J1fKDKOProqPFzlCOKtAhMIXUbkpFZKjqKbyE8UvkfAc+aOCgsA10iBVPlDxOoCLrAikTiYRjuuk +BPIE8KBXAnkUhPMkdgr/xdFsF4qX3IL8SMbrCq9HPEKpkcVsIJFGoEElDmMTfYdCPwf4obgCP/TY +lCjp+BkauHfhLwZ6EiKtRFBDwI0l4GYKUgaxES9Ky7AFN1wGvEOSC9sdSiUinANGL+Hk0M8b6BcK +rYZ+CV2TQUUmhg+YyiEV72FovImoSvHaCxqHiQZx3gVeedNliIVMI+lE+EMxkp8iKqTcUw6FsjxU +k1Sh7jLMCXaSMHs5cuCbCOMG94QaPsI9tTwETz2vzBCqUXLwZVUhCI6SDEBj2S6XbReMUxS9UC7c +wQ+Gyx6McNRtDQ6YkXX1cCXvnk1aT6fts1m46lhdAzF1qeDBCYi4K0eCoV3xFzPcKyiDx+jUmhuo +BPQzQiCqAltp0sbPWdyLkz4ITikyiigCJdeXvFMuB/SlZT6C6KWgkfaKYhkJ6WaAl1B5l2JZzknO +Pcii664OWhZqTjAWICJ3NruttfUk6DtB6tKptEIcJU9rtruK1o6L06/4rr9+7I3f2K94aM7xubf9 +yUNve4fp+4GVn8qzroR01JmoZ3IQLS/ulzxa6nYrVsbeq+b2v74IjV8AaByzaOSOXC408m5PQpV9 +QGzxUKvZo1bp8R3jeRpXH9zloBNPZNKJPrd/30JENfdHEq5MVe0NjUpB7NIvwbSxAVExz9HXjjeK +Pw6io8wKyspaYCN+lP6xgoXwNeiLCF9G/ECVQeQaTP00LrqiHKI7HnGTZRTUAxNJMTiF7OMnwEbn +j0FU1hRiA8dG6RyZMoyX0l8EHKKOAiY8o6Zkk9LHVHIvUvkuMARcUFWGcZShh1+AJBUKv3tar537 +vbTXS/oosWgnQS+Fd4hIaYwYKOkyVJzhuEMKDbRGAxwKqVQEOuI7+88iZEoGKcviEERVqkzSCpE8 +VikAEMaFFGIIy1T60NJ9lHtKNRtBAlk7BwEZJKJGvMKx4IHyJkcS9qN/pU8pt0j0heQGk2laKKxI +xZ5AI2+LlHCqpoa8N7R6aF2QqyQJRdJyRbFAJA4QpoYwjVUuQQ5cabxZEAR3SybaE6JJL96EOLhX +RukLrBa4lcwkwtkHG9eIUc9gQufVX0+756LOySw4lYXn9GTD0QPsOzOnpAwSygGIVAP6II6KMYV7 +hTIIC8wnE2+EQEdEU8GCyjM/gxp40kUENUt7WeKnkGeDmaIEICSpQINEgkg0uUUGGNBYLBeiFC8F +LSTGS82GCClwokjvdPCDYAiAJ4uTcspgFrmIzTab7d5aO2lD6zRGCAJTEztEqHym1uj3+6t6fsOb +vumOt7w5rM42LPPhP/nTe3/nD62NJmyFU2GvTTTmAxDQZVVVk8p2V5GXrQdOQgUj4a5LPrxXywB4 +bpex5+JoX0hoLKKpg/qq4eVuX193jMO2238txmkb80dNFIamxne+zdHEH18wU2obpm2LrApKcvmU +VU5dlFzsZeYaJwZUxd9Sr5FYNLsWSikFXVcVmBPWh2L8K/STyCWXXOasZBFWgTkhdQi1QzAPnBf8 +54HkIN4J0lFoA1QQZEAK5HvAPziCZJCCvqH+RAXOkoReKVFKb5I0EfGFxBFlEA2rH6gVaYg2e6BH +5GlkIhUEYgYg0O+iL1/a7UTNlh6CaxpoYYi6beQKGd3CJAJg49LyHFKlrAWES5hbcEdApUEtGuVo +6JqontmsiCBxBqsq/gq8JDQCWMF35A9KVk5xLuh8krTKRVEyXkVMmjbOwLkU4qmU829ZJaLTOrgN +sqmAvty8YbiFOyuEEyToILWRKmqMawEoUm9O6LZqhHCgIpUoBooYLhpGkm43nXMEUNHlA7FTBqUB +iqi5N+Epliug0iBlCKE3wy25lbrjVtBxmHUbrouPwZsCxiGHa5kJRGqcrGWGK7F/Lm6fjlv4usCe +Gyj0wK2UG52DvYJxZXqXRGRODg4TWjXlJhOAGWozTOTo+maKcgek9QCEMGwAjRAN99MsAv2X0WpO +PpamwuXlELJOn5hYKAQxSjmwpJXTSDlZ3D/eZxlGPDsAaYFjDbiOwlZ4vIgYey6GsdVsb65tpu0A +ZRwcWEjuIZCe5qAO4RZfDIMjb3jdl/7g96VHDiL3eu5DH33/L/+X8PwFNOI42e93shyC5ZC5g9XU +J7OrQGiFjrz1gxtdPIxiOe3Dmxhb6l70Gr8A0Lht9RQ7V0yewa3h2ryn04UUoScAAP/0SURBVHeN +oXErTKrMrcLoEqaCgsYCEbfIBlv0HDXtXgCv0REdjTar2yGL4xcAGunvkSUjtRWy6rLMQPwO6TRN +tCSPRuomBMwUj0ZWaBFbKygY2IbFEliFrYwkUjh8dE1UHotuHxdPhElZ3kZpaTRkEgIpA6rsRkTP +kl0MKWTNA9mgbEJORoogWC2IlTUCr6Kvx708aKdBL4v6RhceYT/qIonYM1D1HYPRDzuerBgsUxRo +w8qIhRVAiFAbOKUpOBNIb8FBpBRoypJEEHXY+hAoQ2FTZq0Ap1Kbz3ILBF0BfvQamYAUaFQ+Iu8V +Caqq3bEiX6iyRfGqxREc5hrZwnsMGqVEY8v6UV6jKjvc8jmKiaHWWQ63CmtLBBn8SV1Dzw4q1jCF +qAovxIChvcGiTXG4BReBieiGQWi07QoUD9Dj3jYAFQiWwlksVVx8t9ErkbX5ep1/xf2Dw0keFO6b +EenJphms5u2LcXslx1dvNY6bht53nQThUsjK6rqLSnuUciLgjDGBfUQfHNZEindh0sDDZL0L49Rh +ghye2dc0Xwc0Zuin2EMf4TTBxaBgFFreqM5nd0gKl4rNRfovnw6xPVChQY9Z2QiDhYz5X6E8U6BA +Ji//aKAAFUWSGDW03YBMQQm9iNn0xLD8Vm/t4lq/C3BGnUZqwypCcaVlxmlesZyzfqf0itu//B/9 +wNRdrwQX6+xHP/jpt/5B5/Hj4Oyc07Jl1PDI3QhNIDhLjwphQ56SWkZHT02tXi9C45Wsz88CNG5P +Ao8gnvw4lgUsbufYqe/Da7wG7JctO2oLNBQJs3hJHGIby2bL1B6A6PMCF7fYp6OxlHErcKTARTZX +4VRuL58RaBzAvNqLulgVLeNmyifZdZbJmsm/DY1WMV1VloqSLEMDAtG1rREWdiRbKhRNppnPQiZQ +UobiCA6dEixRDIUiSMaEoqR+mAcUH1HgjewYFlcoagxREIuQkEv5V3BtUGjIEgHhoHID5KtIj5Tv +grNIQ8oFimOGlQ2rqmQQYyzNIUERfWmBiH4XPmLa7TGV6IP0EYBZiq4IJERInFS6OFEBBqOJZBbd +RqqxsassyxORSmSzQ5a+iXgbyB1CdZFKRE44RkoVg5TNs8WJBCySiEh3c6ttKD6unPpBkklJACg7 +jv/K/Sii5oW+gVh7MqWVY6NuruCourPFfSrgUO7O8H9UoSuhAwlfo4APSzpGT6hMqnKC5flC1KXb +zUwiUAulL6zytOkzuQ5ygyhUBPtUR896BEsrVYgh6PQiKYUAVM28DD1+PS1zkgD+txZ0uhvn8v56 +3rtg9NesCASUmN2AGSH3gNXQi6UWLIaJpfcEJniqoAdZqWGRJhpFMXxA08QbASrzyStFHhF6blk/ +D/0k8bUYGgBATeE3CS7S0VSLg0QrmGdQNCIVmyRCSjyBE6XI9fF+u2UbbYfTUmyAPIvMNPg9mC8h +oNGJM8SJq3CLgcB+L2iuNXvtHvxEZEtxi5GgxpFAtHVTezUK/WMHX/ZD373wui/HBOx9/om//Plf +6Jw8BUht59p5sJiB+jKhQmZTkc1EJah4s4pJwBsvLv6Yvb4P83180d75jO/pquy6JLzg33x2oXF0 +QIfO4kSCzH6gcVtOa5d7OelmDzDhkreQ+ZxtpL3n6+0ejtiYO7gdGsfYtiOJ1d0inLtd6c4YizyP +BRrTUh74HKpwgL8JlUO4e8VmwhaVp5foIMEg4p+UEorWDBcmBubEV1TNzUVXk54IVlhwA6HYSXAT +pwIFheTKKOlRdv9RGUe4ikwk8k1o0DCCKoFT+jFMPeKoIISgwZEI39Cu1jMkHzUQZEArhbuBiCcK +qdMw7zODqCODCOqj30PdIZRLkTvM+mEWhqjVFu9NeKBEMup34wepsSdSSXZQoqDCMmX7X9JqaDIo +cRT8QC9ASYjxM9IESgKc8is/yLArY63EQinVUBCIAgQsrEKYVM4Ct1b/yc+jL+ljIYeVPcrWxU0Q +RqW6R+oTYuXwfqmQsnBq+KI2rGQNJaDLYglKjdILkoiqcEzxRa4TkAuQCJlToiNEX4CCLhKKdgkd +hgEQ7J0Mz12Suy7uJnibsF4QUMSYO1po+C2tvWxsns1aK1mvmacBZQ+y0LKgdcNEMIwpsUNYx8IB +QYlEwupC9MZgehbx5zg2Y1aAJlYSORHDDoDGboLUHCKoUTNL+hRjRxYS7UkQkcSAMvMsU1k6RIro +H+tVikmLO6mCxmrecpjER0OCWDxEjXJ/U7l9UDOOJs4CMot2HljJhr1xLgr6aHWFkHEVDuv6Gug1 +ftBDKSX8RTJzGFooJQzk9yPf1zpzM6/6h98794bXQh+n9/iJd//8LwePPm155rqWr0d5l3EICV0r +H1F6n46wBeS8xIQd591seQGXWsB2PtrP16XuuTuvZwsat1kZo0HU5wk0TsyKcZmYaCxdA/f1Gtzs +/UHj+MUMMiX7P/ylnh+1ZIxAI6z5AV5Kpmu4AX7BwiZHphUu6jNELnZkkOQUobGoEeSaKzxS1uML +FNIR4TsI1gHzhCDDhRIRVPwCsW44JUqnhluiwS3dRqApdsjYqYT8JPBHl47ehQRtgYdwEEW2G8Vw +gZYE9O/Q1CnqmlEbcJj1UX3ha30/CwN2+EXJIFmOJJQiSKsKBFWAkslCcRBF0ZvZQqanBBFlEef4 +kWvDjcG4YSAUDgl8L/lsAY1Cd1SKfKpsG3HUSCKncBeo6I3PKd6HtISmkyOrdAF46m7uuFPAxS2y +xiDLWyQf5VaowVDMUoWPAo3CdVIePNZjdUcYbpSiFzZMFk4vU7NUv6N1QvqLCIEDCEm0YRSxbINW +A4qN42W8L0pWiKQoKJvawK+sr0cbcWct7axp7TWttZ50N42oY4PxiwkD+GRHJhQUMhKN+CiAV6X6 +OHDUUQAlSrxGetbCnUky4CIVU2uJuWBAYxwfTjtJsp72z6bhBb3bSgOEp6kgr0pP0GcDJ6W0RniF +xEWh3KrxZPWGvGRoONRSKihhWw5L7jYy61BUvlNvfKljH/Uy18r7bnK6dPF+v3cxLwezWVNbOXmh +tb4pSjksgMUP4ujqVajBRRH03sy7r7/zu9508Mu/rDGzuHHv59/7n3599bHHoJizGYbol+wjCIHY +vCQfpK26Uv7bvki9CI37X9D23vLaQ+O2x3LItSke2mLNvMrz38aa2WVvE+2gnZo72/YiEr2TsHEy +eF7lle7r4/uBRrV6br12XNlwIRhus8P5UOG6rdcIBCqeYjEcqqBBMWi4jgzYHMX6IoYvoZSYB1+E +DaGZRCQAstMEMlj8tQjWKWAkNDLwpvBN+jcxgIcoqEAjFlr4iBrjpSzYMFDnJrBK35NiNFxBmAwr +vEQQHwpggTYNaIwGaxD7WohUIrJPIaovMsRO+/AXQ2hp6tAkDQNY9sQw0aQRt4/+hopZCg6Cr0qi +qZQeqlZPCIFSSJOQiTeYUpR3pPSCH2E8jgJju0EjqIfiEbBsA64FaUBsuyfc01Fo5J94XmP3Zcfk +F5a/JC8VdMrdURWI/IO4IlJ3IUldCSaKsyiCCZLlFU1P1rqIvoEMMvVgJWQt2UR2BsYGkPWGjQK+ +Db6IlEz4iuwb/ooMoov6Q4QB4Zr7ZtKzwnbWWc82zqdry0Z708Lgpz5pntKNBAdCw0O4ggRuOt0Q +TUN01MgRLeeACDFIZNgJijAZhKokDOCEEYFG7hzWyje7pQXc9jSG+tDptP1Q0nkU+AsVIc5YxnDZ +FYtkUlX9x4pCTl+ZMpwwKrW4RcdT0KisCPHfc8vVvKXUvjVpfIVReZ2jHTCAYVq/nJ2t9R9jj+Lk +hLXxRHv56Ysk2/AQLCtiwSqIz0gxduN1zfBeeecd/+h7qi+/20hN+5mVD//HXzvx0Y/OVctn8xh9 +ProolKFtQiVWPji84SrBISn5rbsvMQxFgd6Kqe7Da5y0zFybZXvSUZ5Xf3+2oHHok22Dxv1c/D7g +ZmLMdafhvP3I+4BGnPvEiv7nxZzZBzTuGHiJGY2D5US27XY4HZrSstRyrVDhpgE0FtCpglEqa0UH +Tv1C50NWn0KJhtRRfhBIqcgd9BGZF9QBhErLVIVDlevHv9NfwTLKoByLARBQJXAipwiMLOo6hOQj +AtZSdc3VBPk6LKKIUiYm1E2QSmQ2ETQMgGLfCBFBBb80SPAF3zEmtYM1+awoZFdCIcjQMwEICsqI +vyihLUZh+a64ieJLUkNzwJZhJkuKAYQeKrgIgg0oNlS8GXqNjKaye5SECrl7ICsbNNHZUGlFFtBw +TZZWGaK0ItsqtBsEALf1GR1ECIYbiIOukLC4HcwzCxQon46gSJ0giWNLUpbQ6NA7RFkLy12onwcf +EX0TwSZlnFQcxBJaa7HHCJx13B2U0MgN8VjbiIhwWsKYh928t551ltPmStxajbubGhSC4hjy3rR2 +vDLk1EQRnXoHKGJBZlGVrHDcMWsYS5VAvqpQFIYSAqq0WKQtJd6K8sCuadVjRuNup3qHbS+iASKa +UyTpGbP5yXT1Y1H3vJb08Ra6Q/JCOY4MnnJSsjyIQqjASQx8MaBgQ6ksq4Di8CmQhDDKF928dEQv +f4k2/Yaq/mojq/UhDmAGZaM9lZ9wWvd21j7e3ny0h8oQRDxoEyEOjklLxM/bQdjMzCOvf90d3/0W +4+6be1FUXW3d88u/8dj73j9fcdaB5okGY4HkH+Aia3xVQpsnIsGKbdB4hQFVJRaxx2vygruflf0F +tc01hkZZbscN2MFUGr47EUwm3gZJjEzYzUSvcR8BVdUde49Jg3PYR4r72Z8Q+4DGoeU4QMQtaFTw +L9eyj0EZvZphxcUWNEqnYrWkFFgoprf6mbE5B8ExWZQlvzh4l+w+xT6VeB2saaXTBrsa/AQ4EIzg +SfhUMpHiDlJvswixqlSiWm7oeiJiNXCMWAPBBvHk+kDAhLFTcF8yaGWi+gIQCSBE3gncxBicf3Jt +zChC7ycKylBohniIc4cPx2Cm6mLBkj26LPQ0VGpH2gIz4ClxUQV+Et4TNCTQicOn/DsCNP1LfhAo +SNFTQVzFomFekIM4hEapIheKiWSSuENZKiUlSREE1YZjENVmrmzEY1A/02VW90X+gS2hag/xjoqa +YtykGZTcAhFDoHIN+ST4Qd0XvCmcGsQzJYkLUEQZomqn5SKpiB/whT4j+DDSkBhxUKBAlDET30n6 +er+TdzfT9TNZZyNtbWp+O+11jCTBBySEyftOthFoMmxqiCg3DQMyd1WTEQRNhe7LAn6MvOKisBqU +N5P9sYQhI3wU3O/IndEWXmYvfGnJviVNpwMEWA14kmsN/1PZ+fd1WsfzqG0FAei/PE3aX5ShldlP +55pHloBqAY0IqKocuTxogwWBnCpRAnBT96hVfpU98w2z2kvTqOzTOgurcBzTh8Pz7znb+kQ/PI3T +dunlSqswjDYsm80g7DrOkTe+8c7v+pvlu++EME5pZeXe3/idJ97zAXQN2UyTpussQ1CXsRTMXH5S +WZ/iw+JiRYSBVtnWGnVlAdUXoXHnIn1V0LgTNLZB4+hKu39o5AKwJ5zsBxoV3WCPV8F1HsDCblsq ++YtLv2gFbEHjRMi/1I5GR2kfHvNuuxmM2K4DvuMSZWS2jjSAxj2zj0OHQx2+WDPkXYE98Ti5VksQ +Vd6nbS/ricrrFbWJcOnUakwSjMgxqywgoZEkVAS0hl4jmX7MGKLxLPmoAEL6IHAumTukag2iqAWO +MrJKhoiSKSPmSrBT9ec1pULdQOwU9Qaoh0BfjqRvgn2KlThE+DRir0PUI9KDDE0IWMYoCKfoqvSv +4G4kGipkwMHlY6lCc0Tx58SH4w90ABXLFG+xGJEkG8Kj/LXgoBYfUVxT4XMIJVWCc1jcqR3DxVnq +NPgosGW88pSEHkPPSCY3HVOZfhE5rxJok4VdoHDgP6pJyaSk8hHl5ggCKj1T/C4WiQlU4/v8T+4D +dRKkzzIrX8QiQZqQLUSg0IYvwiG6MYMmioAqcBGsG4QHYzMjMwq7SDMbsAe3u7eZNZdjfLXWMn8j +D9CoF522IgbOeRfRxF5aRUnxpoShpV8zG1VYlEngzeK9k3p6DqqUUuAjwkeSL2qpSyyAbiVeEGgr +5dXF9PBryvNfUdaOxXHJz4S4Y/nT+eeNlT9rrj2o9TeMwAdRFRYBLg/ON/bLo1C+Rjxw3CbJRKqJ +LGFnSRgUuUZOXGyDPiGZ7qXOEcP7Enf6a6bNVzhhLYLVYIQlfT0O71s/95615IE8vWBEoZHZlH/H +FQIb272wOz9/5Fu+6a43f2d67MhGt+2urz/9R2+/7/f/qKRpbT1fjY1VzDEae+ipQm9YpgB9TUUy +Lq5XMYIGLzlBcS23XrsGVMeWK1Qp7blYiuE8YZO9d8C/irN/xcvk5P1f2y0uDxq3uWKXRa7ZC2K2 +wdg+GCKX6d7scvBt0KiQYgyZZK3be7hZtTZ4DafOZd18royDdQx7ot6XjMboTq56Tu5rzuz0swfO +X+FwFCcmgVOFfVwvZMgGEVMVVsX/AEWY02AoSAUii70K2VPR+Bb/ZFAMQFUakbmU2gxUd7MwgOu1 +agiMXeMHiZ9ug0Zh3GBvOIqy6cVpZCsEMk+5XrBpIUXNsByxOBCl+hDwJo8GDMYA0AhODUJ5aG+o +o3VTH63xElXgjVWHET1ZcrkkyzMtK7eKZxX3iCXetOH5rlI3JQwy6SjsUwZdZVklcokyuLRUlP5Q +5F8oL1AmnUI+zgR6RDxg8R1MEQlc8BbgXTmUOjquUtxVRlZ5xWrWcOilPkR5hOxAQS1tTgCRbGM4 +m7AnSussUoBTzlg04p8McdMCoc63h1sChVLIuEDFDUDIhGG5irtDTx91hBTSQ0CVSkOgmxIbWagJ +1XNELRGO7pi9TaO9oa2u6JtrcXMj7DTROgQ3CPcZJ6PUYmlt8LTpNZO4izdlAAUfxbKQ4LOAIEeQ +l0HPUsKJ8i7INRKgpjsulByJMQIaHb12MD761fbSG2rmEVhGIWyj0Aj11EuesFbf0W3eZ/vLJiTd +yY0Wi4NBfJnGSpVXBcppTuAJl98h4acCSKpLMGe1C/BOWHlZyt15zb1Vr76mVn7NdDor3S/7enaq +s/nxtfWPBcaTZt5y0WoziEPXBnnHWO6F9ZtuOPTdb2l82zeDYo16DuPCxSf/8E9OvPO9TpquesaT +WdwOYT2AXys2krzUY7zNSN352O7+tEu1rfyJwz6ywHAGjugV7/7pyWG6HSe2c0eSp+eLw7zba7/X +sq/17Go3ehEaZbYNQHHcaSsyZ3uM8Sjb9gqhUbq5Dw/BB08FvUaO+txA466XOYqO6rkaFikqXBxu +IL8qp4RrilqSZQkWA1t8FvwEL4TeoqQL6QKSZEPnRAr5iZFQPQE0SmRPqZVKswL+y4iqCqiyYIBB +KSlM5P6Uuhxr0CQ6yF5MSBAyHp5lJnBRuDaUOUG0DtCYAhojtHoSCRMwQGM0YRByI6xndpHlYi0o +V4CW3AChktK7EJASZ5K2vACU4qBK0wzpHsztuYDLZvyrSOFI4pABWvVxFRHFGavqfr4jRgcnI1Xi +eFTF4hEnhvgsYK1wWnYin8Iwyh5k+eI33ADxHzk4HHgCJ4Vji27B/Jd8JoywkjZlYT766tJrRBoR +vSeRQWSzEXZANJ0y0ofgiULrFZUToNygVp+SNvAzUblPxZ44TntIzdq9jdLmWtJcjTdWUyYRN3p+ +C6LcvF0wjTgceiTMFcoBARql1kVhJGKmNCBIQlURQ8FC2DBiJshv1P3BtTARy8Gl1SqVLoRPjgnH +mpQZjI3r5NWF5OiXawfeUPJuR6oTeUVExk2t63Uf1M69p9e6X4/W0IcqxWTCSLG+EElNiXEoi0LF +QpgKh20iz6Ny8eVIUgeDQ9GwQGss6KJbZl0zDuW1l5brryxrs4Gmd7Je0DtubHy0ZzxlRRetTh8e +cFyG8RBl5zAbvuylX/FjP1y+42U+6oPSrv/Ek0/993eefO+HwY7u2NoKvqO2RIf8jwnVJWWWfRFA +43CFudpruVrU29fnX4TGS0IjliLpArTf1yWgsTA2L7UX+gcjXuOuO7kCaNxmWu7rGnZc7DZcHO5z +4DKK7TxIKyr2vyqDU16kLMhqdZaGiIKXFC9V0Cj/0YCkrygRPImaIk0jvg6q8pn9ItKpQnJBUIm+ +CrVGPFHR0GGkU0BaUn8ScNJzdIOCE0FoZFsoeofoswcslOoLasxE8h3QyN5KWH0ZgpXaCxxRvBRF +0FfV9wU0Mms4sL7FU1TBUtJthuUWVD+RYnyuaFxlpYqf35WcjGCEmBgqIiqQQTwo1l+OJ2eBCkhw +RRxhRw7Opfiruq2UQlOCLSJljSMKioipQU4T7Q+GrIXWS2+dpobwe8nwFZEauH0eIqJ0BHVEVuEE +AhpFtpSF+kgrIt4t7F/YDVTLy1Hi4vfby3rQRe4wbq5FnVbW2sw322YYmBhqeOFZBPDIKNHAkCmK +DmFu+LAvJG3G/CGFYVEOw0EFZKjkK6lJiuIrUrFEQPwgNe0S7mTckyxNOGaMs0oti8R/ONxyD5C3 +hER5vRocuis/+NVe+eWWNQd9JCY90zN6875w5aPhxil4toRDRhsAjYicQzdHoFHkANW0FtG3gak6 +DFMRLDnnNMiiAk7B0bXLJafqWTNu+YBjzaNwpJnFG1mvF1+0olOG6ZfWV1H5b8OptuP0RC/MvuIV +r/nZn9Juu7292am1O6sPP3z6Pe879ef/x0ILLM86lUTo+WFh6dHRQZKlsSpEejVwIqvYpb1Ghjkv +Y6G7xGIyYaHjdSh//NLXUohD7mu1etY3ehEa94bGiag05vAVS9X2u7bXTgQatz7wBYXG7ec56hEq +g7nAwYFzLdRTBZDyg3hvNLuVqCl/oF0uauCyLb6kRZTKbxXv0FmUZkVSP46qOKWGw19VoFaYq7Ka +S6QVb0j2RCx8kmSEQCErpyr0E7QS9S4ReQYrUjiOqNBA5A6IyDUUiUaqhzFsShdGoImLNNcQYdaQ +hSMO3CCgSrdGOYVy+XgfqFpkAeVNJXHHpV8+KGfG9Vbyj3TdqHrCknmBSfEdhc4qfxX4FRgeQmMR +RKULSN9QvCI1VWRvKswtXqPgoowRRRJ4DgR46SsinF5GTZXuAYmjVLsmlQYeILsGA/kAfLmHGkMU +vYBfWqKirONCBxvGhAW3L4ltjGdvTfM3Er8btNZCFOP7rdxH692+3vONAFwb8nMDivaA8uSAEipo +J4oHGCYR+0FHpxD4xAJNuolCa2L7LYoOyRfdY+WZS/ZREXelJ1fhE5JqzJizRDsRUKWJIY6rTAEO +ClXbNYiz1s144VC++BK7crfhHDKdipc2k95TQe+BKHg03+wYXbBIlUIuqu/B2iLg0TzizFV3l7eF +hkyxoIsjSXtDWr7wg45HdjF0C6aq5ak6NFyzLIjCjSzsQmkAug9GbHZ6aGylpX1tRqum7ehsnEx9 +09fc9eM/ol13XW+Z/T5Wjt+//t57zrz3Q/F6Myg5Z6NkDVEMXJxpBGYWpkmJDKirhUZ5sPeCxh2y +X7sAz6Q80cSlUlLnI1uJVTt+IIl7POugt78DvAiNl4RGpQi192u0uuOKA6rPBjTu7+6PbbXT0Rz1 +GgX/VMCvGBRxDgvqfxFZlRJ+5RyqUKesIcprVJwP8V0K5BOHUuBTkSGl3J+tGVgnoJBUuPTSc1jS +k7KN7JImqniMEpKkmwirnyuroCNOUzoJkffJqBzJfPBHGLyTEB7Kq0HHL6BOVmh5JlUuhNdJfimc +IwEniekRb2XV5rIs2Ub8B/9T8FkIO3RaAYQw98WBlWlFXCTjVCrIJdapAqriOBLKGYOVVKJaM6Tr +mdqycBLomypmk6zMW3rfqvmIOhA6NHEc4fywoALoyAYeMDbo7EnvSUAfiKNkzQD8qBzE90V4nelH +DCfGv+aRvpSldhbaSWCAQdoBU6WZdTdj9pj003YLQMiiTwgjQH2NZBIEodn6AZcPwQXsB54fWlag +oUUEzItyQg7U2VAXgxcKMRCUJteX40zg5GcJnyyjgXKN5B6LoLXCVEFH+tls9KGQSoAKcm7U7VbQ +WHQjlM4XQtjVcscCYOn1ijF1UGsczirzJDn3u2n7fBicy7NlI0wN8KwYDxUGFLxjTlW6iQyfSvpR +3Qxlg8g0EGhkKJoWGs0zaPMYJYjBmk4VDKY87vWiVjPpth2kVE1ontvo0wnF1lA3Krrb2uhvuNXS +N77h5T/2j9OjR/zzy9VucOGTH13//KfPf+xzmxeXO671ZBQBF+EPmylDMJKkBi1XKnCvzmsUs/Z5 +kWtUcEhrVkLWQ1dYrUTbfr2CRewafuRFaHwRGovpdFnQKBuTz6+yYyqaKtDHf1RtAPv7EiCJcoVr +CVMbDA4VUJVif+6HvaOkop/5RJSTy6NTiFXLjgcc10KTDDsusnUCOtSSZMfCAeNGXApW0ks8ju0Q +oIYqoTqRq2HUVDgz9K1ULouQysiaZPX4xAplhgFZrreDTcTJI0gqw1cWcizIhHHSShnJZJsF6tSo +8Kas1dJjXYHlKDQKbUS2lB5X4hioBViyX/KzWi2YYRyszrQWCBYcNJF+VacL4hJDpBSmgSMIn09J +uKH0HnkwViIiTOp4aDaCVCLXdciVAjrpfKO+BDTdzIbSdvMkKg7TLuKiG2gkqEMeNg6GyUC4brRK +lLHBY+J6EdEUGVi2kWQNImyCGF8RZfakWNWKkM2DQYJgKpEPfqG0ZVbyeHhHSlagL1T8Kvrqsj7K +rSskhOjESyknZpskmAUa0W4KXqNK2wqlRCLXItZLKrKNCkDgP9o+ZtP1pOYxrBslRgc9NAPoCmCO +kpnFEDoTnhkbVTM/XVT7iwda8HEUDYcRBdo5wtjlF7qYGXHFrc40XNfJwri5uuGvrVvQDkzSslfK +LK/Hdo6xi88HURZkG25p8bvedODHf6RtOIYf26fPPPWp/5M9/mjw4ONPP/VM1zAvhNkKeDrsdUUG +Ks1yXBimtYQgnv/QWExLOdli6m75FUW4A5NFZK9E6t+ygiiJIgaMh8zV5y80/jJkm/Z8TSyTlwda +PdMS8xoaAiNNgK8BuVQZcVf32okEO/d3KaNm6E6NJoL2afjs57jbzkRoFXu/hsN+qc3U7RjJKAi6 +DE6GP4gepPICFdeGkS0FgcPrlb8KT2HrJbw+cQkVKPHv9PdURFQWctVIQ3w/ODZ0tbA2CxlEcj1C +BlG5RwAMe14oGQDV+Q+rEM9ucGKyZhUwouKoOG/iIs9TxFFYHyc9EuQKuNBysaPLpiJzXHzBu2Gb +RE5oASACk/Ad1BY8BC+I7EAiJWOhEGdh4lDyheIR0lHjhgh/qQwf+hKzPg+AJ06ISLViOzJHBP/o +QcrqTSeSPgkBcuuWcKRVTZ2CSXmWQAvCICm3ULelyZHo0SAELbWhgLjUdimnBtdINWEGUroES9Zf +ACI1vZxnJTKS4hShPVBz4cW120bPN/s9zd/M++hq74dxD+ODK8RXMQCsMeWPHHCcK2skSB2FYwOZ +blwRXEbRaONwqWaTMsBk9tIdFMFYtsKQ2hWpTtECKHlLBefAa2TYm6Fp8RqL4kW5meru4f8oAxlF +coDU01UhAcEpVpJKOpdFnoyCy9ylE2mTO4Nxo3qEmA20XziowuGhycPSWU4kGAcDP12i8SJkyHsg +xhySxtJJkWF+HJjcI+ZrEZZGv2U3rpdqjYrW9ZvPnA7W2tLzU5kP6EmSm5UKmEt5L73Ya69cd+iO +H/+hI2/6m92+bm82e49+9tRn/k+2fip++uLKQxfO9LoX0rStsaeyxC/EDhjEFQt68aTnf/TvEwBm +bDEpFpaJdY3DNX14oNFaDox/BLlzhPBz3clTS0eNFOa1gw5gnLtZAu1bXFGCqZrbC/XgW7596bob +Dv/vdzz52ft98IYxN9gKTlmEz5vXmNf4Sygz2vMl92yfa/TzHRr3Y6Fsg7FtmXCunSMciRcuNKoQ +KaTIZAUmWkmUisAiKMF38YtEeZSDSNVSNTjSwl0+pQKdDB+Kr0gCKhGSmwkTUkKiVEhlJEuAk3FU +dTxq4Cj1Fa5kEn3F53lEBQBcAsW5IlECqxVnobLTCmgUB49cfqaI6N+BC6mQEWE3EDaUa0gZGUFQ ++osqvzfI9DK7KEApKcPCBWaAWAU/iYiARkIgvxh9FRQsKvSZZQTyYbkWSU1CI9ZfRFltlBLI2RfX +wGwmBxTLAEcQp8uqABkzGU+lqC4ZRGVZFj25pC5FxlF8bnhEjmiwC423hH6TpqPp8MchkSA3EziG +xGovj5t51LW7HasD/IOVjqa9fgZ8QpwvSylea8DTSwG2jlaFbhrTqOKMQoOUOTym/TgsuK4+mzEz +FajEDWhpMDoqhCVyTVF5D1Bgu0GBSZGpoWyeatSsKmHQqhfAyuSiSjSCvrQVWRUOqEgP8TZwc/mO +JKb4qEQz8dkZpGb4lAYQb5VI9SgsI6wop5vQSM9ZZhNXXiHXSEIY583iWAmFD6FRjA96jYBGTmHR +VeCvGFIRkTDR4JOdri277KKtpFH2jFolvLCWnFlOu33sDKZKxbaTfr8Xhl6jHmDEetrppDf7dW+4 +44d/sPyyl7eardJms/Powyc+/hGEd81e94n7Hr1woXkhCNZSrQ+/Ficmz9HAu6AN9UKBRnmuspKZ +z9XyqRqNm2ZXW8ek0yBmlODJZpdJPjDhscPJD/y/1x+67dgf/urnP/AXTVG9xSpCdX8xCq/a6blG +4Hq50Kie4/28nl1ovIIZs5+TngD7ku9QUKC+465f/W4n7mGn17gDs1WZwNZr2wYDJ37Ma1SIKFsO +vUb1K9Zc5YaKRzDYbMtRVEXRRQZwGCslarHzK6kRzAsOnUVZvUR+BV6ThD75s9T4iz9Cv6QoLBBh +Z/E6laNJ35GCI5JZVOcJbEWhuPLuhHoj5Q8FKArfk5OUzBpxOxg1U/4X3+c+6Hkw4UUvlwuphDHp +U6gcIUN0gg5CouEZCFWKVE9p+qHQkZnFwjsU9g1XbOYRqb6t5LCBmmzdKDxd5Y4zNqi8Xy7lvHR0 +jECbocL6EDwWTpBgnkSlcQ7YADuzC3eaSrDinSBsykWfit9Q9om7edJFC6c86uUJPEAffrORhIao +3/GSEG4FWnA/9MwhAcMotzgQEr1DeadPXi/C08QfWabIc1G+Ae0HMRtUclG+aKWgcQSLETGaCKiC +kBqjCpMpWERYRcmUIW2pVBSwTHIjQqcLNtiisgv9N9kYkVjGaSWPKz8Mok2yUqLmgTaWqMzzljFj +WQSpWfhCxxwjqO4iURqXhOQqTShRCqcBwSyxqs3gneAcRChCoJF9XJStgmVbBAUZ1hfzgEipVGMp +PYhcrZMhBu06lUbNLZfghoZrzea5C8lmp06VIIu9rVDTb2nQBEp66YXV3vp1S7f8g+8+8ua3RAuH +tE7HXj578WPvf/zj91w/N18JzAc/8PHHT104bxgb6KAM6XB57pA1Fj9OghxyI0Sv/vJek4x+caCL +V+E1TjyEqhUadeu2eY0YPYR7lmbyO2+1Dy+xSPb4M9Gjx6ONnhGIIAW+SDbPskopf91XNxYXax/+ +8PmTpzArMLFwQ+iVESVHI1uXd93XeOsxaPxPaJU34VUsl5M2k1Xr2QyovgiNo7dA3LvLg0ZJfg0x +njtTDXZkAaelXcDEODRiM/EOC2gUuqhEFhWU0YYfgcZCx3ToNQollTpoRT4SKUZxGnFM2PjSsFiJ +kYg0JEsO6EBS40y8xmKdBr6wGkDCTvyivQJEUl4g8XJQ/i2LugpYCjQWKiIycAoTBckVNAq9Vdw7 +hWrS3p34h6ipxEhZACFwSCDEySGwiRQWXEOJGCPUSV8YPgYWembx6EQqB0bC08qpERJp8WgoWGJm +THxEVb2hQ2ZT8XzFFODPOFckxpAZDY0UpEUIqUHQjlEo5M1AimHsKmoZWRfpVTge0mYSywyzYfAl +KSZEMELtAPzAopge6IM7waoEObwU6LFLonBgGKeE8xfkWij4R5ebbScBO0QxOH1hmoYoE810H/wa +kpyY8mOgNSYm0sQQkXW6XUVMVMkAAQ+pR8sBkcWbt1D12JJImnpTbgrtFAKD3DfkTzlhGFMVKgnl +b3GlHE2SfZlfpDqOxLP4MXH1mAOnPUKhA5khuGTq7xAyhRgm2cotaOQZYqTwKflOHxU3ivQcMnmh ++4OPmlbJqVSrXtmLwijebEcnz0Ugk5p6BbcbnSFN+OlZBfZBPz/l5zPf8DU3/tCPuF/y6jaGr9ty +Tz59+oPvOf6pj9xy842z1blPfvDeBz/9kK+by4YO/xIBaj5DOKiQpVVkUT1kiH5MxK1tq/FEaBzZ +YGKmptj3RGgE4armaXfdZr7+K6eOXoc6Uv2RR/17Pt556nTeAuVJSnjA3MKtwTTzLIo/+AHugI1m +pxJGoZLt8wcXcdmXC40TMXGInc8uNF59rnHSBJKFVXlMg9cXU0B1CI3Ka+QKrJZliSxxARfnQX4t +rl95jSqgqn4eQiPr58SLhkNHEBpU9EsOTuUO6Qbhb9hQTGLpQgUTXRnyQq8gJHARlE7CXMBIP1Fi +z2rho4MoK7oSRZFnSfwyxXgTRTF1KQoy+ZOCHFlvCw+IV66o+qrBr0rksUREPkbXUMKk0shJEJFf +8AstG93YWRrB78w4ppCpUzFVVVGCH1ibQU1u7Jk/0EURJ5ZnIHY3RrPIyNEnAi45qEIUuTNJuyHn +Bv0T8P3xDsKeILEkRtw38E4CREQ1IdQJmDrFMGOpgZqBgADaNpFVozQUqOCCeLKR4IKElK9KJeBq +MSwIpMd9jjNSRhHPRFkhOjEnqbMZuNACisOIsupx5qd6H7WgCKLGxDRye/E2ikKZ/EOukelGNJxn +QScHShGWYqyPMuhyqURkTh4Cv9wAjjrTfOp2FK6LLBPynkJGldjkr4NVGxfEoS1qdHBNjsAGF1ux +YsSMEOUa+uf8mGR0JQE5MDLEOxTxXc4o3DMG/segEb+KS41tRFoJnj8UDqAL5DhlasNWKmX8CCsg +anc3L652NzYb4DzJzETFZgR3L9dKutNtxuvl6UPf+z2Lf/dva5WDDiyPcK1z/72f/9P/dfKB+1/7 +utcdOXTTh975gU9/6vMXEOyG5oBiLMvJWKKxSxjnPJZh4KPCFPhlvZ77XCNOz9aTmbL2pa+yv/7r +56aPIt2anzyuvfcv1h9+Imv3GJCHUUixKcxYlfZn3S6jTJSPkkkLdQjaNiP6Ypd11dd84zFo/IVJ +XuM+7tELBhqvYCi/qKBRrUEF7u1KwxHLXIX/1Go2fA1oOCrmN0BMiXYpNW6s00q2pohMqZQhs1g2 +e5oLNColT9m/suVV2LGw58VxI6oyx8nIoQT0WP0u66C4AgKB4uwJ0EsQVeElA4BbxnZRkiE7LFZp +OJ6SMlTWgOj0QPecu2bIVAoGAISS2COXA32vYO/adBNhNbB2UMowwNFXTCFWQMAPKZoOCX2US7TU +VtIjAWxQkYcirgGQD7xQMDuRZ9OjSI8CKBKgXQTeNFF5CRFX6J5TqYCCPix558lBx43FGaxmUeFm +gCD4R+xWgSFiaSSqBaRHB25ZjK5NojSHIkIHsNfR9Q20mugnfivFF7TT2504iNH3HryYHN4hfJ92 +KDLdCHsqXR/KmdPcoUdIUwi75uEkeipFE8LjlfwgwwwqsStLm9x/LnGSXRQ1Gbmxyj0vCB8CWspi +UUQYCTIzsjaERDXtJN1GZ1v8Ckw8NkvhmZEQLHApjC5OMBC8xNSCjqtYXzJj2e6ZfVfkNhNE+Skm +gdVtFhqOYqMyJChxfzqoFD+wdEjGljyvgn6TbgkBVT/sXlwLljfsMAXjCdOjDDMly/s26DronKxf +3Ohu3nHD3T/xY3Nf/9eTpIZTTE89+tQ7/vuJt/9PKw1f/ubvnH7tV/7Zr/63z//ZR6zUfEoP4elX +mQ6gRgQIt4BGzGOyow0D0qk8a95WBLkv7/XcQyPO0NGS6ZL2Za+23/DGxvRBNBfTTp60//y9rUef +0HrgfikFeN5GTgBOGLKRSW6TricMwCptwG3Rr8u78mu69eV6jUPkGwSpaRIXDqI6scu1cXZezk6y +z348vGs6LFe2s5FM3iBRV0TP9tqfEBpGBk6W77EhLRIQxU52O4oKJglIFXdhPAxTeHuy1KjJJ1sW +RBv1EawwBf9DPEUFnMOX2PZk+EnoVL2NjwyhUcWDpCB+EF/FDwQ/8R0IZrL8oGU5HThJIg7gViKO +cjTZFl4NY2xFXIt+RqGFogSVlUMnC5/yaBUGSt5JVsRC50sOKIcohkZWwcIDJRuGDhd7PRLJpLNV +4Qsz7oZkFNRh4I0xFkpVUvBdPNRFsJgdUVMhcmJBLhFEkQtlUktOg9WTCbQFWDcJXw15vhgNkIM0 +Qnf50ABVPUIACUXd0P8KWWECgMwS1APQVqYGnYpv4nTYjRkH4n2B6B3dHcCBKIeqfsaM+FHdhnFF +StuhOIFasGFsBqm90QrB/NtsReubQddPVnWtN1sKPP3sqZbeyadRcockocCfeMR0vFhzqCaGjCbh +WI2u/ELPTzQMmP0C4qouzUW8QSpQhuWfg6lOf7Hw4AtzhMMjT7K4zwJjyq1WXmARRBUmk3IFORuQ +TaUxwMw1byeSk2zuLHdFXH7lpTLeQOiUukfcUjCMgGvUvyMZh3pKDDlQNUgA1UJkjzE+RS6WtC5t +NbE/EEdFNy1kEymPp3vVCrQPYBLErV7/7EWr268gRptlSD2GmoZbS0Ehw+i1+xdSe/Yb/+p1P/5P +46OH9FK10ulc/Mg7Tv76b4R/+bB/6OiX/Mg/XPrqL3vPf/wPf/GO9210oxx9uiwXvhWonRxxmTuD +f2UxGNyFUQrzro92EX4dDPuu/04Iyoro3uDO774jeWSHi7+c7WjiEc9splet/Lab9de+ZvqmGzwU +dD76ZOfjn/JPnOa0ZKEtrRRViTKK9YMLLebdlj275wU9F398ERqv4ShfK2jkEl48GsXZjU6mXY6i +auiGuMjlQQm7bK1Tivm2NzQqkmSxnxFoLMxq0t6LzGKBmAOyTFG8wbWKS13BVsUeZM3hXgd7yy2H +nqGCQa5/BXNTskBycKQSmfoSvS6VoZM4qsROmVJSWjJSAVd8Ai39uDtBRzG2FW4W0ChLr6Az81zK +i5Xt2PtDcIFBSSS04AfA42CfXiaWJLUlyyRcMLSVJz2VriQDbSaXUEmASd0ZXQdI7SDyGWYBaC9B +guYSQT9D0bsfwk3MUa6W9fEDC+rBKGUNB6O01DkANNtYl0AtwsmxzFOirDx78CNhYLNNpJBRbbaG +V2FKrOmWHyTdUO+EOIK51kqafur7/Wan1+6lPisVi/6+CHti+24l8W5r1I/NXTy94p/oVFqGg1pQ +SaxS6YwVdKpqYDDbBj+OTDsx4KTajnWK5MsobwubEtGH4eu9V1gJmxbSNsrEYlcRQTgFdYp1Qt6R +pEglPKqyziqXzc7OeixELmUeDU6VNgKZIBxAaa+oQfFcAFVKOKA/aArDFBVDPC44k5zMOAq5NtSu +h/aPgXI73PBSiUr2yC1W6mU0CQ163aDd7F3cqPK5Yl2m45VZvkKxurDqlk5caOU33nDzD//T+rf/ +jbRaNkE1ffzBU//l1y78+Xt77fbmwuwbfuZnpm552Ud/8Tfuecc7TzZbfRdOodHIPHKGEUTZ87XT +K9j2zvMDGsl/whyYbaQ3XWcuzjsIvq+sp0+fzjY6jCqAMM17rYLcL5DX8xEapcb6hfjaFRq5hO89 ++cf/OnQZR+240fHYDRoFRBRWqP+2Q6OwEkahUR10aPoprBvAjcIOKcxTOFfY/bTo2RZdkoKDUKpw +cGiuE/OK8FkRPh0FRYWQqfRw5SrGj8FPknSicDTlFLkRay3wJTXt4rMN04p0rwsnQ7k0sh86spLG +wxsCzHz+pOpe3Bp1LQoJ4EGogCz8PKSSsHZyFwKHXCnxA5tOsJaBTiRcCFAwEJ5TjFPSdnkkNVIm +MnT9JvifeR9w1MvDPsRi8hCyl2SWsCQUayjcC3V0BfzCC1E+GBd6nrxwS6S+W8RWU2A0Y6VYRljF +gVAbNdrQOKvVS5uduNnPWr622Us6QdLrJ91+2gf/j06GmFMy/qJjp6TpxF029Y1qat5eWrrtQG+1 +13xw2TmbeTEzuuRdIUvJBpKo/JcZNETHEdQZvCc0CSlQYbU+h0HuuOAib4Co04zOdjVtRl7cmJgi +b4mukdxEOXu8MBAKKGgiyDUx+Ua7R6WimZSDeUKxHzKjioOpTK4I2ZDjIXipbpDkoYWZxYg03UqY +Fyw9LFUggQARHArXu9SJhedHMGQDUBfuouUyiEroilutcGU9g9iplMnA5pBSDqgZpKZnhP1w2U9m +vu71R37oh6xXf1XXrlYC37/nQ4//p/+Q3vfZCjpU3nHs+p//0YXb7njnT/y7z7z9gy0UaVhW30n9 +JJxBIAIWDGidowO0j5VvOzTuJ0w3Ybe0NicsU8prHDvZsYWI8yiHzRhWPb2CslqwtIIMUXrME0x1 +5LrZc4XP2ERo3Hu13DajnsVfX4TGazi4l4LG/RxCzRgumoOvS31qd2gcrNiFK7UTGmW52fIa1d4L +D6pwwARFCoAtgosKrIrvErQUpBy4hsoHE0gT5qpYhlsBVV6RoJfag9o44XeBCYEaQVkelcsflja1 +JAr1RrZXBM+BLyU7KRg9g7OVmL7ggbpERmuVOyJH4nfhgtJ+kAwolVzoJlImjVQfhEPRg4mOBriI +HjtOIJJJiinOjSUmPJAwYMGRQVvHuN+LQt+I+marZYI7QC1WKpgzZKdGQnJbcnQodeJV1MuJiIyK +SooAQJ45XOsltCs9KZFXjFPdj41mN19vJ02o0wTpxbUWCC+9IOvCRwQ1lMk/Aj0KGcHwHQSJmdnD +1VM9VbpbMAwtEI1zb9a07C6nctN0HunBky3jiX6pR9sjplAaAB+JV6Q9t6BxLFQmzplMTXqgSt0b +CjYIv7Jzh5pFBTRKnHcEYNUN2nqJQF5RLEsbQdlzCsEHcRKFagoaCW5MFaqIB++lNHIUlk1RXiTb +8UUfnxxU3ioJUrA8Q+R7xY7jO/ys1OzQ7HFtzbMtD21EPBstRGq1ku3ZTslCwN+GAByUE6KwfW41 +3eiWY73ErqGoAM1JVUU+NogqptkNgvNW+ejf+e6Fv/f/tI5eD/OwtHyu/Xv/7em3/oG3vto19c1j +N37dz/27rN54x//3Lz7zoQ93dK2jlTzDzmIfiWWURyI5jtzu6AjtJ3O0fZt9QOOk3Sp+9J6YJMbP +HtAoxGay1zAnJUSPMh4hJjM2xGw1TGp01py0FE46jUmfv4Z/fz5C4/9lucZtd7MILMpqNLCwxnTx +9wrbDrFt28OAGcc1+RLQqFBrKwcpBFRiDFekYXU/11yVMhwmG4eyOEKoEVhSjAbFqCm8Cum/IfsT +t5IQQldNflWrI4192Vr4NYozKvFQCahKrEw8A2aWBhsK0KpFu6A18hiS6WQZn6LBKmiVM5MyN3iH +qlUhnAT0x0JolDxOhkzRlckBfRYSo3CzEDZDdNXDygUiZ4QwaQBQTBgsDVE1mCbo+4hIaVZijFOB +YYHjGHYhd4j2KwKPLjKH0jMSkVo2A0GgD+FHjJarax4cIgBSmESdXtLy9Y1Ovt7Jzm/4LT/rRFo7 +THvgYCj+K2tEUHuuIxoNai1rKJhxZIAKDpwAgMrrDeeSqqThQoMz6zSi+I5yclNZ9xz9Ytz79Epp +Q3PR9568IYxughOTXGbxGgGqrclJpo1SWGCYlyu63F45hNyynZE9mUWjCy7IReJPy5kV0Dji0HBr +5Z6LMcPVFeOIPKHaCyY+VdSQfJRa0cHxhHkkg8DBoeVBkVUGqaV+UVGEUGICAipdRrFTEM2m6puE +Cai1zgZb9XplaqZWrroAyDToNc+fT9fbXpTXbA97FhOPwVooHeIaVtrt6MB1t//gD5a+9W+1pxYh +76bdf+/xX/v57N3/+4hjnQnijZfc+upf+8+uPfvnP/xT9/7Fu3t6vkJxVaMemw7IwTCpwO2iYTQB +2V4gAVWRAGSHS84KUhhQQSTGMmx0GFLq2RsEBa4hfj2Lu3oRGq/h4F5ZrnH7iqRCRyPLHANJI2e5 +y1GkMgwPr8QVi5DqmA0oi9cgejW+gg7RtPC61PqkYrMKTIfIJhokKkMkQT+uxCOUHOIfklCF06iA +TTBRQI6nJ7FTUbEpCtWY4VKljAOfU+rY6ZvyY4qXr9CNDivr6dXgFFg+wGNZpAdFyNw5W0IXXiMX +VjZGpr4a+C3S1B3n4aABhWa5SH4y/4nvLL9QHazCGGHSGDXcMeAQuKjHsUOfA/WFMjJSZSIDIzFn +kSUgQLCZLldzxQ8RSQGKkpFPRDhkvgzZyn6sN3v6Wjvb7OoXe/GpZqcJBwRHC03wS3BxdJ2lEF/6 +ZqA+X+rqqRyDH7nASNti+G8sf2GJPWGSozL09iTuTG6sSk12vDS7pdq7xYoaZsm308+ueGezcsBy +HTpllAOEUrrc+KH3JhNuOIcEzNQ9IEuYfbxwDLl4bqbC3oTn7Rnu0SgtvT52H2bgVSLLQrNR5R5i +BPLo4CuyJF802PEdY+eIx4hjY3DRRipl4UpBw5EEqPI6FQlMxp5niQnBoeE7xeRB5JIhU5FTZ1Ns +htM5o6C4hzAqfLhKqYSWzXHo+34XcfJqHk/ZVsXBuSGHDHHUrJxaVae+0u+vIsrwpa++8Ud+0njp +q3Sn7rTbq29/+8nf/PXyhftLhv1ML7G//Ctf8W9/Hi0b3/vjP/mZez7R8rxOlpeA8mkf9Qup4aD2 +BU27kAGdGGB8oUAjTRcJCAiznGpMuDW0S4SnjLkqnU73dEyv4Up8LXa1DRphkBWzfWvnI2ZNUaA1 +8tTIO2MXvA//fvuJ73L7r8W1PQf7GM+Jjv9WLC67vHnpE9tai8a3GctH7Ppx5U9xnVLfxzeSbNfY +mQjYqNCohDMZcBL/agsRxePg/8yUKa+AJr3ScVPOm6rNl12J58TVj4Aorq9sUuyEy5UCSxXjEtjj +3gb8ZvURBYj8TxUIiNcoyKr2I/ozXAaZwlM5peIHflDcKH4AITMRu8FbbHQFRBSFOukHSUYh/AWn +DAIqzxnHYTMINIwIdTQ3jvqgj2oplLEhhAknjwIl7GyB4Cc8S8kCAmjZvANk1CQRD5GSplQK4FoL +0PARbuTPDlJXcHncsG+0+ta5Zv/8pn92M2mG2WYUt/tJ0mdxFxpiQLcmC9IpyyOTj26ZCLJxXc+T +uubeWI+7cXLWdwJKerMvstwLVWWiiisYfSTDlbeQH2TpBUGCZFYz75Wy4IDVuc7e8MKji4vx45v5 +k/5URy8jAJzr/TSNLS0rm5mfwJVkqT7rBoAaULGR3B/3CbiSbFGOCDJUBki0V086vnFSsOaGOEWF +IVKIeCdwMixoK3xKqeqRpXJAVOStk0kkxo6AI7K+Ms+ku6Uo1kC5lEehOC5NDwmyUhmcu+VnRRtB +bDA6jmqmiJkkf2Jjau5EbBOkFcWVYbwbNgf27Do21MDLjltzK7kf9Fut/mY77PVQsj7lGnV4lNhg +BrCZOAa7h5xoRt3rb7jj73/PgW/49nDhVlowj33qiV/8xfaHP3Aw9quJ8Xi/XPlb3/PSf/bDGyee +eOs//omTn30YYYkWahsgbgsCEdpTsgeyxE1QDEMHa+xB3XslVA+1+sjQHx9Uze612m1bDXaLrxaa +E+M+/HCfysLZYymTPw3hQRn2UnAsP/GuSIz/BQyNw6DKSLJ0JHEqyk17vfhYXv7lTwqFPwcY9+wd +gkPy7O19dM+j0LjtiAKNu3NWh58aQqdgpay7A2hUztkAGoVWL2Ao/o2CQFF7E2gsfKrifU4HQU0m +9WR/KvzJjQXnVNRPxSRVNFK24UbSMlF9RqGgKIMJz5/ZKDqUsh/8hQkp9UEJ8gLNuOwT2YR9iqxi +0QaZzRNkSWIuBDXtKKLI4whpEBQUotaQ+mqyntLBZOpEkVnoFirVM3YHocgaYdrRPWqioU2hFrqo +2GLvK4RmS0lidwJ9IzPPpvmFVnz+RGtzI232sn6cQSMAyUmWf7LYO0P1hztf0pzcv9htJI7WT4Aq +IYktvBuxp6WLZumueroexI/5lbaGHhLwN+QGseACl88cIREH7hFhTHVSY0kiI3VQbtNAje2Wsvyo +m91cvag3DxyYj091kxPdxqZW6qN2QMc5ZVUXjQHDtX7JZw2j6lEIfwkhXEyaCOI2YvWIeSRhVdXG +eWu1lNsvvh2wG843LRO58aAkidq6oL2yY+SzQytMhdA5PwXMMOyyE3R1VKFV3D3whAD6rBphxR/5 +TaxvkSHAP/RFJJIgZY5Cwy74rqI/hltJnqoI4Sh6KlC/aFiJan7Pg6foIjAexHGnG222QfmFay5U +ZaMUocDC8OzcraabWrA2W7/+Td85//f/gXvdzQksmv5m8q73nP2t3+ycuO+gZ62vp8vVgzf9fz92 ++M1v2vz4x9/2L/7VJz/7UArLTAVz5AFS+eA9XrtA48gHds8BX/7qsvMofMCErqT+JO7OKF9GCiz3 +XMfEUn1u1rnn6CjbvMYXofHyxn0Qw7vkpyZOqcs73mBrhWfDl5rQe0GjZOy24ejo9oI+An/iu3GB +HcCScs7kT/xBhEUJDZR9k9YcCpDE+MefuFQpNihfQhgZhUZa7TTnJdSqBNTUQZUCjnBl5NDMzw3i +sVwJBUppZ8hJcEUVPBCPieFHqYJnaFP62nP9k8QheveJ6igAUWj9JAcwIAnSZxwhYsqllsqZVKIR +OTFWILOEHHumRDYhXYTisCcXV0MRcklnwqOCVgwaFLkoQKQWqN6OjfV+fqGZPrPiP93snUUji9tm +UdvffKRlbOZlzSmjOxQru7EzA5X/oZH401nplkapUl559GJ50yj7OBfq4uAfpGSTmpkf9dy7S+Fy +N3kkqK7pJTAacQkMQauMPE66cNBUsJdEFxFApWylqPNAv6blxNkxFJ1VL2Qtr2ZXQ91aT2pd02jn +/c0IHauMSgUEkf65prGeeAAlohVtKbR6jCh8g9aELjxmWAFoM4WyTIlvFksnBlUtmiK9xtOWGyvZ +Rzp1SneGNGKcCZtnSA9GJo7FAmPqUcXYZWFm5I2cY2QLGYnGjqDWxtyhdPrgzaAEAm/pFjQWLB4F +jdyNIuMo+RsWNSr6KVhLNhxHKJRJ88qSW0KjLnwgQi+oKNhAN8oQAIkWzNCDB/BhABqxNm2Yvdi/ +AEvmFXff/U/+UfWvfFPgVtCk0Vo9f/F//tLF3/nvc2fWFw6Unmr6G7e86q6f+BdzX/vqx/7w9z/w +b3/lsafOBLCWMOt2PKfjj/wuD/LYR/YBjZcLSVcAjYWk/KVXq/1B4+W7TVe2Pl6LTz3r0PhF7RGq +iuO9Xi8gaCQgCjwShuTCVDBzFBqZ0aFXJvlGrG2kYyuQU4UetPi5NkpWQeB2ALGKuSqJyyKISjCT +BbHwGpWlKgdU3HwVSsVbJClKyEyaGxawSs+RITUVqRGWBVk28DIQ4iSvVFoI0WvEWUnbeIiAUvcM +RfcgOqagZRLRScohsNNLlsuX+izir7TEEngXf0n0sYGdqEx0ucJDeK2f1VpdbXUtXFuL1vrZxW5y +ZqPXRiF31UoWre5c2jhWKiOn9FjbOptrXejh8ORJAzFNcBRDhDCXtOpLZmZmZs/fd8Y6HdU74E6K +Gg3kUKGhPm9bN1ecOwBarfTBvrdseCEEuQhFCMkxsUc8F/NebpeADG8A3D3aCzEqPyDhY7bKUe8G +I7zOXTV9u2bNOCU70Op6CZHczXNrqBEFudLV3ehUKzsTViQYDYRjvBjUVfB/QON0Pfi7aT/qo3sV +RMTpe1mIMKrImbKiqAaEcnwJUNBjJGmmqPRmQQm2zzKoGEgMkSNaEGRELU9WXjG/hFysako5G8S7 +Rhkrec3s0YB5wTLKUWiUUCCnyVDtCPdcXH9p7gLHkfoJLL1wHKtkIbLJfpXs44ypAhIUlIFQDdrz +HSQys9xFFhrXlmo1066axul+OziycOBvf9dN3/O92k03w2v1/Nb6e9594q2/O3X/ZxoH3a6eXFwO +4694/Zf89L8sT0099ou/8d7feOvxjU7qWXDZixZngxVit8ThBLQYdTN39RqlC8rlvXaHRrmThdc4 +Yvpw14zmSMvtq4XGMUPh8k76Od/6WYfG/VzRCxc+vyDQqBy1MdOSi7nA2MB33Dbskl/ZPrO3eY0q +ST7wGiU0Pg6NUhA4Ao3ETxHTVjFSZTfS/Sp4qgz0MQ6nYqGDPJACSElDcmPJNuG4jKCK5yn5PDl0 +IUfHP6qjcFdwhKg1ShgWqR3ulxQbG2LfLFWUgn3DRdte4cmxrS5k2NBpPgxRV48vcQdZ607fUoos +1HYM3wkcqqGlogowAj4KyhupQebqXhmLfy9KmkGw2fM3e9mTz4BKk234cTtK2DwBjhHU5sy0fMBx +brB6i0FluuH4Tuehdf2Z0NrEka0+gqniMSFnh0imf0w3XlpfXFxaf+hC+linsg4AYYYOYdUItI0j +VvX2hn3Y7Z9pRg/3ga+Wz4JHGWvlaVF6nH2zxHHGNZBsChmBKdcs2/1uP++kTqS3S3l4qx0ftnul +FOAAyEI5/HyjMVNtrJ9fjvsJeix1z3es82F6MipT/xLujh7j8suGVWGmNo8St5vaIclAbFWMdKhk +i4vYuGQ6FbTFcm+xBML6QLDWrTpsT9MDsqHuDYMoHFXitjKfuO7yfzFKVCSP+oFSh0EaDpTwsEN4 +jZhorKaUWthxaBRJAFo5+JBMYNGBJxsUgQOQbBBLZ1rRs4CJ+A6LiVq+bLkFa6fXj9tdZBldnBvV +zlgPgyOWYWfF6TLk2l9+593/9B/NfMebfKsGoqr2yJMP/IefWX33/z600VpwnF7NeNDTb/x/fvCm +H/hBzW/d97M//Znfe/dalJ41jR7kA3CjqYyw9Zq40L0YUN0PXjw327wIjc/2OKsH/lq+nj1oLMCS +tp1kHi4BjQIi9P8IjZI/Uj6kQCM5maqEQzBNEFGgURzEAkflTQG/HdCo6hwloKrAfhi1JQ5Kcl/2 +xgSSaldIJgvr9PkTOfnk1qQRyw3hIKLcEOlDACSq91QrDzk2ixXFhC2ukUch+RbrZpFq1ULLjC27 +gk7AYWS3A/vcenR+M77QCi80g/VO7KOAQkPNAy4KunF6bNDHwtGREtPLqXnU1G62pm6ZhcbN2pPN +6MkgOxlYXaGZiOY3yCxaxQxvsKKb3EPHlqJnmv4Da9bZ1Ih5ApmdRpXcusFovGzKmav4J7vpY/38 +ZJR3mJOjo4sEniTnFGNFZhiuB5UxaQDfZ96qHplG9WV3vaN1NWh1Gjd52QGnhxZYtt5vpvAqDx+c +vvOWW5oXLqyeX7b10oUTTfis2jP9cmRAWDVzrczTwL7E9nEQZJ2kjFhuovdCaN8BQDQTFX6qChwH +B5ZKyaK8QauDmI2OyaW0tlCD5RGs9ayEPCg6iAxrs+tjkb+SU1dPCLOkwuFi20lCI96Gc89ef6iO +UAFVqXAd8xrZT5Gzb+A16qivIAHEwWwgExXTBLX88gLoSlAdSncQ1ov9ft7rm2FsZ2kZGzNDTZ5Q +AiYVTubQzKFv/uabvv/7rZe+ArUt8A3b7/yTB37tl0vPPHPX1HzW7T8TdTeWDt74wz908w99b/eJ +J+/91z/79Ic+fLzvL6NdBoR1UJ/Der4xsZsXofFaroPP8r62yYvvylAd9YKfLzQcLJnXGHB2H+hJ +4Y5JZ6ESQtf2Ju4GjeoIKro1NMaVF1gAgOQaty5n6HfKBhI/lKowSRMWUTL1PrOAso3KAjKgqqCR +nxRoLICR3iGJhYp3I/spcLFIH0rOUeJk8lnh6ct5F7uTfUlSUSKcBQGVp63eJ6opvg1XOzYMojvH +wBnyilh2EX4TmEBbXXSuRxk+qtmkDFwyj/JZLlXSe1eUw9VCrKgZaCyBQ3AtRT8NdONLnVbkrrfD +i6udc+vd1c3eaicD9T7EoooR8Vy34YZ5N+tlDhZC6K5BtcZi1JW5OisJZzP7rvL0S6vVw9PnzvTa +J7rWcpZe8JNOAmJMFusoB+nPmNn1rnXQrGslt6uFpzrZiX6lh5iqlTpJPJ/YtxuNu2a8xpR/ppM8 +7fvHfaupm2jxw+YbNA/4uGKpF/+RGULJNfbNvF/X3Zsb0bzTTcO004/QuHHaMxqeD0MB/lKgxf3O +wQNT1193yN/c7G/6nVbgd5Jq09TPhHZgodOHVSuZHtApC6MA2UXEHmFXtHqxUzGnDk1DEx1hXSOx +g80gagboXyXC8pJKJJKxMBHoHpSzmRtnATgb59bLuVlCVtMy+ijb7OV2JHRihrqFiyvmFcOuiDbT +QoG9oaY0/GcGUQGNlIijQUCKkTK96J8ieDwwZyTXSIUG1Cni1jKCyqipQkeo7SGeAHwFaypin8le +P/N9BNhhYUAbt0KeFTqLQP3WLB04MPOVX3Lsb3yb9dqvzWcOxP1+9unPPv4rv9p+z58vxfFMlcp8 +oVnP77rl2E/8wwN/883ND3/sPf/fv3rq059t5vlGGiUephQMCimdkWkrQX/Jq24tn8OHdGxxGF9e +1ZO5tcG2gOqlVpXJijo7tqChKv6tuPBqxy/ScBgRVy/9P6JP+NW9ilm+504mmk4TT0GF7kRGs3jt +Cj57w9okspiMyARk3JpHlz7nawCNQxi71FHUqk9TXB5AOfOR+KqKakqOZwirWwgqfYv4EdESEQ9v +qwJJ3pCWgszGqcCmVM2JC0je6Rg08iCi3KagsQi3ysko5inr6SUzRGUv7o+3kucuRYLcF2+rALBi +2kiMlX9lRwTAFt1EkGsQrrIheYm2CxQNI3yj7C/WgzAPISEqpfYsnVRgioIyisEhWchiPKScvBKq +NMAvQTqPUVZV4mGXUDIXpO5GKz1zrnVyPb3Y01Y6frsPDWjNhewoxK5QDQlaKwSTK3n5uopfz9vn +fUiS2n09dXVzzkMVRILt4YFWdPuws3B77eBt151abn/+c8dr9lTcDoEi2Cjpol4yTGd1645GXDM6 +D2xOg9kR6Nppf+qiaQdmXE71GzLvbq9yA4oIyv5ax9hINx/asM7rThsBRlJ1MIJkuKBUUPKxcH3E +XjSR7evC6TzqZLfWulW0GO6hRCFO0TcCdQQuOkfBZ0KseKrhpgkq0fWpErRV11y4ZudS7QLMCi0u +6+5MCQMdtvooTzcqev1QVXNzaGrffud1N95+7MyFC48+9HS0pnWf6fhncboieicVJZA/E5pMprlZ +VNfn71oI7AAa55CPmZuZb232105sZitpJTbtAi1IZlVGHFCbjndh/TBoXsQIQH0CdRdBWdxJ9D0T +TrIUERF2WIlBT5tbI6voQFOcIVkmhR0XEjeYM9Q2JHJDkRYKbxBw6Pq53wdDGDMefrwL1fg0aZTL +Qd2rvfKl17/5O7xXvUY/eKOGdlRry8t/9EdP/8IvWidPXe/W6rZ5Pmi2vJLzuq+7+ef+ffnOY8f/ +5O2f/blfe+KB+8+Y2TmUwGCiIcRtZg4iFawfkp6lRWxfOGvFUqVWFulWOWKwys/DRYeXv61UdOLa +OFwBxracVDYwVkQypuVWnM9E8U61Jn8xvca8xi8aaOSjM/E+TZougo0T9jIZOq+F17g/aKTmh5qa +knd8tqBRKc7gKELb2QmN4ixeAhoZDaNODc+N+tyFGo7yQgtHQBpQUONLCIrkYkpVIjJGFH8mbcZF +2o98DvapoP4n0l8RdWrQ4BdOANZH4ANyg9SBQ2Q1FuFyLsNYNOE5gGKZAEStBN4EGg7pVhlY1u4l +K83o3Grv1MVwvZn10KSX0IObB8U4Nk6Gw0I/CRWNyFtaaVLRvJvL9VcsrV3YDB7fdFvIW2n2ATed +d7Wq3Wq2YWPOzzUax7zqfFV3Z//0Lz6VmeWa4y4hvXdxLTX79rwHpyubcnyoTT7TXyhVnY4WPLRZ +Ow+dGiueNpyb7NrNnreAsGap1+w20lLr4dXeY127aUKqgFgBVMQYQdVcYqo2iatM4WEIQriw07p5 +uNSvZmgCopXM3EGzdTqYuGPdjWhmypuddXy/WavU80BvLjdd2BKraboO10wzpjy440Gvr4eZVdar +B0rT19fqS87CYqNarqAhLZTSH/vsM6cfWI6X82wTknW0QDDE9MeZyYNzlyal1FjQyzeWa0vlOPYb +1RJU1pcf7XbORtlGVkq4Hacqb6JI8NJyo7QtO3opdXeBegnSMwSKkg7o70qjShLByJ+SElfR5QOx +holjmk6s4WeKEW6iC1yUPaFKJwtgl0SwnMBHxc+IDFTskk1/NGQavubVbr7lxm//q7Pf8o3x4YW+ +VZvyGp37PvLQL/7S5rs+cGtu1hzYUXnT75/3Gkf+/t+56Z//v9rc7BO/8z/f8y9+urN6pqXpp3W9 +Z7ETi4vwbB7gDqDYFZFrLkUCjVI3XCwYg3VF+ZGjq8iL0Ph8gddRaDTfOCIWdcUnePmEqcs+lJo+ +e0EbPRopdrr010To3J/XOOnk9zqF4uz2ga/bj7INLOVXWUjkJb+OQuPQFt3pNW7BvwIoFdGUwGex +K8WjGT6/gwit5JnEc1SvwXHlzYEbMPiBfyx8zYKYI+CqzGLVuEGxSugOiKoUOl1Qv81FmwMLatBe +1ShVjFLZcEvk4Es6UecyF2qBn/d6eth38tQ1dM9G1Rq4MAYpHxY6G0QgV1hQRjUgzAbvUSs7WsUz +vHItSbzllfzRk+m9j/Y+/HDn00/6T1xIVyEGh8XaUUwZ0oSkFo0FBbJOU7c9geooKg5n8/Id1cZ1 +jSALRQ8rRzGGNefe8trbyoedbt6xpyx7sRzkacOrnl9b6wOxK1rVte6+41grai7n3XAGpM3cCA3o +hldm0fzI66/6RitHg9y8atgHy86Mhxwp+iJ1/W657CVBDDcOlYggoSIbyNCw1AsCs1WoAFAZmXqE +dGcJizIK4+PqVFX3St0g9PshqvhKbqXb7pYQZqQKDGULqqVaDzwUNCWGF40IIAgs0643X4deKKKs +1bI+fbA0e6wye7TcmHVhFvgdf/XsRutcv3Wq1z3d13ooUADGAWgIQAJa5PpCZUhDtcqcffDmpQNH +5+tTcNOTteWN/EKcb2qGD2xnJAInLRKiLDyRKcdEIoUOSLUVMZtCAI/mlMQUJHpAP5ECcqpPo6j+ +gYMK7il7D8MAcl0XYguARjZCpNvc77fbcdtPej1MGFCKPKvimWXGi5MgsbVooXrwr3zNbd//fdVv +/NawPmeWZ921i6d+4xce+uf/Uv/4525FUw7XDhP/dD/YuO6623/6Z4790x+JovDjP/Yjn/3lX4nb +662ScZYatLpN0g015RH66Ep/y6ILF6c+xYtosxbPlQhYFa9LQ6NYD5OWmP38fcIao0JbxZc8/8pN +HH7t5xjDR/hSP1zBQref4z5L23zHv/qp4Z6vDTQOlspixdz5z9VfyX6gUS3be30V5VCX3qTgVhaJ +MrXkj34p/NgH9k3YZuKA7HMMh+HfZwkaBwg4MH4ZO50AjQN85dIgSMtHXdFvWCAiPyjZG4IZ4lBw +F2xH2iI4mlvWXPhMnlWuQfWbTTBA+sPSiIRZP9D6fd3Hl28EfQg2u4BCtmUnrREVBKzaQBYqzUuZ +XiFKBobhu1UzL1fXI+fpVf2Tj/offaR37/HmI6e6FzdjHzFG5i3ZmYP8j1j4ImwfxSJJlWSlRID6 +zg7CWloG2yVZQC7NA5oClawANNgsmlpyD9+wUJ6xEivNoDMW5guplzW77MxXNnpZeOHihUMLS7NO +rXNmvZ566BC/3g7cBrvmoro+2oT+QG7UdPdYxZuvIIqLuole2K7WPPjT/XYUdlJKwyEUV+SvZP2S +Okg6a4YRg+jTMN0Fr6tHaFpYatRitIkEk6YdVktVsF9rJUvQEflD+NFoTN+my+Zg9Y8xCu4McFFv +tru2kc3OOweO1qcPlks1NhZCYvXA3NHF2pHl42ubzzSTTfjrVB6AZ4/+wuwoRS8/BThYdcues+0l +z5lGoFnvd8Oz5zbTvm6upCimtOG0S0BeVn26qcoKw55Il5LiHlW5WeCCVEYWVauSiSRCEhcRS0Ag +AMF1YqLo6kmlBioZqTwTxf0g7vlRtx8jsxhFmBqQzUUwoWQYURx00r5Vrx973Vfd/rfect3f+T7/ +2K2RV6m7XvMjH/r0T/3L1h/88Y1r/cNlt5NF7SxtRbn1ilfd/nP/avEtf+3ivR++75/8+Nk/fheY +V6dKzkNpEmWmy8xAgjSmBdIOFdiRFWVkXZVxSjqbramVDSAgMbQ3v/DQOLaIFZbw2GI3eY0SG3vi +YjhxP8+fDZ4NaHzWr24f0CibKFLkJb/GAhm7nTQ+u5frqezc58AUmhhQLXwyiUkNNr72XuOIC1gc +Zp/QKGNbhEzpLfChY1UhfiAPVer0lUKJ4TiIlxouxL5dA82BPAg/Q86NrAquNND1hrpp0M2Dth6H +BhrJ6qljIryKynusk6CtYnVGPDYvgeSYI47owhcjG9+p5t7M8QvBp59of+zh9kcf7z56IVvuIbNY +omIYkI7Nn2J8oarBhiRcXsYppih2F3FUCEJG8LAYBCaZCcdhAYKLkvzMadjlaQ9Cq1apbJa93NVb +vRVND5DwCsPYgaZKbDSqsxdOoxCA6/0m+nagoWMvumPp0JzurJ3fYK2jlU/VHOqGZ1CpA4lU06cI +jRV4jSHwrNLpd1lEYRpRL+ttIHoMwg/hhBNU5h+E6WKWLBDGGXyc0ksHKwBvkGigSA7g6ocYf0R5 +MZwO6kYYWjVMFHkiIRayr1VquEatXEYUGrgMLxNe0dS0PbVQmjlUdRvoyZWZ0MU2KimCvb51/MET +rfNdLaDfR2cTZZCSLUYlCWAhg7zPnO0CVQ6U23GrUvHOnjhXcstg8YbrIXKZJA5BMIdGkoLAQbCC +7Fulzk4lWSnrkfiPol5J3FS8RJbEFl4jCFYo0XDJP0VQlXFVTKc4S3EFvX7U7UU9P2OYndwycFax +CayvXhwkZWP2zttv/qvfcsObv2v6a7++W6rWp6e886fO/tZ/efDf/Vz62QcPaBWynF3DN/XTidb4 +lr/2sp/+N7U3fs3JD77rQz/xExsf+7RenjtlmI/4aJppomsjXHrODhH3kX4TEkqVTmNFm2dGWdRq +ofLnylAsDMbB+jMSnynWp+fCaxxfJYsgkbypAHKy6zoRFJ+DdfLaAs8LFRqVtXnJ19bN3Wu49r5b +Stzl6sB18s0qnpUJh9nlTMffUkHMy4LG4cM4JAZsBVRV8FMB2jByKsuU8GJUZAD/DLxGZQgPvFVa +j/JrcYhRr1e4N8KvVFo4ImeJGCnIMSYQEZCCgnr4M8gUwgnAF71LNCqIUEOghWBPdPMQwSqUOCAv +lUN21IYLILIpOAqWRRSulUBMBHEV/NVyqWt4Z3r2Z05E7/vYyqceC566mHe7OAC8UfD1Uc/HlZnc +W4MUDWQ1YddDMg7SAHIJlAxT0t1FteMg5owwIDoxmjN2aETl6VKMNBoO6SGBiUo+XBJCj3ka6CY6 +5uJ8jix++slTiNLaqVVGTWEGPWoQW1EAj17EkU7iSKbHSID5bqkCR0R3NWdG9xY9t2wmvWCqPkM0 +BS0VhX2hHQJMfaIQFXoKgwM1G0aEggcbzBJk5LQchR/TxvQC3OQoake2U9vwGU7Gkr18Yf3gwZks +Ae8yiFDAAGkb1q2gjX167NAhCL9ubPbgktYaVaeil6fN2oKH5iHIvEJnVDMrTzx1sdsMW8tol5UD +p4HQSAKKvoKLS2VRClztCqDRrBwoIW68eGC6ubLpaYhgVtY3mnFqRCyLBJGIFRkF2VRdBbAeeMKe +RgRDKrbKFKPMOC0pksAIe7IZWMWk2dAeQpRBOKj4IoISVVGVkfhB6vfBl0JmGZ/A2DDPzS9cS2rO +ztz49W941d97y9K3foN/7GDTzhfcbPODf/7Jf/lT5972J8ea0RG3AgJraBttP2pVarf+7b9757/7 +Gf2Gg4/83ls//CM/GTx+Wq/OP5L2T4btimWh3DXE5ASplt1aWFcEmitsPiUrLJ2ZhIjLel8xaAor +UfG+R3KNxTMzfLLVD1u8nVHYVH/YN95M2HC3P4+9NxGft53b5IXveb/FKDTqv4CcwZ6vQtBpz22u +noA6cdCY8N//rLjE7nYybLZlSeUoYy+1QRFHlYV/H+oOKlx/yZdIao1tsM1HZBBPgZ76UlGLwakU +nxQeg8Qmi9foPlU3V/WYKZSS51N2NHhf3uGaxB+K4g21ZUG6kcWLWiuyHBMv8TeFpepXeeylLkKp +2KgXGauDzUiZwNrHoKjwNti4SVAJfAkHlRhYYWHVZ1wcpXcUdgnNmjSysyiPulocmKjHQJGAlkoT +Yhs0B6xGloOSN2QF0c4ABf9mmc6n7hvZWjs/eVZ78lx06kK3C8kzKJ6SasslmUukEHPkdhajAP21 +wQBzUODTQMhbhkz9KrWWolmqylP6aAd8t2Fd7zhL3szi1NqFFfpr9Fvzg4cOYK3s9Hwjr0+X7euP +Vj/wiaeeOhfVTfOGkn3o0PUfffxRe658qDL1zGNnI0ObWSoZTowsoKWVNi6CupHdcMPM/GFQhOzW +pl+DFp1VXl9r+50gbvnpeqivauaagXQW1l10eITMQQhXcMFuHGysr7dQnqB56dQhc+kYagr11qrz +xJlmWoXKedLv+PWy/ZK7b2mur/p+p1QuBUGImCTaT3qmMTO7cOrMOmpBoYE9PY3evb3FQ9aBIxWM +Qb12CKm8+z9z4sF7z7/89sNat3/+8fW8DdYnwDGrNByk46J+hN7ufUcrHURVJTK6seua9fLUuTOr +czNLGL+1jTXPrQUX+tFJP9/IASCcmMznspKf8VIEDyDjDXg1wUFGLhTtr0ArZfsNyMlzQshDiynj +eZCugzFF9icqRmgVoMEUtme3rizsArl9JHJZpI97lyEJTZcSJTa1heljL7v7hm94Y+Plrwhn5lOk +X2F9XTj/2G/99om3v32qGc6XXMQmyvxYtJYbG3fd9vKf/LFjf+M7+qdOPfpbv3nvr/+muxlWarMn +wuShqI3I8yAZqiJUW0+UenLUMzZ8MKVxDctxUXuryoLBCJM5yJdQWQdc0aHhDzqPvGDFFc+wyLOr +pVgOOlrfsds6M1H5erCOqaV7QHEfhUbxffdc7dSi9MX0GqPhfD2jGHu9igjl82AARtf1Kzud/Rhc +u97rUWjcz04mnJ4KWIx9FVN+MPEL5Ny21ZjTXMjRbJ3v2Lwegd4RaNxucwoTXqEmT3mAoMMo0DA8 +jT+LGg03LAggFPUWHW6pABeslGCY8jFZAq4aOLEAg86dNIeymVa0XdMrm17JdNDkFV+knuJDIlEd +og9GHiIm1skTaI5GADbH1ajn4lDaBFItqJbzYs1O4rKr16bLes1bDqLHzocf/kzy2Uf9h5/pXURm +TnOhnAlHE6KkbAohQTuG8oobw6sd+Zl/UiNf9BWUzVjazpas7KGkSEPoaBGV4Zx5RsVGTyMIcq6t +bpbK1TjJDiwtwZ9p93oeiCGaEfb0sjV94ZmNBMHgyDcrqCDoRz2sqyiiMFsbqDZEyYPtlT12TNJz +1FrMzs1MT09hVFcurlTRlSnP1zY2kQxFJHBmfpYSsJ0YiU1WcOg5TIZoSrOOlLzryrXrGrGTxpA7 +q5lu1arUqgCs9W7LrlqOBxE68HXz6ZmpAFFUQGWSl2sV5jJheVQgf5Nutv1yAy68FvTCsBOWgeUH +Ds1X5qEasHZ88+F7TqRt7cDhKoLJ3R4ISznsER1U2Hk389I+ujwjmbvgVhdr5ZlylPYPHDx0+tTF +DB6+ZmyubSLFOWU2NGD3ZqT1UQop8kdSO1QoJMEwwrgyZ8wQOuYLhgNa7tS1YeCbAQdXBpvEGOmi +UfRxQaw9TXugybQ7zVYnjVLKvcpiXik5C/WpGmhc1drSK77kxr/2zTf+jb9q3HlLF4SomjcVRivv +fP9nf/o/rLzn/Tem1hRmBfx9jyynjlf1vv07X/kf/u3SG76297F7P/uvf/b4/3in0fLnylPrenwc +lo+EU4ZP9zZoLOLd2x4yJasuj5egUAGK6lkbVBuqx2/wtfXuqCVbzNLhVle2Bo6e/LY9qFMavBTq +7bXaqTP5YnrtCKhuX6bHVu192B/XYHAmGh8T7xNOQhXr7fE1jkZifY2f+6VO49pC48QpNfRsLgsa +R1ukqEdYvDnBveKH7YPDOvsd0FgkQwpHs3heFPtXRVplGKVrnhRs8AFXhHpSFQs5HBj0lHymkBvL +zVCeaDhMJepuyQAoemUL35FoJKcCaMmKeS3q5X4r81tG0PO0rGJbqDEj89SMkVkEV8LIulDArKA/ +e33KnlpCP6XHT/ufeqT1kc/17z0RrzTDDiRa0IaRkTYsvOykxHgib7EKyRWAOFzdVBvJ4dIlf1bL +l3yJy6g0ztQwouYAnTG0iuPOVFp+C/0bUIjfbPpoXQTkn56ZDQK/1WzCYWpvhAdnK3ffduyhB091 +Uc6oxzXPay3HqMvsB/2SDtcta3cDci4hOAPR8yhpNf3AjxpTtTBoV8tut+vDvWMtn6MdPrS0sQqw +g2gbzwyLuA9583nHmPc2ojbq6r1aCbV7Sd6fn/OQBVzehGy541VduGQARNyBhfl53KUWtEOhGoqW +WGnilNwgCtGMC224AOd9P6hX9UMHG9NTzhu++vVBO/3o+z93/oGN6EIMz+zArbMdI231cSma7rFk +xTrkaeUcYgda1YDnWpr21lub9enpONI7LR/FlBraX7bjtJW2ITJ7oRdvIq4Kn5+JQcRkmW7GPWKn +ZtgtsH5Yf8GiDgZc8Z4JJg++s3gfPzOvyG5inGGUUQcpCeTgJERLKYxSHxKvyJ9SI568LsuoTje8 +hdnyjdfd9PVfc/u3ffPMl716w3U9pHEzLfjU5z/9M7/w0Ft/1z1/fq5R7oQh896mteJHq8eOXv// ++9G7/80/K83MHf/Vt37in/+b0/d+Au2sq6XGWqI9GHTOko+r1JyK1zZo5MOwC1ZsLSqcSqJlL0+k +gkbFDxj7GKs+Cix9FqFx5zoptu9wiZgMjTz/Acn8i5Ch+oZrUbxxDbBxX7uYYKNMxNedn5/4kWKO +FvjBHVy9oVTsZQ+LBHNOlcaPf03yGkc9SBWElUOp8OdWHHFrrC8FjUP7UZUry68Sy+E39UWuqdAO +BoEhodgXsmyIHwkX0oCcG+KanotgmQEf0fMgdQooBFgyhsrIXGKmoRV2Tb+l9dpYyD0tAfukjtgZ +ig0M+IrYB8ggkDCLarWS16iD7fj0evapR5r3PLDy6cf8k6toP1wpWwgGxsi7Ses/1MNJEFWGQBpa +UXVFyCOK4snnfyAfobbhS+oiRlRD5OIZUGXVHq8UH0lAiK1ZRsNOLPTwSOq16W4bzljsd/v1Gs66 +FAbwmJwTTz9jGutHr6sfPFSv1uthLzBTr7upB2lSr9ba681KtTo9O7eyugFfaXpqyu8GG6tBkiBH +aFYbXqvbhT8Kcmm/g46S4exUNQJutkP20UJPLZxLGaUa8Kq83kY/aPpoQg866ey0A2wLk3S9E8F/ +RFoxi7Sgi0Yg9NWQnGs2W+VKBf2m2t32VH26ebGrBx6YMkDVxpT1+je88rob50s1RHTbn/v00499 +blnbABVXv/72BX3OaaVhzE68mlHV7VkPpSZgrIBdPLU4VWmgA1YahGG5NnXu/MrM1Bx4smkzjlbC +eC3K2hkrNxAjxW1B9hRTm0WrUokBmwkFkqATe6AXUwqPhanUjIO3TBOH1RksbGWjK7aShM8LAaOQ +Uq9ZEMXdACdlZxo83bJjosSn4pQwqnO33lx/1V1LX//aqa96RQeeouUcbsxHT5+479d/8xO/8pvx +I4/DTIC7HgXRtAZLwn4yT6rf/LWv+vmfOfbXv9M/8dRD//pn7vvlX72wcR6FoXPV2Sxx7o3aJ0nE +tVWnkT2gUabSGExIlaaaYypESUtM8vWisiiU3DFI4ibFgzwarRUaBJeD4ZqwryXz0hsNAVktaLJa +jK45+4LGPdawa3WeV3mZl/XxsVzjv4dRetWv50mukdIge752yTVOUlh6NnKNMgvHQHk8lMFr4PI3 +JEYPnq1RMSCWpfNjl8w1Fqv/IAsoCDd6UKYrxM4eyzXKrRzmGkUrvIDX4mxEak0eI5bHSy9ZUa4R +i13wHEkV4RUC/yBzyhwLeyDxd6wt4tEhvAlRktiIKF9GceygZ8LdgLwLgFCKFAFDoLVgaSx5ZWmW +4WS6c26lfWY5PL0anN2I/BDN46kCxsWa/R4Ae26OKCXzM0IrZcKJcQSRG6A8eWESjySt1bo0tHYk +fLqVa2TdOXM8lMVU0AiFMR/ZwJtN55ZS7bpqq9VsVOY8s766stFttw4eWvRQucG2tWbYB9sTN6dz +2w23xt2p9/35J5fXg+mF2tMnLx49spSn/V4YZNDZMaxez6/BvdMRSI5mF+f7UdP02E4Zsc2D04vN +tdUkCw8cmNZTe+VEM90w+suoYc+mapUO2ipluekgvqzVUPJRzsqz5sx8tev3j59ps2IegjUwFWLR +iTX0648dOHX65NT0bA91FXEwNQWyTCdHiFfPpmYq84fLX/qalz320MMXTi53L4QbT2dupFfyEiyA +u99w45Prq07N6rSb8N5IcnEgKR75sYFI8sx0pezZ8E3rjWmorW5utuenZ1PIsZ9pBWdCaBzIoLIV +I3tJwiEC7OE2Q5cOyWMUpbIdFGLqkGWgA+shLR1nFcwCFGHmQE52XQTuJmgAho4u2A/gmKTeKAmg +hUfB8jLsEc+tlG3HLc0eOHTg1ltn77jFu/m6aLaKVieHG3PV1c7D73zfw3/2Xv3MhboOBMUcB3Rn +pdxqZXnrwNzt3/c9t/6Dt7h2ZeMD9z74q7929r57AdL9ilWJy3bonIp7D5iJz4woureQjDXMzw2g +qlh0BDWV3bXlPg7BVMUv4PaCbARXWD1HUBkcCVsMHs/BM/1irvGqoenydjCWa3wBeY0Tc40Tiyom +Qeclx/HaBlQLI230aGNRfnnCxpKPhak4yWsctWcHz+fAr5PHcuwCBeB2D6gOvEyexEgaUn4RHr1o +dg00xfmmZBPFa2QQVZpDsQkQO0ZB4w10RUdyjWRRMGiGUGfSNyJQT1sAAUiZlSwdXZDgK2IVpygr +CDYgdExNeZVaEGlPXAzueWTjE492Hj0XbnQ9tEmQAjeeCrGLmiNYl0T8mhUFZF8y3isUh4FNLAJc +Kjq6ZfZvmbYKIgtalgyUCh3Ldwkns5pEg9BMWk1LBzyrYfVDHy4jllR4LP1Of3Z2Bp4TeJ84bmN6 +BvJwKLx75PNPhavdznqy2mzPHXX9VtLe6E7P2Qmot/0+8BGkVtQClB1nY90v10vorYhif4QSKV4Q +JBEAU89DuFsWFNHK9fI0CtbxXtZNIayqxelMHYRd1KBYfavy1JmNGAUXidvpJK7dQNI2CZDadKuQ +TdDS6647vLm5gQHAcev1GmKqmYM9BwtH6q97w8ur0+4jj54+/siaE0ytH2+XejqApQ9//KiXL+on +znaOLC20212zAgaUloJ8g8RkBGGGeqOC8g8L+txAps1mB2HzfrdnQFpoOShhbEIUmkqhH0PdpB+T +a8VuJygepfwf3pEgPKv+EUxeWJqv16qgziIQL32qJOOLCKqo41JknEFNyKBCyS9zPKcxXZ9dmp9a +mpu64eDMXTcdee2XLn3ZK72jR9BUs2KVDnm1tY9/5h0/9x8ffvcH3I0memuAv4MJQ1FZ3TidRp1X +3vj6X/qXN3/Ht8WPr5z5H3/6nn//s6uPPQoDDqHyOR+lmM69WvejVh/xW1whnV4VlR+8tkHjlvs1 +FlktPqGWLxWvZ2R40O9lxGAdTMzBc/5se43b1rsXc40TvMbnwAXcCUESUxhbubedBn2ZSR7eRO6o +OsCo67SNj7rlQQxOcbiy7t/8GOauhuez3VvlYS5xFkxvib8I7uh4tHeQliiyFEUWUdRNBfZo/o+c +pPwsfyn+x08iiKrIburhZTMLKcxXz6eob2Oc6X5zf5SuAZuOWidFh0P8Qco+VaEZK5q5yAEJEQTj +mwA29lJAIAy6btR2ZsMhp1rG6oZAmQtvAVzEEGjg60EHBQJETLQNzFLwTgwQVklDZZNhNJ2NUjS+ +0B491X7yTKcZoMZicJXFPFFwiAWTelzirDIJh/OGnwFGBl1iXpU4jgKeMpzDeSaXKHOBPCLVhVOU +2HDx4L8K4mL5toF1LBlx0bAoBps7qKTRUj51Z9lbZBgv7GRhKz04N9Pz10t1z6yUur2g22kvLU7X +odZtGiefOhN1jKCrn3pm7YYblmZmyp/83ImpxXIJcUL0c+z0UPbfbPY8z6tWa0jXHbyxFqU+KC6x +n3V7vUbNjVMfjtPSwTkkIMPV/ubT/uqjvtaDpgGLLb1ZQ5vT7KVaP7efeWK9UZ4/eN104rR7fb/X +hna2VmlUcJf8du/QgUVc7unzZyGoUCmDj4n+E6j1C2anK9OVUrPVNu1G2DfWnlipNzV7M/BM50IW +3vBVB1721Yfe96EHpqcXwn4bKT7k8k6d3USZJDDswPXzudVO+53pyszyMsK4uA6vfWGl3Df0i5q+ +jigvWlKgv690UsT94izBgGdASRJNS2DiUsYcnUK0Jevwy47E/bC8afYeb9tohIFem4iQo48Xqmso +kEthIzNKTRTyo/8w2pBVSvWl2dnrDk4dPLhww93m/Iw+W4WnVy45U2gl9sQzn/xf7zh+zyfdXjjv +eJjVSD47JauG8lBwkDx3/lv/ytf85A+5jUrrvocf+f23P/S+D2ZxgHgFWcIIGNje03ryeNxvQhxX +Q+0tpHBjiY5e5UvaPIOPJJEIBDmoWYEZqgqGVHMbRJ13QvDwsFJFOb6M7TilfTBEdq6WY0suqzWZ +ieCCOWCx7gxu4TzGbIWRRXznYnuVA/ccfPx56TXuyEXvAp+TxuZyncLx8KI4DYW7sEVXUcdUM1F9 +TT7KiHdWbDxq/imkGjcDtq5M5fYKf277BW85c/IYyZeQRuUD6nEovlQWQ63/g1T5SJpCGa/iNW5L +QzK+ozwlPqdK/huIJ3reTCfyU/QaqXEqciVKu0tCX+IjslrR8xAPwwpjIbPolREfrHgoj9fQgiHr +rGu9TTPuOvAU8UeQ/WGjV5CYTD07bZQRd7U2+vbHH/fveaT5sYc3T68megJSBp/SImUqo6JOG+rh +BGtpOAUUBBaXUGiYMDlI5a5ikJWGA+LP8sHiS4lTD42Uwf2gayAiAliT2akX7h2WZwMFJSgUAOfR +mNaqB92ZQ9NAY1RZoA4iTPvrrTa6ApYrJRBbINB2cXl1ZqYSh9Hi3KETj6/FfcSPoeudrayt33YH +CuPNp54AdbOBFFvPD8GsRJljq92vVVEvZ7db4BzV4DbBAS2BAovrAfUoxGWayA4G7WjtTFMPDS2k +ah4D1xXDnNKO3DlfnXWr1dK506sry2uCO+REhWlUa9RQHQmiKhzTeqWytrKC+4WqFQwgApscKPa2 +TBv16Tyxn370DLpoVULTBtczy9CJ+dDtU5rdP7eyWavWsVt4rOC84Jynpr2pxhSskna7NVObjv18 +dbnt96nagxCv4edxC6p+Uk/Ehhgg4IDswplD1ot0i4JwOZRhdSeJvLhx3eyxO48hYwsR9milp63T +J1YapAbMlShFMpU9pHBXIX0OTC17Rr3auOHI0kvvnL/r9qnbbskWp1LQkWxr0fTSJ059/vf+5GO/ ++7b1zz86lepTjgf3FxnXKrzUKDsLZYA7brzt+/7mG370H6KxyLn3f/wjv/u2+z7yEdCAarpezmha +6eXK+Sy5ECOsATlwBEKo90YDax9P/6RVipA0qHfiw6r62UjAvngYxwoft+bscMeTV6BJ51DsdM/N +VLxo7Fg7oVE9UKNnNvz12pzlfq7k2m0zxlD9uv3U6V27Y19yT9cCGvdzmkNbRuHikLV/DeedKL8M +XBu1jo8sycUCvSc0ysdVSm/rS1mVQ7QrfiKUyu6VO7j1krMYH1XFmFGbDP86Co3FBgKoKmpatIlS +il1wEOWLQjbMAbFRoiKggjPBwgxotVHsDaQb+ZltgUCjgM/VR7cio9PRWk0zQHmAXnMRLs08K3Ut +mP9hvexUK/U0r59e1j/1cPdTj3aePpM2u8QzkChgwSdUD1UXX7yYZZVUoorvsn8xvTx4nohzgi2i +FGOKa1WuroiRDXdRjKyyJIAyomfCQm3iqk5JT9SN+0YcVtNsycgOZdHB1D+Q2bfqlesdb9a55fpj +d11/EzrFJ+FmAj00LVg6MBvGYbPdqVTLMzPT7Wbz2KHrT59YBlvV0spB30+Q4nJC0E3bG6gw7OGC +2OgSRReuVirZMZr/Qv9Vt/xuF6rkURx5JTcMAxfwiNr51R5Oa7o621nrha24FFsuqKOIVKJxZCWd +OWTPLJrXH10EG6W13vEhcZZotToKTcBGEh4uqg+b/vzU9MZ6E6k5hhpQrZ8h7mrVqlU4+Z1mf+XU +ZrQS1SLLDvUSItpa3nfTYy+Z76fAK0iDg0YKImuXPTNZj8qCw07bn52aqXr1tZVWuxmUvRpbV8a5 +AYE2P4XQH/tfQPydHp90KtZB0UUlRpnjjbrMaqbPmY2bZ2uHGmEWJkFw/fzhteMXjDbkGGjs0LVC +/w1YFh7yjgBxFq/YlfLs0UNHX3LXDa98xfxtt5szc+DgQtlnsd4wljeOv+9DH/u9//XUvZ8pgRhl +o7211U2jAA2W65VNlHro9s1f+7Wv++Hvu+tbvmH1wcee+uP3fOp3/8fqE0+WXacE7YMk9qA9blib +tv1Y7K9AcB65UJwL5hcjE0WEZj/rzF7bcGoWZU8CPxK9EVU53BapCt5ugY/Fy/jHq/Zdd5yfYP+4 +hqpEZUY3/L8LGp8vucbnChpHIxGjqbtC2UDNhHFM2v7rxCejALAt71BN5DHzam9o5Ma7hIfHvEZ1 +WuJNDb3GMb8QfydAjGKECpsqcpx4YeqbwA6/K+VQya+xypqBVAlMCtOmKNtnuggUfLZOZJk+vmP9 +AYsU3wGDHvq5QsQLcm/ogYB1BfZ+VIn6Rqtp+e2KmVU9gCb8OdSG61XokdXLsP67vn38VPTxh1Y/ +/EjraZQChtyjB1cDbiG6ShGnmXYagUYE12kHwFnEu9RayYECRMlSqYzfwG7gMqZEXGXt2WKkDvBS +aKtsA4jkGYAQzEmQedCkHoiIX4mRqB2EOOZ0bh7VGy+dnnvVQuOuau2wgVI41KY3l5ePzi+87CV3 +QJx07sDs/OJcq9VbW9+ErBol2GyAvdftNEuec+bMKkarudmBjYCg3k03HXG9dHl5k2FCtEdCS0P6 +ygkKWZCnDPv9xUVAbK8ECTqRIPC8UnPTh2+5vtYiKUk3203f7KE3ogEySWmmXJ5zdA/Yhd4h7enZ +WqvZX1uOK7UK1E6hEgQCC9hJ6Akch+n83OxmqychZgbPqaCG0chA9kzbG71grV9PXC+E6BrppL00 +Kh0yDt4y0+p1owQh8H65VPIjgBPlfjDIkCNoN/sQCoiCMOhHkGiFh4uYOWwZx4L0EMSC4GcFsJvo +Ngo0YhzgPkLkG9nnylLZO1qbum3JmrHb/fbZU2fvvPW2mXLj6QefcGHbZGzGyWZPoOkg5IiAJqwy +12ksLCzdevPhu+44+tKXWDMz/SSv1qcW69MzQXL+E/fd+z/e+Zn3fjBc35guQaNGByL2khgp7qlS +bQ2aANXKy9/87V//z3/Eve7w5z/w4Q//9h98/v0f7G2u1UxnIUGxLC0wUIM6pvNU2D+TooEzZh5r +K9nzRLIMRbBi4gqw9wbCB1MPnOj/Ktp3oZsjs3UnBo8tHtfAd90JjdtDtBI0+r8ZGt8Ic278dZX3 +/Qo//lxBY7FSDiBDhS+HGDmOJLtfyhio7baJ8hq3sHDcg1Tvj8Rc1cI9eKmAKvFOGiaOwPSAECKg +VuyevuLQayyArrib8hjL0zx6jspvVIM9YJoWW6nSPbxUebtinCpVm6I/HkrHEVsiIoJWz/KMQtcN +YSy8CVxE3ogFakbJ1ssoVkhCJ+zaYU9r9iyEB82kUkZRXWDYUaWGZr3zujN3djX77OPtDz+8cd/x +9oUOuxaBxp/aOYocQZTIDBuSzQhoQh5ugHDFDaTU28BfpF/CWQwuSEYRVtfth6EUjFFNTuFiwfeV +0RByPSOOklakvDVycmjzFIImCmFSLIWKYqSo9lDiQUIItJt6ZaZarRpOvwVyDLomm2curjz8zDPt +NH/i5PmTp5ZTCMBp0t/CdMKwX2+UQaOEKCs86k67l0JfW3f6beRuo/kD5TDoIqMESIfkJxgeBj/l +ddsIVWYHDh7wnMrKynqtUUaWEeeLMgPU5fWDYBMWBgRR0VMeLpaeBKXcXqgEtrHW9etzDc2J6rMw +TMyLy/1yueT3e3i4MRpRhGtiPZ7pufBM/b6PIoeSAV5wD5cKBVcocqNtotnT7U4GQRkMPAKi0JS9 +6VUHKovltY0OTxC1CzBXLLsbBLBUANhJFC/MTcG42Ww3MXNwINBjkDMmvQaGEltkQvOIsQPGGViP +YRM8a0ZpseTMG6UDlbyCJiMoczUSSLoG2sLi/NrqGhpjwWUEnLLo0cVO8ClkYitTswtL1x07cvsd +B++8vXbkcOyBqWUuTk+Xkuzig4/e+7a33/uu966dOouwr03kh++aRnCq7RJmxXroH3jlS1//w3// +y9/ypm7Y/9hb3/ax337b6kOPoaTULIFmgxtOoVZMiZ5XOq+np+Kgj1Ngr2zNRokRPW/1yI0/uFe2 +2G35fATEQd6QUZ9CtnFHQ8exFfoaAaPCveGXCuYq6Vql6/h/e0D1i8lr3IqOXsLtU/N71Fnc/vPg +CdjLcSzA7ZLOZQGNY+G/7dN5AjSKx7MTg8e8RkX6HkCjoNowvzgIuSgQHhkNyS8KKiogVWS5wRUp +r5EJeMKK0OiUfDNTiZR3RgCVOiWMjwlDlHVpWMDYB0EgE2Y9gAChstgMe0a3qfda+AGLWBninBV2 +XURkbKoxb1tTT59ufeqhjU8+sf7wctBCiyfDQEIRgIve7qBcIPmI6g3ImwhPiGmebfdNoBF/YHsm +YHRjqnTd0Rro/Ch4L5Us4A2Ls3mzB5zCARyqojN4oliHEqYk6SwCr/Az9zko1pF5whKUUqWMbsib +636w2eltBhdaveUWqsw1D2WauXHxAmr3stmpReijx34IhE7R/CFCvhCYljklp9Vpz84tnD6FjCP0 +ZKqQhUOlRC9eRfwX7tTswlyz0wYpJgY2gGoS4oZYK6src1NzcYhBSSGGBi31CO00XAino9ABdY1Q +6yTdtw95tSnDm61ttiEXgFaUxtyckybtpelG+2IPzq9ThosJ5E9RMA8OUYAgMU66VkaTqaoJkboA +VxyDioNKSeytk4RrUQ1qrGiaZeW+rh27e/HIHfOdpLu80kGJgeN6axvdOlR14gipSkw1+HOHDx9A +O4sYdYGNRgL6ULsz04CgD7AJxfqwmICXsKdYYOJAWABTp2zah8q1O2aCqRh0lyTou2hE7XdREHL9 +0RvPLS+XqqWpWi31Y9RveBA/Arm2Wq4tzM5df+zgTTcdvPWW6qHDSbUcwgYrl+ertebjT9/zh3/8 +2Xe/b+2Jp5EPR3cRCvaiATXmK8MNVitJNm3jK7/nLV/zQ3/vyEtuv/DgY+/9d7/2uXe9J25ukFkL +wIaevGF0bR1J1JLlrpjmU0GvzRQp9oFZAE5sinZbeEaEG3PV0Ci2mXo+VYZR7OKCNsM6pSIzMu4m +joecrgyRt31qkon/fz00Pje5Rmm5OvgSkiC+WC42/CpC3SP/jJNf9hVcn3S3C5AYmSNjGKqCrYX/ +egl0ZXHAXl+CN1xhJWoy8jU2Mbk+j/5ZYV5hmMpZkvZS+DcFBquUvfLt2GlCubtEOLpE+Ffl3tRW ++KyI0zB3UZiHPDEWUSlhGKVaA1aDlP1J8ZWKsor0N0AOWxAI5Y1BJyDkpNhT3XYrNoDOQ78KlhRA +ILsE294y6hXESkMr8vNuK2lt2lFYd52pigMtTyQUK1XPq8/2k8ajx8N7PnnxsWfis+tJN+LVYMkH +4wXnTmIp2zCAqw9fjg3ZpbGBkFN51ljGVH8raKkgiQbnDs028ump/PVvnPmKN8KtME+f6iKG2e6C +KoL1Gw4W60koaS09DkUKlcOMbksxsZAtGTkJRbhbQkjEYnHVqJodzBjmDdV0zuwEkK7W4g7ptzAE +KrVaqKXQUnFKVWj89DpdiN3AvU2SoANqaNDHIEH2DnZCEiMDpyG4eu50t1q2on74DW/80q96zS3z +C3a1bB47eHjtdFvrx1M1u+d3IUxaqdl+GLQ6/VLVXtvsNOpTvR661CduyYpRaoFhsvLaXCViQ464 +NF2BWt3KGvR3atD/XJyZQhiY+d1qeaMNQXYT3BN4X0qy00WLxTitIHOGopMI5GB4kRHrWVCOn1pB +P4GHhmuGdYEp01i0j33JQmjmbT9YWd9cWJz1W10zMtzMRuYN1lEQBHOLs8jvrq4uk0tFJpQxNVN+ +9Ze/bGPzXBC0y1X9xlsP1KbAL836YS80ArQWKR8slWahC5jV3CqqM5sbLU5g35+rNuYXF7pJGy1N +KvUKtO2g0VMrN6amp+cPH5q/6cbq4cPewlxacvWSU7atBafUfuzEvf/rTz/2x//74pMnXHRgRCQU +AXZZP9irS2y+OE0XbrzuW3/8B17xPd+GUP+H3vbH7/yV3zj76KMMwjODjTAta0thgVnoXWV7bU0/ +H3bRjoqaOhwzTDGwnch79uCLksoFWlYCVXmwg7GmoXwGURRsCU2KUtmmOOqodPglEEwwp4jdyMQW +E06i3Jiz6P9IS1QiIlLlr6BT9DUK6aYt5vslzXOhzMltv+SauH0NGo8vKXN5hMAnqC1kPPnackO2 +eZ/XBLa/UDsZo+E8N17jKLNr6KiNot2+kG/igE2Exj33UHz66uzCAlH3vB5Bt7Etds7gotJ+23xV +Dp4AZHGgwnFUiKw+pKSnZGIrsB18qnAgi+gMtwCpQSjkW19Src+ngPWHWB+AK3QNyW+hrg2/0FsR +X6zfRzrRhtwbukYgw5RlYJIYqGXfWClrwXwFAiUgE4LFHyMbVJ1a6saVzz++8dHPnXv0ZLsVZBDU +ZBJRzOaBvnJhkyuKwvAXuRZZPVhzAqMATEdpXqRyZlp68Ijx+jcebtzsgMr55ENtLQGjotTrQxzH +QQFbBlaillCDGvVwpODnPkoQENZE5ykLpQWykg6EYTF2gElAKPaM9TmoZNaBUmmuhCJx0CeTbmp0 +oqyHD0nvCah6gnYEOe8NyMb1piuVxA99H8QZN0WDizDxPLfv++iqe2Dp4MrKRpzY0HL53INPeg37 +0K2HNsJWB+FNLV/rdWPb7oFZm+i9jR68H69qhXEEZRhkGefmppE6RVU7RHegsA3NHWjIdTp9Vrp7 +JeikdTrhkSNQaOtVQTspA3F8u2y1ouRCK8QaD521JIRgHrPSWC2hwtPudQOcB8YXlkZKwosBbzVI +Fqcah+bnUF3fDCPvsFu9vgYd2zPLq+XZip8gWOwiawkuERAUgwN6y2x9qgVFOuQVgViMQGeIoG60 +LnLKOPatt9x2y623PPHk47BOoB5emilNH52uLZZjPel2gySEpgM8PDsBMcctzyws5p6DrhnlKQ+q +cHMzi3Gsz8wvHLzhhuqBA97MrFkq4ZbXcPaG0z1z/t53f+D+D9xz9qHHAfNV6EFYFiof4UmjgUif +jrcRJBlKRm59/Wu+6Ud/4KbXv/bMfQ+846d/8eF3vC/ZaEGgFTYk8qWI9WISoSMI5lM9Q1jVOJlF +qxkuFeYEeMQ2epVgXuBuBkmKug3KN8HUoHIwMIyTEmiK0AZUCmq1coDyW6LNxAqyLRP4UqsRCcby +eLPAV6V8BvoffKiVqM6k1x6gqD6q1pDR1zZHdWDdK195GH1SNv3kq5h0gs/Hv3+BoXF4Qybf3ssd +vUnQOPGIxTy83OPu3P6SPmdh543kN7fQa3Q3A66MIILCBilfUFBXvKNgTz09W9A4NBVlY6npLyr7 +5Qe6RfSPxCIVMZuBlg0crEFvIMFFySmyTyxbAEE1G2Jvg05SrMtwQKG3K4jzoQYsCUFeRDWD3W9N +OykWdksPSiWzMVOvT8+v+u5nHtq457PnnzoTtkMHDWJJwGf1IKVnqBegkG+vlzyQvFKesWwtrHcA +Y565pXxukT0ezz3dffxhMFa8+fnG5mbTsLGQgbWP2gTQUGwECnE0ZNKkeEyKMqmTI2LXstqoIJdi ++OD32MkDT3PnnenFBgiSvY4PrfC8oyVdxiHxOdtFuUAJiFsqVSDSicp9lP+zmkXLA1Rl9MH1BBYC +mADEdhRYx59s08HSrBNPLq+c3zh34sK5p5YX6gutjf7GOjKJaC7oYtmueG6IigXDvP7Ydf2e3+t0 +yp6HGC61uTOzudGB9ltrAywRI4rg8ID/Ar+5koJvYqaVCvQW4tpUxYid9dO9rI/iB+iAYiUHFEBO +wSLpaaVpd1H6mLtdDd+dTur5abmf2L0YbRvR9RCipFMHppBZhTQdviCg0+6GaAYMFwmXjWpC6L3N +QqfURGePoNZAaAAUIjgUkAGCmHkAoDq0eJ2tVz/yoXs76PsxO5XgbCqoAjU6QW+91bGdumlVMWio +35iabUAADtIBgEn04QJRCDGLudpiozKD0hevVos0wJ6OypOG7XXPr3zuA/fc8473LD/yFGTR3TxH +ZBgzFPMaIeZNZIBtHAQWTewdXPryv/+mN/zj70X4/r4/+rM/+8+/v/LYkwg3o1SIFNA0r2A46BXG ++B/TGjbgBS17Mgs7gD2qVpBcW/Ec1FYGsbCjUc9CYExwNATYmWpA9aP0ZZmbqUPaHPYGaLIMZewD +t/aY6XwWxGOVx5ytYYZEO+XE7fM1ERq3bOfdcA7H2RIeGNjhV1C7su/z3edlPbubjZX8/zxYhFf9 +mgw5I7d0+ONYqcGkcn6uhpc/zNt1A4rwxFVf8J47KLJ3l95GbMsx0bc95rGyGIuXdOgZ3bgIqMqb +CKiOeY1ES364eMoGrqr6lSkNiGWTcSK6zaSOFwYvizFYqcEmsFjm6EZS4AYiqGiFBCoJYRKcG8tE ++8LcA0nB99Fq2IjQEqhXdRH0gh8A72YmQW34heYTJ9tPXYTDgwo11nuT6pdKn3h55IlHvIbtTXZ2 +6E7wyorCTTrNTHaxPRE6VCEyitqKw+ahA7XmevTMCd+rVe6+dfGhx07AW8T+QTEJEH8ELtporkch +ThZ6SEcNoiPWNsFCrL8Iu8IvkBArl6fI0/wZrXZbdfbYjJFm555ZjpuZtq4nfWrT2FN27eiUu1CC +r4Pyyn63TxEguDwWQrdgunRhdyC0Wy7bERA1Me+85aVhp9xe7Xzu/gdml9xapXRoYQ5yZc88g8KH +plWHsgt6ZlRaaxsb0G8zTXBwqtXyTTffcPb06eWVjelpjyeeIfoIfXI0JsFwsMdHvQ5GVAK/Bwm2 +mRnn0LFqam7UpmubF/NHP7PZ7uYgsKC9CR+eMMrb4WK51jm3aUPYPM1LWPMpbUD2qLjufAjRDqQL +U6JqamVt0w/LDau+NGNUHQYPPWetvQ558XIN0c0ZH+U1YKjgI6ApWcgiWxAyiALfs0pmVu61oPcD +uffQqWph6OP+IfkIXkya2QvzhxGFRjJwdeOZ+QM1KJsvzB6t/f/Zew8AN6qza1jSSJqRRr2sthev +1173Dm6A6S10AqEmIXlDOqQS0vMmhPTkTQUSUggkJKRQE0joLtjgXtdbvL2pd2lmJM1/njvSetc2 +XlNS/08RjnZX0ty5c+c+7Tzn2N2pfKq+uRoVXJdQBXIDQ47oyGHLnRZ7fCS0Y9NLB7fvzYyHxaLO +AscNtWi6aAa4JuiAFDFuELgjZleLJ51x2mk331C1cNb+LVuf/tlvBrbtLeRyOosZnLe8UrJjdbM8 +BeSrYCwRmoOQMF5S+0ulUaxNNLTq8CegYktepx2p02gcLOlgd+NzJejAUPcSMvOwpkhFwL2r8jtx +Q4yMRtlNSpx20+5SUxNGx9gmyOUrsxETvyFtfWXkGHszXajp7a/mK7PPHnt7pqhxSuJuCk0A/qRV +bdh/laQpVe5P4NiTzokV/P9jHpNb/vVvimmc9tThYk+853WbxmmPMq3tPBHtSW0tvrHHNMsB+++0 +plGrNdKqrLhqtMQrtUZ2g2hJR1ZrfBXTyH5/2JSWu9wrCVUaBDr4WSM/5VEJkkkfYMqIxHVKGByA +QLA/EcUXAIYmbIHozUBPP/i+uUIOxKcAg5pSWWMpZzYV7MBO8CAPc2YK5sGg3DWS6RtKRrLYvywo +zxRJGkIFxyiKM6jmlOt81D5RJt447pxr6CAyoVQeo3iPsVWinseBL0bG7oxKDQGAsEs6DMuX1Iaj +42mU1PR8KluKJ8HvDVY1AzKI2NEwtcgQI+mHvRJuORjAgMRhdUiUOmlvI5MBiSwRWFLVMtNmchO9 +TxrSwFljelguJRToK8FbAMbS6OcBgvHXeBEgxuIZIDmhA1UoZMB+jQ7FglRwOqFUnOH0xTkzF8TH +5YvPu1LvyLyw7y+RmDQwEMmkia8N0+Kr9jqctlKuEOoNp6MAH4mgwimp8qzZLbPbWjZs2IDAzO32 +xKJxiB1DkWN8LAnPhbAuaIYBOQGY0/LFhkbPnAX1idwIasHRlGHX3nAuWfSYXaaMvhBOG+J5Q1p1 +ENCphLZSirTIRSAtD1x82EcSYgSABYS3OhNJciD9jEQlp89zoJTTBVobnXVV3eHegl3nqfflFGl8 +OOi0uFCuw+XEZFoROpuMQN5mkzmH1YtrAkJUSU7mdWiCgJEz2ZwueArwkFD7BejIbCzGUsMuPzKo +Vr+vyeMIyErB7XNRV1CRz4fTbs5iM3Djw2OdOw4c3LY7Nho0olWDEqGI9GghkPQx036BdwU8qk4u +AgS05ubrF771/Fwite23j734xyfC8bjVaISGGBWUAWAuqrxqRJk5A3I9TufE0tSbkqqhB6lUqrEa +qbqIaaHFRZ5Ta40fyeLQeDijwJ8jtgLGQ0BpfahsV3sc/irPof6RrKSADB19MsjIHKn4+tq3EsIS +w1mlFmKWx2D+I0uAahuLRvE0zTb1mk0js7kTg8WhDpMsVuAKGhn69Gb5tZ/yv8kn/p9pPN6F+Dcx +jdA00EapYZbohmCQdA2GQ8U2Rm42TdTIYq3DpUbifiobRvoDQihKMZJpJEYbRiNHRhERIaCi7AXa +8mEkqC+DQkXkzajayMG6SBkuEzdKWUtJQhIQOw+AOCCYTkuG4fHCoZFs90AiWiCYK6D3siwj0UY3 +NLu7SY+IkqjEj0V0bJpU8pGFjyOvEbtXAa+njUEFcoK4NMFniiQeFO5phvAtRNJqLFit8kkne71+ +QygcQWgUChf7B/LBsC4PSArYCAg3Qwq3sq6YzKA+xAJGmEuqfeLEVIQGVIsCj5uoA32otUXMGVJI +r4FcHI3jybCkS6gQICzliwaH2d7olI2ZlpYat8u9f1+nauAhHFXU5RGjoUKbzaBPEKilopQvrl25 +ct/2yPNP71+xyu6pMafynFKygkI9EYtUBzypVBodfxCnioykzZyYzQOVisKoAhjs8uVzLBZ+5/Zt +VVWeDGJT0qXg4ilIFCsFBR0RqMDxgAClkoV0MrdsZb2iJk1W2B7LaH9KGcuLWY6LymaMpKQTEOmr +iJNQFwZ21FRScFGgZ4mWUBMyhUgGAsoC9m5qIqCamgLAsUxzoU9hMLwprVfMNVZ7s9fW4O8ZHHQ7 +qyDCxcPZUXXE/Wq0lpRSMhFGtwXBVaRCU0ONmdf3DY1h3YH5z+33qCZdmiJISlnA77LaLS6/XRAt +BhVEr25KTqCfkdOJnL0Uh9rF2MCujo6dByOD42qhgEwoLBwVx6lYSAsHWQijCm4CAhvH+WLNnNYr +3vH2ucuWrn/6+Sd++8exg4dwHCfRtkK8BKwLAFPTIjNR+oK4VAs62UpKwjoQLh0CEzycNqSdCcqj +Z7qPxNxqMxvrPW671dw7NAbeAxDb4Q24T6CRgsJqfUNN31A4msihtxeJ/SLCd4YtfIMP+I1MXQt5 +3bIpJOzZ4S/9f6bxDU7wq378TSaKY0H34dqaxobKIBPlF+zHKTwLmuPxWkPtSQcpV+wqOfBKAa9S +lmNFtrI5mQLEKhfpJo32qFmaGNurvqgwGE56gxbDlB84opYtPXzWzPljKcQyvqTcXF8pbzPgJQuJ +2JMV38umsfy15XeydgstjmRdd8yn1Ho2aAxaWZHZuHI1UXM6yfQwGAa1xWvdiozijfxgxIcUFZJE +BmA11KsPTDvx20BlAeEhnHooJMLzRm+6zoF8qZJB87mQTLhLBVGFbkbe5jZ4q6uKnLN3VN7dmdzZ +ERsM5iF5i49gSMCJwAISpo8YWRm6l0WvZZ+UIYqOtotHZcIJWatNhbaqCPbBAgZs5PQPvhaJMmOJ +txa9VeqMVl3TDIPbK9sdwLoiEYl+PIu7ymX08ZJbJ9XySZ86bs7paywlj7nk4GShIIslvdOkiHzJ +YTJ4TCUEFD6DWA18rTmThZYUsqWWHPoLbYIR9sNYzME3ESEVgZSfJS0rMVQikV1U0XGYtVstmEvk +9pCdo5puSY3HADLVn3/+aYPDA4N98WQUCUu/MctduPqMnc/uc5REO1LXpNClQkjKaLJCvx6lMCB5 +rLwhm876/OBx4/sGxqkKZuBQcywCp+oSaYunpjsVcTwUsWAqnS5rNCJ17Y1Z44LhUNoaLojxgjWn +opSH+J9og3AtOHgVJfTQ4wxQuYWTYgSYFYBawJIIqIvgu2hGtpxSCayvVQdKW6wLxm2DTg2IhkTS +frunvbU9FA5icbntTpLLsIgInYHWicQSuFr4kpYZVUUEwsDHmFGK5XkrsKm0wwNC6/NVudx+u93v +tFfbRJsRs0vYHbRO4kpxhYz8yt83bHrs+f2b9+SiSR7pTVhvo1kqFHKlUgqXE+YdqWvMBqU+IJys +W3PpWy7/6IfQBXn/d+965v4/pkIxfBEZsgIMFqDODLuKRcPOiXC+RZCrGwpmc9CgHwZ4h1qBsJ4Y +lJ4WGa1ReIQ43WQ+73Q46+s8Sibl5ArVToPXUfI6jA31vlgsMwJaO9Y+C5+D3D1y/97oo1xhoCwR +tUxVaiXajc52hglMjLaTsaiygpWZAM3QhqC9sYJ2ZyHoxOZ39BasbTIVc6z53ZUPTJzUxD5XPvYb +Pdt/p8+/+TCcyeXZyfnSY1qX1z8V0y26ib9PTjZMSTwcxq9M2LEjhzNdomKSwasY+LJKDVudWubj +sGWrmEvNmpWXNLNck5byZL+hbGInMbmUraRmP8p5VDpA2RayO7n8h3L6lNlFzW9gOVJNUZ3ZRZY9 +1ZSHKzrDwBOQyDrCQgOpKhK6hrcC/SjwFmjuEjREtBhtEO5TAP+IqImgE+62muVKUIrgfPW1Oouv +sz+zdW9058HEwHhJQvYNeSfi6oZpwNZFZCZ0ttoo2M33WvAE2gxqe47GRsA6VVgQDW8d5U8kuNCF +QL0ZBrDAqM0txtY2i9eHZGYBGBHCyJjtSOnFEtJwLDMK1rAazjfP757pypnTJYviqLahqcCAkCZg +tNYIJp9OdSkGj07w87wNwAodULfoO4TXUJDQnl8AYRq6JvIGxWiDrIQOAo2gi0mn0CGIzKLqcjuR +bstAmEKCoCG18YA0D0TjqUgChN1gq87FMpkQirzKWDB81oVnWKuE57ftRGMIsCpQPaIKj75kd9tk +TtJbwRoDBLAxkgzVNPmKYBxHs2NBh0ouUroOG6Sh0BWDeQBqJ+/3G5cvbwJcxWV0Jg4lpf6cI1sU +0T0IOUPs8rDmCJgg4cQZcCWhx2wG9hJMClQ2pVojAj0kmUlADHOK+hsQCIiPKK2O6A6JVfobaWeC +1Q4Rd1q2A4IE/vBMXKxyQ1fTKlrBMIDFFgmh6saJVpvTI7bPaQtFQmhIoHQ81hVZM4uVt1kFW8BX +43S6KUwlkCfYuwXBalbNkKccGxzp6tyx54VHNiQGUzZCg8FPwCyWMngfCoRGI9UIsZ7QcWIwR+Wc +pan2xk+8/+Qz1m1//qXffP/uXa/sgkmA9TXCJJPt12423ATMP6N1SDYGBhLeXxIs6KViBATyAKWi +MwaVUG0TYVEa0svY2YDLBfe6325cPMtTbcvXBZSWRmNzEyJYY3dPCOErCqhA+pKm54Tpev17HH2S ++nXLdouSOFSKZ0nVSsXvSArJiht92OOvuJDaljGJWGvyW46Kbtk8lR1Q2mA00/iqtv5EsbhvbDL+ +qZ9+803jlOmrGKjXGhROPwcnYBrLPlHlAk/BumhR27SP6d6iFSynPMths7b/V0zV1ANp3lj5Lp3k +nWn7fuW9hxciU0E8DLBmfyh/tfb7ssUoZ0sr5rNSWdRWObvPtFCSUDW0TzMbiRQZlRZhKQHrZEQ2 +8PfBIoNN0SgIJkKPCNjB0HAtOKlfnNel1HSoFAtZId6Lrymm7Q6jr85vsvt6x/XPbw3BKI5E0VJu +RVcdNPnIJGq5Uu3enBZ4MF0KAd9C4Fm6yxHqkJkEoxekBqHplAWPChrhDXrIuAPZ4XSodfXGujqT +TURWDFoLJAIYGpc79seGB5V4WieD4dOqWn28IyB4au0lUymvyvYqt8VjQ87R1eCwV9v1YG4TUMJU +ES/KeXwJwg4EKlBkElgfOMeL9pysZPAf7D8StQD8KAD4g8Qaj2IqljSWDB6L3ZAr6dKyiPpdVrXI +huED/RYZjDNys90mxbLJWGk81Dd3aQMnZhJZ9NSZoPrBDD9oQ9EkkiOLxAlITuYyJE61bs2qwZ44 +RpLNp9HFUJKK6UQGdAEQxUKDCnb1TDSZG8/JQ2mlN+0pgs1GxwN+CTtBtSsVDgraDxH58dTDrlqo +jmwQQF2Eaw5Lgh4cMl8kc4HzNemBkyFyB4TijOFIE0WhvLtDsNh5IYOIKRx1uJ2825bIJyW1CHyQ +Es8qsWyV04uzqGuoJ66fZAa2zcJbwWbjsLlFwSFaodjhFCwWxu5XtFpR/tPZXI5cIbu7c+vuA68M +HDoUPhRMD+ZEsBaAQE9VJcyLqlpRVsZiKhWs1DdkQDdOSlc645ILbrj1f+L5zH0/+NnfH3sqHok7 +UBQHlx3y7cyfYmpXtOEziRmsI0KSUCiMphUTQK1yDN2IuC0oS8ran+iO0W4essgYIjEXgMW+kG2s +Myxfyjc2FhobzW6fMRpHz04R8FUsEAI4a9UNLUJ7o4+ya63xjWu+JOV9ysiBo22SdsypZozddBN/ +qOwXkzabozbow5uG9nXHMI2TT+7NONE3OlFv8udfG0L1RCycptKpPSZHjYd/OR0AddpY7eg5eDXk +FY1hapX6NX35tOaTECRTH9pItCKe9hdapOyGrEzK4Y5DCvumkBNO5DUqeVLtG8q8LOx1GXHDbHt5 +vWqOJAvH6H/0/VpfozYMbSSaTdKaFElSkeFOSWNdE5CibQ8lDYggII0FAwnZQR4QS2yiJDJl4izI +p0IBIhbkcmlRJxkLGTQMImcmii6T4BuJFXYeHO0aTitEPY0KDofgRZLztBnR4Vmyl231ZOSPNWOT +18zxl5lGSFA2iiS+RZw5GscbIU21naRYrHYb5syyzp1ra201OB3AwmTSGV1wzLxnn7JvbyESNaWL +prylIPlkQ5PJ0mJFp53b4x0ZCiaQtTPzyAJTk4PRDDMDnAih8YpqPpNVQSfGpILguGNnh+wU8m55 +pZAC+gJZVTSpkDSJWoLVBRQ3XhCNOg8oAJCzlhTwkQL8g7ECowJybMy1hLeBXtSkQ+scmi9TktzU +Xh/KxkuCOaVTACjCJYHsYyyZATsQwB6w/9icMbVvecvaSCy7a/8ePToPDJZiXsll0rwdWpikWukV +bC6dpfvlfmtSV1UC9wLodoBcJRkoMJAzeU3CIGFZCQSlIsMBpA2ayxF30tIg2CbtuQgY8RPMFgh0 +mJ4KZeCRFwYeFnKHRFerIOVrRGU3AzQqInavRZwRiKsyam5mxdxc1ZDKZK0uW9PMltBYEI2YqOCB +Yc5qcYLcBuYYawF22GKF6Ia2V5TSUiSVi23dsa1nsFuwGoWiGNoRt0T1TnRjokqLAi8g06hbI/qE +WImFo4ibU/0NzZedf35jY8MjT/795Q3rEVQKWGxU8SsKRhh90sKmHnVUUlktmvpLaAaAKyJtDZxR +Rocqo5ygpn5GQsOwLaxaQrcvmHQRwDN6dID41Tqvfs4MwzmnCLUBxJGSXDAdGlB37Fb2dyvjcV22 +gIQq3XwMQj7Nljm9o0gAbHZvEwgbaGXNqiMq1XYVAqFNaW0s31+TTSOraJV3IbroGvx4slfNRLun +PMpRYwXXqgXbk8Oeyfs866Gq4CCO3p0rv/nPRahy07b8T0zPq72o2ILKZFS2wROxqRNTOu1R2E5/ +5IWcMAPaC80yMP9KsyBaCr5iniaZqknHPerI0zlDr3ZeR5rGI5cdM3Jlk6WR2UzK2tMoJsY65UUl +GVkeJ21y9C3sDDWat4qlZWIZ5VIja9rXdDJoaytbRIQBFCNSBpUiRTTsU3siergpUjRZrWYrZB7M +EEwX0aYBjd18Uo0OC+morVhwm/Qeu97mMtm83nDS+PKuyObd46NxCCpZEUYC6glMI7YlFCvJtdfS +uEA0MCdAy8tMfhxx4djJT3lqAf7hW5Ntoww7SqkrzS5CnVFjdzPznF00zW/x1gYsogAXH5zS6G6U +wfSZTBn6hvIDw6VwUheXipkiMKQlGVQmVo73OeL5rKyUXDY38COJWAq5OlCbQf4CuyWAG7yRQ9UQ +2he5PHrnqT8+ByysgvoUT7U0vclisZMsJQwGLIkkmyRFVEpOVeeB85FR1KzCOiD0wNuIyEuWdA6L +GV2DosUMWysK4KopghTBYbSnx9P5RMaILhTY4EIRiGDMHEp/pMKErk0YRvxBUdxufv7ClgM9HcD8 +0gFzAOEg4QibXASLTnYwnTqUB17WXuSgrETlO7WIxCnwnEgN4IUF8Eo9sMJGq5mnpC0I5EjysQg8 +UVEqosXErIAUhnQQYbrALIrlAR0PEByBbRU5eGzQVPAjbBbBgUGXi4sIeKcuI2WSSY/XE0+kLnrL +ZclsJpKKtbS1AC2Tz5dcHj/IkpB/ECyQhEKFkPjzEJ/aAMfVF8PR8J69u7dufTE4jgJsyOW1uJyW +VCJh0QlwCKQMWBmwDkgim5Ap1BrDJWXF7DOtOXfdxZedOzoavOsnv+zcuQ9ALvAVIrjEakOIi7ou +EYUzb1HbA5joCv6hrRwJYhQU4DeFiwV02BDzDXiMiMieyt5lf5MWK4WMBJgA0segc4hcc71Q4y/a +HfB28sgASznT0HA+GlXTOWKxIUsP61V2U6es3iO2mFe3IxM7FY2E3UG4kagji27sw0U/7Y+THoeD +w0n32RuOGsshK/Pqy1vSpNeVaPZ4ZzrFrk572v8Gb/ivTagerjBPAIPYdE8xZlMtm2aqpm7b01yi +cgw3aePWfjNRSGM3ydHGYIppZKM4bDEq1q3ssWrmZOII5fuknP5nbYgMM6ANXXMC8A+5mMwrQO6U +7CUTVdSgN2hVJBQqbWyoKSIhxYD/JKyIuhkZRRC/maGpaOHRF25RZXM2BZkiPp20FbNekXPAkPBG +lXeGM5bNu6Iv7U4cCoIlHPzOerS2owECJhBlKRwU0oBUK9F0FjBOln2upJlfdWKPdowmTl6rrxAP +OJGAo3MfexnZReyw2KJtFpPfba/xu/xuZyQSHR3LDA3m4ixHBvVBo8EdGlcP9eTCYV0sppNgwtkQ +AP0sWHWWatFV7xkaGwF5t2ixQxMxk8myryVmMMRGOJU8CENRxsT8WWAHrXAs0LgGTAm6PIGxQSYQ +G7FBKhjTeT6dt+eLrpLOXtKB8FqEUUTMTbzaBLmkjg7UHdEagDZQYwnalVkUAm1IhCK0kWF8QDNX +AH1PQY/vNUtQkMqjwwHhJaixLU4L6U7wuobG6mBouMrvGRqOQwQD2pC8oIfRgfqiBYSqPSlzRHYU +YQWRh0T4U8CHKGTUk9lAghQJUXRTIPSGNJMEBUdc0KLBbne5quvE6lpLTa3R7cNQSOgyI3HJnCkD +BI2MvKoVMCzkCshC4XxAhUQK1bCscK4QSCPzDjsXCo+fc+75dpd7+549TTMa0HOC9IPJioqkDf4Y +Yke0/tASxVSYOZtNkKTMwYMHe3oODQ8Ng8l1xowZqPYppRxvN4pujKVazueioTytaHywiMiTmiYS +pWJgbvVbrrlw1py23zzwp0cffp7DRJFTRdcMHBXo6IcPgdsD5l9FNoMKpkRrQfcFIdwI4w3nEDYv +VSxGqQGDio8EZGI2kRpmD993FDmijog7B8sMeF23Qz9ntltvkPFdnMESCem6urKgukvnEKARpyHB +Xibf1a9/r6+YRlZTxw3AbneGENKcziM3rfJOMGXXecOmsRxeVHx2dozDZvIETePrn4N/xSdfW0L1 +SNNSGfFkE3PMhOrUN7ymGPLYszJtXvTobtspXazamUxnGqc9ytGJXC25ocU9eEF3F/M/Ge6GfqPV +MMo/koWroC210VRM5OSaHLVHlc02u98YekCzfGQAGec2/Ypgn5oNrTRmaBxvdBCqVyLPSUkxxg9O +BpK0FTVjiZweY360YD+hrkUrjwBQD5ZPOTJqyqVt+gIPfhMBPeMmPYdkI39ouLh172iMbCJiIdCw +ABiBDB32BWA2AAQl/lOEMYh2KObDDsv6yYnv9Eg/4cjrq12TqR4vy/8wFC5eIJUpa+4OfRm2SpJp +BFRItAqIJFN4AB/K2l2sHAdiUadVZxeLtbUukK/tORCWClwUTKUq9HFRo1TzlqJSq7fNddlaPdF4 +LBlJ2cxOpBKzsowIpygXEZBZLXbId8AwOpwAXlKXgAzBqGSGy6rgOdVZLWBVLeUUSMYbE1kRnHAK +dUcgFYuNGJ0BSOnBwGE8iGipUIapQHiiRfpFFVVdRDiKTH8FgkZmxNXoxBAsIoLUfB7knkbZKIxL +SgTRq2jIqGrb7Ca1IKUjwYUL2kKxTDCYAok5rLzTYimGs6VhxRU3ePQCKLwLah4gVtQtLaxhERNF +DPDg7tFzrC7L6Xz+5jmt/jnNLaeudDU3+ppadE43BJ9ocRUVaWhkvKOjb/uOwX0dyf6e4tgw1Jp8 +dhsa9kHuBk8IwRQ46uB7YB3IIIFDmClwqVIu0N4WQXDL6b1V3rqm6hzC0QIoZA06RRJh+QEWRUm4 +IA2PDCDr2T84MDo2npcUh8MWi4ftThFGLJoKA2BTUOC32PKDaXlIFojcW7UjCEZ61M23rJ5Vt2ym +yuufffTZRGdUyOlwRak7n9KfGBHdeYiEtQALp4wbhZlGrVWIgki28MEnVwgVlAjS8kxfhRh2mTok +Jei1TBMB1dDdgaIltXtyJcUr6s5ZVT+7OW+zZq3wY4rg8LPs2pPduTcZglS1ASV2QHi0S/1G9zrN +xGIUFDDipAgwR31btH4YTFWrVmgbivZ/bFOaajAnJVTZp7T9aBJM4YiEaiVKKKdecX21dE/5bLR9 +bOLOnfDM3+jJ/iss4Kse87X1NdLeNumrynv8VBNznLLfm3jm5XUwYZuPWoLTElGcyDinNY1HQHuO +PkFW85uoAmpr9xgLSBOG0D5eif+YiWU3AOu2Yx6sVl4oN2Qw6sZyQb5SzmSWk74LjjFhUFnUSM0N +SIKhCIaebGD6YP1IbxhKO1RmwjsIjAr5OxcBNAxFu6C3FGU1ETVkE6ZCjjcUKDSiTJwoq5aRMXl3 +R3Q4JJUAxjCDxkzCHoh9g9lg2nHe4FUm13hiEhj1BlJjVFCkjnRmX9mtzTJL5cZMJruB8h9VBekF +9kaeqh9QMTSqHBr4UCADPLWpPrBzX69U0CcR/Ol4dKoVzPmcDbUjg63NaW9wx5JJOQeyAheV/XI5 +wSokgzFAH0WHaxzJsmLBIQImCZBNScrm1HTBa3YgHNW7rYjs4C9Yk3lLKmeDUaQwGR6IDqAdDAMh +IzrwEF+Bo402ONI7olIf5e+wF1Mhi/ELMBoiOBWMoA8hONo/VcgaFxCtgOCtpE/q1dFiMW0xrT7r +lBc2bKpyCrABM2c09Y6OjwXjVXa7IaIYxiRzvOAGxpK61NHciVqaDqyvAm3whCzl9SVoP+J6meoa +FlxyiXfFkvqFiwA7ptZK2mapToYFYQSQlkfMSVqZDL5Vivb2jK3f2P/kk+PbNpmVlBdIJbRPILC2 +ow4IUAziPPhFaPZ3KLwpC2CP12mwCu6An3eYx2Mhk0HkjXZQxyFpn85kx8ZH9nfsP9Q7CM3noeHR +puYZSAwjOSmlU9FYpKm9MTQeg+QHr5hK0IcOpXkJjhnKggU0zrrsloaldbpGYQgMr4pubNdQaUgx +Z8h8ITlA1A+YWJa50dodNMcRdxKy/bh81Jqig5Am3mGWOTNMeAQZcNb+o7VCMRJ+hqCBuSSUMJkR +fCWkTyDWqC8ps6vdS2cGEsGOKj9K81wyA9gVD23pobFc93AMVVusOCiVATVEHUtv8H6Y2OLKQRpZ +NHggiHcxMMoVozzKtgzcgtp7mT2bYhonbNrEG44Y1BEIgIn8mXZDaaiiSTk1MqqT9s//Qngq5ue1 +9jUe4Yy8SZf9dXzN0cnPqV8yrVU7oWNOPd1jWL7p3lD+iHZ/TKlcT/myyV9TMY3MTE4kU8smr5xd +nfhwOcjUDJLmvbGUJevN0OhBKUak0hCBTgUtiUp8yUDb4PYi/Duw8la0WvMi+LOKdgAs8kl9KmLK +wy6mBcr4QTHQZrA4B4JUVty+PxpLF80CPHLspsT1Ri3XtAVogojTRYXTzbtWNqHZYoaQPYnxg+wi +1RcJZ1PhE6e7FncvyRUTxLHsTDAPgjYzrUAEJ0kqUYhjtprjmXwiS3Ta1PaIHJqpmLPrDNVmaw0K +p4BqUrcEPpcH06ihCFAuuGbQe5AGxxrxrhqKORlMqZHxmJRSqv2+tiVzdu/bb0ilxWTWnsg5s7JN +1cP2MO4STH5JFBF+lRQQAqDTnDZu8H8SkzWkFa163o7iHTQcODRgIMnI2D/JsFOgjffTr1AzoyY7 +7H3YDYnXndfpodOEv+87GHJ7RYlYzpWZ7S3BsTFzxlgclbhowYNSoh7wKapSojBmBfYENgEFY8T7 +LGrEK7CbO2c1nv2Bd1ur/cl4Mh+O6rMyyqecopqL1F8OO4oaXVYi3jssJFh4we2vXrR4xtmnIrhM +JtMDBw6KnLGmsU4SDCD7sYoOCblLp9Xh9dm9HshSG6wWThB5mz0ppeFksCYeDgTrnQe7Nq5/acPG +rfv2D1UFPGB+RXMLLh/sUjQapoi8JKPMrUvJukQxMw4nJQudC+J+Q5+JqWTxCsZqAaZy31Cv3mKJ +jKVyQcmUQ0cKTB5l2OEMMaCWpidDRkHbuQmRzRYWmRYsEY6TjYaEWkogAMe6ws1SeScjn6I8hYYC +gMcA3A+h0bA8SorPbm1oqN3XMzgeU6JZ/VhCPxQsHhqSQ8mS3RMAXjaSSmulyjelr/Hw7cJsNdsS +6P801nGWlipvdVOjhSNM4zR33VEAAC01NSkwPMI0TrnLT3ATnO7O/zf7+2utNb4JpvFwFbC8lU/s +6Sf6At9Qjh60GEJrf536rNiJV//OiRrkcYYx3dU6AQNM27e21U/694jvnWLnjzaNlNjRbmrGeVO2 +f2VUKjOEFKOwTjSQt1HbGdWBNEJwkJ0SRg8t96gnokxEPDawiwgDIbVrNghUWRRsdqjdWgwlc0k2 +ZZNKaNiQhNw5kPFGKygyHVWxjHFrR2TLnnAwphjMhMoAywtkbxn9NowTwzXQCCcnaTRn4EinmW7l +SjL7mC+0uoVmFMsQGyouknWkFJVWS62kprWrzxaD5huU/wZOFGhWwEKS4CNrcwR+FKYO85TIQH2X +SqGKQcmJqq6GF+psBhHgC1TzqNlEAvcMAmGkX4FG0XNupwtdB5l4tsbjnNHc4PU75SIoUotLFy9E +7S402F9r5MW45CHeMrRDEIcKEE9ojUDjIJoMrUaTXTBBaB45PTCt5RHn5IrIv2byELfSpSRdTlLl +jKRmC6ZiCUKPwA4hJ5mX9WhTsHMC9DNQfqNNmsIffDcY3fTR8RDykAg5easYjSUtDnPA5R7ZHXJk +0YdpstMwwF1nALcn8LLgMIWYJCQmcPJ4WmHkkH+28lmDbubixckiWGfxo1XhwcgGs0LaG7DkJfKo +iOsBy4XQXkDfKuhMKKAoWjVvUcspqwWPf3RwSEplPEg3o3wK3emAV6ivAVU4IhpaqJSKhB6JxQTk +jdMdiaR3bd/33Ivo29wzMBxMwY1wWmfOnjUeiRXQM4huQoT2OmgywzjrXDa7HM4VYxA2Ib8ImX7w +LsHZMVp0Ojc3poeGYpYDs3mRS4ylXAYRETxrw0AxF8QHrIJQSW9RAr+8PTDbwrp84cShuJA1GGIl +BRgcSnnAs6PGR+2B5QUniUTH8F4iVaDSRQnVYZvB0FhfOxqND0XQo2JK5LlkHrl3PlcwRjP5WDrt +dtvtFj6ZzuKWxO3IoOPH29Zeg1Upm0atklIuprB065tgGjV06as9KxvOxBtoP6I3l7fcSgP3UZvw +EXvyazjZ6Tbef8LfX1ut8U1JqJ6AOZnmxCkhdORbjtyCpz0KC3Te6OMEEqqsdl6+K7V78+iDlpV2 +y/dl+e6clFDVNhr8R21WE9+lEYizXzNacBapMP4bqvmhkEZc4DCJGjYVuBtEjNhjgEdFexqVooBK +BYEmuEQoelR1ybFcbFQo5Z3mksUAYAVo3RyKTuwZSe/qCA+FgcKA2w7eSIQySJ5BVhcGEnaHcKGA +RbCBaHb78OMYtdjp7g+cIeUacQAmGEtbIyunsCdtNPgf8BIULzLtRryNcYvT9zKVSjYfDKxAPRQ0 +NkqigQfH57bZ7GLvcAiHkKHQZ9fr63i+xa269LliGihLAF0wSehO4wURSA6ZTpOogtKpdDaRQvuK +r8qdBaMaaF0imSVt8yKj4WjvcDUn6BNZB3KKCDDQIQE7xAAveAlkKPZUQFgycgHVOF70NM6e6633 +17TNstXUQlwJRUcpnUsHx4d7Dg4e7IoPjOXDQ3az3u2yIQ7KUGtkAWS1yNGhb4EIO2GiCqhi6bOC +YVQqmQKuolmHvolGayC4c1yMqU6EVgYFWXGsBQhQwAqCCwCxI8rKsHWo/vIYHq4Z6OUE25Vf/aK+ +IQALgdMUrXYsBJScQWGD5QFuBoSrHE+gIFBAsJZAUq4EizYFeLqSwBtG1z+7/Y+/Kw321bpE3mXO +mfi8wZKH5BfIbCiLjbjXkZSU/mToUGikp2e05yA0qrKUzUd7fjTtcop19dWJRIzHIXDpVAnthRld +VhAFp9ES3BsyZ1Q0Tao8Ze5RvoZ5RmtMTiwVnGYjiIpUXSohSbGSz2RMB5OYbWBprYpJn6UWSbZL +0/2hxYKoLyJ3zVjzCQ+GdDcCwQQkwCChCaOIIaG9hW31bG8pR43apzHbaJVAvw40IGt9Hry7bzyE +HAa8BirxIkAn/lZ8lJpFRMEIFBj6X+OJNImETtfGO33zRuVmYhsay8owMijC0tGdQiT52i31RhKq +R4N5ynvjsesjZO1fR0L1xE/2je7Lb8bn/yNrjVgLmlmYeJxI4fCI6TqRj0xrX0/ANE42Zq96xY5f +a0SjcrlcqSHNmJCShuQum0YWLJFphO9NT5hQopckmAGh5Ek3AwVFxIuwjSAdIZobiP4JRgF9bIW8 +IZ805jOgthE4tAeAA9NoFgCFcIzFlK17R3rH0MMNcIYDLRkQomPFMK03hIOVYB46K9BrBuwNuxsS +SoDaDsVkeKigWkYaMPT+URddy69O5H80DAK2CQbEYZLFjEiOKnsmfW2VZwjpUPBoOnVcg9lUa1e9 +Rh0wKpACNhuhIe/zB+LxFJi7x4cTGdSQYJ3NyBYixEaYjGZ+HZCayIkqEaXGZBPiOT24YFCR04Oe +DRlayPWhgwE98pTDpiyo2RhKyJ66QF37/Lkr11bNW1ZT3WIw2nRmW9HIYx4J/YA8t46X5VQGKMx4 +JNV1cPOfHt698RnQzNhdRjCLgnxVJh8EmFz8o5l6QxxaxqIRclBJELmlDPlDMVO86FF1bhNCRISM +xCkK4Sg09WPbBgUsyp2knYSsLF6TP2FI8uIln/uUvr4mTz6QFV+LfgiUGKldQ7Ci/ZAknQE6QuIB +RWn0qoJQnjSQTUUJOVaQluKsS/mB7k0P3Jfv76qtc+vysqCaISxZEhH/lUpJOTkQ6+7t7ZPHxXk1 +/anYi5t6lRQa8TgbZFuUIq4FJgBryg61KTCiK2AeByd3AYGXoDekB5A0JRYC3mWU0eIpKbDcyClL +ZlUycUBFuX3e4GgYEFE36IVQVYUcWr4oZDg1BPkTgtuQJWGrkfkodMMwdjyKCPH/6VIR4WqW3kD5 +b8ILV/JPZeQnA38SUQULOpGgBimU3WUfCYLTh1Yl6vMQ/yqSo8L6BvVgFCKmOmQkHEgm67hkNkcE +BW+0wlBe8ZppLGeewPxOUqrUbasN7w2axmm7jafuXP+/qzVOr7zxpkSNb9yi/weZRu1kJ4V6x8gx +Vu7hslUp7/6aI0cbLN3c2i+nwHDK2hj0F61lv2IXmXQGAcjRqk+VRerUBgkmhYxkHhEk4skDhK9X +jIUslwurySBYVTyQboAnDKZO3hZJqru7kgf6M1kFdC8OdOvl5DRJHWJ3JMebwjMEMATwoRZycmCZ +ghOrB76ux4RJzTLTSLc669Vg5paOQrg82n+0GuTUB/vlxK8wRlSPiB8TtpWQgrR7wF3IlIpNVS6U +gjKAjDQajTOsJQuX5SS7W/S4HOkE2tNzegOfTQBCoQ8HcwW0z4kGwQUciqLksk4BWyFMhpiJomWQ +8+RUWwYgHoRl1IsNXwQ1OlgLM6EHiVShZGJc5SbhbR/8QMu6M1RZjeYArAEOCj4JGhiQYUU0Z1Us +DnyrHgE5ehtwkojMpGS8Z+/zv7hr9yOPmeQEV0UthwZEY6S0CCUvgC9QkOMSRX3OyvOBqnRvNnco +7AEeiJryoDjIVFJ0KviyEY6RNwTUiY7aGBg5LqGEoJqS4PkLPv5xU129RGKTgh683uRFoRIKORX6 +l9Tt0akJThwKwoGz4WEdKbqjRCmXxhzpVRt8l9D43j8/NNC5u8FnM8p5RDSAQ8UGRmN7+yIHh4OZ +zJhTWXPLOudpLT2RTOeW9EvP7hrtjxvymRnVVYYiEscIgIBkSaKBBKVRUkHhwMaXAwS2SnTANlGL +JQjF0advsSkGNZHFdWH9QZA3iaZBcuB0CM2tdTreEA9GS2FJ6k8aktAnI/sPwnF4SphSVBUq2U2M +nYQW47ISBy0F8MM4IHko1LPBVKtZ1YIRsTEeXOBiS+CZA32wYLdFqS4qAapkhjgJ0DxYpMjhApnF +5IwxeGKmB+AYJWczJxcADXvDrmJlWR9hGomsgNUb/wmm8Yi6I7stX0/U+Lo2hn/Zh6bAcM4iB+t4 +qXGGqmM5LvacyE2/ucPHWiPuCta2f8ynlqA8cnt8jQHLCZU8pzuxacNKDaE6YRq1GdMeZZI5NuP0 +PUTXUX7iFqW7lO4zFEUIUcdCM0ruUUBI6UKtFF/etbB1ctDLMRspOmBdGagvIksEPnAj+jEgrADd +PJJMd/HoyLMZbYDbgF0rGiyFR0zZhIgKjohORmSy/OmiuLsr/tLOsaFxhUPdyiCAsRkqg6gHkW1m +NprBiUgFWRsIg4OXKw/ayU48jp4/qgtWpoBVKClhBaQHoDESCEBZTVGrz0xce0bWxY6rPdmsMDMJ +5ApB8VnrJis6UoEI4oMIGkwULbLObrxAJYrUp1DUAl2cU836VN1Moylg8vtcVXYXZDgiY/F4LAE+ +T9r3FGgS++HuI0aGOXI4rVIuCwQN4K4pAFUQj0VzVZLOngboBlhERvADSRL8AQERdS7CoqpGE8p7 +4JoRwEk9c9EytPuNB2MEwKFaGBhX+SxnlU1O9BkKigRVL3B5I3DBR8GPU4T5DdQsOOf8+WvWDAbD +I4d6EAIKNjNKuwXke8GsgH5C8pfQHAJuc708khJy4CjnAT4m5RO9zBuhkoKmPRJNQarcQnopsH8q +8uhEGk9FRINkNM1eu7posaM+DXFmyrlThZrAKxRaUSBG15b9ANcMJwYoJBSnCfeJcxCAcVWA1iyY +IDrVXC+PjOdiEZtgyHX0xjbtiL+0N98zrJdk3gzCIX1Xd5+hJR/1H2qYW1h+qued7zkbVdux0dyh +rrCcMjlsvMVqyEp5ojZQ0GhhRm2ROGp4XVJJg8TBaLOjtQUqnyn4LiS5QthqKZMDlQ9ylmirwYpP +RpESN+hFXbqYpdoy4UxxC9BdgdJpgTcWbPqstVjwGMz1toQKVS6gt7DNEGYHa5txSDBxK9Y6yBYb +lba1H4mtHhpbslREZytMYjkTRMoaDMDJMhVsrTOvjXK36N44kbzUdBsM/l7Gf7J9WXMLsUFqpT7S +UNZwqeyW0u5Nllia3I9fvlvKWzu7tcoP1AC0DmxtM9f4nzWGfi09pf3LDqBBdivjpXZfrYRLd+dE +Lvd1+sUnMAv//LdMgeGcOVGHfpWBTN7Z/3FjreyEr3oEGsZUQ/h6VuGbcRmnNY2T40W2eg8fdeKz +lYjw8MpjdvDwCtYKbCwXVG5YpEBtgtcGL1BPBNyU4kJUEpE6NYLIBqUeM2SQBOSBwPlmg4g6yCst +IrbOFKFBgiPo5XcbZBu4RvFnu0uxOPf2xjdtH+0ezGUU7KMAIOSKuqyKNkJKkJGqn5a6eSOPw6fF +7m3mY7HOfXazMUDNsRyfqYdkM8aSQMxCakQlxH5AfWzUvYZaGrY6xqbFmFOB0mS9dEVrSRcwci1W +/+xq3mFJZTLjoVAynYSxdbhFlOYKeiWnoJdQsTnsHmgijgPYL9ks2GH10CHCO4Rs0RCU7NmiA5U4 +5hqQtDN6QBmRGAn5ss4zStKhE4bTZxV55qJ2d20tMtGIyWHMiaGbVSJJrpKgkXAMaANk9VJsVgjp +DODvwcy4a1uWn3Ou4HAc7OiOj4f9AifoQPtmgJkvEtIKmE/VlMsjgoGyPLJ5VvwN5S4eISnSfcRj +Q12M6KwnbBZ1tBIDINMZozyD29m6ajWwSZgriqioXY4GjThKw9eyBD2bUqYmwerYLEqhdxO/IBLM +CPWK8GNFq7+x8eDjf40+/JS5azDbNwrdMb1ghAgGThRV7GhGtvjtbYsXQHy5kEzYjfrZrQ1nnLr8 +jNNOcftsnQf35SQUNKkqjxwkksfw7QKBAFNr1icyUioP1a0c4LLofVQwj6g76tRcDg394JeFLKg5 +HovH4xmcN5YnC5mNOqtRb+f0Tr3qKOo9uoJDNvlNvF9w1Xl5lyM8lsinwA5PoR6Fh1o1oPzQVtXh +VajdwnhgOKQQfqz1OWEayzcue/+bVFo7ap9itxAWPcOfU3540sgn7pNJnyqf0OHTO/pNx7yrJ/Yu +ZnInqjflT1eMcfkmnIio3sjm8G/12Smm8azpTONR4Jcjz8Xc6K36yMXycKQQSx83/pwSnZajDpRd +5tW73rrKdeHyXOdQMZWbBsl43Ink26pdFy52nbskf2i8CO1adulwuzrOXuh/2yl6h5DvGn3jV0Iz +b8LMatf5iz04Vg871pTHYceLLbDDiLVJ92LlVjy8ntkmVAk3tSAKT9aRobGBk8phGYxKigiUX6LC +4ux66znLhVMX6oJJ+HKgpzSJNh54RAhOiLCgkJiPllKRUiQqKrKthMKQ7PY6wIPSF849s2X0YF8y +nKYec9CLw0oR4AS1FGqYIJ5pLZs5+TGxa7zaTB7tsmgf0YJMeL/wzImKDYEjGUjGQT7tl7K5qAyE +PsLqkQx4yMousDIWwGnInoPrhiF3kDPkzLKhhK5D80zRMtNVFKBpHE9nJYtg9Xq8TpfD4a/xt8xp +m39SPpOQs1kwfeKvhWzawdNDgvkt6BC8GEM5n6RzlQw8qkqUSyXiacSOsLxEQEo2Ej+SoYGNBFIU +UJ6m9pm++ka0DVKwVvbXMeASBbaMDL0ALwQJWKresm48xJJk1FDbA15UmLF0VcOcWb1dHcngsAW9 +HmDTphMDqAZVY8SrUP8gy40+dYhioZ0emXLgUQlFBeNNsSPOHQuESB6wQgBPhgeFZgyxtqZp6dI8 +iOLoozgUqY9RxwcGQeVq6sxklLukUaEBoylVwVqDtEyIdjK4iPlCyer0GwtK15/+ZArH3TwCNVVC +HyeiSrmIvh90z6Szajhsfu7R4Ia/xJ/4fdejv9/25OP7D3WNWUwWJCmHh8ewipGvhmkDagkXdeWa +VS7i0xnEaSDXyZqE0PRJsCj0ElK2k9pYCSqEpYD1yXh51HQ6hxIp3IQSSFShfQyFRree8+jAJ8c5 +DJB5tHhcw2PR8GDSACpDpsWhNXgcYRone2faFaOeWQJllR9aNqhsMjVDWI7atJu1YkgmLMarvJjW +Py/HCZM/PpF5YvACLSasDOFfZhq18z8Ckvpfg1DlpjWNx3QuDFbwMJXZFKpvu7z9hrdypzaFf/Xc +azI82PXrv3RN7e2Xt59xbt2Kk/I+Xfyv24/7Da+6qPBVsx78aM3N584659zaVSuyaFx/cR++yray +bfZ9tzRefMaSsy4rLHbGnt5RiGdf0yCP8Wajof03H629+dzZU491eIVOmDf2KwJbV+6oVzONlbuP +3W/sgfwg8xCJx4ZpKzL1POxd4N2ibg0YRkSIgvP9F9vecW7zSWf45i5OuVW1KwjJCWgKQiLCygN0 +mtZlxvXJkJBJu6AvYZDdEGlyucJ54eXO+PNbo9E4pID1MKY6QyELNULQYZPAMW2LWh6HhTVTpv1o +KzZt+I6YA+aQOhTJKLKQjoxixeqyQ0y3XzCMnlZjmdgVGJkz40BBtQ/1NvRdkEI7ibAbOO/K2flc +Lsfl9PWcuYXPWVHMiqOf0+3yIIST0sq8tecuOOWc5fPPbW1aqpiVnr07iqohGImTwoYoghkHiE3s +7wJEgGMFL6UTKc+NgAgdB3QpYE0oXiTriNwkQDvUO4EWDjQmKmrD7Fn+hhYJeT1QhBP5AoiFqJmc +GvAozIWgE5tkZt/ovEggnhQzqV+hqI6ksjNmLmydN7Ovr3tkYLjKaQdKGGypVGmikJAYsnEuMB0o +sDmtIrQhkEGAUSRCNowEQSIWCHvCKEKBE08wAXlaW6pmt+fIUuJbYBqRw9Rq1IwviUJGyv0CoUJr +jqW+6cKw4jJ5Ikz9msJKwHI5EwI5b1M9OkgPbtzkgk8ARnIZIiNIQiL+JW6DSFzauW/k0GA8HS16 +rH7B4ImMZ2AR9+3bn0jEwTBeKppBmAeriD5aHHHrju7hwSGHiE5OI74cf8CBLGinNZqAfQFwV5Mb +hdVE+EiiJMCysjuKRCgh+kwnTFV2cMViguHYpdN5r8sP49hzYFSJF0lnmLmpmgzlROylKcvi53Lf +oGZzGOkSsTJMWpvHupHLHnjlQ9PvLtMt9Sl5pslfp2VX6Pocvicnf9k/O2rUnIPjP6Z9w3Rf8E/9 ++9SEKqHip8RzEz+Cy8J28TLfjet8N50ltNel1x/QhmlZ0tzyh4+m13cUI1DG1vHtNTPWrouWQrFf +r0dtejqfaeLr9Y3fvclx3iIA4xOZUCw2PH7P3+Xe4ISPdsQLsjGvUlmEzWj+v5usy1tQBUlJsWQi +OHb3k/JARGjxt/3sg6rNJKWSY9KgMhofu/dpMAG/kcnGsVr+7yZx2eFjjd79pDQYmfyd7P7RqgUa +elMrm9FzohygrRh6I3vFjKC27ummpR9IJo91LtLexXKp2AyQIqSeRcYPzvPuT1xuWjkLXJ/JbDie +HNNv6rbkijar2capAlrbMzF9OMSlwkIu6dAX7ILR5XVJnGVfX/r57cGDAznsLRYBxJ566Dzk89j/ +SJUP5NwAVTCUKHHLsJD1cJr3aLt49GQeceGwtRAVOL6XkpxkHYkFhkUhLO5jm+4RTB7HuEJsDivv +YzhdbVYpzwa+bmhRefyq3w9QEaKN0pzPXb/k1rdVnzv/4OMv2modvAPzkRdzRjVSSI6m8uH8ykuv +rp83D2jDOAhW08MHdm3MJMJAZxTlUjSSRHbOyPNySuKzRVuq5JSKIJch3UGB6EmZXQTCiYIuNDKS +ND0LGckmkRHUo3ukob3d19AkoT2ASTgzq0I1UXL6CRvJ2jxYNlPLmiMs0rhZtKWA4wMsG2hobWxt +6+7uCg4N+JHhZQciOgDU2ziwygnwaKB+hewAqp4AdiLYojQCFgezhYxDnkDKxCBvNgMGHJjbbmtu +zqpAK1swACBVWBaCtXjATJIOWpkzgjGCwvIzZHIlcGJLFvNdhEEFDyt8EBDhWH3+ke3b88OjXl4o +ZHGFOZnT5ZF+gLHkiX48bSoYAQU2qclMTC5IVhEcqvhW9JIWsOoAABJsQiKTxXgBAIIJBOVAHIR8 +AM4KIpoqwb8jS/RExIjVT0n1IrBBcBVUGEvK3nJQjQbWSQWbk5QrZtP5bEbJpWU4QLXeOr890Ltv +KNSfAh+QgVpQyvs5Jh0F7IqnpUV9ZdZFbfVRjMiyTpPX/MRrmobK+zQ7pbl3U2t8x95Zp91/Kl81 +6eMVW4hDEPL238M0VvyM4+UKpz3Zf6s3TDaNr8r3U3XrW2a88MXAF66c9bbLm1au5tw27RxsZ8xr ++tn7kLsBp6T2G2UoMp7qL8UlEGmc+HkiDWs/Za5YEgfe97ODJ3/6wNrPJZ/efeIfn/xO53mLa9eu +MCvG7uu/v3f5p/asuj218SDe0PLtm6r9rcVD0b1rPrN30ScPXHiHKhGt5Rt54Fh1a+hYnTd+f9dJ +n9q55vbkJjrWEQ/N0dRs+bRBFfssbYuTb0K2iTLON7KN9EBfl7iq3WQXqYEfAntNPvOiGTa9vXDv +3/Rf/QP//Sf5vhAqTw7QlBSyfDJkjI8acyFRn3NajaLdprd5u6Klv74y9tz2ZChtAWofWwXMIGwi ++JIJyULYBHJMMRi4Swz+DozLNIXGMvx9godBI8mZ+kD6FFsmpJiQ62QCTQwfz+hL6EBMnuMEHyzS +wi0J3A3luzC7sBZWUef26pau4NestS9d7GioMngWtwS8zSrISZO6bH8hsVvyqe3FHTp5c7a4Iy+E +bHXt7Rad+PTXf/rgbV98/Ef/N7j/ACjdROAulFJtXU2+WAiGY8WMXGf1QrPYnIeAI5odYAzJk4BF +ZHF72Tqy8LH8JG172BQk/4i0DVnpPFeSjEU8Za5IuhbIDxJxAthM9UCX4JcFUHQji41wF6R0sANa +ox0KiciRZkum2gWnXfah20yNzSG0N1ohc0JGzwJJDNDAm/Q2h81ig3dTIsoj0KdSjyIpZmCwRpSd +ySCSVRQ08wiCI7cL5gVJwrJAVcUTI2+Mbe7aldP+ZQCQMrkCkbzgfFQ9cSEhcVrMMzFHPcRM+LaF +S666ZhD9gvi8zUb0hjBKQLnqQMmXcSj6Qg67hbUg6zIpMMYh413MpYv5jAEshaJdANNsJInufX0Q +zfP5QlYpQfHLYncazEIaFLLJfA6gT0oDAFVE4qIA7QK6CsEY/AvuHjhcwDCBqiGXyUNGJZ8tZtEm +mQZY1O33NEo53f5dneGRKClgwS4yp5MtPQ1GU36Uc4B0zlOCHK0pqgy6mZRNPWKtagv+zWiZnuYm +KF8dhn87wfvl/73tdc8AQfuOfvpuvdD+jrUAAwKJbDFYxyKHgj94HMeAPav7+vXrZl+F5SL3hrSj +ui5cgegis/kYFmJiWAAeHPF0XXtqS9WCVDGR2TeAPx1NmIKNb/KTWmmPHdzqqi5f01K3WAbfyMGR +iSPyM6v5Bp/FIEafPn6SdsrUEViLNTa92nPiWFLHyIQUGZm0SU/4oqTWXiHK16IoLWiEpWGQDc3Y +UVBIyEEEhbBN6GADDM5UZE9qMENEQGh/pJdA/L10Rt1vb/PefpWwbh50FCEsZD13ebN/fqqQ4GI5 +M2SHeXCGFKqNeVt2rDDaXYwMiip6M/QWHWe3+3VCzbYDmb++OHZgUAKtC8cr2CGxC0OLUMoh7CR4 +GsFuGLgFrrim/cRgawxeOulBXGyVJ+FocGZ0OuX2MS05ihKaDE0iiN3rVajiySW0H5C4LPGpoXBG +neRGTDGZAzQGEubviINMPSTboKm6RH4+S+mVCggOmSwB8JkoIusWtvPLF9vmzi3OW1xqnqHv+9qv +nvv+jzZ/4Gd8ThfwV13+88+u/PgNgfkLjRGjLWhccNYprf4FaSUxsqcXyTdoX5QMir+mCtJQvKj6 +qmwL2tpaHdVeSTDGsujeAAwUR8bGbKFsFkaPDCoBUxAjstiR2EvBcQ1bBSyJFWgoAyCcmP4ckYij +sghWbRQKC7JRlhj3i4IDltC3CEhJCX+EFhWYSSWQDyCuLqG3AeGLGd8j8FiORX3byRde8ZGvRHU2 +RPV6aFm5rCUr0qcgxMXV1XN2cOFwZguPrgskWUlyDJpPogXmGTRrYGAHjxGIANLmQt7ndDW26HJE +wCaXcgURESqDVLLLoJDNpkQxbbtgJQCmBqqIuDzailAxfglpTSSCsbhBuY3qJ0JHUu8o6houv7r6 +lNODGVxwiF0Qu54eeBfqdCihWb8VMOliPpJNFgwc5gDRZrqkA1sbmAFkBYueOlmRBHWik4SCVqgj +6nkrEq0JI1fwuXiod+RVGFYDBiSVciqcFw40fGAWp3IhLUXiBme5YExcQQZ7bXWVF/36w73jiVAu +Fc3nE6T0zJTBySVgD/YTfQU9NZQRnTejj5hIezDSp8O1xiM2WYajYsEmJftpzSNnriFX3vBDQ49O +PGjklC4g9LUBPgF8G3bwwy4lO6cK9pRJcJVR3xqWlDIAdOfgyer0TLizDLNlbpA2KRWC4lcb/5GZ +vDfnZN/wbP1jvuAY/jpwMc53nAKB8rHPPGguGkWrO72xA/lJ7ICNP7nZzFvhM6rxcoDIz64Vl8xI +p2PRhzaiAGk/e4HjwiV4cfRo8UvXxcvsp8/T/uq+YIlHrC4NJ/Hace4i2+pZkz8CY2BfM9t7/Vq+ +xa/9HlbZc81qzmfHksG/1uWtpkYvLR+QTC6qR6pJOlCG2GCc9lPnVL33rICtaSzUk97SffRgOJeI +dCsO4b5kGf5qwSmft5iBunW2k9tq/udszg5+Kmjb8K6zFrovWKqNGW8QFk49Fm/EG/w3nsY3erWj +YOSO1bP8160RWnwTxzUFHN4rThZaq2lsa9udZ87HyheXNHmuOJmzoYWOBVDY1WwW5xmL3eevMNut +hLJhD8qRmU22dYvm1J4MW1nqDxmwD1p584qZHms1JOMtLotuYb06s8rOc+ngSHxgEHhN3bJF8SVz +Si6zo7puMKw++WLXzo5x5J/Q+m+tc7kvWW6a14gclW1xi++yk0H0RSNn5+s+e6HnwqV61JLZ6I00 +UVUOXItLluNH67wGT2WiHCvb6v/nHCObKHIp8B8qfJcsF2bXEo+pWnLgTE+Zo+Gt6EJZ+aqzF1Zf +uBQvJmbGAJT+7Nqmd56x7K73m1zoKNHh39rLVzoWNtVedvKpT32p7cNvqbyZdi+Tyyq2+L1r2usv +WYGg2jG/wXveYmhqeTy2XMuMQ3NPEfy80OSzL2wafXRLdmAMm0rTucvm1a0EDjW8vTeP8EIpNp6/ +DGsvN5YwWfQ182YHmlsCAR9tFKWiz+2SEkjwR2ucTvThobXR01JTf+m6mbe/B13r2GaoBMdcHdrE +2ZNiEdpWWMcnHAAMEhw4olVRgOMB+5yERhLGW8ekl4n/DvTQZHKI/ROlUa3wShE76+1nkpTY2ijz +zOlkjhLRC5avu/jSd4ChASw18JTsIlDHsHsGndNkrLErDqPMq4ITAlC4O4G8UeymohMkRkSYS0rV +yL2DSru6oRE9i9jsEamDDoncN+ZtMjQTOxvq8mOeMi4e0sd4EgyF2QfmOzHu2gr7NP2VDAE4Rjmf +Z+app6aMXB7oGdK90O4EOD/0Cjyo2WQWzOlQZM6pxSwURpANBgscwuW8BE54wQxSbmRMEUYXBfQG +GgzRaAJzmJWLadXQF0I5EuVwKyYWLgQm2iJa4FxSSMzzCCIlwIjheRl4kPt4vT4EyWBviEFpRTXm +c4VsBtV0hkh9vd23R+8e/+83/3+YAf2XSVFoysP/ofOq3nNu9I+b8wcHV9352VQyvPPT300+sSPw +8Ytd16/BWxfXnvbKY/cfetcP8br1N7faF86MP78r9vgrNXdcAwCdi/OMdu7tvvzrk7+06iMXea5f +i3SPTRUjnV2HPvTT9sc+M7d+1eY//Fxc2uq318elUMcN39XCPtd5i+vvuAb3QL0wYzDacejD97pO +m+9721qfo67/+fXdN/9kzp8/JTbVZseC+y/4inVh48Jf3Bbwztj01W8Ff/qM0Wdv+8WHxJpAra91 +VtXyv+3/1Z41n5mcRzXVulu/dZNpTgC3rqvkSqjx/k/d33zHdaLJeeDLP4/+6eUFz/xvdX3bnh/8 +Kt891nTHtdoZjXTu3Xfl13GsJfeyY935reA9zwCk2vrjmx3eqoC7pXd89/5rvmWd09D0lcMj77r1 +55ltPdU3nVH7P+f6nY2jwS5UOt0O5Osyyf4hR1O90xYY3LCh+9a7cfO6zl5c89mr6XBG71jP3oGb +v890ozhjS3X1F67Xiaa1My/bcOhhPZJ9Gw4Ud3ZbPnp5e+3Kjs0P6+r9PltdLB+0/t/PPcMjpcXt +/ddcgu/xmr2pkQ71Y//X3ZcdjRVgldHkHXjf+d6rTvPb68YivfJo3DWj0S56O37zyKEvP+g9b8mM +O67Tzne4c8/BD94z69s3medUT0zUodvua/3qDZioPV++N/ynLcue+UpVXeueH/16+Id/pR1wdu3s +b99U0zg7nBvddsXXsZGd/MfPwEHd8j/fS+zurz5v6byv0pc7Oc9g556Xrvg6duOZH76w8YbTzEWz +x1Y7KvetP+3z/tPnt3/mrX6xLpgaMOstbfVL9/asf37t7fh+S6178bffbWHjcZZcSTW+51P3Lbjj +BpvJ2ffDexelO+KfuM3mbIlFu3QmW7VzZk/ntpc+dPfqez7IOfhT2y5f3/NwMS113P9C18ObL338 +8/PrV/3tsZ8HFs3E2kvIoRf//Iu+rkOgq4Y6IWSN+w6MVQniyZecM+P8tXyJhjeS7+v84Of02Qwp +DoKNnYGiUNfUsqkI8dEcwRLgrFYrOs+99rocWulNIgAr6KpBrhMCGBBMVBGBgsQFcRx+xAsjSGDM +aIpElz3gpDAT1HyBuhxsLOswQw8NgiKgTZTx7gfveG861OlyiQhBeSBWi5DZIpiptWAyR/MedFQi +ISEYiyXIWKFhA0AUUoFAHw9sTtZonLl6ja2uOQ97a7Gi8xWml+ORQoapxQDAFWeizkliJScqUFYS +RdsmpS9Y0yMrClN2ZwIMBZsK403YIDO0kHdsvf/G68XRYSdnzshKQi2mkS9GJ5BqHDVyu+TcELoq +gfQhoWa0lGACQSnBGu1NABVRKATLhf2hYNTlERYhMIcV1BnTOfmUU9bixHa+/BLpKbMIiEw7WkNl +JZPJORxWp1PMZNKwlJRZAOwZQTmQqlZILBqT0UQ6lKOCJ7DCBeKsIZNdicfIw5kUm7GSxutMVGql +EKoZM8WP41uOaQ9y/PYwuB5gSsCqm8gJH12y0bJfzN2h82VlkXIalq4gc4ng5hwxzilVVfyNAs8p +jC8MpqEBKdi3TmJzfbVTnvZk/62s7OSW/2NEjaaAu94x076ijW8O2ERPIh+Wu8eFhY2u69ZQlo14 +C3kN5+m58VTj3Jp8Mhl5/OXqO6/BzMqp1KyGk4xNHmi8TlwNfNZ/+RpTyZxcvw/4QWOLz3Zqu9vk +Gx45IJ7aDhRBS/0ip9mrBYhkF796LZa/3B1MSuGT51zc/MOb3DeusZud7bUno9Kmh9BNg3tu4yp0 +aOASU5DnmzkS7szs6EMs2P7gx02NHlnJ9cb2Z+REYShxRH2x9c63m+aSXcRScToCsUe2wZ7NqFrU +UNVu8tkxAHkgDASf6+yFjXdei6WlnZGpyUPh4IrKsbb3WWbXtv32VkOVmFGSg5GDSI76rz0VphQj +l3rKI699+xkz/ve6wAfPtZoddd42vYPXuy1N9QtJPr0tAKLMxc1nmJp82IWsy1qrP381TWAmPat+ +BVfvRiQHfx9PS3tdyWFy8V7Ej1iRSHBzVpN5dp1HqBoPdykt/pKJa6ia5wKdtYfnmj29111Cd0Q+ +3VKzIu2yr+9IDSf1UFXHlhi49VL3DadBubi1ZonebuZnBaBsOKtmuaWpyr68teXO6/BBiZ2vuclL +dnEu2SFtoiKPbBXnNk6eKGkgBNfe2lxDt4/VPO+3H+HrPFBGnl29vPlDb8HGhoeD95h9DtfymXO/ +Rl+eT6VmN5wkNHmNdmH5vR9seMdp6JsHPc+s+mWl0cyMm89p/+JVZpPQEpiP2Ybk4I4tf9398V9o +N8+ir73TUhmPyxEYeeQVx9zGVnbhJKutpzdlCIe9tga91csbrE2+eVIk7ZxXz3kEp8nDpk5nAOcp +OFGXtbhMvoHRAzUntxsFc2v1Qhfvs1gcUcBzMqXR0Xh3Z1AU+PM/9f6WC9dieDJXaKtdhja9Yi6P +vZMgqagsonODyWFqABxqq9GaNyBKpSp2nwNCUIhnIGWBGdWpEDqEecILGYUIehLPK6we/kXtFb8B +RAZAEpZQpYZMSnoZ1ALsA09JQENWVvnqGWvf9q6CYMeysKPV0uvQ19j0Nfai31LwCeZaN1pYeYRT +aPSzOVB7JClI3gqhYYPNiuy5p7FZ9PpzYMEjJA+y2FAZYRsfIVHwYDQbdFCCEtO/aK5haVXEvSSF +SE9khYGmolIBTBohieC9FsC5o4JRSFy4uOqklWGJSo1MjQUFUFbBVvWCqgd/EKj1rJBdVPVOg9EK +mr0CuioosZeHCKUk55HR1plShMgD54KiQh47XfIYuXNPWvKWU07KhIMKeO1VEAgh2NVDejqXkZKJ +vMft8nm9STCXFovJFEhv06CHx+ERL/MCDyK6dDIHm0UVXOr0f1NSnf/6nfx1Wu9/2MAn9vljvviH +HfYf/sXHMI2R367vG96d3d0HAXVYwTxImwYiDd98B3bJ6C+eM8rlFSbMqq265UKs18GP/KL2M1dW +2epy+wbBXj2e6FM6xvXArVdS38LMmmpvC+RqUus7ur/x676b7xbnNtXXzXW4q6isH6acaqQQSr3U +ZbAJ9V+6utrRlHx0R8eV30gqcbfFr7eYSpFMWkrEsuOJXT3ox3AbfSPBrtiLe/BB9+mLvY66vEHK +7ulv+er1fMCtdIc6Lv86rHgiG0puOaj5TeUnyNLm1sI1hb1cUXf2QKxj9Md/cZ8yD5Hk0NiB3L4h +fCHgnsH0EEAufltdtnJGUsc4PuKpHCu3f6j5zhsdNl92Q/fOVbfvu+FbB2/9qe+SkzDy+GM79l9d +Hrm4YoYciSuDERRkEDDBdS0l8qLZIQquwkjcUFCH4p3ZHT1Qx2i84+00gZ3DQCyMJ/tKvREqRAJu +aeHVrb3SfS9AOLhnfGfx+T3cj54WXukzzayv8c8SbV7QPxtShBMOy8G6wb6+d70DIZc4FoQ7jwuR +OzCOEpTZYpKLGRSh9Ba+MJYsYLNjj8yeflnKjYS7opv2tn39HW5LFeq+6JvDB+WeoNBWPXmihn/0 +hKcyUZgWNlE5TFRi9yHshIWMMnrPs51ferBr5ybebPWdPr+IK2suJdHr2j2++HvvwmQm9w1qX57t +GG993wWBZXOV0eTWD9+F7TmSHR17YZdY7SuE0wgiqeyl6vZ+5oGNl94R3dxJVg1517l1gDpqF64/ +1tHzo78ETpmPCzc4diC6Y7C3uxAfzewb35xPxFHW7h3fM/zMrsFHtm//zG8USeoa2d5xz5N/P/+O +XXf9pXb1nKa6uS5XFcxQLkFTF5GCoqTOmdkGNAzMn5wzrb7qkuq2mUo4tfebP9OGF9+xi6pSZA7L +6Hm8qDATUYlu4omtOVAbQOWbZD0IeAT7l4N1xFMF5gn/knXEv/gR5hA7PoPu0gsqnMEgoZuS1BqZ +0gwOyOtKkEMBKc6MJWfOXbJWBiuZw/pcduyu/u0/2L39xwe23zf48kYuFKkyGHxWkt4AIgfKUBYb +dbjabGigNLkcrvoGiTMRY6lWX0MdEKaiTIRCA6AgVCsilxCQwmyj1wOVUUlflEF6imIf1ftgIBUZ +pKd6ANFBqlpEfp+0hoH60Vlsc84+K28C3xLFGpohYiljUkLE+QBrihQIEL2ZggS69jyoY4wISuFC +E4FQ0QKrpxNKuiq9aabR3qKa5oCqKSWNdHXfe+9Pd+3rFC0WWFIZdVmAnMxga1Lr6wMgYh0dHk8l +oXKJBlLqzKSuR8S8oH5VwQ8A8huEp2BEoMZXhrWe8viHbK6Td5tXeT3tcadsWUd9yQTEYfL3/GOq +gEd861E/sqiUFTJf9Tntyf7bvuEYphGqaAdXf2bo9vu1QaNiW/WxiwwBMff8wfgft5SRykZDwzdu +xD0Wu/c5zmV1i4FMKsYvqEez8cEdzw/c8rPJJ5zb0TuWGTi5/ZLaj1yU3dGf3dFnXznbYwkMRbv6 +P/RzfJzAHqF0MZ7x3XBqrXNmJDocvP8FfAOgCpTGUUpjP3mqyt40OLo/sXG/a+28xrr5odxw+uUu +OPCmNj8+rfSELHPrnSvaubw69I0/WRc2BSz1I8HO1FRwEDbW4W89ktzZA3QhdvD8/pFCOMV5oSxk +TCnx9OYuDEZcNROYflfJjTOyLKhH1qxjx/PdH/kp0ZNWjiXMDFjrA6WMNP5bGmeuc8S+pKU88gde +xBRpI8emMPyDx+J/31Xjn9nd/8rQnb83WGCnbGOR7sRjm+u97QNj+5JPb7edsYAmMB3n22vRqt21 +d0PwW3/igMgXEEHxJnCCNwRqvG1jyT6+O2LNAaYv6JuqMIHDsU73L39fsqLjTrWk0rG2GU6xOp9N +JGv8ACLu3/l852fu1plR5ElDxxVlsq7//d3gA88FXE0HB18e+enT2T0j9TXtA+GOXF8QFT4pm7Yu +aMD5HtjxXMf77xr45sOJykTl9g/DTnBeaB/RRCU3d6EGaWMTldxGpVykT/vu+uvow1tgsToHXmlw +zw5ctBzZL8jxOuY14OzSqZidLY99O57r+vHjjZevkZX8vi//Tqj1+Mw1I6OdkS2d2z/y0777nq/2 +zhgJdQ/+4rmxJ7ZOrCIAMDq++UhkVzc2V1y49P4RKZw0ebQLl4hs6k8hGpnXqobS0Rf2NtctGk30 +RjZ3o6bnaKutr5o9muwdfnZvOhgB+qf25HZM3WCs62+fuwfFXUxdBie0ftvIaCSayKHBbunK9nmn +rVaU/J4f/87ic/j5mrFgZ27/QWBOkQkp99iwfgwmvA5GP41DhsQaqenGzPmrq9BuQA2KgFvCxlAo +dtj8oIGFlR7hoJBd1Ip7TGeSyo2kcIwqH+pwKOZBNIm+GOwIDLJjci2+6PIuh/6e3v0vjcXa20+7 +/tp3Xvf2dzefcuqzg31bQyM5q2AUHcTLanGgImck8gcBGBaH32+0OQBF1vMCDDWwQkzODBYSZliL +VumgiBEhz1WCUSc2calYAMU8bCRMIDOESOMiiJShpyXDQBJ6SAH1DYFdjbwZrYy1K5bCBjNSNT2x +9IEIEBxwSJYqyHqAiF01yOjd1JssJqfXUeXz+ATRJ+kb8vr5smlBVp2X0c0vmNpkrjpTsGcKplSe +kyC7YVEMBpcHrOmgNQUelaYFPnqgyo8abCySRI7WisojtayA88csI1SHCnSJQ/+GlAcVEa4PzhP8 +6mCD/y+JGv9tbcl/2cCmh80biwbHlSvUcG7o0w8AAYelnley/ElNhma3tH0g9JO/ea9cM6dljdls +hexa+OfPdV/7PSmS0ggKtWeuLxT9y7Z9g5tWtV/a9O13GOwWQ5Utq6Tz+4bgYaKkhLa86F+3Yek6 +V8xuqV4AUT15NGasdsFLySsZXTDrPXdZbVVbvBBRRqLu85fYzC4Q0Ej9EdS3rKolnByOPr+75qaz +2xtOzuvz6Z29nrMW1wfmRAthBEZT2Bl0avB3G4Z/9pTNYEcHZGJHF+ez6Wyo/COBS9K4zV+5ju4j +gzq3ckbjv3h233XfkcMp2C3tWJHnd4urZ9d7Z+V02cyBYQ3kPjFyaTRiCoAnk408lEV6ybVqntsW +iBSCymAMrJbj8b7IY1vcZy3zOxvSkCrY1++9aNWc5tVIncKSpB7ZHP7CrxGog+hNEIAzQAxgMC1s +Fi3uAohsJEV0WRwus2zlMIH8wCDYKgEhTufCjj274qtPm1m/CsEmopGhXz6396bvIyBSsoBGcoA+ +5HOoUpYCpy0OeFqiSnD0wY3ecxbZEL9KOaExUOto1a7g2M+f2XPNt+Rwcvx36wd/+qTIJiq+o9vo +c+jZRKGChA289SvXY6ISz+/P7B0GSwscAeqmUHWRlw7m9FnR6mq4YiWmMb17qPGKNfNa1sCeYVR9 +P39m07XfbrzqtPkzTsHphDd1NF59yozaRbFiOLa1G1cqcOoCjz0wmu3rvvvJyRcOgI6B32/o/ulT +2ngiO7pMfoehPB5k+oozv3QtxtP18787V81DlhiJtfjBAeBZ69ctsose9IsnDg7DzlicAu8TMXWR +7iGYNqy9RC7cu2NvrmAIhWMYMISr5py+amHTWgyvuK+j5txTWqoXRZVwvhOkptB+IiCqRtfHaGRY +LyrJURHMEfszYiOHz89bLTAbIAcvKsgwgs0FhTT8C1sIVA01cgCeSxpiiA7RR1kCgQxTpSybTyYc +QlTXBAYta1ZSTAeAJ9+fzD/6Up9FbPzabb/+zu0PffSd937i2ru/ecsD111+y5bI6IAxZQnYjRaL +3gIXArhZJHwFwVlt9dQRh0wJRUWoU1H7BMaDPKmGY2RRKvpGACqF8cNgiyVZIi3mAlQske/MG5QC +mPPkfA58NyTXiQCWCk2EKqe4sFAwFID1NFgdNTWzl0YRwjGEc55Tc4YS2lOQU5hVNK+2VoH41Kzj +W3h3Y0pfG87WxLJVOdkmFYwZxZQpcSCvpcxqPqbm8kbYVABwjaVsqZAphEM5EIzjOND+QE4X5dh0 +OhMKhfKSBIk12EpFLqFtg5TBFKiFWSF1lU1CG9nJA9vA4K9wYhjzKcscTzy0notJdC6vY3PXvkwD +KZ1Ym9brOMiRH5ncaszWyZFWvwzmZ7nXI+lptPYsxnFwnDBaS9sekbwtf1CrX7Ji43+xu3E801iI +JuWiRFt2UTfwgXvQtmjwOsDpiK0RHBUwloO3/BxOnLnJj+AtnhjruuhryIjO2vgly/LWibxz87fe +2fqLD41+57HBju0AvcFs2E+fa9PZg7H+4OOb7atnN1XNHQzuNzgtgXedaa732HkXlSjsAjAvDp1z ++6G/j9z3NL+4gQqcI4nGz79tXuPatByXx+ONn78ayEm/q2k8csjoFpFodQjewmDCcepcGw5hdql5 +JXD9OqBPJ1ZWeVRgZSxB7ttkm9M4+/6PiBYX4O2qYGy4/QpxbZsureiyinZGe9/yVU4UFm74sm1Z +q21xs3Yszi3WvOOMRv9cdLAh0uVctuYvXMO3BbSRo4qG2Ks88vufxeIzzfQjCij0h63zGwKIfccO +SPsHuCYfHODieNx11ammBjaByfHgLXfhcL573i8saOIhhAu2D6tF9DqKbgtiUH48zZ05d/zdp0bb +bE6DPZLod+7YJs1uqfPNGYkcyAvuiNeN74kmxrZdcofZwS95+guWha3IbQEfmQMJJRNZtM6vp8b2 +WI4aR1wWpSBlDo3WXn+631WP8919IZ3vkk1ftS2fSUynaDqrTNTcB8oThfRXy6evtK9tU9NKzx0P +MeZjtsMyjCr2WGWUspTmgBP/dv74cdT9cMRYYmzDW77Cify6jXe417RaTXapNxo4e5FnRqPLUlXI +5Ns/eSVQqfb59QgE872hooRRTwIPsEtYkmAF6cI55zSuvP+j2oWDu9Z++6XutW2ltBJ++ZDRIWDR +5oLxRV+4Bo1zQoMbUxfb0b/otssv3v7tWTecadXbxxMDPVu21Z00u9k3tz+0Hy3nK847o7GuFvIk +QATXzW7B8OSRqPfkBc56Gh7aggLXXsnPbKZ+G9aLydpVWFMmo1xDdQ2JO6rd6XRoAkEQA4vB2AAY +sgNlN43MmXHkAbzCQJx4M1HxUfVO8yGBq9daGRClwY5SYwI+h+8GMhZN+vpYcvCOO7/WWN36zc/+ +5PTVF8OUvLDpqXQm5TLXvfWyG+Ood+TzesB5KEzS8w7IXqGOy1n8foMoIlGPCA8yj9SPATJAeEkk +BlU0gr9GLsLgaPavlMuWYAKJz4baBtGFWMykQ1IsLeoKNhQHQbNL9hS+lmREzAhgaIHDrzgJJs3o +sbla6tMKvlYvEFAIzUeA9KKuWmgwcMvdrplWzqfKtoxkTRbEjMEgkZALmnxSRn2U14dNaogvxpxc +3GsKudRMo6XUYitVcTMX1689o3X5as/K07xnXlh/6pk1LW18AW1BKHQCwpoCOz3sHuW0s5kMdTgq +YBJA4pqlvpmMEyysRs2t7fSaIrpmM7T9f6I393VbrcmNEK/7S97cD042WpNvpYkWuNdxuKPb517H +l/ynfOR4pjHXNxZLjqLdavwbj0xuGcRmgRph/3vvYpSnpH1mA6qR59se/qTzqpOoZ0v7PVCFCxs9 +py2qXbbUefHS/FAIpSnYA7QbIsE4Gu3O7h6wz2v22GpyRsl9xUmeC5Znd/dHs+M+oRpmsuG2KwoO +LtzdnXm5S9AJ2BO5JpewpCEWGw1G+syzAq7LlrnPXuLzNIAWzvPWlRgDqmWmRnfjN29ARzDMp85m +8rzjFA/aLqc+sPPmuDzebz15BldtR39bPBfEbue5eiXs4oEbvqeCUwZAUZ6f/8inPFedjCatYjrn +WNKmHcv31pVE3qzXiwVr+70fnP/kZ52XLFGtxomRN37iShp5T3fkoQ3CrGqTzoRugPj6Pa6T5wS8 +LUldvJjK2/SOeGrMUOd2vvMM8PXQ4cx84Ls382ctgJ/NobUQ9OAO/EGElKrVYIEORq7OnphTw2Fb +s5mqPK3j8W5X/4DaMgOWOqnLR5bPLVmM+B4Tzy/78+2+y2nYWUjV64p57GXorkMkUuOCMEG+kIlv +6QSMyGlwh2ND4pJmS5W7yTUH57vw0U95r15J58uuYK43qE2U7eQZxkkThffALu69ntIDJBhCOJzD +DmZuiGhl4FAqA3HEgtrywJevfeTTdVevoiZHncFl8QutvgXfvBGlKkw+5xSqLlrceO2paGmLpcbG +nt91rPtHzfQG82w8rpNnmCeNp+7qlbCLm2/4nrXR5zb5o4kRS6O39uIlgdPmonKJqfOsnDHj2rVq +vuBoCtQBtBXvToZGq9pbvLYa2SC1nrSkZdF8REJNdQHQH8KwYXh8vW/GR9+OdCGtDVGwrlpsaW+j +vnfkFGALgSZjNTsSVGKMz4hMEDLanaLf74ZCr4JWBLJtQCMVIF/BVEAQNTIXApwHjHJIa9BgJpB+ +AzoE+hMykXpKt1bKkDCWxBMAytXHnnzwYFfH+9/3sVlta4OJrvd/7G233HJzJDpCJAo8BKmQtQQr +AeSlBMhoySZzlrpbHILfowf1DRqYCxA1zCCrokoZVcnqlLyemizzKj3lEppMAPpUoAIC3SbcwySg +nE2GQAgZgBM5ljYMxdRMSTKYJJuz5EY2NACtEhPv03NOk8mqR1bAane0z5ZEO9h20CEJACqAtgi1 +QRKUKCQK+uSCtgA0rKRCOlPKoq8f7MYmyGKpFquiCrKM1lf4DxgoWA3qGgKBZp8lYFaNab6YgnhG +IiiP9ueGerKJCDQaza3NNStXzly8pLaxmfdX6Z1O0tcEzNbutKoG3KwpZH6hg2yyAoSLtlMwAJIq +BlEVsqYZdhnKpIOHg6r/kEb6fxoM5/j4mhP863+KITx6nMeNGkdiA+muyIad0Qc3lt0iVQXg5aUD +D/f/z12g6tZ+md3Z2zH8MjoxoJNe7IsdvOzr+YPDmsef3zeYTcSKRrX64xdZ17YdGts5+vVHXGvm +Z4vpvI469JV4JiVF7SZXcSjRdfOPx/+wYUfP04Gq1pqr1ul9ltjOjk60iKBBnOGhAQAsDsXTcsxi +deLrBz55v3woCIAo9krIfIMfbjzd3169Aq43EWbgRijpU3/dPfLtR4447UI8g10SfVQwWqlnD4z9 +8MnhWBfGUApnYRdzh8bTO/smzqjQF913+ddz6CopFg8fa8OBPX0vLmhd5zlpPvD74V++0PexX2kj +r73ydBr5rgNdN/8Afqu4oBESwYCwZvf2CQubxlJ9pdGkCky6KgNFh6FGv/VwYefAwZFX5tSt5Hmr +AY3T33zEGM8hegOhFojCwU2G8QPjKuh5y1Cw4WvfMzk9mUIa7r0nGtdlwY1HE6gMJbLbB7VhC7yo +9EdfufqbkY4hStgx1CMAIeLCJuQADo3tCq7fB94RIA5tohv7cGY0tHvwRe0K4nx3XnpnunMYWwbw +ghMTlXj2wPAPDk/Unhu/l+4fp+2G9hpqfyN/nGWUUn3UYOrUOTt+/Di+JM6Wxzz25VJfdMNldyZ3 +DxwK7mlwziJckkKBEq5U8Imdqc4RwWbrC++P7eo7aqVSwlFOpAnRyC5c+Nn93T/8i3bhCuHsSzd8 +N9MXBK6VQMuiG8tjx+0PJA6OINWEqUMeL7Fv+KnLv2EP+HKFtKTm4+kIsnc0dWZXPpF++Pt3h8MR +DMbGmSI9g4dCbHhEtkLDA8wyv2Vn+u9Pa5hLgHEIgMP0NIjwAC8AnMQ0GPRVdTUoJuelFNXaALcp +5DgVBTnoJpLlI0gOSc3jCUcFVT8ynEQ2jjwubCHV81Db015oCkCyDiookO0Cw60c+dWvf3bmmWee +uvYszPTP7vvuCxufv+qqK+tqGxH0vLwTIADV74VjhB4dKKiIIBk1Wl1ObwCp3yLkK+LhIogDczF9 +PqqXE3olUSoklWIWFFYK0aEjrZAvoo++CBOlgJQAESRwLFDsAh9sIhbTQf6qBVBfxZgZLG15duz7 +39n2sQ8//563//2G6/523fXr3/+Brbd/ou+Bn3qSEafVkDHIGTAAob6NHn/OkNbrxvW67nQygcyz +D2pWXku9NSdkU2oaATJf4iwQhTYh2W3g7LzN4fTbUX62czlS3QSUdTQYHxuPWy1um1CTTYtjQ2o0 +VAiOx6E8nVdiQDYRr6xBb7VZqwI+l9dpQv8L2mR4FFb1vM2MFLpBgC9TpNqwxpbOkLlaepUUzSpP +4uj999jIj0iBHv3j0cM8giOj0kv8hs6H2lw1T2Lyk0nlTPz+dQz1DY3pn/jhY/Q1Thzd3OIPfOzi +odvun2CAA3tq413/M/Den+Z2HN680IDvu2GducYdfXJ7etNBQF0m59zRqu88Z7Fzzdxs/1js4ZfR +YoFGe0STUm8o8eROFAsbb7sisbUz+KvntaMg/1lz0zkgfAk/vT3+yFaquut0DbdfKTRXhR7dnHxu +34xvvB1wwJGf/AVBJzoLGz5xefLlg+Hfb0KqtuGjl0rj8dAfNlS/82xYsuCDL+JwkyeT5VLoMfuu +9wuzakbufjL0u4225a0zf/Q/4/c+O/arZ2nwSI757NXXr+NrPKGntqXYGeEjOFbTJy5PbDkYemgz +qiGei5ZVXbo6vqUj9sQ2aShMI59bX/vOczjBFH12W+KJV4ipBQSPPof70pWlvBz/40b76QuFZn9u +W498cKT6I1cYPfb4714oDkXMPrvtLSeZ/C55V4+hd0xA2MJbVCLGNLgF3s4Zwovr8i6Bf3GjZ89u +mNPigrm55gZzNKm81LEjz3PvuCi0rWv895vQDd7wttP4gGf879uDGw/IsL9oBKtwYiG1y3vtDVeu +AuRi+IH1mJ/2T12FEfb+6Ama4etOE2rcwSe3JTZ2lMCszR5Y9/N/8kHrrJqBu58cf3AjGjzaf/w/ +o/c+O/jrZwtI5zFaZ4bTKm832qfW/OFTc04+/eDeTS9e9GXKt/vtLdedbqnxDD+1LbLxAJKiaPmf +/5m35WOJ/t+8OO/2tyIB2H3v3xBfii1VgXMXoyd/4P4XtOs+6aHlcnQr7nq/fVZN191PDv5ug3t5 +69Ifvafv3md6f/VsibTVIcpuXvKla3EjH7j37yguwmbM/+ilrtn1XQ+9OPj0rryq85/XHlgzI5mN +j3UfCtTULb/g3M59u6PdPSVZHR8iJXcb2Mhi6VOuf6uaSo4/8cKMd18JXGXsL08Vu7rBeoNeQWpq +RFBMXRxEhUNdesia8jB3JcFlnzlvDvj8iD4UCpmCUAKtKZoaKcmJlkEIBUO9EfVKKxQx0dEIKlO0 +NuAFDAB0lvRoX4RGMhKU+I3eygh0yR8sEnsS98z6xz70sQ9+/3s/PGv1FR2d2656+1vPP//U2z76 +FZetMZIe+tgnbkgHD3z4tPNaZFsBqVzoFqo6t9MOwE06GSPRDrVAeoik6GnSiSK8LJWY5ajtkgw7 +jk6CDmRkaJNlUl/Q/k0BB+Z1iQ7REIkkXnpl9IVNoa27DBEEgSm9XEBmABcJ+HIslziiXbsVwLEs +1L4MKlpVCuiyKCJpCk0MM2jHg/rSiMeMai5ICXjQ7iHEjityDF6KihqibNXJYLmz210uj4jcsQ5p +i2xWVaAaA2IKo04W+ZLP7QAUFhxEqQTqmSYE3tBS5EE9buISqViukLRaqScVH0ATKSJoLHybxYIE +TCoUlxNZY54MNa1qpug0kU0lPC0ziZgxcqxe40Or8xEPAgGY0eZJNDOv8Tte29uprxHXkaS0WVr4 +WIcrp48rp6Ml8LWomMCBhI9AsmIaNmnyzyinP/V06Du1IiNVEI6WGH9tJ/Nv9u7JfY3HM40T++Pk +8YMw5QiuVFbInrKkjr5aWth3nMdrX5OvZ1InTKNGfHPU/kvfeWTl+egSNzVuaSdND61WMWk0IEMj +cSJGlMKUb0hLCFAAht8gCAeVp7Dhsl8S5Q02RiL6Qhcjj60ImroWMF7ir9DFtYCvKxEphIeNuagT +GCjUo0o6u9eXLeg3H0ruHcyGonl8C/jAJDlHYklouAMrFjKiRFaiKSPS+iWsiEbiwopc2mqne4w+ +QK+ZyjkVC9kfyvcMfQzFNdI8PHwXMUo/DdHINFxpyg5ffffa9iU/eg9XMGy+6bvxPf3l73sT9oqy +aTzOhYNp1BYeq33S2LRxkmPLpgAMeaUa3tBiSZtkbKjoj129dtG+fXvR/QYGtpGRBEQeIDJsT+W9 +isGJK4iioAkoR51F09nA06yDZdGko0HxDeeF5IihJo1qomBqnNUqepxItkISAvUFaOryFpGYu2Fu +TbB5QgkN/kQEirSnADNJT2YRNdMIoCd62cl8oveAVKqApcGfCO0J1oDP3/HhjS9v+fNvn7Va3Y88 +/OuO/Qff+94Pej31o5m+7/3gs88+8fA7Lz9trbNVjMKzM/LFgtOoUyDBmUzqcnnq6uc5JH9hpXSC +FWAkEC8R46oZnISERsITxpQJVAFmR/giQu0AxhNwwrQGn39p8NG/F/YdNEqSy27OQzcZQovYWXOF +HBo6QBCOSqPJEEGUbIRKVR7BMuXwi2quBFo6faagzxj0YaMa8kHpi0tmcqpJ9Vss4DtABhf17Cw0 +s6n3BbeBGb+mEi4a94uIrTn8HaGm2WKQQXWHmcMdYbFnQB1eEgAGImQOkh56tMEQ8FbGIRnOFyub +hLiMnGhBPpXPRTP5aJrLQIeZFjuJgrCKI1u5ZYOh3QIMVfXaHv/dppEVyI9nGlly+r/n8ZpN4/F3 +NvrrUabxiI9Mi916U0zjtDvwBGnZ8Yw0c8QmQF9Ho7/oZMmxZpJv5X+ZQSlPAv4E/CETVCCtRUrT +QAuBnHL6kXSCNOZUPZGigAwaFhHKrCBPBh80UqrAG5G5tOlVS0nOhQaV8KiN+E2gb1Q0WgSjzTsS +K+7oCO4fk1OySpwfMJjoMDOiWENmAHlSYOwRCCACIFavyqkSWoGpdmvUqMw0UiuYZr4JKcJUvRkL +JNsk2DuYaSEORs1m4i/MIjLdcdaajr9YarwA1KCbAl2tcz5zJfyC/p8+3fnjv0xM8vEZMbQxkYuh +ET+z25H+I+KysmogQ6fQ246DidP8AKonkSFkGx9LB2mbH36vWAx5r8ncbE9yUN9Cf65aW+/y+JyD +faHQcEy02GHEROjgxjIuBcViEmXiuCIcEh5d5HA7CPMJu4MgrqxdLKDQyBMFOyqm7mp/dXMjsKoo +r6E/AeQyKB5DyR585CYB7G1E8VZCsAgbqYOypFU1C0SVixgRgSOaWgwiEqBQvgKgE8gbUh/UucxG +Hzrywe2XV6L/88Gr3W7P97/+W/Svg2MD2EvRZtvXu/muX37zsYf/dOPlK89fscoWUsU4OBSAZM6a +pUwqPI6ysRVuF8IoM2hrwW6DqM2i5+1G3qEzC6qFiqQUIOJJThoaaQXwqqHvUAx4kJiMPrNRenb9 +4M7dXDbvg+wzr8sUMXUKGAcVtCrq9DmdAdpdBShBI3bUGyS2Jgo6AwjhwNSDXHBOb8gVdJLRELHo +Q1CCtpozQEqji6MAbiYwnlMDCe6BfBZoXtRTwf/Kmy3EzABXD8BZKQsNDzgPRmgx4i/I3ONGQUTI +glvi1cvLEEIGo4IeDBFghsObqfUUPD7oRFEk0WK1GMxSPFNM5jlAWPGJipw9FgjujvItS+TqtNSY +OOVU6vHyun/VDeOfbxpxk0K+usKGU3YHjxgfLXimJab9fjIbDgM/k8MOpPLxbVrZQ6bbkPkR2mTR +ay1q1H787zWN/wuU93EfGmHV5Mcx7NzRpnFqmPimWL7jjxN/PZolcVqTfPSSmrCL2qLXvmGypYTX +WaGFYuaj8qAlyHj9USShW6yC72eqGSxqpG5kjRmViT2h29liMlotvAUk0ChygSqVs8I0Qrq2KFtB +MjTYa8jFPYJqZWTW2ExlztEzktmxPzIaVeD0MyJMRHQsEwZDWERpDGVWmAYSaMdvKM1TeUwJbCu+ +MbFJMduhUT8xZXfGW0wnzLq2WYxJ9wa9jX6H+iALyEiUWLuspz/5pfbZq5PJUF6XS5YSQ/e92PGd +h6e9WJPfUI5iK1ge8uhx/9HEwuzQG+k2Lo+5fE9WruxhL58MNsPKksHWNrhKEInRAgkDbuykBWS8 +omIz5gqIMWBlSoJoWrpwwVjf+EDvGDKjAZvVEU151aLdTOcOGnGwlGEDxwsoNQJRAqAn09xgYo2M +VRzVQpvXXtvcCM8AQEmzFQ0FoAkyAxQK/lsYRZMFRHFIq4KcHHrDUA+26ozIZHIlg4UzO/XwgpBH +RQYUlxO5VvCFcyLobIBfRjODyW47dGjP3ff/6ME//OlTt97+vrd/VAEBnU1UdOk/Pf6r3z74k9DY +wbdddP7pi5eVgmllNOYAuDoa1SdShoJE0FnOgsobjRLXFNTgZLnhe1kRNZZMlpJIPhpcNvSFIPdr +4kSgi1SEuKBbMuq6n302+chf/Mk0/kLKYoxUFcbLzLwfAAD/9ElEQVQP3hJiOfQvIqWuoAqraarA +UrHXKGyDyhsv8rBbKCXrDbKqS6m6MbUUt3HGahGZ6Dw0pwDldrhwS4TDYzCRuFdAdQAeVXiIbqc7 +C51NQveiJgFst5yW8hxamaDvyNpArSRHalLwbjCospIBWmAMSK6aLUAwk1Ak0/ciIwkJk5wE4TEz +eFvRTqNJE7NtAkuZGkdZTMSWMZlHDXlxdIrrOIiMf6Zp1CwUULfIjePGYMubZb80V37SJsvuBXI1 +j0MUN23AN5nxjrkM5blivibxs9OXl9mUXtPt/u/75slRI3c6pQeP+ziBHMMJ2M5/xnS8KQZYW+tH +BItTfkkrriwgNakxiFYn45gGop+UZtFtTAyoTFYDPrsmtQgbiV0TcSG2LHjHAjT30LEMfi+Rp31S +0NtNJUHK6uNj+bFOsxx3A7Kpwh0vGCBAX7DtPJjZvCuYziH2AMcmSVAwTfZytYPkPtj9wi5Hxa2b +miKubATly8HeRFaQEYVpUSCVGNiXkGmhApSGr4GhJQAlew+9Wdtf6Fl9+qKCXUWzYDaX2PmRewd/ +v+E1XmzWP0/0k6y2Bi0sbG7M5WD9U0TFzY7EgkrNZGtGtDyE8tHIYE9EoIctZvmd9Cd8EZwJZOwA +ceJNyVS+vqkOtNej4yMupwOxECkvSoqjqLNqcgzINVK8SJVFRnaDXCMdk0ltIPzXhqcTHRZfwAdz +SOyZJHqJ5kf8lYmJ0ROhJbLnWhsBpdOBJpVNYA3XC+B5UtHiwSkFdDzAhGFUiKJyaj4S2fdKvHuj +HNv6t7/c84U7v7Cjq/+q66676ZoPi7wXB/3bxidu/9/3/eq3dytq6JSVy89bs1KA8vJQOD8aLkQT +hkzGkMlaMZXUdQ9ZkYJRAfkBgWAZXAozQSya6GyE1aHfYvaRdCjQRAPDKsCUp5L99z8UfvSZ6oIq +4rQJZqUR9pDTR8IsMEi0SkgIhdH2UHYBzovGkq69R+P4AZImj/8DfFdniqI3ET4HGBBRXJRyaB+h +tVZSYbwwOdqaw2pOxBNIJ9sdIJUjoBNJM6oIccExq4dsF3KwEHBkUqZUk9DIEohSFGcAjTXmg0II +kvEWUcET9H6FdB5SIyaMFeTnRNyKbg6N6I6tJJaHR1MkTojaS1mhhEWP9Jxo3dNyD5OWVeUOmuRA +sxzRMd7zGm+HY78do9IqI0y2p3wHTByNbVCHR6fd5pp3O+UPrA5U+d0R2/aRx518spX5OHzA8hRN +r7T6ppz9P+lLJus16o+OGo+ItMgBn25gR8zP0bHam2K0phvFmxA1kgvGQpgJWzhxLkebTPabw5o1 +TGaKFjCFYOzeheQ4JT6wIyINx0SJkWYDLRzkgxCJkGU0g+hZMFgFaMObeR2ypuZ0ohQK6hJhavGy +momLywjEhHMkrtvWGeoayQLHQTcHtApYkZNldyFqhE2DnF0NS1ZWy5k6X5otOeJSaqu/nIektCnR +Y+KXwASz/BIlCPEv2+w0GjFt56jYJZYHxYyB8gZoRzRXHPMaTUcxjGCaUmG0j5IbT7curDwleJmu +ILF2VkLAI+/0ysKiuJAAdex0mJOvfWQiiGTN83pFNJRqreYam8yp0WQSIbsKEUVFBuEDovVCKudS +dI06oxswD+AjWVCItDeiRiRRUVJE+KSFjBpdqsGkWkSTx+8RbBZIJkF7F9RFKL2ZLCSUCD4j6Agj +5YoCGkWNkPkghnGTiuAGK0CFhRCgQwV4jGp2GExufL0UH46NbEsGD5DMkUv8+7bd9/35YOucpe/7 +4GdXL72A0/EQJExlxj/2mY8BPN62wJ1MRw/s7vDopOvPOa3BZM0NRq3JUp3eYUnkUaiEsi+a/DCV +pNwomBDdIhFMsSykHFHg5Ix0khbIV4kmFJ31JlDKWX1OLhodefyp6Evbaq1WJIAz6PpnaCvqJgF/ +AWXpDQoKe1TXI6AtxLQQKVLUqDOgJQNxHGC4YN7RokYobLHXehAVjijIUxstbgs4Q9CPSna0CPS6 +AbpTwJ2hbUSGkCUWF9ETGJxOpyRJYGdFVRiTJKOfFUh1VjWG+UO+FIUJyHHAR5TRb6LISLzkJRyN +0YuDTI7jpFwe8Cdg15Vw1pwnW4hVjS+AU8lYXqmwyES2iceAiWzTzcxix4lg6/By1m6cVzONby4M +hyLhox44NTgomB7MA7YTmPLJUSNb8ZNMI5M9eYNRY7nhs3zirxI1TrcpH5PWbroP/cv+PqXWeAIJ +1SP30yMgNWyPnsZ6/geZRi0WqQRF2h6rZU3LtwbcUu3SaZlm9ieKo5jkMEm6U8mOgW7g1xJpCn5H +4kDgsYLuFDSITZBHQDkKlSjasRjfjQCEuQ5CdnFUFg2ppKgrOng0XmcFm7vA+Tp68zu7o0PJLJIp +gkUHcXOqY1JPC9k68uiJyYciO+3mpVRnGVNAIyzHUlPuHTZ+KrGz92so9rIFI2k4Fv/QGxAa4VjU +BMZST9o3M4M8UZ7X/Ok39iAZRNzNyHoiOKMJK49OBcoC2xYLKMrDqzjB5SVXvhasxAh6g/IwtAww +RQPEmV1mZQKQUrHrDfVWs1/Mo11cVYPxJDKEzY0N8WgY+TkvKIjiaTKNJQhEFFXo8UKOAipILKEK +rjOCpJrh9aCWrMKVEUDEZuNtLhsSohiAaAOFKTAwnBmSYYBOEq4K2Unw9lpgLfGEW4NFAKUKaGng +Qikq8C9O9ORjneRTQ5GRvdnRXohNIONgbql+8KU9dz2ye82ZV33s5s/PqZ3X1bkfaNfm5hm5bGo8 +POjyOK0221horLNn+y9//t3hjt3vvHi5L6+zjknNqgctQCbs1iaESExuquIUkTQoQ3vBLiKSo8Vk +BvwHqo880rm83cYXiyNPPBl+eWeNaJYMJXC/UiMnCSyj/gkaA1IvLKgGCdAXiiMRCyKtSow+FdOI +F2jPBF1jxTQa9Hkw4xT1yaI+rCuJzVXIe3b0Drh8ILMAHAk2VjKiimARgRzL5zOiYKnyV6WTSZg7 +XOE8IkocnfG+Ujt/EbYev4AVJuEI3EwOpw0OKGwu3RFwWyDNjdIjWGfNZlQ0EdKCb6oQyQuyAc2N +lP2j1ARqkmxiADVivTLUYUqpEU1XRLMI5SWtLa/ymmNR2xGPf0xC9Rg3FHO6kbtBTQZPTT/s3yKh +evw7/2hn4o3tFP/YT7+OhOpEDH5EdH7CAz2BGTIFXK4LFoO/BgV0uZ/aIV7rAwsXvR/+K1d5L1yR +6xpGZ/1r/QbN4mkG79X+reh90ltYnMgyfJQ50xKqDI9apnWEdAbJ7wDfYGmpsp65WFjUDHJIQiUC +igdkuYANFAAMvVXJcpFxNThqzCWtHBJtuHVlwebN6HybOyIb9o+HQFligbIjtgAk5MxkB1i+h9g/ +wLXDbhOWF2USQhWNUzKdU9Mi2oRoxcKJhCR9ETOl5C+zooVmGYHCB1gFFoVYTVh2i+0cmjJqORNb +trFHTbQNnPIXLK0+Z0mmZwxkN8e9EIwQBmUuPSSbdDararOpTgf4PoF8oUhYUwgs7/GHtyntKxGJ +GavPXtjytlPQFZfsrmh2Vo5HBrdiHVXRaKgSFJtqsLIEKWp9ZpGxs5VESBijMCeXxGLJWVQF7J6E +5aerqSVUSXYKCFV0OmCCUBYk2ClqdjCDxJiGH4nCE++j8h5lUymxR/+xhCpDX1EGllWACUOJyhqy +5LBLFocun4oe2pbofykX2scpWZez1l2/4KEN+3/ym5cvvuKDt3/kzhZvy5atT3/uS5+Y0dra0jQL +Q/F6nJRMBNJUDMyoXzCjtWXD5hfz6cRMX60FaXdqm0TAhH5GavkjI0CngKBYNRd1yDlgnRaRTi0V +kVckKUn0xgMIhLKkXBx/fmNw8zY/qGVhNXDCCLFkWgca2JqysJWsKWucpygMlW2WTaWgnHHc4QVO +rZxQxYs0ZWwhRKymdaWUTo4pcjSjpKRiMkvyjg5kSgGrgWQjwNgsHQ0Difw2gkKQ2oCpjqVGCMGK +xAhluJENRVAIoC8rpcNY0gIHmXgKktzg+KGxUPoAI8aEI8xEB1MOZ2qEq8Mq0Vrkp+VM6UeqtZI0 +FhIkwOkiv00er9a3x2BplcxqRYTpmKZRM6VvXkL1iP2Wfpx4wM2eKCn8myRUjzHcoxK5r2cr/qd/ +ZnJC9Ri1xiMLhydQa5z+FKYzjaC/af7eO5vPWrfktMvyyx3h322AMM3xv/bI3CBnaPriNU23Xd5+ ++rkNK06SfLroX7dPP7Cj31G+H/T2lW3FjAQMnLYoy9ZEsx7sYTupVc2ASYSkWckksdYMQt/AFgI5 +Dm8WtUZgD81G73susF6/btbKcz3z52cEme+NAoqKVCrh54EZBJovFCwGR3gp7eBNJPKqUy12Rzxr +2rBrfEd3QgFCx2ZWinnm9qJ9HKEV/EYoAmkK7eX2Ee/K2QUMeGLeps75EQ4vi8sqhTu2jrX5ZMUS +th9QKElZL2rPJI5Q7CCkNqFhRVkeV9tkyrenZ+Us0OIWUTfiDKt++/G2m8+bf855zatOyhmywRf3 +Tnrj4Q9V7ij6f4TiULV32vReN+dzG71ek1VEVY9CUpKfpxxTucCoVVC00XpWtq2979bZF59x8pmX +Gxe7Rp7eocQgl0ZlUQyWdV2xciUiH+BL/WZHo7OEvKYJ5V2xKBVFi03JS0R0qtNJqEnFstWi2VIs +QgoKVpBAK5S/YsVPIFQZxJh+g0uKS2dGbpzZGVx6WCpWf0awCDOJKhYBkam8qGXUmd9EbyWziVlV +9EjvuU1GG1gV4v2bUkM7zHLOZa01CjVFu2Nzf893f/X0mWe/7cuf/gZ4eTp7Nn7q8x9C3/oHbr4F +88/z+j/+5d7P3XlrZ88OmEqogNXXtnQPdezbuW1eWxsaKpACDRWzA+bCK3Jiazb4cmJ0ZzrUkYqF +dKUEGATghsCD5HiHCbhPWutSUQbloWg2JTp7htZv9iC2hfYhZhhWU0K+meZaq0aTEDOFVsTZQ0lW +ZgipoFjRX2ZZ93I9kl6TjYEOihEtHEkIeQiGWKmQANkOJg6gJHxQknBcYFYBSXW6RCoWolRQLI2P +jwOvDVOXyWTT2SzRtiJmxB8wKASO4O4n94PmGC4GxkbEtqrO43bH4wmsYCSzkVbF+6ncj85/SheU +wJA3kfEgRBw7p3L5ANeXat10U1VWFnkCRAJQSRZpfyjfRBNFvLIjXf7Qm2caJ3YlZvfpp3J/HIZD +rhZdhCkpXjbOwze8NtTXVGtkubEpu+FUp1r7/ok3ld9LHvTr2WT/TT8zxTSextbDlCerYR3vyXyk +iSe7bpUt8tVeHHcqYBerPnQuNr9sOg7NWKUvMv7r57RFOfHQLvZxxtn6nZtc5y1CxJmERntsePie +p3I9Y6/5CmgUJ3pdyxevbbvlavt580O/WU9mkG4S5JYI7c2iK0Pj566e8aGrxXPnx/6wkdKouDth +CKkBw8TMInKnyL5Bisrsevd5pjWz4IXGcuPxbND4cr85raLKBYS/qJNNmZAS7C0lR0VOtiFzh60I +t6ng6I7wm/aEO4czlJGjNrIiy22yZhGGD4XCAGo/QEOw3KNhwZeuX/zRa/wXLOj79fMs5NNM3OHH +0VeGROkrtwOMh9awSL4yAyUTXwEIm9EgDsJmopgFZJyFPExFiSQnmGI97fiqbsGXrlv8kWuqLljY ++7v1y773bufSFuxhROCeCB66+6/ZgQgLZGkIGiG39m853qb9B/RsqmjVVXn09dVcXbWhxq9z2FX0 +LeAYIDKDWp8CCwfCL5bcpvKqqoozqtb87MN6mymP9nxpMDca6/nZ3wigr+ckjB7OBKpIFGmYJBAK ++TlzvVnv4FweNzQZkskc8tIM0lrKZrIoQ8FJsRhVaHmArxr2HyfPxDQQOrJIkZkHau4X0A3IDB+1 +NlLZF29kwSHlzCkTDHQHOh/JSUKPPVlEpMER9VDjvgkZVJwBGnNgFxGQBRN9W/LhHpHHOwW9WQQF +qmx1fu++Rx3Vs77yv3c57PVjsb7Pf+29fQN93//WT5trZ5agqMJZtmx/7k9PPrjv0M5oInTmKZfi +N2op+5en/2SvcQzpCgdK+R1K9pVCfqehsFsn79aXtpWKGxTlyUzysWTwL5GRTcFoZyI7Cj+GyqK8 +HdTvaPwfHenbvMWUzzvBJSOBBkhP+FKsCoZPoZw2LQ72A5V/SeeRKtBUogN4FVeJpClhg5BlRZSI +yivAq8AA4TXQqlHe2KMvhU3GHEgNeMBqkBclmRCwiSczasDvS8aC0FdGFTeFjl0OUhqcJGVZN5MV +SRECneGKoJMGDZpwoFCfxOTJeItUyBeVbEHOZvw+1+LF88eCowQUo9w7JKRNIIvFV1EqVsoDnwtX +AKsQPg+ezPBR/r2M4WS4MwTTdCCkARC/sr4mZjtJepII9Cn9QPCj8mcI183awRi5B3u+6YzbWE+V +jiuWrmHCL6yOMqmgUNkqX5tppPC6vMeSh13Z1JnnqjkEjOP+8CbCMEqssqHVYuns2fZzeFue8vbJ +tvo1b8T/mg9MNo3HI4r754wOtOAN77vIXrIPfP63u9d8eveiT+y/4muv9dB8o891yjyxJHa9/57t +Kz+1Y+2nY3/f9Vq/hDln5RqVbVGL39sMdAb97nAllfwmlkhVrYuaqzxNwKrjjoX0j331XJPNyvZH +tGkRLzihTwE4X9BiXznfqXeV/riF+8YTpu/9zdgfQlebxSKJxaQpNMSN9PKpUYcpw1sk2ZDVC+aC +ybZvMPnYpkP7wYRltnLo66AsEkQbCoBNkCidHrUcjvJD2ojZEnQvaanyNqPR+sTPmumRs+XOHuRF +s4Ie4VkoNYvslg4cl26HWuXjqqs4r8dgd6LGRhsTbiXCZZDaBn2Le/GMgLcZvJU15yxpXnOSSTFu +ueF7z6z4xLOrPxne2IEpO8zIpWU42bNs5FlxFjSfTqvO71XrakuNTaXWVkNri6Gpgavy65x2VQD8 +k92xWiqYcY+qK771rlp/a743+pdTbn9k0UefuujLYEIhsUEMCg0L6KLzGHV2tDMWESkKHovo8yVy +IJtL2Rwo8xIoo5AtIJ9dXVWLTFw+nzLzpZycZ7lq2o+ofkuuB9smypkDipQ0vJOGBiakT1l1gfDB +7JToTwwQpYXztAWzOi3xF6JSphTSqiGq5vsSg9uU5IDIg0MA/amQQcy5AlXr9/bsGZTffuNH/N4W ++IqP/vVnmzZvOX3dqe0zFifSMTT14cizZ85pqneftKxx28vPRqLQzizYXA6ESLG8KnOCZLTIQEHr +OK/OGNDxDQZLi16coRdajFaf0SYb+L2y9MBo3+f37Xrfxk23rV//p4N9Y+NZuTfuiEluzpjVZ2OQ +ddErvFQywnlg0SOZiwrPgwZGZU/GNa4ZTTYlLA/PwkntPmKYZ+Q8opKchjgG8is0q5Sq5wVTBg2S +nD4uS8F4WrQ707kMIkbYTSNaJalHl4qKcBMFAa39SMdCQAZq3ECpAmJjKgAbVECjpsFmFyyiweN3 +50uFv69fn8xLMH/ZrExYWIwbR4f+slGVTaDpoytxdBFvohKPsWmEOCCtm3iS70h+IqF3CMDDSg/a +wqVTnYgjy3fQid98J/jOw/fm4UjhvylMO8Fp+Fe8jfFCTX38k4fR8OFLZtefBDbw+NO7X/ehq952 +SkvVgnQxARWq1/0lmp3RCifdX/j1lh/+uOPDd5f3cZZX1J7IvOCOHfjqb7ffc8/gp38Beu6Whz9T ++8XrxdMXIqBAlg0ofOBqUEdEs7XljMXtDavjhSg3FDPbgMXjrSJnNxeshaQhNqILD1syCTcaABCQ +ynreXJUv+F/am352RyKPIA3wR6i2shyStutoI4ClQW6JsoyUyip3uu/63H3Pf/+HWz/4k4nf0CZ9 +RJx91I+adi11DdJeT4eAg6z9EvsEgIwuG+jNdS11xpYmrr5W7/fqRZsK1jOA3RGeEf8YswA7Pnvf +c9//8ZYP3d18+doZdYvBcRLvGiZoDJ6oEpGvXX4yLMThHwm8jz2LQmUkVHVOh6EqYKqp4zxVRa9f +7/LobTbE3uQ5a4hZxs1De5JzVq2twW8xiANPbwUMhFxXjcMbxKMQ2EXbXoDja405e1G2lwxek9HL +p9QM77Gm5awkS3X19ZzJjBotOFwRZbrcfpNZlBVU/9CVz+QMCSVPeEU2LZUdld0uTPWX7GH5T5qJ +ZLZQUyaiHDf7K3UC0uwyY6G9v1A0mSROHc6O7danRkQEj5yjqAeHjigXlZxeeX7bzqb2BevWXYWd +OBzpe+zJ39vs6sL5S+GAPfn0w6lsCGr2s2YuNZmqkwn8LwlcJwKhSDKJcI1HndYM8tGsAd0nopzl +EnkuqRiSBi5p55I2Oe7IJQNFqc5SrHVxPifqf7png9FPbH35nS8+/u2+/Qc54HZ4e9EooG2fK+Vt +nGQqMzwQmyyFiayyWMYqk8ksK4WUSTU19iW4bgRpZsAWagGGBFcOF5wgMmSpwMqrk0pAnSJ0g/Aa +FIvHIskiJ8CaoUnf5baCDp46+lElhDFEsleV8B6IKCOWQ1kSgXsinKqpqhbQ1WEGH5/gC7ibZtR5 +3N5kAhUHkMoZzCUewFQ1LYMnB2AcHFrHI6/LsDZHVN8nvEstFcPsD4ssdVAdAWU7j74RGEsS80Kw +WoZtk4NM83C4Z55MZmXfmfamm3aDOvY3aC4w87KOfvwj9vCJra+8xVSg7Ie3xGnP5D/5Df/iqJFz +iaZZVcCsJF/s0KhK8RvP5SdDkcpz2Yr5T36h9kMX0mKF9trZC90XLMULbbbxwnPJcucZ87XfeC9c +5hGrC8NJvHaftxhiVce8KHizd9KnQIvqv/4U/Is3o7KIb6OAELJ8jT5xQVPk0a3KUBQ/mtyi97KT +bQuafRefvODRLwTef77Q7BUXNKSe3lkIxh1nLJ5TezJyNcXuUZhGVBYBUwSFCSo2YPHQz64mIriM +bG7w8LMCTh6QxKI1GykNHpTTY9mFTenVS1TRVlKAVq2NJsXndgb36+017zh/+fffY7MhM5UyOI2+ +S5YLM+uK6NNes8C5bhHcYfuihrorVpns6F6jaMra6HcubB55dCtSl0Ktt/7K1a4lM3BSriXNdZed +bLJbjpgNsJXWXLB06TffaXRbKxVDSm9ia4BuVM3Fy6rOWGC2I/Y1uBtd3gtXVy2srW8ye0+b4z9n +gcOu8y1raWJHh0mDKTI1eGyLGvsffTkzHLUvrEeImz6AvBaloWCyIFsROGtRzQXLqEeFHWKis0rr +jwT1gX1OXfWNZwa+9AGDCzh+g+z0HGo8KeZvgJaCOrNVPG8l70SuWctuUeXOf0r7rJvPCdiaRkI9 +41u6sElR8pdZMWTw4Dyo1qJQY7Q08rJLV/IYzH5etaqqwMmgRFANXd2jr2w7VNccEH2qZIyY3Rl3 +tcHpM0ELE2VI8jxQ2yL9XuqBKxeoWFWJ/sesQdkIshZyiixZEEmuCElKobOA/BnER0S4gidqwmAU +ZW8gRA6IX0JROZoAehUZYgA4AakEB7nZZRuORA8eHHvLOVeAn1VRct39HcHggMvpnNu+KCOlHnn8 +gUO9O5GU9Tnr58+d29cfRvcCkc/oSnt3b0XyWQT0VCkS4w6yumAe1VtlvZjQCUGdYVjlEqDsdhpy +Vi7PG2UT9OEoXx1wCHa3uceq+60+80Mu/TuD1GcGEYANDHJ5lSjfgDjFASiLTvAh4jfQIDasx7/s +GFFydcJkTlBRk5dJYs0ZfBYYWAdvQM8+ch4QGuYglEbuRhbUOhSsGQeGQ1hHCA7RywK6dep7Qk2V +rikqzYRMdXm9cI2isXgkmrHb/YuXLEkkCpkMVYmNVi4YGc/EEx4BJlQHwTU+U9KFZGNMNsExyBaR +9kWVAw2q8Ocm25XDmdEJkDYRG5VF1vA75GCRiRewmkqqSE/SEqGGSEL+lh9HfOE/xhawQmIlbDyi +0vSPOSJ966sVxyZ+/4879L/DN+u/SPpBUx6T8of0+6PrrEe8gd7z2mN8/1Wraz54AWow1S7o9Lp6 +QruAB8h1jloWNfrFuvHUAK+3zKxfur9n/dAdf2y44xpU7VycZ6Rz774rv15/60W+69bC67epYriz +q/ND9yx47LMQjtj0h5/bl7b67fVxKbTnhm+TXMakR8OtF8EQTnxKjqRcy2Z5XfWDPbuNouAQvSgb +9T8ChUWd9/xltZ6Zfd3b9130Ve8lK2pvuxRDCmJIBktr3dL9wS0mxYA39I/soYKbzXTKzMs39Dys +B13W33ZJf93OWaijnLjhauzmD1w6o3oxPuLU22Eqc+Gh+l88oouPpVoDo9dfjZPymr2pwX2t3/n1 +eE7ftWap/Yo10FECucqI1LfrnC/4Ll3d8K5zq5yNI6Eu7LNue22+mI71Dbma6yGP3PXC+q3v/fH8 +L15Te+FyjKena9voX7c1XbfOb68bi/TmRxOeGQ04r92/eWTvl39Ha50zzPrIRQ2Xr8JxoS4ZK0U2 +vPXrmUNBGAA8gVxv+8hbmq5bg8DXqoqh7s7Ehl21152No4di3dgx7NbqrJJJDQ1ZauqBjTz4wvpn +3/ejk75wbfOFy+s8M7u7tr38yV+e84tP1XhnPHXntw7d8wyMVM25SxbecS0O5+Q8A517X7zya9p9 +RYPBwsJ4PnRh4w2nTpwy/8UvuC49I77oNJ9YF072mlNJ1V0vCt6Ohx7e/sWHUGs0ecVVv7zFXlNd +52udVbX8qf2/+ut5X0xGU8itYZ9GwyH2XNla0jWUZqyusXot+3cPFGVAVYQirwARGo9m21qa6mtq +D+zrtVhzTa21yWwKZHHjY6FD3YMwU6Zk0ZMvOoslK9Ac2E6R0EPHH7jfAKIsdzSiERHBJfOCqDUD +vapoWQRhKgjhwINjpkoZ0VujZwN8uNTCoWfdjehP5SCSBH7TXCIVHjBC69AESjbwjiISQixktFZV +b+hMfu7bf7nrZ08sW3IOzuO3D3//+/d8Hivsl3f/uaV+4Vnnz/nErZ+96Pz3oSVh086H33vLdcvm +L//eN34FJruPfObGZLJz0fz5kbF4QSrGU3EkECHNBGpXGWnZXCabSUWjcWQD4LmBDg4ds8BvJmSW +NZUBiTaG9IBwFbx53al6y+qSea6qsxfkHNSMy8lSFuwD+kpVXqK/IUFrMpbEA4DXwLEiDSqxNDtw +a1IJkWFJKunTnGGA1w1zujD0FAtFHrEb8qII44yIn8l/wMShiAc97RuvPX//7l1jw2ErWDBsnGBF +systS4xdgiKKw41GDlCO9w9mP/je64OhsUcfe3rW7JaSIWe2lgrZTDEsWUwCWkmUdA5ktcU0krWk +GVYC5R7xqpt1YF7NFlCHAMQIFo6YdhiuhKWBWSGBSt9ajW3CMNCP5RoLg+Cihk38BixupqK25qYz +dDdlNVgV/M1+sIKoBhhniC4CQ7CSr3bww4ebtAufCBsOvLaptUat+F8uH9BBqZA40TTFbAHNlhYh +a6lljXPn8GMyKudwvefNnpF/3PdNbt74l0WN9pPbVYgFica5jaujqVEUEPCjsLzJZBKaA/MRSRRM +xT1bngz+dn39ndfgismp1KyGk0xNHnFZa+CyNaaSOb5+H2pBphaf49Q5bpNveOSA/dR2eM4t9Yuc +Zq/QUjV5BsWFjdWXr534lHlmlXXVDDB7NXrbjTUOk9OGRXfy7IugvOi4ZAlvtDb55xUimdqPX1Tz +2cu0IUEYtmAswsjhawW8wTcPN57qNLvMXpInRDiB3gDK8tD+aALID/SmMwM+ex08b3wESlQLmk6H +slyqkE17XCM3Xk1FjFx6Rs2KrM/dEVJ6b7rUdtUabO6KsdBWt0wdyzbecmXd+88VzY46b5vBzkPk +sLl+AW+yWmcFSiZ1cfMZ1mbf0v97T9VlSzHgZv88U4298aZ1gsk6s2aJwW62zqqC3v2smuViE00F +7NDKX97SdP1pFsGG+uiSmWehuSXdOz6xFzgWNLawiR1ZvxeuPSaw6h1n4+g17jaFN+d5obZqIY5u +aAygT3tJ8xlis+/U772noXL0fCTtPamt1jdzKNwZ296Hm8SzrHXBndfi2uVTqdkNJ1maPEYem2LZ +LqLp4qSffbDhHacawfrJTtkQyhROO2988Wk0vf4lktGcdvutJmdb9XK+ror6wN3i2t/dJjR6ZSXX +G9ufkRMy5J0tgmpGgx2lMqkqRAwL1HehImdnLVq9ZoubF5wWu9thMUL4pLaUSzfXOd9x4xmizfjS +pgM7Xhl6/qk9u7b0JUNSPlPMy8REW+65oK2ONoeJtmW6ZJVS00QKi22e7H9a4ZehabVylJZe1QqN +SK5SV5KUT+ci+WKwZILVQd+kaETDJLSEQU6qcNFE2mbj7SLCa9oNkTwDVsjp4oKhkMlgMegtm7e+ +IkOxopBZMvfUKufc09Ze4hVbdu7d1dG5z1stDo53v7xz+44Du6P5yCVvu/L/fnzfT378+3vv+vNv +fvnXPz3w5E+//8D7b759zrx16Zytb0Q3PJ4H84TRZcwg52zK29VCQLAqLttfDfkH1NRLRkR7Atg6 +ta2RCg0a4pSBTlnnhtanUS4Ya/damdVdm5oyhxmgv0Tli5KfWW+a0dzicaGAgMSoCSkFENCBO4Ca +LmCJHf5cRpdL4avNoWAiC0lHPJieCs8L6XwGTANSwTBvfn1tvW/9pp3AAqcz2ZHRCPjKTVYBgC05 +ky8AT462SrlkBzQc/TEgqYP8Wx6NIRgpyyto10hbhJM2CGzwWDVIoiKnDFg0IaNZ9pUlYDUSD5oJ +1s7DOKioqPLaA4Kpm/rrM6OVqvc/zkD8v2+mGUB+QyuLvPpzQqCr8uLoVPgRc3l0/XIidzHxou/2 ++w6ef2cpCBr9eEwK4XXu+a5SGMqlZuSysH77P/ObfVd+reqaU/y2uuy+QZix8USf1DEOlSsIAqOU +F1+//8A37ut8z4/FOY31dXPt7iqquYcTGEykEEpt7po8KmFmjfap6Ib9+79xnxwE2WceIBoENHjb +0A8fj/X3J/LUTFlKSOh46xvfE3tup7XGX4pk+BI6oWlIo1//U/75DjWFWhHXF9ybfuilxA+ehIRT +z9iO3GOvSJ/7vf7vewQECibkPgtI3+namqpcTTkl7ekN+vb0hDMjotHOBYTQzdcgDLUODUHHIZjs +K3UHh85e6VkypzCW3PWJH2NviGRHQxt2S5GoPBjB7kCDRBtaIg9DJQqu/HAcrvtQvDP8SpeSy+H3 +6E7oHd8jjSfksSRKNtqJQxYRdFwj4a7gpn34sf2Tl/kWzsKbt9x2j6zLYzCh9fvR4Ia7Hx/IYRec +Gaj2NvOcMPrivi3f+GXwiVcKI1FsTjg6incmwtk7rLyrFIwjfYajj2/rlHI5pXL0/md31J2x2Oeo +kwxS/77etFm/5Ls34drF9w0ihsa1y3SMk8IQkd5QCnLWLW8JLJujjCa3fbh8yuENu/IlsyGWVJTy +Kej6+rVTGN+0V9IXFt9xgzXgznaHX7j8W6hlJbKhkYMHlBmyDh0ZJWKhQRyC5giEC6hOQpNLUdKB +Fl3rAhBlQ5kRWoT6SDoZypS+fddjX/zab/J5FyBPbs5SzdurLQ47lC7Q28aboAOBMIjVconTvYDu +OBLyoTYEZM2Jwhq/YSUrJkaMmAe8M+iNRC8NKMpA744XRvyrFMA4yhhhdJyEzjoV3NrGXGw4lwjx +Jjt1laCaBrlEPWhPMwVzOiuNlwpp7NiAZLILyNXVNtsg9Ctn9u17Cdfn0suueurpPx48tMGkoorg +/dHX7z3n1PPjmUO/+t3/2T1G0Sls395z3tlnvfudH0ikix1dI7U1c0Q7aAycDmddXdPyU8+86pZb +v/DQ757YsX3Pvff+4txzropHfUMDQB6ZFWLkQXdD1oRKnc3UbeV+mUvfa9QPu70mQKAUas2HmEYW +epKoctPZAnpKkFTAScGMg8ARF0ymCSZmGQi/4P+Y+BEHDpuwBf+aTQXjmgXLzlh7ajyV0RmFHEgB +mNSIGYzhOtUt8o89+lQkkZT0iPrCACzZoVpiNudyGai8oUiBGn4yn9aZMstOatu46cVYKGwuGIqj +SUusGOtJ52JwrREyEk7AbDEabQJYydEIggZSVS6ZJRV6H+YCCI8Yux/juEMERgVh1uVPWjgMYKpd +Vu1JIRRrkawU97SACm5hkQePqw4Zeoo+qbuT9tEySyRjSQJsDosQqVmgY01o2YGPBJ1l3EfUemUk +JWusKxRKkZ0mKDNWHcqaVAgg0BdDApWNLhsJfgnoUhHldtbMinZOWprH33IrkZ3WgsKA2NTwQXKj +LMal9UyhH/lt5LpRY1KZCYg4Whkl0P/fH/+yGUBlsZTKc14RV0DaPaSMxHpu+WnwvheqvTNGQ92R +X7wQfWIb6osuMZBJxSwL6pEJOrDjuc5b78ns6B3LDKxsv6TxI5ekt/emd/Q5V832WAJD0a7uD/0M +pUpy0EMp6BVPvrYTn2q+9ZL0jt5d5//v8E//VuVs6B/fG7z3udGfPYNCCLbPQnco/eL+5vpFo4ne +1OYD/Z/6efjB9dWeltFwT+LBjelnd49/80/5V7qbaheOJXvlPQN8c1WNb9YYzNveIUOadgCg4QUr +wVMdgo2r99nMrpFYV9WTzyhyHgj/sBI2uBxOa0DKxlMNKB9ynbufP/B/D1dfeoqs5Lu+9gch4PWZ +a0ZHO+Mvd/T96PHIM7tr/TO7Bl7p/OrvwSxnMdlGwt3Df97c4G3vH9038petu27/deiF/TPYgHd+ ++Od99z9f7Wo6OPhy3z1Pp/YONtS0D4Q7Qi/ut0HY8opVqH7t/tJvjHaLx1TVN7J3+G87GDQftxrF +A9GdPZjY1e2XLProZeEdvXu++mD4uV3V3pmHhl4x/vkhSBTh6GOR7uTfXq73tPeN7ut97JVXPv3g +6IsHWtnRx17usrdVYW9K9wQhxVB75iK3GEinYs4F9chF7t3x3NaP3MOqcXSPOmbXNV9Op3zgy79D +cRSnPDLaOb6xs/PLD8b//ILf3tQ98rLy2DPZ/cO1ATqFkec7vPObqle0owK281t/FhfVV1nqB0Od +Qwc7dU4k5QG3UUrY3LFTEwCE7n54MyCbTkZkt8Xt5O3RsfFIOFxQc6AsrWt22VzC0MiI0WxNJfMH +94+EhjJGRSgCnZmh6hqwj0RSVwHIV5qTaDNhyHnaXkgig6qaGr+KtsMwMD/FikRNxPA4mFwDU0oC +YVshl4nL+QR6CeCToUGCGVIZv0YUqMppk5L12c25Yi4qh2BVES/NmrmsrfXkPjgVXZ27Op7ScTEz +rzz2+ENyIc5xhXlzFjTUtjzw219u27ZZtOu3bh5Z1H7mB971vSsu+YDN6o/F4ulMJJcLo5tJluLJ +5Fg6FVakLEZisbjPPfeqH/3k/l/ef/+1N7yH03mDIwqktZ0mfA1nw84oGsNe7u9q/InEeL/ZnDdD +A6YIeUlEcSAnzXEGFEcZOJWxp2qU7jRXBFeloAw2xmiAZGMKpKdWo7/ancvn29qaLzh/3TPPPQ/N +E8Bt0KeId4JOz2G3ulwiZigUjoDIxg7ny25zima7aIPKuQm6krC7CnK+sN3CjOaqg/v39nR2iyio +GgDq1dlVo1fipMFEFtrLvI63wIqa4KeQfwEcDeCsis4KgRqEksARMRxNOYs41bicYABIEZsGV2a1 +Z6B1iCuAun0ZPQ01e1InSyWYxnQQFSCWBas8wxoBeovGRMI5w42DCAvVbVmbKBmj8pAmR7b/AvtU +qZe9vpj2XzDgf9AhkRuotOm82os348jHxFxZT2p1GTzjsf7IM9u1N7hPXeC2B0azfSP3PIUfA1es +nduyxoy2K0Ud//kz+679jhJO5XuD4Se27hvctLr90rbv3MTZLVyVHdzWuX1DuKioUCaz4fBftx0R +ueYOjU98ata3b6KC4ikLfO6GoVRP+A+b+GqnwWONgMHy+d2ONXOQhyxAfKdnGFufe808NqTeyP3P +gF0aTp91WZv2htJY1HrSbNHqVkqyIZKmrI5owZMTbSarXXDZClC4LSnW4VC2p7vYOgNDgiRPct78 +mQ2rjSg7FXWRB57d+NZv1F595vwZpxRKcmpjT/0Vp82oXRQthpPbumFKvSvnoqYYUYKZ/hBKXePx +voGHX6o7ZymMerqUjG7twSz5187VxpPsHKletyjgaYkqwYGHNoGJxia4EHWlukZb33X2zKrFwC6G +Nh6oPWdpS+3CZCka3tajcUgSC5e+lOgf7/vLtr2Dm9a2X3ryt9+dU/TO5XNh3kJyMLg/As0JRH6R +v20SVy/B0TOlZHhrDwLOGjZdShE1KQAWLOHk8MDzQBrr265YM59dOxSmun/xzAvXfjsTSrKOM7rl +Zr33Iu2UwxsPNF59Kk45VgyHt3TH0qpp8UK/uyWUD3bet4FfuVg7hUj3yKx3nttet0LS54M7OxvO +WtwQmBMvhEO9/ZzTAp0pOaBHiVE2lSSolqAuisKgICA6Qfx3aGiwvrV61vz6uloP4gjIA0KjBGgb +MKAWONkVcNbP8EMBd2wsmknls5kSzBa61NELQV0cjHyPmk/KXdaMYlwjLsMTYlQloutDuyExV+NJ +cSU5GjBsReL05nTAeqI9EEDeopJJRqRsFNhPcP1BTVGvZHRKqlTIKFLaiHekUrUuIDbVF7aiM1XJ +5bMBT+NHPviplStW/P2ZJz75hRv/9Oh9sWT+ib8+sX3XFpS98pkM1jt0l+wWc9feYHvruts//kOv +e9bfn396aHB8+dJlCFxoHDoZdpSj/AB14SO1WIDIIlo+Zf3Kk8744ue+8bN7fnnaKReEYsUINDbg +2xWKYkFBqZXz85tcht+b8jsshgxyrRJS1DB62o1FTDSKvgg5KKSyZeIgZfqd7NyRj8bspQ3FuElN +mVSfz75i2czFi1of+uOfx8ZHKKWuFtx2q9fjQI8j2ktjqRRob9BtgdlGYAQKRa/bE/BVjYznYFNh +RVEdTcm5VeuW2LxuuD7g6wOGFUVE1C9wR1rQE5tXRZ/bUu3RiTwGIxGraxmezcBTrDLGYn7NhcGj +vCOx/Cq94ViGQOvYmXgwcDpR4BOah+XWkUFAKRqimkCxMl5WIHOxBChKQxWV4ZWYMgWnB5U7owpC +aI7WVCKQBCcBUTxq1AmVPZZ6ZJCGOGowWhXwBO33CW7YlcpAOSXOJqScPGcVgClTdPjNE4H1pNT0 +CR7xP+tt/7KoEdPkWjOvoWbuaPRQppL8FObXoTVa6Q1jdeMNfJMPZbx4YmzvW75qEIVFG78y594P +zf3FLYPfeWSgYxt8czO45c6YZ9PZg7H+8Sc2O9fMaaqaOxjcj+2y5l1nTVyJWd9+19xf3jL47Yf7 +K58yukTr/AYcCzkZJZjwX3OKV6wZj/fHn96pd2A/lZRQvPFz1xltFvOcWrytNBjFgiex1YBLb+Px +hmIs7XnPBaofikIGQ19UuHKl7o4r9bOqjVaAPkQevNUu8JEJsdSIYe9OrqFWcorhSLdz116pyk8n +lRzbfcmXJaOwZtOdjlUtVpO90BfznjnX2dLgslQVs/kZH7vKNr/R3OpHKUbqC9nmNgTsTf2j+xO7 ++4QZfiRY8iPR5hvWCbUeIxtwPhhf8KVrHfPr8eWFWA7VRKPLohSk1KGRBV++1nfqXI+tRh5J2uc2 +eJa32nmPEskoOYk1MhO8cOW33n36vbfu+s4jPR3bMbFWnKZdtOBAOHp/OONr8tmaBsb2j2zrMzX6 +cPTsaLztHWcbAl4TO3o2GF/x6bdSeB05ZHaLlz/9v855tRhJLDH21wu/CmDwORvvcC5vZSxe5IK5 +lzXjlKXeqO/sxe4ZjThlkMnNvu1Ky7wGy1x2CvFcDH1uDjqFeO/o0i9f517R6rD68kPx6nVzA+vm +Ihwv5pX5Z65z1tUKVTah3lq0ycirFklhgZQ7ULXNJnNuD4Q0pHCmf+GqurPWrfOam0XVnRpLl5A2 +JZXLQk7N2QKOprlNoteGPQkigkBRUi4OPGtaYY31NU/4j0TASUwJhLognh2AMxkkguXe6EekvhAp +lopcUQFzuYH6bphWhQ4F0mSiBGbtEijNkoVSAk+1kNJhCLIC3nIpmoSa5KKWqqceeygcD4qiM5bM +zW1bc8fnf3jj295tMXiaaudefdkNt37wzsWL1gE5A9SYLKnXXPn+vz607dHfbP3qZ++Z2dh+cOTl +n9z7vfnts89b95ZsQi5Q9wL4vwuKDAPJWlFYowv+h66RZDqOlonVK8/8+d2//sJnvwIeirGkDIAW +Er1uBGUFJWHhdnLypkK6R9CnBB5aVJqAtabgQR0aDKTKevwpq8xocehJrOIGXcakz5nUgcEhtPhv +2rR5244O9Ol7vDabFdepmE6mMpk0rANDLCGNR6xvCPUz2SxqjW6PGzg2ogtGpz91jsihSHhkfFyj +Y3G7YECxdCl7TU3GAuqYAO3gHiU8MDo+GL6mEtRqramMqqBMhaghSiYMkoa7OYEwSesggpGiRkfS +1wHXjoEpUJMeNCJITcKckQEwW6kDFTvhpyGECYokULGD6S6jYIhIvKINWmAAOGaQNBNOofcU5AsN +TItT32TLWLGIU6vFDPDDmnIZ5OZI81l5s/b7/+4Ho3U+/vPNmIBjRqTOU+aAB0sBW9ShECaab69F +qSeeGou+QA2ONPVAkfMuUMzMf+RT3qtXwqNyLwRcYynaNnJD4WB6CHu00OCv8c8cjXZndvc757XA +AOSMkveKk30XrtDWk21Rk/+0xfXLlnkuXZEfLn+Ks4LUBNRrIMmSfFeu9t1wSjwfGv3eY3xTwGPy +xxIjxgav7fzFjtPmmfRCPD2W3nIA0Fbkaiyz6r3mqlhyVB9wcGtarUAAFnKFNq+6aia8RBQTAMGx +g0GmmDWk49g783LSYDUFb7wK2rPJwf0tz7xc0BlxUrhZ5v3h04GrT4bYLNIsmAfTDG/r165Dxiee +CxocgueChdVXrQVGMpEJjT23y79yXrW3JaVLyLGsqLPHUmNCk3fmLefXX7LCbfJHEyMAp1RfvATF +iHwhE9nSaZ9V4zC4Q7Eh55KW6ouW6q1GjzVgrnGc/PMP+MV61HeTB0cYMSZiHYNrYUvjqUtmLFvW +ePGyVGWKhCoIxdPRh5/bZV3UDrOH8mx0HGpHdHRro3vBh86Z89blOHokMSI2el3zav3uBtDCzXzr +KrMLaH0DThMQ/PMe/VTz1auQQsyn80gtYWsFVAr+Pk5ZaPUt/NaNCL1wypxTqHrL4sCFJ2unEN7S +CYCydgqexc31b1mqN3FIm1saPSff+Q6o+uEUONHUdu6a+va5YMz21rktdbxQw1m8BsEOmlkID6Ki +mpeS+UKuFB6PHOrr2Lln87ate8eHB+oDVS11DQBQCkYR1a5gNJIrSc6AU7CLklSMJgoxgCKJyJl6 +8RgCj/WcULBINaWJTaHClsAwKazqRGxqZCxJ4IlAJOhLB8etgn50CdG7lEsi0CrKWVnJ5OD+FLOA +5ahQZ5L1eQhEoMSQTK6ZVWtOj3//B1+OpAbdDmexYG6uOemTH/rBw7/d/eu7tnzxtvveevF7Utno +r3/3k7FoL3w1YFBESxPwqz6nu2f0lU9/9gPBYPCWm99fypZymRSlNuEmYBCynMtm8A+ip0IhrxSy +aMQAJQVmW5YVIye++1233vOTn4rOqt6xtNnrDCPTazHYSwXRyB3SFzbrcgMOIQaxDurfKEoGxJyk +BIyWVa3fkTkAaNUg/CrqkUgHo5wGRhlIOiNIHewNoixdU80HAi7QvGE0uO+KpLaIYiV2YvgkBdAm +erx2iw36WlnkHMdDo4AVo084jgpkMO7kTAd2dqDRMZlIuaxWF0jupExJn9fb9Am9nDBIsXg8k0hh +Xk1KyQgFLNgeXDD2ZKsc5800tKghZwp+ZgIXNu0+hw+i6Kp9ISPIoNcoN5qxhli1kInRlZ+YdUov +ECcfh6mRgBsu5Wr9prdfv3bZAh8mUi4RPgnstixJrzVJUu8Y1WiPRPwfjlyZiX9zrJJWUJz8nCg3 +sjIkI9lirsCUp/ZLzUWYdsr+k9/wL4sakQjVe6zYBLNbD2kTaDu5jbfZ+sL7Mzv7tIuf3tXXMfwy +ujLQGVzoi+677OuZRAx18PqPX2xb23ZobOfg1x92r1mQLabz4Og/OKLEUZ6I2k2uwlD84M0/0r42 +u2/o8KfWsE994xHbkma/tZbAnwuX1t1+GS7y2E+ejD38CgAjeC2Kbljm4Ff+aPQ5keHqCx+QO0aI +BwxbEeNlEa0uqin9bQ/ebDU7LHqLeTxtv28Tn8ihTd6my5pjY8btB+RkBCFI4owzsx6nkgi2/Or+ +Q2PR5K5D2kkJgqj0x7Zd+dX0noHe4J5G5yxgbQhKgCilpI89tRvvNAviYORgdGcvLNxYqk8ejivx +LOpCJvBwF9Q9n/h1+tA4xmPTBvz0HuBiD43tGl+/twhCkJJix+/l0rb33SOPJkeiPUsa1yHhFUoN +5ZRUdH8v3WawIKopvncknYjh5aKPX1y1ZmbP2M6dX3/EvbAJRx+IHBzbdcixqAlHz43EM9H0xNEj +dz2+rs0N40FHUXXZwRi+FuZQzRaffc/d4xu7Dwy/PI+dZqYv+sgVd0Y6hxChUgMAKGn2DB4K7mmY +esrBv+xO7O4tn8KLe4oAHJYUnBqQFBs+cHduMDKSPDSnegU2OII0Mtz40I79e557Ehur02d3zA1Y +5ngsraK5GhJPRYuDMA9UO5RxjmImx1U1Gy64xn/Z9UsvvHQFx2UR3ufTWdgspOX0upyZl6urXTab +A32JEnwujYuaRY2ao6YxZDHogrZ3UWJRo9hmLf30kwbNZNQwTPEXJUMVdihVAhuvFC/KKQgqwxyi +2pYqFtJIgisQFIRJMaRSGVCzpiKj7QH3BStnPvvCA5+/8x37e55BCxCFeYz+GjttMDL0wJ+/88nP +X/3tH3/kti/etOGVx+wuBFcYzPjDT37jwx+9sbd/96c/+e6ZzY2RUG9Jl8JTURKFQgb9hGhvgS4w +wjzs1cDSFOU8IDRookcVmZr3S9zKVef+9Cf31wTmRoKKE1IYWdmsKnlrcaTGuM1S3KnkiqCrgHNA +BHK6LKciLkQjImaGNTUSayBMI5lL5HGBSoKRYFpjIAKvq/Z7PH4knDOZIpKk+AyamyAMCXkSal0B +dgb87JwuJ4HIVkInjNNjDkWCAJimUznRItRUVWWzkmCnnqh0FnxCeF8eVq5xdn1gXmPapc/7eHdz +AI2hWoKXaneU+j2MnqU8vgY5YYgZCs1YxrX8YMby6I3+iIQq/UgdjYQc1vK0mmtpBHsO0vOIGolE +mYwbY9wF/wSmhOqxODnAZ7CcHG7ThZcunDnHgUkzcaB+JP+CSaOz+iIZRWqkPGIsdCzGXkk+2j/e +HL3p4el/qH3Uf45qycd7HOnBnIDbcvTkTk5WaF/oOGvBiu9+Ml/I7vrgt5MbD2JpoFvAfe6iYk4J +3f8i8Ylg9fjs1dev42s8oae2JcE3hjasRp/n7CWo/2X6RyOPvJze0e9Y047u+3xfKPbkLsvs6pZP +XhnfenDsV8+XstKEhyU0+T3nLMansgP0qcyu/pY7bjjlnR/YPfRiblt/KZUd/8OG7HbU9qC+bqn/ +7Ntw34R/80KhbxypS+tp85Bfyj2xDXsi0YXbRM97LgRSofTsXi6S4S5erta61L2HrEMRESJ4xZJZ +yesTI8XIiB6S5TVV8VNXZWbMsO4/4N+y52DX+MsdiYzT3XLj2YLfPvq3rYnNB5E6hgDErNuvziNW +/d2GWZ+4Ep3Ofb98OrlzSO/gqy9bUcjKA/evrzp7Ifr0IxsPJnYPLPjiNWavreuHT6QPjnBWfv7/ +XoM8Us+9T0uRVP1bV+JeG7h/A6zH/E+9FXfbwR8+Hjhz4ZzbLkUXdS4Y7/jh4ytuvxHQnse+8NVD +968nIAnr7kLYV3/u4po18xL9o4ceeSW0s1fw2VuvXAmCyo7fbGg8C5le//jGjtjOoZO+eK3VZ9v2 +g0fXzbBdevXShyQrpGf3/fwZXUFa/skrRrd0dT20oZDLmn222decYavx9D65dXzDfpCSaMxwKFfB +HYNs06rb3waO1d7frF9y+1uR3ev42VPR7d2Cx9Z45Wpc/UMPvAiV+8Wfugqj2/2jx5RYRphdvfQT +lyHvGA52V89aYtRx6x99fKS7z2wDZRBEoExVPmcsMm42ZJCsAzk4qPoyaYQjBqQH4vm01fP/sfce +AHKd1dnw1Dszd3rZ3rSrlVa9u8hFtsEYG4zpYAIhhBBIoycfLbSE0AMhxST0AI7pGBsbHBvj3mTJ +6m0lbe/Tyy1z78z8zznvndnZlSwZQwh8/zcM8uyUe9/73vc99TnP8fV1g2ACtnttfso4fChbgS1g +k4LgvgPhCuQ1gqB6NQDnfi5dySmrW7xRe9UHmjlkk8B+Cm5V6uZkl5Bwo84biCBQEyRAHrmtsQ11 +jeiRhCwSnAi8Qx8jb4b+u+CexVidlUJmDgoRmg6/Y1QwRDP8BXD/gZSHygG44SYOEbBHO354fPK+ +AydrzviqdVs3rNsUDoWymbkjx/cfG57IF+YiUXtfX//IyGRBcXa0t4T9nmyqNDY8vaI7/ta3/OX6 +tZtKmSJUD7dUpkGQ/nH5HE4E//AO/4mPkDbnekqgPyHPoSn0sub1BA4d2v3a1/1hUTmJ4tVcMYeQ +O3U/NqqRedvzfC2r04oL1YXIl7qBRgU/OGFTdacTZfoIfkJxQi1lbJV5aN+4XAI9PZKCyMMF/OPz +KRiXqKMA6zr0BuwDnBKNpwAoBcQGlw5NSaEjsJwXjcH+DjSmhLYFQnXNUHcyn80USsgRoFA0vZBD +cQu4yFs6Qnk1o+qaqsG5lP12//zoHMo0ncWqW0UZBtKQ2EkSfGQSPlBUlBlmDBVT+ArIFOfXuJPM +2VwgYec0hCPtFFH2Rz8VsCvSWSRnRByS3kCgnoK+ItkpakARNWWmdSdCx53twVw2WyhAcbJ1x4WV +TfKXo6qkLEUIFaoTiwfLidqgLMY+F39k+TY80qVqc2mA+Cy+5pmSXYyDv0r55CVyXFSxNIb6v+ZT +/Y8q2iX9Gn/LqrFxOwY+/6b1L3vp6MS+fZe9X1jl5380EuiLX11m7ZFCbX7Q+mwqYhVtaKDYNv3s +o1s2X/vU3juO3PAPVEJLlJlcUUtWH3VLoN5SqG6DpKN/mUMaRWBEHO5CZ1pwoxLDtBdlfh4Y3k43 +SsAqEafDo6p6asHIz0jolk6cHwg12V2eQKkqnZjIHBrPp1WbiTZUoJ4StGwNs1Uk9vlBQDcK/yDX +Q/2meCuexVZsXHnjmpctdd6rteDK9stv+WsA5Kd/tPvIZ2+NX7zquf/4bkAe/vutn557+DhbznRw +qu7lu9DAL/DQGPBN/yU8IksEOijA6Qij+XxSOOSbm0XyDBIBoodsZPqc402Yd8E9QlcmXjQKxUgk +U7IOtjYl7si0puQQ3RprSqwOkSx0yHvDbTBBAdZW8a8Px4baxxcW0LAW9d2FQgEN/bo72jSl2N8d +u/CC1UeP72trCybT+VxGTc2XENST/QG1anpAtukzJZfhc/nSM7WTw6VKzadq5QBgU5BSsGl8bgMc +ZmW7C4wq+ZLX0IfaAi5D8QEzwdzg4I5BAyW32yDmcBA4oU9uQzWCy4bC7VgeuN1QlvQFICjxwkU0 +LPBUjGx6DhWu8N6gGiF24L6RiEbYj1oNugEXAhtAGTWODllxRG4bTqUdjrRemM0smPAs0YnXDr5Q +XyQWaWmPx6MhdPrMFYuzC/PZbNaE76lXr7/m+gu3XNAW7SjkVC86g7goj0Xd0VDVAsZwrFe4Y6Qd +QT4A7Uj9QapEik6v68uL3CoswP++5yfv+Ou/UPVCR5uzVADYB5k0l6Q6Ygv6xTVfq1J2oSKF2zgi +8wpcjIr2xSjhoICHXXfaUq6qGQ/mXDYdDhzCmyhZsdVmMzmv5EbSMBAKFGA5uWFD4rtOwHA8MGy8 +HkUpJVoSqXQO+G700JiaHG9rDwZkjLOi1cAijjAs7AhnOlVEu+hAxIPqXIDvUCsFZe82PbW0UUoW +/IhrlgybBkAoRSaJGRw1G7hR2NuU9SU+HKaJp0oFUvmsGhFuFam8ZVJomTrhpUhTRTBXhqRypJH2 +iIjRMt8u+6f1Ek+KkVAlC20aWumY8yruNwbXFBc9oxJDdCHmjc/9DCi0T7DW345qFFf9/1Sj84rz +VrD86i78WbxGlnFwwoJXrPWt6fRftCrx6p15LXXqXV/TJ1LnV4riG9ZImge0RDUutb/4J0xbz5qP +DD6qK0I3ibZo+xuuTgQ6j//w9sJjJ7hvAmlHtASnZgqiITFec0spNrJdgNKB34QfwJOjPBLuA6tG +9HN32f1ORwgFT4VcNTVny6U8DiR4MK1V1P5XXXK27Do4ljs4WlhQMBJY6ARuJ6qOpsumLjnCTOTA +Cv4Ffo2az/F2o0KjM66tsVHEdIijCe1oEUXy61VvuWbDFVcnT55+7C032dyuHZ9+w+o1F00kjx/6 +x1vhyQlDWPzKOqA4nDXHlDsjVB5qtGogbaOYHkWIoAlcHrjlC3kj4PObVXCBYeiQGKRMuSOFzwY8 +DBUuAI0A5bpI3YHfUwaIi6jwG26hTPqYTk/Xz80NKfa3qOjpxhGdugN5G+A9I62xpFIam012rOhE +7SJ+2NvZhSqIfDa9cqBz1WAXnPx0tpBKF3G3kdWifJdDAmixCiVjmkE5bGjOTFL3y3H0kUeZRgA9 +ICAegaZHNYVZ0RQtHAwAoxTwuRAeJ2p0arEBOjOIKOorRU0D+SK5QR8tFl49LG7JQ8BT9C4EMwxF ++FC4D1Sohno+5JURiwT5i+BYI0oVlAqCC4bq1kCTplZQYhh+7MjU8VnD7guG44FEPNDd3rKis7un +o6+jpacl3hbwoJRC0xUVvV4QbIyFI0Gfvy2WeOVLXyZL3kK+QLgweKkm0LF0U3meyeSgJckdtLjv +JqE/yAqkxtwI7BFmhNnUSVUMrhyCk3L/fY+6bXoAjR+J+KYCKjuUH8GNQddjGb4/SumxSp0o2beB +rYazeDXd6UD5aMnncreEipRfLcODD0UjFcyu0ySYCpYTGGqo55dLUUGO6iMCc54+KLliEU1QjA0b +1o6PjyP2i9uO/jS4IgRCVI3pwQn6C3Qx3P2aYYJEHa4r0fgCH51MF9EqE2FK+IY22Q3tXfFU0R8c +OGQEkNFuGVuJ6cGF60d6UPwh2qoI52+ZchSB86YnG3wc9xSOZh3Uw7gm+ocbmTOUhaa3aQmLL4vq +R+RL2GIUQXjSguIUIqVHrxGUYjcQh6Omn7zCnkYMLxOGzTKl6fXZX55Hsi89J42rSWQ9U63wTL93 +3sH+Vr6wpCnVrga6eekqWFwRZ3NZzj3Os9xHrBrZs/b292184Us7nruz5YqtpWox+Z1H5m956Blf +cj2I8PQOpmBPan4ymKyuHXl9Ra7cMPjZN63vvww1wplIKbh1APWLRHHJEo+FIHVjwgs2/EWGEdqR +OhKDzxhRM4Tv8Cf4PCQfXqK7oumrGG4trydnyrl5ZGOQr4QoBHDR4fGnSrUDp1KHx0pFA+14QGiC +wxse9Jkj7pbFZUPDpELbxZwVTHAoIcZssytluWyLs9XYyuJ6LY3YpCPFr0Lruto3rqn4a227Ngy9 +9br23kHwjj70jn/Pn55lD48xcUJOLNnL4kTW/qY9zzY4LHHIPWSbgH1EzMxDvaEQuEZiDuKAWv1Q +vR8B2LGNyi2xaiJkV1TqT8SHEvSpgkyE7RRO9nPxu+VW0h1iQbHEeeUbCKg70A41qRruiMqtfrWm +eAMkNwNebxjhNIAPHa7h4ZOt7dGjw6cKJbQIdELWQz1Bi6FvNCQpbBoERRPREJAgk+MZJL1CQb/o +RFRWa1UASstwTXWv5AkFAkhzATLp9yLcR+FZaFBIWKhsBt6Tt0XhBCwYauLIWobLqLl+AbcO5CsY +LzCbVB7PJRMVtQDnB+4ptRikMjfKiQGgAQgr2gpD1LpKYAfwxE6n9UeOTUmxFugMUB8g+IjyD8LL +1hxEt22WiTBb102TmjLpmo4FqysKeN82rF2bWYCrSQMD7QNcWJbVrLdpZZFCFEuCA2VU3s7s6TRw +Jh+jmCplChhAtHHDhoOH9504dTQa9VXg2HmhZ5h3Xq8EKq4gWpQCV4VkIPjhsNJxN6kPtA2FjBqo +9duiFfKvXXpR8yGcDfyahDL2qg/xTdxHzQQ5g1RDwYMz6g8gAF0oFuFto5gBrTa2bluXL+SmJudj +0aCqaLhKDK2MAg6QTiASSzXLAMYiHGMQC4AHyVCUs3qMMrh70NQrYAepFnhwI5I9AHgzKnCoggYG +DdkFCFJaW84yBXkahIYTClLENXhp8lNopOYnTRGvXvL3+XDkMLJmpJAs/cn7ko8lVjxVLOL8rDZp +O9XDRVadBH9b/L8hDpi4l71bjk8IyfDbV41LE5t8MY2Bnldr1LXJ77FqvILu2rIFsOTPX10zsvW/ +9EGegVlJvHQnfImSDlB6euT9N8998z7xLRHNO98kimVq/SN8rGUPEd9Y8uShiKA5/g+Lv+cdL5bW +tCOvN5MbrclOUGMX7t5vz4PrEeKP2+whakM5GmAI0F4KESnEzqAaKdEESkwgB3z+IP5F2gB1jLK9 +4tEVwCeMuSmbkvWBVETClZqSFDAd8mTaODKWOzoFEmRSidT/HRKJ9jfvnqYLFl4jqSju30BpJ/6Y +LU1RC8zWaNNTvBQPWrWW28l/1l/jZ7ljU751CakzJLWH7JKjMLtw6F9vm7hrLytU6t/O6JJlt6tx +MzBc1NFTLwUUK3MnPsSIOScGFnDAKjBrFA7CjhdtDTEKVEhi8JWWWO2VN3Y/97krSunsxAxCb9w8 +VkiQ+pPEj6XLSYJwwqbxrMd6OaAKZw03z0BjIlfFFnAE2vxFLT8/m1LS5QBQ/GA5NwEGNtGWWDP0 +to746EiS8YTAgaCssALhrKE3oA3tigpupyqj+tQfiMc6gNrywjPEtRFUWQ/JaO9rR1MOXa+iqwUu +CRXkgIm4ndS9j1YIiSoRgCcWE5J37PciTYmKfjxBBVMxoeJ0VDTASxUAHdw6OK9KsUzNDyvIKQKS +SeyB9As4UyZSb5KqoLmhrLiC9x4Zm9bhblfgLrgR/ATlJ8rV4ZMh5otCAJSMM902tbeCviViFDDf +Fft7+1atHKT1TwTm5HOQqcUim50Rob0tUS+a1/NMiwiqUA3sTtLPyHWRJP/KlX33/OIXyUwKNbpm +WYe/iJ+B+BTQGglcOFD+qNlgCgdYlNCR+ZpZRNowKkvxkAJt6pb0oopVj9ArsGo1oJJRY4GN40Xt +E5w4KgFsbYsMDPWPjY+jnAGqsbMzBnJa5FARdKHUPmaWfEQJH1HEG7WoIpZAgQUEpYERKoNcA4nd +UrboQydH2KIUtEDKAtxINl9I9kcDgVgI3eLoZKpBfBB1eCoLBBYOzPpmJSBF18X6c4nHKKaMMnBW +C08uzhCGN/ucNNO0nTgL1wBs0fqm/Ay3WhXsM7z5eftZWtWyShs6WexGjA6yR5Tc/K+oRpqIxSlg +w6KOV2p4uoub+czJOr9IP0Ps/G+/sbSV8W8loCqE78J3H0o9dmjmew9OfuYn2imUKFnmmbjx51ON +i/K6aQKX/W65Ryn0TeNfBMfyDx/M/PiJ5A8fSv3wkcz3Hsn+1wO1uTwtQCKLQuaFoBMwSOlJbdyZ +ExWeIiKpcDmgGrEVgdOjSCtKmipuvVhLzdVSC7KmBGGwgkKMvATAAeXRBW3PcG48i1JpqvEl+mvI +bwwHJHLQMkvT2OLaOb5CepFrzyg+yaYohWd4ipbOUP0v0lv111YAqGmC0HVp8o4nR25+IHd44sgX +7jz6r3dkDo/X2S+FFycEpfW0zlTfAyT3rWbobPWyQSu2NeQAMkAIV3IQSWhuiGYMxwOlkojUrntR +YuXm7okD08dPIduyKI/px5a3ao0bw6Zu7RbbKME74YUsev/U7gJwGgdiojW4IVGvFEMvEUT2kLBy +IbuFPCDADhBZXvBqFnIDfW2FTJ7kqIaCM5da0uBf6GUzFA7b7apH0kMB2VaVcxnX/HQO9F1VQ/V4 +jY5OeefF2xA0PnZyLp0pRuOt2WQGda0ygqoUcUTMlTPQJNfIt6CCetKLcDqh5ah3LzxB3GP8Cc+L +e/4yuoYyVA7kAtUivka6khqKwWuEGoXLWGEfk5zAmtsTPDCW2jOVr0VaAJtB8ALvQjlUTbhnII8C +3QtpcPyaumS7oCc5QWu3IQq5dmhNPBrXSxq5L6jIxH1xoZMSIycsg4OaTtNweKGT28N3i8U9ucG8 +RzgKyL4jVlB7W3e1lL3/l/cFE+gMg8tEGMSuwbXmo7oRY4dJYbJf7LCD+0Z125yt6EyKhsxYv6S/ +oLoAxnV4HBpq3xFKASBX0xCHgdUQC6MwRIFJs3pN18xs0jQdaEcF7z2TTiL/TF41ZpJ1FLfMJCGF +/UetsYjTHG3fiGYWHwUDfiWnIBGpqQbc7GBA8suoK3b7ApIPZCCkVwgHhA4fVRCOU4yWNpMVKa2r +RktNsvBYKuuX9Nwm1Sa8SfLn4M4LvcgbV1gh/Jrz7NZkihCkZanST9mQZC0ptLLlitUloaUdsXIo +rUARq3qk9+nEY7NMOLt8eHp1cx6JK0yH+mOZpD2/tP7fVnPP5vzLAqrNdvxZVP+Z0KazemyNN3kr +Lg9sCpWFhWHM5Yw0iCKtR/NCXHYpZ4FUWetw+RfFDawrQbEOLYeKnC+y5YRXRnkgEkVK2VbSayWt +hnRZuQIpw8Exwu4B0Ic/2WEkcB+8Q8R+CFiGHCOqCuAropMvePy9iCuW7YWkY27Wm8n6y6ofvcdB +QElCD+mixJEJ5anhzHwBHgIUrAecFxxQ4egWrPO6LhMhijryDR4BXGuyzsm4bCgt4U7xTC0J9zTi +PvX5WISPWRPFfgGbpMACFkbmgNqFzBIAEHIRLB/NwraJZrQ0tnqGhVwCsdOtJx2OVSBRYlHaCjMq +Jp/GyIUOLIvxL/dl8s6OVR9+bCFdgBA/A9LWZMawLBFtwxtdjsnRIc1Do4WZAOASGOertYjT3xn2 +hnzwm1D1DYabkqLOzmV8PvwFdYBQty0RjyKcm5ov6CXE4ZzgqUZk3A+aGYL+Qfy6/HLL2Gh67+OT +l+3Ykpqfp8pxIC5t7s6ueFtv5L/v2rdt05qOeLSQLMjIJ9tsMhYF+UjkkiFXbOWTMFIKj8L1Ay0Z +HFrkKbG+iVuc0YoMoSCWVfLNEVstK3B94CYKTgDcaXqBZr646QaYxl3+TC1wz+m5BTuMMC8XqJCN +BMGOl9Qki2aUe0MhKAEdbEM5sIZZw6+RDN62cTM0FWLwcDSgS3FDYIzByuKtwM3a2WnkXVEPatPK +o0pN0oqkUfl93qZ1kWgfWNV/7/33zcwuxKPkOGI8uCD6kmHzVh0+FVcC5r4aqjFyuJZQi9TRbXqR +CDQAurWBGpZiHdBkrmDN7UMRIOIubh94wn1ydAGFGmrZBW7FYqm3t7u1syWXzRAwByx06LAIeiba +Fvaygaw8MhGo6Ke4C6wK3qikMuBoYjthJjVNwwYNxGRvSAZBHWA/wATpMBzKNTVZVObyxkLJlim7 +lKqEfDVThTO0gJ1FzrJa24tVX7MKElWtIvYvulRT5ICBYw1FaE0e/04gZQAQYKQOhVrxK07UAMZF +JR+cysURWGMLDUmriW139uDrGUrLyUeshMdAFjPJCZYUvNuaJaiQfk1BJL7L9dQK3/vGoyl5Sodr +2oMNUSx+wGvF2tlNnssSy8EKOC0V4o2TLdOlz0ZT/dZ/c4Zq/BVHsMSYOOO3IkrGEn/xedYznEMv +PpMRNdDKddXIeodvq7VMOIRh3WERJmG7GDIdNhkZ0U6Hb1Vn8HmbgldtMieTkFKInZJ8JbAhuYyA +3zihHcHVAXQdarCQZPQ4fXAdbWWplLElp13ZjKeqQ11WvNSv3CYFqq7o4Wll30g6B1gqrHuSkmRg +ip3DwVEr4yEWHj8s1xDhMSA/GXEqzErrIVJ1DSfb2q716V38WvNKZxHHBkOdkpEVJTRNeH3Pqldd +0n/9BaVT07W8QkAG+jJgFBzUtOxmES6q/0lHYkVax5pzcMgaPOm0OoCAvweeNQd6/Y2N6ceOF2Yy +ACouFTln3l2hwa3MI6tZjqKKHsAYBNUZAOUpV41wxdOGVrVokQHQRtbts0VifqiHmek0nAa1iPID +WVPhGnrmZrPAFsO9odCcy5ZIxIpKnhpM1dBNGsCpWjTkXrWi78ihY1R34fZk5vMIEq9Y0zmwMrJj +x9qDhw+fGkuC1N3rsXsh10DpVdGxKhBIFO2fCUUDRcsakcwZ1ogQh+RL8q2i8CmUJoIImFtUg6K0 +H78jNhkKvpLjaECxOoFgRLVh1R08mTX3JEsVrw++JNst5KWxT8+8rRSnpmaU4gVsMEp3ueAlF1pi +8XWrV5PqpBVGCw2LWLC7klRueC282sT9ZY1JUprBQ6JfCVlvbFcKI4a+5pMD07Pjux9/0EcdPw2Q +W/PmoeQooJaIojrQH8fuyjrMYsB+StGSuWwNXD9aAfUqAX8A0NrZnIL2kL5IqIZulNWSWigGU1pb +QZVz+QhqSyXbMSco8XPgCSrkEegOwppBWhD3nLp6IYLK9x/JQo/fSyAhaotCvK2YfdRExhIR8K/K +fiDjoLJAxlYpgUqBJhYVkXoNbnqx6ipWneiUrIDxHcqZYxtsogpJ0XhR33JNxY6N7UlFFrxtCYpm +/RAf8jQJO9zaM3hB213kP6iHVA3zS2Aoyr5DNRKy1bKMSSuLkLal1fhYJB34BlFVKNI7hJEiw5Zp +mSwx0aTnrNvU2E7NHzVU4zIN1SRVluhM6xKsGy+uzpJQzUdoVqCMjVg6nGVfPXOj/46/8z+uGn9r +1y9sN0v50RJlmcR3SyxZYfwIdUiy1spDUq9U+Ij9X35r4vVXDVx5dfv2bYpT0fedgj50Qi1CKxLi +hjKL5DKiw5SX9CI662K3+pF1S83bkrNSMS85TXfAbnht+XLZ7gnpttDBk8k9J9Nprdayaw3Q5WZB +hd2MZulIwhG4BF4AFyhBHDWtKlagbPWTSqhrnMY0Ln5z2b44z0QL1WgtVzYK7Ns+8gfb3vfKLVdd +O7DjQme7LfcA1WsSJogsViGJyaqub4uGkXOOM5Hk5xSKqKkmvhEqUbc7FRPt/nC5ViOqcw2WRX/d +zqUILu9NagtDxSsUhrSXUaIWtrna7N42qYL2iwGXC9NeyKCL0+DKbkfNUHMG9Z51ejLpbHd3Ym4+ +DYwwlBcConA+QwBDVimBVyOGcWXlYNeO7ZtnJ5ITozOGSkVmiOmhCeSxkycDUZ8G8JBTrforlz5v +W7w9MTM3R30TMJG4y5BScPTg0tG/YMmGRuYwGS8vCDPkESmbSLqSAuOkQ+FGGTVNw1fhW5LW5z7H +8IEoIEnPmktxynvHMhNKxe3zA/YiSg1x6xjySL4jg5a4DJVXNXKN0BwYQamU37h2TTwSwXhEWhGf +i6SiCFKww0/JrroktMIqVJJBtTMCsVr3GDkeK77J9UTOaCx8zz13llTyy5HzE8zZoP9F50V0LzSr +rgVT960J9V7a44zrvT3RNSv74fwVdUVDVSKmCcRvUIggrtWMlrz2IsXzhxXftVrl2mDwImegLW1G +y96x6UKZOlpCldoAaoVlgT4VPo9H0wxkLqD64CTCUwaVLoYF2zUQkts6WkJhMC6h1ZVeLpdADaHo +qm7oFDiAPaRoNfTwQB/jgmkvoqUj6NosuE1DYlgyvq7YhMRocqJ4tbJ+4bC0tYso0cwShV1tYWE0 +O2VCg8F1J4QWCDzEp/ga3Hy0FaMNgjyyUH91QhlhBdOx2KOlWD27pxRMtWI4NC4ecOM2NTZTs0T4 +zahGS/xYam+JZl16rbxIlgIVzqYafzWZ9VvTHWc90e+EanzaeOmvMjdnqEaBRxUBw4aFbMH0RKqF +eJwQxfe4uz7+es+mXkT9CoAF5RdyP3gALYsAtyFCOKrdRhyUGDsAtAMSQ0KKCzVqjopU02soi8vM +uXQFnR4cnqrpqoLwSZKiRiV44ETy0GgeG3rjR1634a03hp+/YeLm+5ClEpESklmkFGkUQtvVU4NU +y0gN4eFVCIXOELVF1XjWjXD+iRKqkU5L/ozdduHn3th17VbUMOSVhWxuqnzn3ba5eYyijBwZnY6Z +k2lOBVVVw42rC4aGybj0RR1q2nArcS1AdaIhI7JGAjhxniNYcVvriqwQIBEtc5tA5L5gXBho/ZCw +SW2SO+KG0wWAJspqEKJMJlH+ZwB3um5opd8vpdPzPSvaEmBUmJkF+xqG4nWjcgOhbqLXxLxCtgJn +qRZKp45P7XlssqI6XKaE2oRQWOpd0TMzvQAeFnSC7F/T2dEXQyh2cnqmkC+B6Qu822hgyI3tqW8C +Y2mYZFXsfZKhcBOpPQW7Hmzqc4cF/AkNDSsBd53oVVEFSAFVJ1LP+BQDs3mCE0ptz3RepWAFqWmu +lGPorvAX2RfF6hD0POLA8CoUtYgqwE3r11EtEASxiNAJJ1NIbJHhpaEJjUhqkEtOuCGuiCbW5ZkA +iAgwjrgVqOJHG5ndTz18euRk0OctKzpGR30N8UsYCVpVk535hD0yiBLXyvpNg7PzhcOHRk6ezlCD +Z6dbV6i7WKgtnDNUI6lsrzivd0U780XdKMgrYp0tif5SbYvmnte1pFtStDIYg7x+L8wNAvyg4JOg +XzYQDADrixRGPBHs7EpEgGoN+uZTyYV0CvsWhKtESOqogeSPgUsgkCsDcePSbM5SRdIc7jLK/uHu +UvyGEgR1Y/qsqnH5lhIzUn9isiA8GiaGuOn1uiTrp1SXArcXA6Em0oj513q6pERCKpbg0xKFMBPB +CToda4+RDVPXjmyWMuaMolriPlj+nwUv5tvFwkHIhybFs0yxW4p+iW5r/v6ySM6Z3xM1V0uCfw1L +m6WUiCQtU86NP+sv/p9qrC+rZS720wnwutX1tP89v+S3dN+i1yh2NJMqkXIh+4usO1EYxKkUWMkE ++nKGnrel79UvhHk/+e4v5b56d+knj9VSeehEoRThIAK36PKxv4giD4/LL7l8NcNdVt16qZaecpQV +VHxj/WsQ0YgouaKlovfgseSxyYLuIFaUgb+6vrd/U2pmbOpHDwOh0ZBV0DoYAgUMxYKxFCRVJjAH +f30n0gZYnJl62GbJRjj//PAM8O2gM8p98c1/83LZ7j/5oS94fnxrbPcDsdIC+R+gMlNrOjAiXPUp +ljzvW6s8sj7QsytGh0fquGSdNo+2SvR7VqsW/gPZRlwX41YW5ctZj0LivHEaCoFb0VuK8eKgkqPi +r5oxm9zrDYLdL+h0gznFrJVKZZQD+DyBXBZU1XlVV9q7WwaH+tZt6BsbO5lO5jDLqHxHBAAmOJwK +lHDQtMKfo2IJSVOcU6PliBStlghfGmsNur1o/IVAgQtUdhW1DATTwuw8CiVWrFgJj75cUhnRQd0l +hEAQIH6qlyCXGQU5uI+kxSgTS0FWIgel+B5emADXUiYXf8LxojYTeEckFGGXSMFDC+qJko7OaPgq +8daROmTZKQKj5GoSk7nlnRNMg1pfoSJloL+vs70dGB2qfSVtKnYFK2eBUaU4CkVLuISRFhbXNXLR +HEO9hM/C009in0tS8E1aCEDBBvz+dGb6wYfu9/spBYFelehqgfwiEong2fetiNnaJFCzru4fXDWw +5tjwZM0eBE8xMpBqAQ0+wFLrlYpGvGRu9wZWl20RaP9Kzevz6yCUnU07C0VUZaYC7mP5HOr5gbRR +gbs1UHnizJWwsYxwVFo92LlyZafHj3426DRsprPJ6fksHCs54AcsDhkQ5PLKhgndR5lmVIngBlCt +ZQXgHxeKTAjfSzX+FICwEgRWSImvu2EYWGKksa0oPM5eGp6YEbYXKOMpIEyNZz0uVd+tXCIN7Q5N +BtN5cND52j/ZtHIoOD2ZyqL5KR2K2cfEuQWuxxoE3zEGqCFOTiUbwrVkqcBqrK7NRWbeEhCWsONj +NMvSZx9QFTPAYkMMbfEpQnL1JyvppoHxMlpUno0Bn19S/c5843fCa/yNzMYyr1GsDr6nbO5ZDxIR +1oMoKagguvOvXrpu69UzmZO5b9xDHDfE+1XHo+IV+EOgIyFUCW3uBiMHVbCrxWo6VcsseMoqgKiI +sFGwDEkXRyhZcD14aOYkmrd7ZOwnxBELR0eSyYmTKKsvqJbpZVHs00azpJ4VsqCREsSCgAdWQIZ2 +YpMN+GwDqqQaSUyyahz6s2t2XHX9bHFUueWHfW3O7hZHNEy1aFoZnDJVxL8I70huhBCs/BR5z6fX +a4mt/S/40XtWvGC7MpVCkUpjz8DqFaYtRaBhyAul//QPIu9mf1/IdCELRLYO4TEXyhljNddKV3RV +wN/mBhcfcbOhrY9ZK2plrxxAUQ0gIgVFPXJiplDKuCWbLLvT82kAPEHNAJwk+ItUBQXmYA8HpNZL +NRoupwoq16QWdgcLc1mgQf0Jv92D/noG6vUCjrBLd9dU5L3AMwrYFmFIEQLNJVUkNjm6SdKWlhXz +B0H5kQZjinFAVqlTFdQkt2yG5YEoLECsVDoJfjjgcbjsEYlGgtbAkQCcq+bbt6BM4z5xfT0UFC4c +2pvjnJZ25PsI0UphBQrIA2hUppL5odWrIa9pqkivsWgiCcupdaroJ6OQd4LAoAqRV0/Jk53G0o99 +SeFjirg63if8CKwEQwnFpB/d+h2XHRwCDviRmJAqqO7wh9ftwwapekt5OOvGT2/fg4Im2Fk2rbZ+ +/WbkI5BdBXC0XNAiFXeHJJsltWhCq1b8dpdscxYUPV2tJMEkHvAfzuU1yZZF/KVmtkSl1oh7RW9i +aPWKcCSQTSWnpmeyippM59LpPIYcbQ1hcmBR6ChfQaUryjccADRRUT+8LX9QRtICZcag+8eTijFp +JVvmzDP3Gq2MK0MU6MFTCcVrMd9YXrnlu4mlTbFrasiB+w6iCUxhtafXefkLV0PTH9g9mUrhRqEX +GlsurPTY9LE0CGNs+BbiioQG5gSn2A9CnlkalSICDekgtot4NL/+jahGYSUvqkPLjrKGQqfj6MSi +ujybajz31v/d+vTXU428t5Y52kv+bL5BS25bk8Gx5C4+3ews+744buNNcUuE7cWmS91wImFSDx8J +g4xy2hxGIhEA61Jyx996fVdkcGrvY+qDB0k1wgDdvELaOoCgmQt13yCcRmQ17HW1BGxdoWoiEEA3 +v40rqN/rxASsW3fAn1s9qHZ2ewr2mWR19/H50TxqklEgBze1IvckohcMpn55qHRq1tsRbXv+NuQ1 +y7O50Jb+yMVD6nQahjH33KZBY+qQ6Wp9wQ5cg5Yq4J3WXev8vS2lsSRZZMAaXLwqsWutWVT1DMqa +aQrk3kTndVu12Sw4xD2JYHhtD/CIRk6RIv7OF2xDBLjlkqGL/ulPPdFg8vFhgfPEoXZ8/LWru3eM +Hd2j3f9w9PJt3tZISM1llGouX8mZ7tDODWgvXJhMAf9Ipeu85h1eZ3hVZ/+LLtj4lmunHz6KJlA4 +Re8LtgG723HJ0BX/9KaOK9ZtGboynZvZ/6+36dmiFWDhO8JgVxLtOmoUzne7OR/GooCihXQ7qWiQ +CiOqqOtzRl2ePsk3gL636DaoaioCb2WYLfF4C/m2jhpq+RF5Q6gUgAxDqR0bHm/viHe3toIMPYWO +fy6nKaE9nh6OhLEGiiWNfTUU6iFwKeMuVAqapNs0uEQe+GzoZk/cm6ZeLRZKACsDw1NU0ORLQhYQ +fjByhojzCpASwgUa8mNs2ZNOBFKFylIpSkC4HO7GBwVJcXIYUfAUKb8IiDQlGg2wDOFqifFIWjDd +R7NKgUsnUUkLM4mgiQwLRv0gUQeKUGo90Yh5ZV/U7Oho7+5sNXSVSj2shc6VsJxlFFLdshA5impF +SsGIjbpA+IzgjkHlhUjDg/0JFgdfloi4kveDAgu1GE9E7vzZj/NZwJpwT8G7hnIMsPbZK6Wqu2Rz +pOyjpzOpbEmWXat7O+DTFXJ6qWygNQ6KM6Cx7T4nSj6SZaXAPG04V6qmL7jMyaqZdzgUr/xYXh2z +a+HOcEdvfN3a+OBALBb1KVppcnJmegqMf4QvCoZQg4xYjtMhUR0OaDgQsEXDcNx9uGiA4/olOK9V +FaFU5CfdHiIfpam3QHCMwLMCmEJoiNlaKmKWSy6aRHalCZVKGGkyhBpiSJjhtIEbh2HKQ+w2SqE4 +MHvgV7IVk6UTh3LHj2locMJE8NQg1cLfsNxio0SoWK6sgV+Ju8/nRZpZRLuEhLMMGysSuigJ6z8/ +U5DyPlyy+xav8bwBVWFVNynd5uiPdS4L5ciDb0hn8ROh03+3VN/5RtOsGu0fEBCqpoewLs/6EO4O +gcbOd44zPreyF433n8UxGLkiVjUeliPCm9/yEUVJVt3Go7slvs+Be3ofSw0sNr71vYOfeRta/j71 +pS9qtz3uv2y9/83X4dNe/yA6dti+fS9QBrWXXFxu8ePnMVc0W063ffeO7KtfBobNwY98RlkzOH7j +SwDLiLvjC6NH97zys2kVhCVY4NR+YM0HX9t+3fau2ODpk3vm7trTfeOViWDXbGoEqjHa3xP0x498 +5ycnPvEdKqMHmxf6EvR3bPr0Gzt7h1LqzH2v/DQG/dzvvw+i9qG3fEHuiG/5hz/A1unyDoynjz3+ +9i+jd/H6d72k/8bLE6Gu4/c9+Oif/dtVP3pfpK8rPzt34it3bXz/K1v8XXOFcfRzXtW97dCpB39+ +2QeR24Jg9bZFnn/bhzZ073zw1q9GtwyiF3FOX2j51r9NHphK9a2L/+VrcDkhZ2z0xME7X/Fxykw6 +HRvfesOqP9wlVcBk3jlTHr31yo90X7V+x/tfIU4h2X1XrHv5/Sd/CKjrg6duNYv6sW/df+hLd/3K +64L3D/unmGwGepJSIGwn7pqOnhBRZ2ggJA94nXH0ukPdHmxqgPBRZYjCtaCKUvkyCifcpbw2P5ds +97U77IE0FQGmVnaG0eP35NhCZb7cqdnlrI6gnG9F9LRRLPpsgUq1zxOOtvY+cWAsN1Z0JNVKlyc2 +GEcjZFTbUJ0fxJhkx2pBXLUAqmswkIK6BZptajqQK8erThC72GAlASoJdCQ2BHtVZBCg6rwh+VhA +YE3yluG8IyUhsS6JMM4DeYt+id7Qcd32+AKYT5mM1wMnuwIVALZV+NtwM7g7JEc7mJhJNIApqsWO +1jCAqWG0bjIMGcF/1ORSES4y5Cg0giclgUkACXPU33KGAKyqbgR48dovhx3Eu11zewNIGMi+EEFD +bIiW+FCN4uB0AmgvMEoYHEDfgPDovR/4i1tv+1ZLAtwIZfS5QqsQKr81HZ6c3ZtEBYsDJ0TmMdwW +BX/PzFxG05G5cLtlORSV0bMbt5UI3nQzkVRWGs4gavMrpoz+hWZlwmNLr+kwe8NeryuZBYKqWFQL +BRW/d/j9MuZYkERhAoiP2INySie6qQKBg85WvnCgVEPLZyXq94OJnOEytIwAlqEejzkdGBxn0ebR +4Pyiwwgu0so1slLEGuJABaeI8ULY9+w3W/KNY6qEjiZQGQdRWdlaD1Hcz3KMyZ2sty02YJHIoFQK +eFNpQ0FZU7qbzkjHFwTDdDgYIjAhuEoVq4y4mamO18rzIWxOiD1uoEhHrLNEWWryacVpPQnIpdKW +eFy2N5c4n/xZA3TOrzl0IYRoHX9O82Q5rNbl8kpveEZW6oQPZlV1LSkQeVbS4bf5o2Z6ceeuMwrO +zqEaef6e3VCf5c+aT1aPYQhT2PpEeI1izKwUhXVcD6JSVIlkE7lzTBcO6z909dZVV143nRou3Pao +Z6g7+GfXYfk55gp2v3tj33NmawvVS9aYES9DLR2tcrf54C89Dz6Wu+pCj92DBgrTr7ieTqNpG/uv +HtNHoJNwBqpZr1U3ff4t0eevRz5vbffFE+bp4JYVPoe8rueSSfWUKx6IuONDXReOzxye/enjpKcx +Rp/nop+8Xwr5I1KiM7ZS8avJJ04MvOrysBQre83Vb3k+RELp5IIU9m4dvDrtSLZcPNT96ouCzsjG +3stHRvdN/nzPmre/cHPfrvnyZNtzN6Lzx9rOC6dLI7il44cP7P3wzaXpBSGKWy9fu+Ga52bz8/a+ +ABAS67p2qkpues/+jOmL/fUfYf7KRWXHqmtmq+NHv3kv5uqqL72167pNripRbW1b8ZyTR570xP0b +3npt8ynG5g6HXLGu2MrR9BHUN6QPjM09MfzsFkfdJGacrKUpuSc9pIoXXiGSSWU1q4BnhmKKYA6r +mqpRggcHlUocRuiZZ3fnZtDRQCsohgPEpao5X8gbNnOHMzS4P7OrFLywfWhgIt+fpdZ5mox8TsXr +sGnZkqKb/miLDrY709nd1gHEI0xFNwUAqkgzo4MD9bYH1UpQMh3lXDHrDQawmKpGWZbcSBgGwSRn +InpmwQ0pS1e3HIVEEmKJITm0d8S/7PaJOwP4sneypM+CvtULfcgwDZyAwGKsFJnkjPSDCLbRtVLE +HqpwsL8vEgygDSOXhTOyRjwokGpV7rH3WC+ohUJCpTwPqFwuQykyy70TNGw8PJzUHUJyz+snInJq +1gFFC+WPQ7imp47f+4u7gyG3oPcRwFsiAQJ5hVEJGZWIVg2i8KhWBTMqWDEGeroQldDy2flU3tBU +j24EADpSVHjOReIjcM6b5pjTdtRnm4p79KgrWchOTM+OTSWLKhC7MIicqNZw+0HyUHR5wZVKe5zO +WEP+AsyxfpB2F4sFzBJC5TAjbJoBiBCoapH8t+vEGiRMLQZ0cdKOiklJv5KuFS4jFzXW/XBL0FuB +KOsWsd/NUU6RWRRas2mFN2RaXTLRNxuHEgEtZJS55RcrRXprESoslgcjrVj/EFsWF/lztJWWSSMj +Qn/yYEnyCPfs6R0YMcLFMYk/z/z+sm8sFe3i7NaRLIe1ge5a4hk1S/YmjX2+8T1LSfE/+7Nmr3G5 +M/cbObMwVc79eNYnEiEi66bVNWJdLxKOhCNRlkwgEUHcb6ggoGptQlW7XIGd6+PBThV9UBcKoTc9 +vy3YZ9s9UvnCbflyNuprMfvjdq/bXYXFbruo7wXj2eP+W++QC3kslZJDT133vJZAl2dyrup0zeVG +S8dmqXKJuvuRTVpR1EpOg7k/MnewPJ8z5vLg8xJDRVNGoM+nk8MLjxzmuCGZaHC2Jr7yywMfveXE +vkc9ktx25XpE2EypmjcznddubQ/1zdy+9/5XfSpv0MBiFw6gFMRIlUo6AqFzC/tPIdYacSWm5ofL +6VI5WYSHB9scu2jv3958z0s/vvD4CbY9aR23XrCmt2tdJNqKNJeeyuGdhfLCyXuPR97xR7gc8OMA +sjubG80dm6vqlc1vv6Fz+zp9Jv/Lt98EazWlzIzffyDYntCbTvH4R7/z8Ee+reulE9N74Sz+9LqP +H7zp58/6nrK9TsEvq+U5dbMjinE7mFfyZnEinz9a0A6XM3vU1MFScUJzlO0xkFYjKmpqEPeQO6lM +Vg5HvH7JVMEsjgglcFMJc9zclg5dW4ttD3as235hly+yeqZ2xZhn2ylnKOPIeJ2TLdUZb7rgnG/v +j5bLteRCTtNLKsKBICdVVbWokJ5CWz3DA2pVVznotseTmjfvikwodjhLutO7UCiXqCmiLWdgpGBc +QQIMIVM7ytaNClhPF//Fnwiioo8hBUbJLSZyO9TaoV4+A2o5NuC4bAL9MChAK2ogCQnLCGC4cQjL +E/TGUPFeV0erH80Lc2i7AYZ0xGypQTCUDj1BXYOMKco7oLmsJ2GGNVAEUCqTjgfWQ+g4Ytuxm3LA +1dIRjiQ8BXV6/9EHH378Jz+7+xs/uePLt97+5bvvvnnfvntOn97Ti0bZ7WFNJ+1HfjEKksD/inyn +36a3SsWouwCmASgblPCrBq5qej4FfMwLL7/olZdsl3Ugc/SLVN8LFPfVZdcGdPRS9aLbud9Wfbhs +HFMqx8Yzp08ns3k9gV7YvS2hqCsQdvrA1Us4T1BO+YEhQ0sw5DgwR5wzpNp/HUgp9H1zg/4hiB1n +LzrsWbsjU5PSNReeebtLR9QYRFaovIIZAJZZS7VglQozoyHTmZ6hefGyic0akSPnljsgChbP/RDA +AtZ4jFHm3CM33SCjhcy4pSUipIG4bBI3EVIKdOkwfkQUl56iHocHU7e++E+cRVhb9afArS0+zzfO +s3x+FnnNV9H0eBZH/f39yf+IahTe27kfz2LKGgesK8JFT1Gsc2J7ZkQBJ0pEJEr00yDBgieRhiPO +1Bel5Tid9T1vU2d4ZSY3Y3v0KNUXAzTO2L3Ag0fcU3NyzQNDVT5+KqgU1Uu2IdSEU0S9LaqSVXta +UXt1+KlfHnrPN2CAE98JJ1GOfuBbmQeP9Hdvns2NHHnn1yZvvq8t0nd84onJr95TOjLR3bFmPHks +89AR2jmE8gde0X783+6c+PHuo//60xPju3ujQ50v3IYsJAqpO30DyfTU6Zvvx0lBe82oCNvBz/7o ++L//vDXYNzZzZP7hwx2XbljRtWFBnXr0zTed+tZ9HfGBqYWTI1+/b+qOPTQdYn8yeKN955qYr20i +PfzQW7/sCiNnVisvFLT+wYjcVixkIht70HXo4L777n/XlyOru1e/7HLD0Hb//fflzlhc6piaOTH7 +xIn73/UVhEzFKY5//b7TP34iOtjZ3To0mx+ZuvdwaRqIivNgbc5xxxmEKZwskWIj1YH2fb6KDYJV +LtoCWWcgiTJ+e23Kpk9VqtlKSJKDfhnMMVAhJR0gGxWeyo4tazfuGFizrSfSEizNZH0pV3FMsRtS +aXhy/ks3u2aTCAL6C6WhTG3Tgq933N8+Hu7MtcUr0YG1vY6uykx5Ar3XxZxRfatbsped7opPtoWR +E8tP6ZkxJTuuqHlbxek/lVanShWl5tErklqxa2UbKB/Msl030KkTAEk87fSveI1YPPQlnlW0qoDK +pEIdsAUYVXvRrOYB+gQrAdcWMY6SnAxC95BUoqbIhPrBCod+MyrQEd2dLeGgjF6GYLHHUmd6WFRO +GkYV7DFERwrtSAQ6BB0iulXxRDCgVCwQ4wA6LKIiwkTyVQrFXVl16vZffPvv/vFv3vq+N731fa96 +94de996P/ul7P/xn7/3Qm9/yttf9yZ+96I/f/NJ/+4/PVGoqmBIYMcsFK+RCwKDCbZCrYV/B71SQ +xa85g6bbVaoWsuqx46PH0C67ZIacXlT1+WuO9TbfRabzpeGWP1m59nJPqLOKVm5glZJlSQ4HvOgs +nc+BEieFjQEUE5iliJzBEbZpXnSN1KgrFRkIQAbh3MSR40E/rGqhrKoVAx2QXQZYcFzOksNZsLvy +NmfatCfL9qzpKNlQZ4JIKFnUjEkSCViu5LJ0Ce0VRp2JQLhQX+S0W0AmqwLmvCKLg6WsRy0ycS4W +Y0+eDHaqzqLS5SVK2CJZJNOd4viUXiScl5DLxKlDRa0N+h4KJXDGj73G5ud5B3feL5xFXoszLD7O +e4z/m77wP6Ia/+cmSKjAxr/NUQKKVRC/DZtdtHxYR5LNxqFU4lNB6sjhXtUm2+RUYVo/cMq7aWVf +6wbDDhIpzZEIYM9oRslbNCMP7PU9sjfmay3oae/kuD0emX/5i3FeV0lf1XOJy4UGt7Wxb/zyiT/+ +VyNdIHwcZBntPuq2FLtknd8TNipl5cR06xWbkdFMG/Oz33+k5eqtAW8EiAnl1KywCEUCQcxV6rGT +ql3xy5G+l18syroH2jeaDkOdziBNiEvDwIz5Et7vuWZHV+uqnJnCR10v2BaQImZJU8aTXZdvigXb +ZpXR4//xcxHW4E1K28rtD3hag2hulzk8AdcMmcickhz52d7uG3atX3GpJMm4nCNfv+9nr/28upDd +/OfXbxy4HEJ29uGTQ6+6cmXn5kwlubD7FJ368o3iFAf+nRzE3udsDvpjuNLM8alf84438vuN44hi +afIlubcHSuhqKmJ3UlV1FDMGuE6UPChXHLFYKBQMdPe2b9yyyhN1ab6KFAnmMvnc2FQQ8ljXMqbq +H+rRWtxFuGc+R8FpKp6aUVLNk/nyk3OzD07rw9rswewDdxzQ0xVPzV8zPIZmy2dBy0l5Fd1AS0fA +YJH9SqmVok92hDymByRprd5ixJmUq5WWcMHu1Koo3gcIB4Q9yIHBHUQfCTyhr8BBB7Yz+lc8UfIP +PI4Odm0QqPI7JXi+8LhcHoh+0o4cURC1RlzYxukBVPZpehnMag57OCCH/DIybSjWxBoHGAec6lC1 +SFbhSRQCFOaEWwU2c7CcY+KA3oXu1E2jqJeLMIqKpQwKQOMtwbGpIzd//9//z4ff8fef/tQvHrk9 +b4w6/Fqwxd7eL/Wu8vStkoY2ehOdzlJ5dmpuxOUBtlZn6U+eEafogP8E6BYsOaCkBbUNGlbZ/A5b +1Kyudnm73OGJk+OPP7lHCnvMFt8pZ2nWruXtqM0veIq5rTbX1fGWwRB89gz4WAMBOYim0uGg1wMK +QDt0vVYEbkcr5kvQl0XkHksV8AhA+yOla6spPrmWaPOjayNOj/6n4ZAfXiHiPNRIGOYG7gdaVOWq +rmzFkS/XFLOqw9Sg5cR8+vX4YL2mSKw6sRVZ51NxIUU4xTXyByKf9kweIlprhRaJyY/rbphWw7JV +lxyFIKtETwkpxb6q4IzFfaXeXWIPczKTOYv4sMKjrY9NvLBuR/3NJXHfZzLo//edM2bg/KqxkWN9 +hivjrJP86/x22QGXxbCbtaO1tElJCkONDEX2ICkXgyJ0PH3relrCvfOZEUfYa+9Ey51WBM2cAdnR +G4+4ovvH7/U/tr9aysDuLavgFXMZba2jb/szsHWEnhoGqReqijPZ2cde/EkkCC+790OBDT1Ei1mP +dnjaws6QFxKkvJAd+tAf+Nd34/uVLPCNTmfYh2BbaWRm9Ydf4wr6aJdy6yWhHOFylWeIWhZHoL/N +WtATgfuIbyKvGLCF95y+e/ib9+CT8JYeEL7o09nNH7pxQ+9lxXJWncvgdXBDF+LF6shCFR4KP5hD +BSE4V2zrgN8WnMuMnbjjsfZL1/a1rhufP+IM+cJrO+lycrO3X/8Jt1962YN/17J9Zcv2FbI7qI5k +up+3ITHQE/G1QvVu/z8vj2/qjfIpinwK1PP7eqKY2dRT49vf89JX7/vshj+/5tpvvrPv2q3PbpcJ +ecv1gvRPGTqR2lRQvyskYQC+ALsKhCKCkAhRA40ClYx4INwmwwBwl5ojoZA8NZ88cXh0ZjSNzpha +0GEEAbXQU8ePOnIZW9A5patZu2/O7j1Zq2YHZO913bHXRNteElj7/NZEwh7zxRExR/kA56mdXp8X +pRG+oBSIeuLt/pYuf2uPN7FCivV75G6XHq0G17dV+8LZiHPBa+ZcdgUYIXSiYKYb0PSiqB++G7WY +gKYCPBVcdsyfA0WPrCJ/jUBHSNVp1HGSE0ui7k7cOav8gtNdhPSFmqyhIqW1JRwJBcwyVcFj+Wia +AjIYpBY59MollNCOTGAOP7KClr5VFEvAskBgGnoUn4JmWw2EvTa38f2ffOcfPvfBH9z+3WJ5tmdA +iiQQ0TPdPghhlEuy4oX6RYpXV5Ha470jFCKpCCpZpygGNE45bSgGiC/8diXgykpmsWb4qraoWo2V +jDix9YNkPOd2V9KankQdh92Rt9cmwcivFaKm1umstobs09OZsYlcJqvk8qUiureZWjAsgx8cJTNq +SQFte8ATiAVjIKYCKx18cPjGYGT3OKseqQKWdY/HHoz6XcDMebi1CUVLKiCXp1QqwFyUDaW0NcEH +LeW3JPHG667+FEgcJgqukzueX0Kee8ELLUV2O9G/CloiIajqTzZ/KOJK87sEnkg9loVrK37SUIqN +iGsDIdM0iOVa3NLS9I1nruDrx1sSaH52W/v39FfOK5uaUgnnmYI4Tagjwd91TgUpLJvFe72URILe +p4XZdBCxMhqPp5v+hidfx53ii1QCZ60tofwa3B4UCuEdi7gpdzcnMB9RogpGYpTiuwD3Dr5w5+Dm +XRP5E7beBH7fHVmlqJmSU7VduKbm9xmzE5Gbb1WLC5LdzFy2bUXLxmFftuLz+A+POv7lVu15F61b +cdlI+nDnKy8ObOxCdGzu+4+W0ZKRbUxcYvjCwaHrrsb85b1FeagDvYzioY6jP70T+I3B5z8HXfu0 +RE1e1TH73/sLGZQ0o0SMcWd0SbX2y9f3r9s2V5woj2cze0Z7NmyEYNA9+srXXuWJh+aHT+7/4C3B +/tbVr3pOa6Rnzj7t7Qi7QM6sZfRIJbS2011B+0j/npt/tLD7pJhqzBSyQTBZ+2/YvuOa6yeTx/ff +dMfql166afOVY4VjsXVdqKJb374TlzP46p2xzT0IYY3ctnvlyy7e2HXpmHm8+9pNmMiI3DKjj4aH +2t2yN97fg1M8fvOP5nefjKzpWvviXdFgWyFSiG3qrWpm285VV154o22z59BX7m5aC4LcenF1LLvv +ViKHo0Tk7NeFFCPxOLbKNJIEeCdsikR8It6qu80W6vCj0y/KFiHw4bbDH0MkFLKV+qPIrlhbPOQO +5WaUtFkLu3xxrSZXbW40DXZ6krJjos+htgbnT2fjObt2ojBzLD03XQSPZ3u3c82a1vaOaAwdBx21 +bD5rqmYxU8ol80q+CDhJGVI5RDksl1vGQNSaUfZUy7JbaomVYWQUENIEFoS6LlseICjR0V8K4XBq +o4FEHRxKkLxgplHXCE1p1xyuVLlSohQx0qOoPwHkhd0WQfaNVyjeoKhczSvbgmEUcGKaOD5IDKsg +F4WvgYQB4XBEm1Gy0piaDrIWJ4ESo5gJDoCwYtnAEgnEg6fmTn/9B1//5RMP+MKG5IedgdUHjnJu +MY1Zxh4iehmGXZKThWMTZwE132DIHvP7YIVR6yui0q7a4/4QuMc9nmB6oSR7fJQRrJYBYwXKBw6l +TYGutel+9yQZCU5PGc3FnMelynDQNeeTSnD2bPA3yadD9QW4EcoKFX6C0R9cOOAs9YAlQ/KA2iE1 +k9ULley8WdGdat6ogF2ubHoQtTb0oqG7fMDyks+MWkbquM29DsnA4JgksrfCu2IaVV6S5ITR7qhj +QRn6wrF0/AoqnYlvRJxV+Gi8yTnnV/cIFz8l3J8otqkvfaa1oh+QOhMACGEAcSxUlNEQI6LDhgpp +ht6I6hKGRtNJxW8piMCHYbC9YJIj04SVqVWdI+KrlnYnwgmrIJKTFItSmQYvXFdRuSo2pVDFwqNd +EoRjeC11Il9MtvK5l8rtun+8GNutz8DvpUJcDsNpyCPLrPhdNxTqGpM1eVO1BltfZGOTzGCdySqR +6qGopJ+SjQSMrpaMfMATcYIc+YkTjwzfmoiucO5YV5ad1bGRyL98w8zOukzTPZ8Hmi6vp7EYoruP ++P/pu8eH57J7Tx+bemJd906v12+Mpp94+aeKJ1Covaj1gZbHZAb8UbyVuvcgenWcnt2/8NAhswRr +3aD3y9VDb/1S8eQMQfyWgqpzozOY/5AtfPimn57+wQN7Tt3T0bpy1auuciV8c/uOPvwn/0IbB+zW +/CvAirTJTLGckeUwzpV+asQXCIwmj6T3j5y5JLsu3YBgICr3EPkEJhNR4qA7ok5mU0+OHZ16Yj1f +jjKa/ulLPrnw1Ej6wMTp+YM94dXC0yGS66p94o592ePTOMVI8khyH58CnC52tOELSTZP7vDUXa/5 +HAq9if6mhN7vyx9LFhjvYfE4h73V2KVWsgMNfR0oWUSVjOn0ISuDiXabRbOUKuhArqJLsKKG3eWh +Xn9/r3TBpYODg63A7/t6ozND/gMXRCafOzgV84MJfsydza+UAn5vdaJaGXPPP5WOa+5rLt92wXVr +nn/DhV2tPQtT6tHDU4cOnJqamtErekpNl9D0CZZT0C3FI+G2dq/Ng1o/jwdCA4xI5SA6KUveyanC +sdH8VMHIOKQCGl8hO2yzq0gxcoiT0n9IPMKDqaE2BOxn0JOI8gP8Cj4BB1CbDI9FOJZcZIKEEKGQ +cF8sSjfCaLhIVlthBoK8Ml/dWR5E7YoyEcbjIIMJTQcXFoBe1eYGm5r01IEnv/Xtr4+MnWhr94iK +ul/p0Vy2hRFyOAauqIkthkBtBPDRqh1FKFSah0hJ1e4BYAotF52e03rlUb36iM32sM3xsFHZY1aO +GKhurNW8rr7ueGtIrqATDihsKuhx4gLX23wqjfipPeB0x7FtzcLMjLtclWy2joQ/4nPCRcQ9qaVd +5Xm7Pm2U5xQtp8O/xDZ3yy4Uh1LbRu6lSdqGQqP1zCIvO+EpioClxWsjLANCBHNjDbpOkRS0HC3L ++VuiGn6lmbO+LNQWpZKpshmhVFHCKk5E/zZXUOBv4TU2Xli6iVWb5VAK9ce6uxFcZT26FJhzxp/P +ZvT/f/qNc1edL5Fvi4ALWyvimc9D84Y5669E0LzxsAyq+t/n8BrFVxYFpTB2LJPK4oRrgNYtflRq +AEpuIzWXkgBzYxcSVVuERXc4FnJ5tHl/7KDrx49IpxfsI1PpWsqZLcj37/F/72fObBJdNACGww7R +Vw2kUyOtX76lfOeeYyPZkbQ5s/dk3shMnz508qbbT/3TbWa2tCycq89kXD3B0f27j33w5uQ9+3Sl +OPfEwbk795Tns67WwPi+PYfe/ZXiqRlEzqBZlqnGDe94caSjY/zUoUMf+35pIjX7yOG8K6+ixOTr +9xz+5I9MlVROOV1Ejf/E9PFjX/zp4c/8xL+mbfrU8b0f+PbM3fuLqdTMwwen73yqEZERZDa4m8WZ +5PTJE6d+/Hju5Gx+dK7SZjt264OPfuDmyfsP5rTk2KnDe2+6bc/nfqJniVJg7J595bB5/KHH8AU4 +qSeP7nn0o7cc+dYvjXypkE5NPHRw7Gd7sLXVVN4V9EzMHd/zhR/t/fStpHEnk2lz9tG//44yl21e +A83CpCEBGm/WBU/diLXEQz3xQxY3m99UUUEdjiS/wxf2eANe5Ncy09n0REGZ14sLenZOTWfMYglB +VrOrLVE1StnCpM2d27omsDlkrx4cC06U21V72HA6QC83rxUy1VnDVnAbc2ZFdUuTsxCuxvjEbCmP ++KHPFwIHEpooOaSQtypVvTGvK+hWkbqjXgpO3Sja3SDKKcflUBHu5lRJqoDA04dou132mMCQoH2S +Gy4Mdfq1OcDE6y6p6FTMVWFuD+oTyDGrgeOzpjnduVq1iFgodSqiSg2iJ8XSYIFN1ZUUyeQycBwc +EBhSe+TWkNsgnjxBNE8kYvklvkDlHpx8h08GTAcwRT4pFAs/8Oj9P7/nrpq7HIr70PiZWIqE2F2U +ySy6LfZMEgdWM2ai/xSZMmb8Yd2CI5M6MW2uKuLrUiFZjNtdPvTXIrAJ0NI1L+Enq0UUucS8OQBx +VbNUqyZdtSm5Nh9zlcIO1IlEQfmnKyAKlyQHdWdGuBkmRNWhg+oG3bVc9mgi2t0Zz6czaPVRqUil +UiWfKsBnl7SaA/uvaHcUK3YFcOGKU4O5BFQqkZhyVJU0I9xExv2yE8bBUv5XeIDsirFbxF2pqbQf +hAYWUaqgo+EJarheS4RbQ2wtkV/WH4v235IQqdBxND4cHYIK3JTsSlpDrIvJxq/rw7aqOcT2EGgh +ji4II4qzEfV/666dNZAlgxOC2rr6M2VyU7KKRyWGdq6nANCd47n0JM9cpfzvfHNpyb/g9ONlYr1o +updkuj6DQS5t5XKWH1A6rultYeY0HtbiOON3TRqRlwjdOUFtLx40uEY1FwWA3JArXK1NaFTuTcyU +wxA5RAXHqpFaayCEAUsURdGgwIYAKqs2UJFoJWT/QYBFi5VEEMEZgPxIa67DY6WZDPVPRf4GPY0I +YlZHmVFKsx6UaBq+taPENVsrq2kjYc8CBk+lTvzAfLdcOnTBv73ZaToefOM/ZQ+M4VfPHu5ZH0cF +deUihFJ3T5dv0qab/vQ32fo9zScPddlBxK1sHu0yw5rN83oMqn4alsXWH+LWNlYgmeuE0xRR6nrp +MHJHxN1dQ32hEx2l/EBbQaRSb2M37jplVStSwOlqdQc7PW1t3opDRdffHm/0Vf5VY/+111ntu+Al +b5793s3+o/tMUIr5HNN2e9rrmbcXf6FVplABYKvpnrIXacWYD7FSX8TrCXt1Z1Uh2Knqj4DzGjkt +WVeq87MLXhnknZLHtEuq9+hjs6Cl6x9caUN7+unRuVQaQwaI1AsGexC32BEltZuaEpABxPSOjyV7 +ujtRfQGQK/KkaL4LCoMc0mcem9OLiABUGBFYYyWbbgp8EB8eGmu7qBUoYorEzIN2hC4HquPRSIsa +p5HJB0pDInSipqJ4C0YhLXbJg7AyopnA2UpSIBRGi9E9B57af3C/auhuL2FfUQ+oE/06S1pMOvWS +JJ1MHbcQ6CddyK0lwYEOFYruXLRiifqO8qUctqM2HEiHo7NyxRnxhnOn030mNXGEyDZIqSNDbM+5 +KplozYvafKU6u1CEJw1qUbB9xxGElR1eNLUCgyy1vEJ5JTosOjOpQjaFztK4JW5V01GGEor6Nm3p +Dcel731nv8vuRbuPnoDfrRV8Rhl0DNSgAksFzceYdRYPsf1pBxkm8WpwzTzH7UW5PftSHM+0ek2L +skXuFooYJQQDFZtSz69FtdTYHUzmWJeTDYHZJNpElSKdvVmcNn+BP8IbVEpGzdMxQLY7BHO80HiL +QU7q31KXMBQzEK8FjSu/4IamwqupWy+WzLFGukR+ExiJzCaSWgIi0TgXvsdWaH1XCs7Z84n/82oH +K33+9PLld+qTJSX/lwsa27oQXxRZ9eA1Ca7zDL+uqhZV1vIf/Ea8Ro6dWi0EaFRWlpEL+smCBt8m +dWFH5J5cRmFx0z6EiEC6gjtqQDuSK4n3wBRc8QBQWM47lLy9lHeWNRi50KGwybHnNQPt3qS5guPY +ZGkySwEjiCT0rBD7anEBNb1uetNS3rwR6WERddedcm5LVPV1xnpfvlPuioU39m77+OuwME999Z7J +nz4pjrNMwTyLBdTwGsVvlzm41ptNFrFYBcueDAGhjcgwXyuVxbb2kqeQOeLZsFws+6Xpi42FsjhB +4tPGaMS1swyzfs7sRkh8AB5DeUcXcBiAoZgmijJU4F7QdZ5eIErps6OjU7XqU+RWs39VPBH2dZS9 +7Y8VKk8ubL78+TM+5/yTD0jZFBfNA+cZ7fMGOnPZQEvEvqYjsDo8USp6gi7UJ6DU0OkFWSAo7soO +sKq60D2xMjSwMipHs7PZuByNBcIByZubzWcm8lJV9vp9o7NTeb3oQXsWl1O3u6gVUtmGiHxS0Sez +pTQQRVF/YkXfqflcDnw00WDRZZZAXCBJuSq6fTpqbtL0KA6iBQ7tJ2acgqncdZfhScRcwUxyzCUm +9ix9j9kG2WukRstsLlLOiCj1qb8h6FwlF4TgyfGR3fv2gtnHH4JfKlqDCA3Ha4Mn3YIInNVr5C9T +zo0/bXiNVMYB7Y3Jd3vRotOtmsg00t1yOzWfK+V3JgM2OeFlsAGdEPorBBPDrEXsTkDRwEteA+9q +MAj4ra5piXgsGg2DohHU8Ij8gA0H15FJlUbGUi+4/vmo9xw+OR32uQI2p0czZJgmROZKeBkQNDgI +ssU9zGASsrnG1aPCX+Rdy95hfX3zRJE6tLiGCMBO1bTU9AofISvKq1KY30IA8ips3pcNMbBEPlp/ +LPp9dSGwuB2gylHoQgzimCuiT7BugeU/kHLlO8JvN16LAVnbQ2QqeVQieylquRkgJVLCzZZnQ3o0 +e41nkcnNks1yXpaASJaJB+u8Z0iDZuHwLATX/+JPlnCoXt4UUF2cwXpYtbGWzjnc+uJ5+i/9+qqx +7idyVpxXiGCEIzHQwPPBRCcggfUOFCQ4szjbSD1yAMPhsCr1ZEQKBqpRgpgtpuxayW1oQAIA/42g +FuwqvYpeb655xX1qRlnIA00AvUs1ZVAPEDVkYdbz2yKhvSTmIrZRXdaLj0SACg9yONmLgoX+nB++ +99JX/EHX5Zu7rtyk28tj33zw6D/f3phCNnHP9VyyH882889ENZ53FdannTJHwthsailpXSlJveYD +LRtZ01panBn+/vJLqP8tAvtCZQqNTioAN8yNVk7glabVBOeGCtpRsYCsGjjAvDbTX/WE7ZG4bfPW +7raIZ/LgVEvW53ss7ZkoySeOOu6725/NorGxLruK7mDbG//Ue+UlRx66T/K79LCZCiLqDIIbn1+W +oq1IZqL4UAvE/JoBrKuZiEa7Wjrv/9mhicMpR6nmdznWDA0Vc0pmIRuMgUdQ073wwhxAm9RKFcQr +MDg4fwGUXcpeh2RHo928oqzfMOiT3ceOTyK3qOjEhg0aewVxchhysMhsFI5FvEKERwljIXAfhAUV +nj9RAQgriyaEuQ/JJhSYDlKG3MaIJ5bS7OSOEJ+TV/YXtdKDj+wDSTr0F2o4hCjF14AIsmQl/XFO +1ShaCHMOrlk1YjXDOUaXYNibgJQoJR1lwyBpQ/gGWXc49bqjEmrxlAwFniIUVaVgJrxOWXKZDrPs +qmg1I5aI6ip8csxepZQvVMpaa0usrT0+OZPMFzTMC+I8tbJzejT5qhtf8dijuwNOPVgz5bJNBukG +2Q+C8Jeiz2Q7OEAtTqoN25ip3QSeiR5wLuuSg+0uVjsi0UjNO7i+EDgmmM4UM6IKCmsJil0ojBFB +oGE9nplqXLbQhcsHIUTl/7SbiOnC2kDWSBdDdezXNU7X2B5i/BbmR4RXxR0V3+C9U/8vX37jKRQt +r7HlW5+Psfhu/dTnEjPWec4rRH5/vrBENV5qhaiXSLfm2eRZPBcAFbcEW4wN0MZT0CFbT3HPxPLi +6L61NYUvRQE3enLzV85uiLNxeRffYNY+pAJFEQapPhIEbBtzfgZ2NgVWqCsNB0pJAbKzyIlG8Te6 +hoOPGASRSEGCiBFs0mrRlSs5czoKsOFOoski98hAZM3Maa7JnOPEbHG2qFHTcSoPrAMtuZ2UKCTi +ZShqlZbMHkIcBF/BZiTTk9cWRyLJkkfuBxzTKL2y2Tuu2mwGayg3LKm5J9/x1dHvPrR0CS1Z00vv +yFkXtnWPBEwHA4SOB7BTrHix6BFcY0geTRziZIA0EqrRQuUxmJGZD7lrJAQdoOMupMWwd1H7Hol6 +IM4RjWSJA/4wfArxg7yW6Cm4CC5drtEtfJ9F8LFko9ZlDO4fgVQEboBCPAyBpM69QKhCYtkAxgdT +J8mAKlhWIHhJ4nEHJiJmgzZCcXikU2rtd3b1BTvbu46dPO51uttrLYGFxOCm7UcmTtdcQdkAfzjx +OiAgaR7ZV3n0IWdJSzvNTCx8GqVvJb075g2FUbaIxlNlhAmLRq2tK+GSKm2Rtpmj5uGHxnxVt5JT +/VLk+MHxCy7cfmQkmyzm8kWVuOIUA+WNbpuMIhOQqGL+ymoJTT9QP+BBKZ5RHRpszacy6blcLCDH +/T4dxElgFvfIKJNAsg46G5wSxNxGa5xKZAUBfaMHElcjYeqROOfJB1sEzDRi68G9ZXcMkFQC9CLk +SqxCDEhDCAThEffuffvnU/NeP/Ynbh/Tn1MzzAYAlXPS1DGZ0K1U/SDQPqSOCc9I0puipNQxhh4W +jydq04ns1jTMNm/QjRIUAEsJLAW+CqLFxlgDpjNs2PIVBQFoGgyyf+6qO+gLtwWJdN7trVY8hYIN +/TH96NMo+2cLmUJJMQo5cBGsWNellgu2ohn2oJeGb3psWs3keltac7MLQfipQP3iyS3PcKUYKprh +IOiOdIebKgLhQjM8lejlgAjGlPCCYtmDS8LHtO6JawZxa0ousjghpgGSFC6nVq6gxSNnKoUdSwoN +t4QmnCap7j4K/8jy29AmG4chOng6LhPUMcBUhGatuYN4Qiibe+0Q/TgNxrLw6R6w/BP/qRvcQpsJ +ZchPHJiTCxQptoLF4B2kmyS2Dv0Pv6mX/zSOTscQ6WkhlagYW4BYrdylmB1BSCXgtucpXPm/XDVe +djavcZmMPtO+X2YHnDv61yQN66rxLHaE8K8sIY7XVqy/7jpwhImtYtp0bCczWQXZzhyyh1YUgG3C +HgC6gHhFnQEHXdyQ5AcEB4oSaXpUzTnKmq2kVosKUAoe4q6EC0LIb0g1tAefzJSn01oaWH3eNnRO +IiRhhSiyFGL8zRZV02sxGyzVeDXXLXKsSiqSpi7wtBjHb3t85pFDp2+578jnbiuNJ5dPSZMFd5bZ +OuPbZ44Gw6AwFis8Nm6QsnIEAzLCbIppSJRpJdg+NDVtUHbSeIRio9P+xvvgIEN7hq62lpKiltQy +lZ9bneZJ1nAwQFz5eazLc14CB4EWcyp8MNHziW4oXjMChW0l6qTLyoBFP2WTiQbcaw8gFhrT+za2 +rNm5+tjYaa1a3LBmhZTUzP8e7c4U3KmMy7RPA3IMYI0/0nLB5qmRYbNQkmzSeLV6zGtMe4xwzJ1o +9Rl2xeamcnzwj+EMmcJMS4vf6/Ice2o2NW12tUcDUJ+B0LFD0+j9CPcPjHAhyRuW0UnXg4AqnFq0 +BQEzDZxczCU130BwXgelmX3b9lWnTk6is2AFuEtfCNehldUiIolwsDykY3DVlPEiAcYKz2oMSBNs +STQuTOIoHGWa6E0ytrifjKjf4MQ730ZkBAmpClDx1Nz08OnTcNoIVMDof2t9UvWFcDH47UYRA5u0 +lndI1i4TPYmuW8IUrFNckyLBKND7yelWsIpQq2SYcrEWNdH5AsqrinPCL8zKNndYBt6Z0ElQhr5g +/8q+ibHJsoYEpm1hIYdECFpLSzZNDiBE6tQ1ZzZdQr/itvZAtVIqlYo4pxzyptLJTDbtgaWL62Sr +DN1DsJsxBvIIcflsWLEbKa7GwqsIJwm6maWDNUNUPMr+E5PUIotiSR5MJ+pZEQhg1iBrWfINIPXK +PG5kjFNMG2eyzHex/lntWoiYZq1hBUZxy1hQER+gmPSG1lsuTi0d2Pz24v5ir6G+5XgDNnxAHgEv +D+v/59p2S0ADfHjLl24+1bn37XkVwzOQXL9TX1nuNZ5ncHWcFm2MegC82TOoJ57Pfhhvb6LvnTfo +k0kzXaSbKiy3Mx5iMUDYyRu6W19xScsLL1CHp6tF8Eezd0ifiVfU7YVxeRxGhaqjpSbKNAjDwEqS +iJEhmghrAxQDkS9S/AJhK7AwOm0IXir2UsmhqA5UelEYBmzLKIrDLyT4iyOpykRKSytoPUspS7Yy +iduTVz0JMK6+e0aqkVcbTRU35yE7ErKSScCsSdAX8qaqtz9v08CNu1xBb2GY6jesx6+tGiHNuE0A +KzMGWkApI1zk93vxlwamFjKvbb6O8IoXbFv3h88BmZgylYG4xBQCzgTvFkeQbJWB9igKAVIgiGH5 +SAdkR5PHKdI6ZzGNwoMdfS/Y2nfN1uypWaOoNV2X9bKxnBrRZloYwu5w2hIbele/fOfK6y5IjUwh +C0WCEAIMihpyiYWZiGkTC6BkegJOOeFwtRrdG1o1Uz++b2TTmt5Q+4AaGdBXDWYeP1JB26Oq/S57 +ZaRWW+9Ezbw51y1PvXTn5HXb0lPJOSWHurtIl6trKGaTyiAea+uIxVta0JICgNcQKu1rvpMHF1BZ +jo6PkYQ3k8zKbpkynbVquYyyeqCdIHVdxXJ5Fp25KGnorqDmn8rMSZ8j8AePbdv2DfsPHHO4fPmc +USoAXeJxuKsEFgKPKvJbVG1E3gw5GnzTOKAqCMf4T34Km5A+Z7vAciWFsWjhLS3vAjcbgVqtYhw7 +ebKoqnC0KCTLJohYyqTqGINTdxFJQbLLyL4jB/ks570RDhIoLP4VmypwJGE4YROCj7gM/x7ujKNs +c1YAQPJBOWMiYHsard6KzwVuAvI8yXGxJeLx0VMg/askIhGk99r7QD2Bfpv5lngoGIuncioSr9ls +towy4M5YIAAMgOkLeZFaBMVe1V4JyNQ8jLUjiHSoVxSUvtVW2tqYdCkcAuJudOyDAXQOT5EmkN6n +RB2FnACHIQOMi7uxT4natgaCJQPcxxWwxtOUYyaY3J2caLHoEVYR9wYha3F7eJ/hfxys4fuzqDV4 +HHjidMD/UYbR+o2ljM+Uh1xgaGUZ63aMuOv1y+NDiq3YCAyJ6JCVaOK71yxlxYCanxQQaHiidctA +XEZDBjUf4eyvzzzu0nfOIuvPf9D/tW8sUY2XndPe59UlEmtP+zhTLjplkHFZ3Vv63/OyTX/4Ku+u +FTPfuDd08epKSaeO8mc+SMw5Bz7ymv73vHztVc/vueBCPWFP/3yvUI3Bi1dVFR1cV0xUIlxDdiy4 +eore43oNOIaESGU1iDpGKvyHXkQRHFuaCDUBMOA0SmjEB70IUCIa2iKygfpFJEWqdhmpjZF5dSSl +53REnaBaRRM85iwjeUJhMrxaZFRsnpWm1w2vkRaz8BopUUTLkFQjV0zzrNriF6/a9c13rrnhuRdd +/TLnlsjkPU+hgkLojLMaEE9/E5q2otimfBAB52WrlSUhsaGAhNMVD4cRzgEL2to3PvfKz79x49VX +XbLrpdL28PB3H6ho5BoS/B5gXbujLe6LRwNTyayCqjKnB+BEHoPIjoiFQfIfb7VfvBoqED0dMdEv +uOWvN7/l2u3XXLtq54WaQ5m8/1Bj5A14ggXmEHEu5i4it5zJ/C758Gt2/vUrdlxx7dD2Cyvx2vH7 +n0BYu4IefC5HRQLEA1EzqGkAWqpOcLb5q74We7hH6l0fj7WGR4ZHuhNtvRf9ue5Z39p5WWj9jtM9 +jp8fP/jfzspwAA2kHP0e9/ibL5t590tbrnx+eMeFuTZ77tGDAQjengC6o4QDEVUp+73R3Q9PHN6/ +ALEb8Sfaor2P3jcSCXlLanb1uu6jx0cVrZLVVXfMVpZqUhj9mMAqDo4gEL+BnA6IVAMtf5EaJMyX +0w0+056eFgBMjh4dw0oMheKzcwWXp9rV25pKFyhUJyGqJ3AjXJtGMTO+i1yaIYSVsJSEgOQ/67EA +ltL0ZyPwJpw79Pr0+6bn5iZm5jwyKuNF5wcWmCJfSOE0CvNz3TvVr9aTiOIj8R3WlM1eo0CNsLuJ +F0RuULX5YV7Ch0PLaZenYLMl0SQr7EWHTPC4lqRq0e/MaxqBxb0eD+C0DlCFu7ViEbMERwxUr1ql +6EYYGCxCmg4HWgo6SkVYGDVFMeVgJBGLoiw0m8/5wCMeDeC0ckgGTQ91jma2J4TXqa7HShyy00Qh +U5o25jpAPL4a8tlikVpQJk1OIXX8kPQi9UkhH5AXsbgsWK4BGY1HnIqiEMskGHV4XmBWej21aBSW +JVEK4XbDIqAYhtgHYkasDS9ulZCauDE089j9XERG0kuYJuJRv5/1/cF+CEkKFhCMlqor3kXFVr/X +wlFhGSOWBBlULDisvf/08oLmyNrLYhwN+fUrqsZznsK6wPN953fn8+UwHLG1nvbRqIqtvxA3Hfov +/qLtXa+/suuPn+df0wVabXGFwa0rNv7gr7MPHjXQmBd9d9d0rL7sqnR1wb+yc+itr4q8YMPszQ+c +ORe4NUP/+Mbo87egs0C+tJDOTM185W59ZB7f7P/wawbf/qrgtRvmv/sQyU+CGxDhILlh7DIS0IaK +GPF/Msq4TEOoRm54imwLdZ0HmbfpQIVGSUE/KVfFIGXpRpIIG8Vp2r0Liv3YTOF0soyqM9iaaHpH +K43MQ0GmLDYQVWNbxt6y235O1ShaumLWmMLLWvyB/tbLvvI2e8CtFvJz+oQ6kzn+lbupYby1Vn8l +e+usqhFpQpyUMj9CNcJ+wD7V1bLP425NRAZuvGzdX16DSywVszPaqDKWHP/OL7xI5WGHIfDlrMXD +Umt76+RsUoUwgIuAn5ONyhlUnJB8HPGoXfrR1136ztf0vmDTie88eOUX/rRlez+g8wU9k83NH/jS +z1CmaYlvdv3Fxmehzy/otbidGKLtqk+/sf/qLbDas6WFhezUk7fclZyeIwwnPAuMDDFgcGW6a6iK +8PgdcsghJ2qxPqlrMNLWE0lnM4pRWX/Vm8uOMLybgpbOq6nTex/ePTuidDo8ndWWDT7PO15TuWwT +Ap250kImOzXyzZ+Nnx4rRxFrdg8Pp44dKK0ZWJ9fKD/+wFTMF8wtaFrReclFO08On8oV5jdtWTG7 +kNU085rrd227ZL0/4XX4nCq8G1UrFRVdATWq3UAYHvUbKPSTaLGAt03TyuvX9CXnZ7MLCiwjXKg/ +7Fo1tCKXV5LJPG4PVidx/JLjQ/MjQKcs2utiu357OVhGUo2TY1ZySdwCepPjLmJhUsrL5ZhZAJGP +Ss4RwyEpvcTBhLpTKPJa/K+ASYpCRuFICvyqgOdYtDH1+gARX2VfyOv2otDUhs4fQOUWKiocLp8r +0hYoY/uDPUhy6z67hve8PlivaGeCbeRzg+k2pBXyCDpTCMAJRj0DqUfEZrEXoG78cS8BXx2SogDE +qiOfKcs+gFgxRd6gTzF00T+EL6eGsCrZVNzTkS+f3WtuLgDLBrHqoKe2osuxesDZ1e4I+XGyakVn +uhyKqVreGG11TIIDGJwK5Ekk7Id6pmoVsh9oneJ0/QOea1/SPbQ+Bi6MVBI8hViw+P6iorOmv2nj +iowhiStSpBSCZY+bT8qTLNTf4sPCrFn3+6z6SujTuiVk6VG+bj6MSE6wSd4s0sWqaH4K+9vamPwf +IXieuddIMsCyjpcfvHGi3x2190xG8htoStX7zhdtfeDvVnzkVetufHn/xZe4ogFx4uhzNqz7yl/C +TSOmSH7ok6nZwlg1q/nXdLfEV4BH66xD9PQmomgpXPUP/9VX9l76/qeu+NvsLw6K2+bfsoJ/CDeU +/yZ5ISQqRU/JX+QQPl6JHhsUYyM8IycX8QIxE1rYiMRUKkXFVgQNMtmM+BvBtzKISyrVglYdnS+d +TBtFVJeRhoVFTDSSDPAhj5FNbtG75tzx46eff155bKfzIey2Cz/7xs6Wlerp1B2Xv++2Le/+7+v/ +HoSbVgb+V1KLZz8nLU6Sl3xCFndoBIE5Q89bbzKTcwxG1735hYFacPeHbr7zig/ce8n7nvijTyVC +rraolEBDSK+tJWAb7IsWFGU2r+eRT3KjqLpcAyObNXxyG8S0Yo+1bulvj6/AVPY+f8uqSy9AG4S7 +Xv+F71z4f7576XtmHjnePMAmA9s6EM0pB0shvgN9LT071/tq/p+970tffcl7vvrq95984iBK7ylz +RzFzpIaoehXfR1/hWMyTaPG2tkudvb6ugWBKncsXU5t3PDcSWu11elzJX5rpn+79+adz9lx7n2fH +StfVz4tf8Op1obVr/HZ/5iNfGbnhw/tf9NG5ew+BCHDWXYO3F6l5srPao7881BFt37gqDoZOZ9kz +dSq1//HhkL/D6Qy4pMjo6Wyt4j/01MmDe07c9eM9D991YupoCSSq3poXtYhgvSFkB2wxO0jkoN/R +HML0Ol2JSCgzl4kFvWC6k32OTdsGkpn08PAMOnUAXSuiZzQLtFTrfXFFVq8e2LQq7ulGUq6P4DLU +xx5wHGK+gYGH/xBFHIfsiULA5cgX8tk8NA05gfRltsosd5CQNbQQ2SlkH9H6l+026ynum4ii1uNz +DREsoqqIAGkUT9Bx+5RqLFlty1RWJaItXXHFRG7bKTu9uBeAv+kKUquaRvSIpqKUPG66qQjnILwE +CByufUYHHs3wGX4z7SjAyQy5o20+l0vPF7LZdB7ctiB5UGB+GGWUkNYCki7ZdWBX3bYyYv/g3eHC +RE6qc1iagr0Ie4BSvhaVbYPd8sY1/u2bQ5vXR3vbnX4PCCTElfM/TK1H7ju1W/Gi+SOSDmjOzJOE +WhDYlDV08Ozp8w7tcKy5RF6xGmyE7GozcVvdsyN/QVSIND/oXTA5UFSLp5KLQ8n1FslQxpJZE23p +xbqSE5qqrnkX74oAzgiHUbwmo52vRPwrwrr1rz3di2eiKv7//B0B0FjyFICHxhPxP1RHNT+733l9 +4g2XEyasUkGr3tnU6fF/+SkmEWnFVZ/6w6uGXoUFoLLDh0frCy+0uR3Zx46f+NtvPfrP/3b4L//d +IrcS+8162NtvvLy/dWOxkisdGoWoIOAFSU6yfUc/evOTN33x9Lu+DDMPWTDRWwM7HykcAmxDXMIB +lPAEzBv6EESUqCIGkpH6pGHPoTLNg7SBBq60bE1HvtNAgoJAj9ijKqSXq2iTj86VTiWR14SryBSW +VISLNgrIKXAhE8sjWoiMBoHOB/ITT+J0Fia4qMdoIHK5IBcjtzhBKSwDZetEqoTe4xBnaGVHoKfF +5/BP3bOXF3cj7G+9OO+iFAqvfpuEABNHgj6nLCNXzXNyB1kn9GGvUh0YuKNxdYj79b/huqHOCxQz +bzx1BCwjUbezL+HvabWv6DZbY+aKTs+29SvR7mBhKg1qT+oTSIXegFHU4Ui8Lbn3IOn8Bz/0rTv/ +5d9+8dZ/H3rZ5Su7tpQrRErXdAkWPJ2ZJnH3MBv0gojdmeiSKNLQzdBtrnn1pSt5GSyMTVYgveAR +gJINSSag/9wSUInQi0HZ0RHyRgFpDLhNX8XfGYr2hPRq3siVw1Iw1ra9J7LeqGqhCDwZ75br/vqC +699w2Y2XXHxN29AqVL6v7k9sLJi5seNTM6aetam6C/qrEq5J4Psu6OWuDn8w6J2YmUFxeUXWo12h +7oG246dPJTNzXvQFxORCozhq05mFjJZubfXLTqmU1vL5cgntqBCUDwQIPw3XH7XwDi/CcYhbhEI+ +FNQWShW71wi2g2pUOnZk7OihyVq5FgAGrFpBr0MAl53QZ1VcNOUcMU+IIXPs3pKB5Bhy93fqdcEN +rYBRpbAyXuNJ3K3gWavoJvp4lFW0eqb0MLKfJtmLFWIiRTYQUHLqA4mDUOttksyWbkTfKwAeqU0a +R0hJv9LupG9S0EE8mYbUyrRT4b9Ug6Pn1UtmNlXEa8ms+Rymz26LeL0zCyl3f6jQ4lH8aBSJhsQ1 +uwYjoezBQw4hvhrqBLECEK1uUMIgwhm1e+KgHXLYkzBJkfvPqpWSSlFaaH7FDNgkI1MCBa5Lc1RA +Uj6rODT4xFJJlooBSZElBVlVhwvse9QGmpDWgCLbfQBWgwioBtVob2+TwrFyOG60tbtjLaCOp8gp +R4AIHE1bntxMXLkOicAds2uJeART75O8LocM+ljsaRXzqhdsal4vKCBsp0QC7jVFnwQ2TxgYdNfE +chdwJdL/wP5hMxCgmJxkUuOcN7bwExxEJ8eXu23gY8I/WOxGnBwlSbYoo4kcl0OnFjecHc42tiaX +9dAlCfwxp+bpDSsIYMVoySUQlBqE1hBVPyTYGnqU8638JGw4gQ7qTwG4Fc+ne5xXav0efeE88Fwy +Gpc+5fXdrW/YZVerpz9wC8zqgBzNPHxUG09CxK374luIMaSiwUcUUyAPdYa2DpSKmezjx+RNffM/ +2Y1vigMiHtvy4h3wMvECb7S8YHvM325O5fGryDWbQztXs1Fkk3rj8oaezE/3GJMpyqV3xiIvvhCH +wnKUN/UGrt3qDAJJKNghKCzn7om7Lxny/ekL7WEPMomwL21VtaLla2AmvmC1rbcDliFUSmndUGn9 ++qozkNekY3NKafXKrtdeGe7vIGZfto99vYme11wmxYP4A25UZMdKvEPG2RkTgr5x/tWdvX/0nG3/ ++ufuiB+j9nVEu16+M7xlBXQG/m178YVOZDnqCRt4pa271q1+y/PaAn3TC6cWnhjmC7We+LT3xTvC +Q12WYbFrbeuutY31BJev9dKh/tddjmCsNcN9if7XXAr2OPyJf2PbV/ppnHQJoAYfeNlF8U19gy+5 +8GV3fWTL267nmw3LpALFElzdhnjz3CPHNq3r27opFgmr/b3axi2u1Zd3rP+Lq9d97i/CXW5Vycmt +waFXXpIY7MBm6N61ru95m3Hklq39g6+8xBOQheQO9iYSG3tP37a7NJWObeoGpCR7dBpngp83cMOO +aP1aOnetxVM4nSJ7S+ktCBSq6bNV4Au6qquupmWgzOWhh1dfsW3ggnWinLLkNfKt1f6rN229ekdv +S7DXJ8VRJJBRXGhBUQlUPGvDfTdKvvCmTduUmowBSI5SxWXapPZ1bRcjxp5IZAY39iBX6W7ZEJPb +jYVC1qwErtwUvmC1hkyzZqJrhlo0oSCC4SB4WNKFosvnS3QkJNmFdvPo+qSXs7GEVCwVXBLw/Q5/ +JIgCQZe7Eo/Zo1EIY1AEOFxeV4W8alRhcCADwQfyCCqxuD8DKlZa95GFeduJY9nR08ilwT8BZU4l +FENTe+LuRmIc4hGqjoEyIrAlcoP0aHhuTL7GriNPHyX8MAf0L3Wi4ie+7EDUNw9tTNWfiEdSExBR +AChsKWHYCL+Fm79wfMHih7O8lgYhQJMIIHeVzEUcChTkeK0YXsMGL89eNHCxReC6wvDIpHxOK5k1 +xe0ueZwl3Iigy/TU0ErF4ZNgWc6n1FQqXbNLOabHAWgOvmNcwqoMINSPshZUX6IjFQCs7hBaUtth +Yvj1qlwy5IIRKtV8mt2tw7wCk59U9rk1r7PshkKgZlzQ/PBDqfUlQ34wTOoVZrOVEJgtg9vdQQzm +1PexCd5EtHpkuZIlSdgzyr5WjHxb3BmL2GQvAhYVVETDlDh11PjJ1/Xbvlx86tEyiHsCPgDfEVRF +bInvEc8qB6WF6LJ0H2sgoTb5bTZxGlHURo5vWbTTiqjyrV/2Uf2tevxT3K667ykOzvFU6yTidQNX +35Dni79pyJf6i7MOpvnNM37xf+Eb9vdQxHHJo8mZY1Eq7mn90fPW63refO3cDx8tHp94zic+WMgn +H3vf55J37O37mxe3ve4yfGtL5xWP3v7tg2/8Z7zedMu7IpsGNbPkKju6YoOnh/fsufbv8P6Kd76o +7XWXg5/GX/MnTwwffduXt972AdB2P/zDr4W2rmwJdmf1haNv+Ke2Gy+PXbu9MzY4enLv8Zd/ou0v +rku88vJEsHMuPVpdyAd6u4NyfOKOu/Jf+Rl2VwAdKq7Y6HZ60M4pZS44Pvc9TyrrqaJnUQFN9cy/ ++INYx+qUOhv98Bewq/MfexcWlPn335h1SS0feDX8li7vwET62MF3fCOz59iqd76k+9WXJ0JdJ+9/ +cN9f3HTh998b6usqzs49dsPHIIZoyYsl63QM/tX1Pa/dJVWkeKBzujz68NUf6X3dlX2vuTIR7JpN +jeizuWh/T8gfP/Td245+4vsQW/aIfMnX3x7qaO9KrFzduuOuI/9556Xva7SRCg11XfS5N3b1DqXU +mXtf8Smc4uofvB834IE3fyFzYKzr2q1b/+G12GmdPNRH3/7ljl0bVt64C+M8ft+DD73lpqt//L5I +X2d+dv7nL/xY70su3PqBl7f4u+YK45Ldt7p728FTD/7gsvevftWlm976Qoy8PbIi4IucXNiPxvO+ +Q8eiD/5Aveo5uW1XBJyeiNwxVhx1/t3HZvu2t73mupZQ79T8MEJysVCHVimlRifiK3qigbYj9z94 +959/8ZIP3zjwwh24ucPDe+5/7zde/NX3dsYHfvyJz0w/cPSKf3xjN1/LHa/4JK7lhh98ACL152/5 +wsKBca6powQRpV+AOwVxrbsWbg3/wVc/vKFr5513fq1j/crWAC2Db3/0HxemJtbu2nbN61+HmxuR +YtMThw586jMgieu+4fre5+5CAUY82DlTGk0f/9JFF12zoG1p8feNZn5erbbABL+8/2UPjdzqdJqh +6tH5J3dLW964rmPn/Xd+PboWy6wLx7//rZ9bGJvytgUMHzwB9DmSEZhHLJ3hzVCKBiK5iqqgCfD2 +7ZtPnxrVNcPl82P8CwtzPqfd43QhEquiBCjoKRYQ9YO7RyY1wRphg1UMhNHWrBkEjnV8dEbTHbmi +CZgYmjR5EK9DTydQBQTCqYyKjJkc8oEVFD0iCRwm6hspA0suAzsRZE6ISAmOj+QBt5RB3h06FtAz +WBAVRmITrQXgLsCDjs0sAApDZQaLaai6jObwKTxCSqWRl0gFgNQDjOE5ACRDpxKpAmtBNHBBlzRu +O0jkYShNoZpI3QEjwT5vhlT4XWg57VFMHcHTQMJvC0lZvVQuGoDjFkGI5wJTLFG5epGShFcMfE1B +7e2OgQ1v7OREyB/AaWSnIyL7cxT+RcUtUoGGt9XXNdQDUtXpUxPelBFRXWUg+HBHURnsdig+h+Z3 +AS8AZ9mtVIJKJVYoe8GiQPUkBGKi0iNGB6ABSCxkW9XvWrnKI8uObMZx4nhpYszUwAEv+nBYVgjN +B36Iume0Iunpda5dH5ueXCjr7mzans1UikWQNAC45wwHgyBIotYptWoBHbTAlocTEciN1CMjY0kN +UuiLHvUeVJSWIznKoTB+UQ+bseK03uH3KfDDTiiCuYs404YQxgs2S+gLIkFIBAXMjWPFvjl1w0+q +xRLvU40mn92ysegNopoTIxECbankF2p+MbXTnBRt0OA10PpnOwILyaXK5Xf8r2aiOEQTllXrc0yg +6bnsYjxtCF8NRi9YJa9oQydb8FMrJ2cDm3rbXnsZFYcTRaIHCAq86Hj9Fd51HdCLeO11yX0t680U +8XEHN/d1vOwyd1XKPHgYdqfUn4jsWhdxJyanj4YuX+Pwe/q7N4eleP+//knwhq3ih5VMqftdL42+ +9jKvW17ZvtXmdztXJPxSeFX7dqkzDnEQ/fBrPdds9XkC2Fqb+p/j0Ez3XJZa+BUJ8Kl88M8qiSg6 +DA62bS+95PkETkGPCymW27Wx5W9vhNdSOrWQ15MXr72h74+uXPuRP+j4w8sCUnhN50US+jj6JE9P +bH3vTqUeIhYTAgdu+5f+qvv1u8BwabjMVd3bq7OllW+6tucNV2KQgx1bHUHJt6o14Amv7tgh97ZA +8jjCvl3feY+vN1420A/xSKmc0yazDb3okj27vvNO8MZhnKvbd6z7qxeJE4U8SKkFoReJTM5pK56c +w1B3rr3hon95U//rL8c413Ze5An68XM0UMQ4i6fnN/2fl2z9yCvglfS3bcA4ged88vGfPfjXX8fR +2i9e6wzB63Ft6L0kXZhxwK32Q7YYyhvekt5+BbJiSs1Y0bo9rKvSi69v+aPr/FKoO77KBbrtqG+g +exMuLbS6HSHrrSueE1yRuOqf/rTvpdtA9bWiZb2WKnZcsKorMTiRPJE+NnXtLe8M8LUMte/Y8lc3 +CORBmK8Fhj2XZ8MpANoHQFj6F8+WzQNhKTE2d7Rnxxq0Xxrs3BzyxHGWgQtXX/OG12HlGGphqONC +KRHL2Zxr3v5Xvc/bBZ5RtKNa3ba9pilr1kA8OtqC/bPFk+jZW7G5or44AMs4r1Fxjp9IplKhiNQy +OXc0tmMIjCwDXZtDUlweaHf7ASVBWguxQJeua5IHDR6oNbGOXpBIjMH/qZRRJgRhlcsjaGoBQOBl +AtCB1eHxeUCTzTiaKqq5feB6gcIksDSBxFAkN9DfXTYUr78SjFR6gBjqqbW1lRPRShDlG/CUFeBm +Ab21GaqKgDWq9ODpkH6lsBcTKbGPKHw1zlKxpwjvkHiACPBMT4BB0TYK/SvByQ19goCyBn4GyHpm +eGJ4ivAXqU4f5eLWE0ejyn06LGcP6DVVF1npMEahcUEDxVMRzkW+3pLumHkwE2ByfUhuGC543lWf +290ZrEYcZUcJxaCyzw5CP5g9qJ+FsiWy4gqV/SPMHZIlHCeCkIzkUKpmsYJYuD6XzlCUEVVXXg9a +eeiqnk9nDV2LxMP+1pARcNaCbhsoh6CTYBYVdYdSdqIzDJpWeu2lkLsQcRfCzqLfoUrUUpoMV6JW +RVDWoRVtsxOV4wfLB/dqxw4qc1M4OF0FpoMNGTzh8qJUAwxZVVmuxuLVaIvRO1jdfKG0ZmOld0CL +tZR7eqXVA7GBwUCoxQSVYAGt7DKarmHWeClzeReC0cI/46JrRNQJSi+UzuJTpD3O+TxTf1g6r67W +ljlw+FNAmwXyZknMU0CTWIVaGRBKglgRiXMOg8fc/A0OLCx9nkd3LIUY/Y6rxeXDIxt02XNZ/+hl +v5i55YGRqQP5A6Oelij11JUMfSI19Nk/xmRPf/1eZ9myEhBK7X3H9Zjb4uOnqzkNBU+jcwcX7n0K +R/MNtrfH+71OL0Cthz/9rWNv+Y/A2p6ernWhaCtIQ4wUxVTh9ulHp2t5HYlF/DD/wEG7T6ouFAFs +EOMpn0ArGnUmdVI/cDLwhqvlwX6HYpS+erteU5OlacfhUTd6oKOov1ByF5TQ7Q8Gvv7D+WOPSW5Z +37ZG0auAqGaNtP+5W9pDfXO3P/Xwyz9RMLJRX0v4ggHgGqHCi3ouo8ylD5yKXrw66kpMzw/PP3Cg +eSpWve2Gtm3rjNn8nnd+EcslpczMPXDA6ZPKc3l0ghXfLBwcL+vqdHI4+dgRLLTNH/tDuS1aOjl/ +zys+CTMipyzMP74IUTEV/dSX7n3qo7cc3/eIV5Lbr1pPwAmpWjAzKDfc+tHXYKhTt+2995WfEkN1 ++txGqlTicc7vP5m4eFXElYB7N/XAoWB7vJxEOwgIIEo4oofG7S/5+NxjFLl96H3f+Ol1H9fnS+iB +nNEXfnnDJ46+4RPUyr59ddgobT/2FXEtsYWDvmqhNp9Csge+GhpUGTkNatLvjShTWSSfJ7MnZncP +m6qK93FzR+YOjt77VN9VW+DClh363BPDR798L1p2HONr6a5fS97MpI5PVqkeDTaUvYYCS/iLoNgE +4tRp79uyZkXHuki4FbExNHfHaLPGgrFw+oV//Cdwf7XpSThHc/nR4sTcihe8oGtonZrM/+zzX4HW +wIDV/PGO7mBeR2/qjpKupFP/Za/+BNm3kfQ+t7on88t/m/jRPe7Qyp62taFwK1JzepqWWcZcSB4d +loJudPtD+FeWkSuU4TMh1OyXAxALVJxNXCiG3ycVC8VqTQWJKzkbtko44PIii2RzGlpFKSFfafMH +AyCZwCs0qdfVWllFF19nRXOfPjGZT6WAVwWbqeRWwcSEzKmjWg5ItrhsD0hq3K8HJXxbBcmCo+qz +IWEHB44VpJUXIjYxq6BCAJ+opTNxVBALPqtDiibiTyhzXTfLellV8R9KKmIVUaSVXpO7QcekJyUt +OSTLGpf6eVjHtzjE2Ue0ElXsoJCkxJHgK1H8lz7Gv/B7EUiWcT1VM9IeRUtwarbokCuOyFzKPjlZ +rSqyWwXXlB0WQG5egV2B7CvOBkfc65c86FcMLnM4V14vmmnq7lpKVdJqiZiOdVtudD55bEqbyaCt +sinhHjjKqJNB7zEZmN4a9Ke/UJZV04Pkq+TMBtypiCcd92RinkzQWZQdJsB4LtxKiqjmM7bJsero +qer0VBX8O+RYwvukKxY4AbpUgeJBfrK1w9a9wh5t13uGbAPrbNt3Bi/ZFVu3PhIOe2bmzeFRZXq+ +oiu0cHFLaQLReNsO+j1O7hNHOS1mQZHKPlsjCcMJGZE7/hUfy3N7QtXyoxF6ZaUoioBENH7xITjz +Gm+RKj3/KITXyLNSRzixxl98EufEUvWxTHc8g7P8ihPxW/z6+XONywZTOjz5xCXvO/G+b1k3pmrr +ffcNzrZA/r5j8z98XNwS2EtDn/4jeGMzX733yJu/mL3/CBzBmdxI/pETuH+lfaOzpfGL17x4xTtf +XHpqrLhvNHLxUMzXNpkePvmOrznDMlkmyeLYu75RfOjoiq5NaKSk7j4189kfZn74YFukb3hyd/G7 +D1dOzna2DU2kjtUmkp4rN2Jzl791j112JaS2KbSw33PYrhUdmgJiE2QJvLf+t+uBJ50/+NnI1O6e +yJCyczOQKqBw7PINJNNT0zc/AH8B6FCmIbGd+NyPRv7j523BvvGZI6mHD7dcur6va8O8OpXlpKB4 +BFZ39b7kMoD2j/7Dd70dSEJ1TM+cSD1x4vDf3zJ+833tkb7jE0+Mf/We4pHJ7o4148ljCw8fC27p +a9uxFuwgT33mh9GNva2+7sn5E/OPH2ue3qNfvHP8x08c+defHh/f3Rsd6r5+G1wHSLyO52zsDA9i +qKduvh/fF0OFGXH033/eGuwbmzky99CRjsvWr+jasKBOJR8ffvhdXz3+zfs74gNTCyePff2+0Tv2 +NM4CJ9UoqN445UQzByb02bQNLZ52XQbswepj3y94w+1yRyY7HE6fCj5yh/74vs7E4Imx3bs/9l2X +T/K5A9PJk6d+/FhvfM3IzOHTd+5+8H3fnrr/yMruzdO5kdknhoOrWnHriqfmcZZ9X7zz1K1P7OVr +6YkOrXjhdnEthVkkmbhmnxngSEeiTlGCdLN3b6FlMJEZ/tFNX/YG/FgGeqm4adO6uL+1pGY8XV02 +p+vAsfsev+1ng7sux+Q/9B/f9UUjcW/HdHq4v8dZAj6iFuQUXDoEXIenrT04NFsYKU8gbLygjDlD +qwbFMtv3wa+AnwXHL2eKQF64Q04n0qZuVObBFQL1GWGNTECJFOgwhxcUoZope0HtZgD/zJKnppdL +AQT0dCM5k8qmNBNWCDHQowUjJCW4FGAhQrlRZ2LEVCfHp9ChHm0zcMucVIDiMw0vHGcwpnpdkPM6 +SGCA1JHBHwYeO+gxzBO39qWImUg9UmLWekHgDmYBtFw90mpW70ZumEnaEarEoAp7SjOxZ2khIhvf +tNxQC8FMR+SkYwOqyu9YqUerMIQcRyZ4olbh1BdcAvtNyagtGGaqasYG2jxR70I+i04d6LWcWigC +tNrm8DtTlWK6ipY2YCB2u/2oMwRpnsNjAzjcH/TJfipzBDEbsHRe2ZM3dMB5JbTkoA4ezkCmGsrX +wkWbP1v2FAjGY/idRqvs6Iz4o4GQzRUumtFCNaTXPADo+t1G3Kt2+nP9wWx/INPhzsdNPWjWZKrq +B+ZGVRzg+dAACWacGk8xecXUtpkWIoUKCNOE3tcRb2cP2kYbUsgWirtj7V6Au0+fmjt0ZC6TrpUN +j+TwMYAHLZnpXnCekkOgHLqmcDjRNlO80mYadW0h8sL0OFN/NL/TnGU8hyIQXmDjYQneRjEGZVtZ +FDeSmawaLbwlU8Cd9/n7FQv9jStNQNIIjNb8XBZiPbd5AeaLlldcWE0qw+/7NmJ3WB1gZPZfuMK9 +IlbaOz55010YcfSydX5PGMZ06dgkdrQyMrdw557DE49csuYlqz/3BnfI62wNgEpUPTKJm+Gx+1Du +lb77KUTtgzvXih8aIzOApIYv2dgaXZEy5tS7n5IvWR/wRgDodraGE75OtaK4Do+6tq/uaduQNlL2 +g8Og53JWTJQSE8MHJU+M2oHjxZri80VqV22nRBcqJts3gvNRn82gCQbWkGaUKvMKcl8d1+zobF2V +NZEmy7Zeuy0gRdBntTSRZjwm5Jdt1Z9dv2HgcgTUUo8c733VroHOzZlKMvPkSUiUtl2b22L9aWN+ +8vsPt169BYOEd4sejYOvv3pN1wW6Xcs8dbr36m1wX7JmMr1/tDlUIl4vPHpCsyt+OdL38p2YhNyB +ybYL1gzwUOGxeVutoRrzSu81O7paV+XMlDKd6X4BjdMoacWxJCR7z64NsWDbrDJ68D/ussLjAj5n +t7VcuDLoiM1mxkZ/sRd2f/cfvWht3+VGtRyaPTXetqsrvnleW/CNnM7OV7wbNiCnmDbn8xMZVIfO +ZkeHb328/5ptreEepZqff/I0jtbNNxchR0yot+ZL5qfG7if3WkRi4KqqdrRkiqx6+cW4lvTBCWQW +wbxQk9CgD3WKgJ4iHGXTAN2IBX3RIJbBzPiEKtcQmMyryeLwwd7NF6/tulRyyXB0jt5zz48+/o9D +l1+2qY8mf2LfyPrnXjrYtjlrJBOxVEEN+t0y5qJcOx6LJCrmOr8URdYwc3SivFAGu6crQsusODyJ +lvaQbjj+/FMHQy1BT8ADRhsPfBujoqogbiORVi6DWJwIQqFaVMUZAA0dOgiiuMaFun4VtAkoyZMj +sb4Nq11hdF/iiiIDRY3I7aFYAe2hwI+KxvfVgZXd0Lh0qCp4J4C1JTcO6T8EQdUqGHVs5ZqkaNDr +DlVDi0fEaIEwgWsmwpek89jVoy1KjaLI4YNDCemOfJoASaMhDFVPglIU0QoESyGmoSARccVZgWKl +liXMecZxV4qI1mOnnDsEKJVpcPkp2kFyo3umMScwNkfQONNIsWS0rgHhd8XhRByhfyC+cn2rFje8 +Q3FvdzBdyui66vJgw+l2pdDurq1121ZgcOUK0DaAAXn9HhR+Ej8s8ThiGtTW1jBluMpVTdELmuYL ++MBDBMY9ZFFtWqWWr7nzDp/ikvEsOb1oEaci8on2mb5aRDJ8mImqs1zxl4yAYrooKwzSR6gktGOx +mSE3LI6KDy0DqOaHo8gC8WtBdJiFgzpakWtHE0mzGwjZV66MDAzE43GUNSM8DYgNiDEUtYg8o8Pn +RnS67HHifoB8RGOkNdkeIh8IAUKnwsKgwn6rKoQ7YQjbhr5C3yIrRIQlrQcDWQXdEfEhUZLZKsMQ +FD9CuVmHYSXK0VHxC/J7BeO8JbbpNe09jvtb3JYcURfULQLCzgB6On0dlVUfAP/EeuIkZIM1ns15 +Nss3XaobeIh13qbFF79xnfVbOuBZco3NdQhnL0LksZXT+XJF90gI/tiO/MV/VBTdFQ8iTAqQHvJz +UJbH3/YVYDektogj5MU30c535Udfs+azb9z4tbeNf/7WseN7qcdeazh81bqALTifGZu747HQzjV9 +resmFo7gJx3vuN4R9OCHZjLX9t5XuAJeaQiEUm5bXkdMxhb0IHRkTC94XrQzEGxxpRVHX7w22Bn0 +xNAAuJpXkI0R7LvY/dQPDX6AYXMmKfHpaAnRBZioBY4QG3DA49nYFbSF956+e/Sb92LVBTb3UMZ0 +Orf2gzdu6L0MsUd1Prvmg6+2y4j9kZ8d2bZCdgf10XTr1Zui/b0RX6tZ0tb8zStCG/uCG7oxSDMD +cILTFfZhkIWRmXUfvDG6Y2VITmgT2Y5d61uvXEfqVjNW/eFV4fU9y241kj36DLEleNsgOGyHvniH +3B0TQ3UFfZFNvRjqntN3H/vPe8NbaJzadG7rh+vjnMtu/fCrkHcMb+hCOKk4kgRbujh+Yxl3XbYB +ccvp9OmZx04AKRHcRNfiTWWTiW2m3I1rcZnl8a2vykf7bT0tmLzCSDKyrhtuNNzT5IGRwEALZGxp +Or3mD6/0d0ZdfHOV+exF738FbILp1Gkp6n/53X/XcekQnbR+LT6+lsl9x6/5/JvbQAiAijPIUOhI +EH77UNvtaFuzwucIzubGDux/bNPm9f2JdRPpoxDEUksH5jNdmP35330c/PCv/fzHOjesxIALU+ne +Hevbe2jyUUM4kuot6ENxf+98aQQFiKryJ2Y1BIlgzk+7eq7c8LGPd7zoCr8jOJ8dG3/g8cRFa1a0 +rBtPHql4nV3PvTixYXDLG24M9XTkcqjbN1T0R4L7gkBfzSyVUIMHFxDxMQ/kPsrQGdoI58zw+QOB +eCCxIt7aH3X7DXiMyFehsSIcmBLAlez1QfiGQoFyGfFHKvpm2hkKz1LIFQvT4VGr7lzZMZdFta8k +y0EkOxGk4haJpLcEdJRrw9mlY/lN8V2Cz4g6W6t+iGCoOhxUy0GkShuqwaBvWkVFTBEu/mT9SipW +VPPRk/KapINFiFWcG6FX2jXsAhvQ5RxQRelTWYG9UFFtui1o61yd6F/TFu8CBDeZn8uHvDJQKSVE +cjE+EwZeQa6a4NzwBcBKhTY34ANAFTGaiiFq4Mwk50N+8ADZFRUk/ijmh1eJDiawCagbNHqN+iou +L6iZCma1WHGiP5hil/KGE/mOioFIdKUzZMS8JloFaOVAQcfTXdSdGkpwVCmlBHKmXweJPhS/4JeA +SkA5DCqoAHiqIZYPNDCZJ1SjUvW4wZXj6R9oX7O2Lxb3IFaRS+dwF/BjkPyUNUwmwYgQtCbXneLP +VBfGHSvhI2NiGJZMhdTEfkR6kRPEdPeY3KhRfFgPalpeutiW9TxkUwEXfy7UWD1gygdbilatb2t6 +3yqArR/MOqlQ2YsxV/pF4yAWQke8tfhYlsdcLElrzpg2UpBLZBdpZOsEFgr399nx/JUDqo25KI3O +ZvIzgIqPfupW5TiB9cUDogqBo8Nv+aJZgE1s86/vjrlb0rlplGHEXrytZdeW7u3b4zfs0CYX5ouT +WJy+3taOlsGZ9Enl0ERo/YpYoEN16tEXXxB95c6Y1JLJTTu7or6rN3g3o2DDDVBP+eCoq78l4oql +c1OOVZ2BtvaV8U2VmGy8C4DMbqgxIPOwkRk8QHuc0jc1F4BsxbJdmVhgXIbdGM9m7jueVuYS3vbe +P3nOmr95VTXknDt5cua7j8r9CY/NC6fE3RcJbOlJZ2bmUqPyqrb2F2/3dkSwPlBt4vC6I74W70Bi +/adej90GzekMe1teuLnj+gtAtYxBpp84ERjqCDmiC5nJ0OYVbS/YivonRPN8vbHtn/kjQAgxTkfA +3f+GK3qvv2CZasSfymRSjFMfz6aePAV4KoYa97avetNztrz3FbWQc+bkyeQTw14ep7cvEtlqjTOw +uq3npdvbr1rntnsyhdmJ+/afeXD4eRi8AfKtkaTNH0LxCf7UE/H9W18KbYBrUV3SVO8q59XPddvc +oIyZfOBA7851HfH+gi2nZkqyLYgj+/vim99+3cqXXBh1t6Ry0/7eeHRdV2u0B7Rwq16xU4qgOa0A +49kKUylxLcpEdv0rr3jpq9/6gk++BeANG2rLvLDzgbAk1r+2we7u+OBU9qQnNzPUvTImd6g1Lbhx +BxJ0AU8E5ETP/8B7B6/YSc6XzYEBh3oSV7/ntRBQGDAQN0p5pVJZEfN1lXSUSWyogI7GLpVN1d7a +Fdi23VauBDq7EByezpxMj4y2rh2M+Ts0u957+YVtWzetetn1z7v6Tdve+GqUbQDiSbEy0iw1OJFY +Q4ViyS3BmwQRqQpN7XL7oHe8AY9eKx8dnjwxfAz6FzV94DtTUfSGYnTEA8FT4UeQ3h6LxnM5JE2p +OhyYGUhaTAsjUSAzUHvryabNTBoOa6RclnT0Na64fG6/i5OZDU+uLubIyOc5XfyISzgsL1AUbwCA +g2EjAcmdGCzFt7T4dTFOQagR8YRCE82RqCiO0KpMblFD7ygXMhKENCGhS7hbtCTRaz40/LZ5jk3M +H5qYPTG9cBxY2EzJ4/OF/CGYFXBY6etwn2Eig50KEQCPhBp6nAZEpih+h5tYKbuS83m1pCC3J6MQ +Mi6HWmIO2auC+N5DFAFYIP6KLWjaZbXiQihbRfGGLVwEWUO1tlDU0GcL9pmnaqILNHiAy9WIUvUC +IFwm1x7tlOOpcmJek/Nod0yFnChzRG0u+LCg4nAxFM5Hmtthj8menpZYb0dbeyIOHXf0+PQTT8zt +fTI7ccoozAe0XNgsRBdmawtJI51HUADo18Usn3DJhPIRrOUEJxZNIq2uGPXeGByFF88lVuqZm/OM +d86fEzzbQQhxwyFTS1Ux7qbxaK4eeQZDONdXhI4Uj+ZTNMLCzyq1+msO6jfz82evGsvTmbHi8NxD +++a++3BjLJpDf+Torcfe/O8a6IMFd40HHIe2gD+KRTXygVuUXAYxtJ533xC4dNXp2X3jn/lJdOd6 +pQL+aU0/OWvmlIKeDroj1enc/E0/ww/9/MPsp2+FQpIk78jcAX3vCbSRglGPkCPwasVcci43sqn7 +CtjkycKkahRqp0epZw3nSpA2QlmxXvXkVftMupw+SSo8ZoufuOlnYz98aO+pe9pbV/a/4ipnwrew +79hTb/w37iVFkhFfA1t5eTJTLGdkOYwxHHrPt4onpynmhODPwYnT8wd7wqthBKAkjQRJ1b5w5/7s +/tOS5Ds9u3/+ocMVpYxBBjH+cnXf276sjaWm86fXtl9AtioSGpR6sM/+bP/hz9565p3MjRLPeNQW +P3TTHXhx8gcP7jl1T0fryqFXXelKeGf3HXvgjTcRv1Z9nEC6Nsb55N9825sIoeXCSPJIcv9o88Eh +FN0BrzsmQ3nPP3kKO1jN59IHx0cXDuFaqC0CXwtCKsqD+wtHxiSvfyJ9fG7vqQQI0gqjCOeqWWSX +ym6g8M3aw3/zzdypWdwjusaaDQFnTD7UWFWp/OJPb5p5+LgwONNjdC0RW/yxr/1Uy5a8Lj+s64of +YrJqgDTJWdMdVU0yV23ZoJpoA6UlivM2pUjLQIqomWzq1OjRmSfWd+70evzF6fT33vrJheMTpxcO +9oZX4+7CMqMBExbgWFCeKRl5DABgyun7b8WQZCnksXm0kdlD7/xnbzymmLTMagCvlsvi+Ih8PPLF +L2t5GhXliDwuaEcGm1BYkcJjKLmrIfkUgaOH5YTKAbgskVgQhQupfHbjloFAIJxOF9RCTUdMv1gr +FkFcjxK6mk6Pcnt7G2Ci1AWb41diR+BPFL6DtTBfLKczpqbbweY2O5uTnMB0OgopYDat8CYjAgHN +sdpC0bpkKhyibavHRRmhCkeLnvBhEBpBv23yuyhyi/QctzPG04rH4h3L74HvSESC2CHsX5LGRQq7 +7j4Kr9GBqAJyhNhCJko3WdESXYwM0LMtb1PnDLRjRp7WhdwbSAvK9mKuBC0rSR4dSWVHBQ2/0sBv +5eAuFrDuUJ4IrYisCxH6mxXQuJtA6IacchiWEwLX1PJMU0GXU0VjKM67IodQcRhVT9nu0x0hzZEo +ONrS1cgc+jfmzbyCQslKwGUGEKK3SVrVXzR9Od2rYShQ6lV/2RZVbWHdJhs2dJTk9lXElENk4g4w +DdiDshf3F71Q5lOZ4ZHxk2MzuWLN0NGtzj4z5jx+wDhx0Jyf8I2P1sYnaOnAoGlWMSJ1R0hU+Iuo +qKbDIttI4W+OsjLVHr1ejEnW66KX7PgzUyrLBILlYy4mhs8Sy1t2EEIVMT2ueC6lJGk4gr8ZFUJK +saEgRayDRywegmHgHM9np/t/Y0N/+gM5LzmD2egZnhUrw9eVGH7vtxpM4p62cPQFm4/+2b8X9401 +DgIN6uwJju3ffepvb84/fiJ9/6HS/Lxb8uafODH+uZ/k7jsM1PbCyRPpn+wujyyUJ5JqS3Xuzkem +P/YD89SsvTswfWhv6rM/Ng6O2YrI3Culg8erDx+zZwu1mC91/GDl0CnH2t4FdWZ+8ojzlru8mzZ6 +3f7cI/e5To1jeLBd4WOUax6KWeWM2Wx5xdtfEu3sGTu9/8QnfqyMz889crDgzmszqbFv/GL4U7ci +wgkL0EiXpJbA5MyxUzfdOfyPt/vWtMyeOn7og/+VeWxYiDbs2dlf7FPD5sjDjx3+25vl/tbTx/Ye ++fvvAIBjZIq6Upx5/NDcHXtxXa7WwNi+vU/99VeLJ2cWnhwuhJRjDzy4/x++74kGh/c+su8fvnfq +2/eTpDvjsemdL4l19oyc2rf/Yz/AF5TJ9PQjRwqufGk6dfxr9x785I9N1Sini56WwMT0scM33Xnw +07f517ZMnzr+5Pv/a+HRYSOvFNKpqYcPjd35FC3P+vEx+K4r1m174bUzmdFHPv1fyEriWuRTU7V4 +5dgvH9j/wW8FBztGhvee/Kcf7P/SPfMnFrxuZ/HA6OidT86cnpk6evTwt36ZGZ6ROyJToycefN9/ +zj45XJzOyL2h4ad2AwQ7+rOnjET16Pfve/S93yqMLXCbI9JaF779xYmOnpMj+x7+9x+tvmYHEMOn +9x08/NCT8EYMYoIhshsnbA90ZsmOqcf3mdMTycmJvM92bPeDD/7nLZNPHkmX5kfGDj9+y22Pf/1W +I6OefuCQLpcPPfLIgX/+btuqrvHpPQsnfuRvOR5LmDOpYn529+kffzP56AmHXZ5cODHynZ9O3XKH +qRcLqbm5uVNz+/Yr2XR6ZrocqI4++fj+7/4YtfDdO7bmHNnJw8fGd4ObsIb4HjhRqLeZ04l4KgKA +Xd1t+Xy+pOihSMTuqSVTc0NrVra1t4Gg/vChccigio5aO2g7lyBQJV57Ir23x6PhsdFZhP8pmgot +xxWxBIGhphEuRa3mAGcGM4Ek9fV1RiLB8YkUWjX50eqDBDnmj8miOX1ERjn9YxWQc0QVAoiSTVak +i+NtjVp26EuoH05RUbiNvsvyyZKTHPLjhFddU4pcFeN9GhUdNFqKO4raR7hNdpVZz2zFip7UfA44 +9HImXayAf0fVfV4pEPSrqkaKGBFLsKaG0JoytP6CDTsu2ohRpGAIkE9LlwbnDsUyckTOqQpcdR09 +zxQ0tqqhEQ3UChhYUHAD/UnTBC4EZAuZYpZbUMFLZ14emBigv3I7dOLwqPl1eJmgG6pJFVDs0Ne8 +VbuPKOiBbAIwiNwoVo2ChoZ8KoyzpGo5RUedC7V3RTQcDmXVpqk1pVhRFNs86kLL/vk5c3QUJMJ2 +vcxJOp5wIQioJSa3kwYOlilSF4OOVhhTFFQsfdKkN3tYoqdQA0FDbAXkopPGYX4AQTHJC8H6ktBE +SwTH0iPQN5l0RwRU6adCnzYinlx62zSMxoCWuJdNkqnhB4r3rK81vyvWqDXIxnmttbsYyG2O2C47 +6DNUPf9zX2vmULW/m+quf2MP+HZwquoqZPGwhCJfipgSLeMbACriWrKiErTs+CWtY2I0JbAX/gNz +m7pOgZiRAG1Ykr1x7wdeb6IQDKUdN/+8tqYn8a6/hAGtfOrzzgMnkOiBdNBNR8l0zubL83nDe+HQ +ppvehPTDnjf9U37fGFvx7G/wg/PkhFxoLmIVdTlWmH/xaqzs3dPPGq9IytTQOhEHIeggsIIk7XgH +nU0jigO2XrrmopvejHHe98Z/yhwYr59lSfktG6zLd8ey8UA8AZbBV8W7mb2WKz//5otf9tJTE/u+ +d/n74IKgq8bawZ7RkbHZpBIAKTt4KQFtBGOqw6UiQea0r13VB3zfyfHZVEkHoh6zD9Oei5c5Rc8C +q3nGRCQHHwMsgkBd+6VDV//LmwE4vPWdn7vkL16eWNutpAr/+defIsok6olQdcgut+xoSdg6/Y4E +cJ6qkVS0DJzHsqOqOGp46iiBQKUegowoQoMr44RTpZWBhqit6Y4kgnYtmI3vCqx8Xn8g4Jo+MTe3 +L5c7rNRmXVVQs5AIqaJVIcLrFRmwLskhS6hlKIEezgWsDDLBlU2vf22wt72cU+78h381NA2JMPDI +QLTDSUWuCUUQwCMOrl55emRSVSFApbSS2bC176KLtz/66IE9j46iPzZYeBEqhPOAvowoASEXDIlU +twsM2rFQaHxs1A/sKer7EMmgGutaiSo9MJN+0I2lMwpqLVoTCZ/XMz0zDxnd35dwe22KIwveMtoD +xIxI2U3upMuNd4mJDggQKgNgrIPFzY5Zp68joI9GbC4UpdfAe95Yv2IB8KLjbCLTmnGWkeMXdaVI +b8Ld5HIOUuCcwoJWADCIMp01Vx5VmCVnqGhzg/og5raHpKKp2/3gHUWAxA86ALVYdHqQKCTVBccQ +BRTR9kgsHp2eTOWypXhbhLLvxRIY9Owuvbu3dWIyGYvFTNzRnOqqSVCxVBKgmwHTHlJMfx5cR040 +wkDSD3S0lPQlGVtFUVdJtqsBpy4j/ODwVB2hQtWvcss3TDXwpgixGnZvmSwR8pOJwp0EN5kCvJNI +3yOtywlWPHBvMM2oT0TZBbd8rbl8xEPe0R73Sv4jxyaRl6kQz6/IEnBTDyZHxbe5pxVFqTjBSztA +OExiPzZEXH17Ms+tUH/CvGnQydO3+T0K5NNJGA7F98m6Z1bikF1RDtPXJYkQMuLBuUfRttJ6l07H +4OnG18j2aPpTiD8e82IcUTiAi/KnWZ9bnd2XxmmbBNDiiZ5e0C2TV78LfzaX/D97r/GsVwIPctlS +WNSIdbNHLBdhdtX/pWZEeE0KktHh9JoDL1QcRO0YuRUVdw+g5lMU1q+5X7Sze/suZWKs+omv03p8 +2409gxdOp05I37ydWI8RSq06lUhE27VVjQSda3s2fOK10EvjX7t79vbdPAZqsrcYKrd2Da2QxUvj +FSzWB7X7EKuHkPM8clIC/GVaV1bEjEMJZFzi63xwKzdNVWS89JrmZ9F+8nXG+152sb87HtnYt/0T +r8PIhr96z8RPUXTR+M7iQn+Ga4ghaSQOQv0tXVesBWFb+0WrB1+9M6+lHnj3V/PjScAse1d0zC3M +T80VfU4v6uKBFyEER80GmhboNqgnJJHQ1SuRSFTLKH6nPBIwfWKK6qYOzQpvQctK9YMO52UXAzoU +29h7xcfoWvbdcs/o4wd3/vlL0qOz3/nbz1d08EkTPNUGnu+Isy3q7Qq6wmiHUNTn8uVkGaBNZ7UI +9w3eGISWg6AgOjQtCgBQuOKgojrT7G2RW6l8oqS4zMg6OToQKSzk5w4vFE8prqTLVQL3mGB7RBU5 +YD6IcyOMTAgOdHJAGQbqMZA9RIh0zUuuLc0mH//SzRqgMZBtlJqyB+Qg7C8oZAT20BQCkiibLWJm +ZNl/4WVDK/sHbvnWf4+P5Pxo5egApAMkwfRD1PyBSAVWBTVGrNnaWiKgUEFhKIQpkDgU6qTaAGK5 +sMGlQTkBCsYp0SoBAok2vVjhPR1x5OrQ3dfnF7AdVLkwkQbNsQAl4hVnrERsjwW9cA4IIcISlzw8 +SFfy3URnRctrJELiRhEIwyotdA+D1BjhyEhKQUTHwh2MMkI2Y+SoeHBqTiltC2Vt8YozInuRrCsB +T+uyg4UNuzUQ8KslDTgU1HVQl2wYphqCo8CBadPTCzW42zZXSSmglQZy7SCogEcWCUfVAvIOVWhT +bGmUMxKRTRWGEfB9TjAiS1Wbz7R5qKJeDI40HBS2H0SThh01G+DVRSssqDPd5zSCkgYLBG3XjCqc +SC+jDKhUEZFyIqwReoMdZQFBYc+Ge2ZT308mXaYgPS4ejit6TOtloJFrqA/K5wogioccIsYcXu0U +mOXek8x3I8xEYWywLOBTWf8Kp49PXd+5TV6jUHbWNl/8XDiJwt+rf0N8jwGvrEHrgscSKXW4jfW1 +5uCvWCk0hEWRQ39bJ+fDWlq06QuNYfF74rvNmlIMYFEnN2nnhlJ+hrLqd+VrS7zGd9Xdpl9ndCLX +2tCCwmgS21a4gGTvsflDf9MmF4YNfYQfEgG2KB2FDiS7mJhH7GhKQ2EKaptIXqKgXSJWcYtB2HH9 +BZ2veGXKWLCdnqp2xqPx7mIhbfvMfzieOoYMVLniUCqS/LX3rFy1M59fAEdmvpqb+tZDJz//k+Yr +PYf3Jr5GAgI5BAgPipzAciQwIMxqjIoYxLFpSVGTJQ68PLVtYi8NIhUyQgPZM/VAQFAHlNAEwxC6 +URy22UC77q4Prx26JMfjLFRzI998+OA/LhnnmXen2U48670j0xJSW5Zu+OWHe0NDRg2kYUZSmxn+ +7iOPf/L7Xqejqy0Bi3x8egZCHVMLa5Yb4i6plyKuZ7PS09IS8wVmZmbgUwp7U3T/E52RqOUWGw+M +3Xa87L8/uG5oZza/oPO1HPr+Q498/faKxwz0xdKZNLXsRRIXNOcRM9Ti6I66uiBWwc2gVtIgLyrb +KprNoTmIew26hBqjIPIFID21yUU3F5R6AGrcF3WtbvNVIYk9jmyb0XZtLLEhmD2SLR0uSDNOT95T +1hzwmJAWpXAFMS47EelUDd3tcSHgB8ZsyGGgSAGh8q9oLeRyhtumoxsXakYAciyWkNZWdQ2LElHB +glryy/5iLpsIRXdctD7aZ7/rjr2njmHODJRLQHWDUQ45bxQIeHxEb425RG4QDDFbt6w7fvQ0YB+o +6CgVNYmq+hDJKIejYdkfQB+KVLqUL6DTI36iBPyeWDSgFBX08QpGZZcfbSnBtgBKeNgkptdLK0/E +A+vN2EBdKvilWYSydGfZRS3DxAary09rdXC4AolFasUpdAM1qOKdS/+KdCPX6wnViKIAg5wq0MCh +zsFdK1VdSTORd0ZdHsPp0EKunLOMYCZ0EdyrjrY4lnq2WCDmujIqQmyI+aNIHnsFY4czXrLpBtPn +gMIfiXA4bYWC1tPboSn5bCYTigURPIVmRFGIlgNdAjGsBytOOVuJl2rBqgNWjQ8IbdYKXKxAbRph +f8ItVbzOrFwtJez2toiiaLaZfIuCFCPOaoI6DqAplJtgZzIHO8NGrWgkceEwDtTajdYLUeNAMV2S +QoDo9q/oymZymZwKZxD8CiIcS32mONpN8qFpKy76ZcInZHXBKnPRj1y6WwVbm9BI9BCZwGW7W4gp +S1gxO3xDJQn4qziI5RM3OWpCMguTiW5yowKDjKnFM1reKqcGG9rvzLDUGdLS4tU5h+44r5g6rwT+ +dRTTr/rbJV7jTo7v/ZqPZvuo4RVZxo7wDy3Dg15YLeWsfuWcPOHYKesY5oPnNnXQguJN8hdJLzGx +JMUuqN0MQadPT+m94UpLsJIIItVjJJOV79xue+wAUHVa1ZEr21JqxXvxhnKgVjKKipo78K6vTX5v +ETH0DC8ZZ0YJAYaBVjVQbxgUCRBezERHTd4toBlE68wGL3GMyH70SHYCjAGlUnePre5srBkb1tWi +mdV11eZKsIaqO4zz8Xd8Y+S7jy5xXs861iVG2tmvhjaDYa582cVw0Yp6Jq+mH/ngdw7+573YMB2J +qOyXR6dmVAOynMbEYR7Lj188HMyUGrhdNPxE9sm4Jqga2uq0ia1MFy8gYUzT/e29aqO4lpKWu/tD +3zjw84dRgmb6YYaXcQ9hdedlsxSrxlsdQ2G5G4DBggHy0XzZVtJRG24zdebQRH1pGcXjBIyHYQXv +SdcBLERjWZtsNwd7Yig4B8ezgXoErx3dAUHynNmnOFKuWpGcJYD2DQcAJBxQZaGClBNWDuXFUVCE +TiRAQkFJIDSnKCj4RjNhIG/UctnlQTVALVcqYtl5vF6g87FcQ8FQLl3w+XzDp8cODo+2tXUOoAvx +/BT64uKqoQdh5jEmk+VaxQZyIdhDHe2dp05NGYbdRMcVpyQHQ4DfEJUN24Ygk8MSh4MI4Aka2be3 +xIyylktng+hhj75hkhPk4OBEZcFWRUVAIz4hAi0NO5/lMgUnhJxmkWnxaVo3keQiuxqWt9R4wa4E +C0JCzlrdqYTXSO+TPcdhOYduj5oeeb4SSVfb0c0QNE92I+euKlAfkhsWAkjh0KV4YSGNjB2QS16v +T4HRQo66TymUgnan7POVMfWo9Qy6AJIh86ZW0TQ9FvV77AgiGA4JtVhl4kBwA/2NG47YtT0UD8Ml +xKKAY4qEIS6SGiaTzezEMkDAB2YWli45hRWwp+I2EEGDItXyblvBY1MhPfAdaiqFAk2KSArEZsNl +Y/Y8tlN5K1n7id+os8eQA+kBxsZuLxYVmllSitwXCHfPkpyWeq37T0u35fk3qSU7G+e3Ntc5f2g5 +f9YNbujJ5stY/Kzp2ha9RlaZi1e86M/Wfc9nJh7Fqc851vPPwDM71W/lW795r1EMu9lrZHFEEy6I +5/lTKyErjCwGOtPy4s6t6DZFRhi8Q2RT6B2sP6qqpiAG5AJ14qY0Iy1NoNmoPonqnAnr6UQLvI0r +HWMz9mQa9jVkEGqos6qZzJezpYparsrreyHz1LEFJgAWRb9P+2gYeo1vYPBwB4NoNuRypbNZGjkj +LHBdeJ9kNsqAyyg3phWC8AtyD8EgyMbQrAENV4VZQLFbSF4C74sE5xleI9YsChyrwL6OLNRX21lw +aM3jrou1c10LZz7AuGyLrO9GFDF7cg7DhkoIB8GI5ktlsooK5wMzjJbOBLnnlgXs3tcfUE/kCUMS +QUME/PiwpCGlQ8hH3F4BRufba6lGJv6sRjd2I6WUnktWvXZTsmsyOmSi5x0SOCCZNu3+SkvCvioq +R0y7ktNgxORQVwPoqAafCsoKFQIOtH4npjTkrIhNlEATFBlDxFI3V3T4W2Mu5AQxYCjQWsjeujZu +R5pywvBWXXZTQ24P1GmQvH6PH9V+CAYiBEtt2cE+Bu1nVlAfmQU8FBOBRCHygGHZFZJzFfDlgZMH +hHAluFNhf0gpQs3akBiMtcST6TRUIwEQPbWe9oStqJ86OopCvFQOYTsJiBEU61NQAfkoBEhVfaC/ +rauje+/uw6DQR7gABQygkcMkaXqJW2i7FRBw69Xx8bzb7uvuSBTSyWJeCfqlYAgIXjCPmdlSMRBB +NFEH0gReE+8LrghHZIUcdnIVOAFJNiZlrIh2nF3FustYv5GLppgwaqzYGtWViyAqFfSL8nEoEPYa +OXGGehtgu9H6wuErz6nRvDOWM1rgwzvsMzYjhXpm2VOFyyyDD0EulAqY8xqBWZzI46l5pTWeQPPh +YrbYEfDkNQWFyFWoQYcBxx0pXhRZIO7anojqJW0+l/XIEjDAPi+wCtTXEJ0csV+QD477Q9jGxmgm +VnT6S1SVgTgqyviFSodqZlkBVVpFc7GC36m1BhbsRsrQMDXeiiNWrCaK1RC4AuD8cg6YVL4Ii9Dy +FW0N2XLiSSItx+kSUWqB+UTZv19G42U/MgskgchN4innWD0dzaK4EZNMd6Ix3Wf1Gtk/b9pgVm5v +CZKAWMWXPpZ6jQQubkRAn53XKMLpDZ9SyBrLx607jnUTeHEoZ3h4nOs+54Pt7d+bx2/eaxSX3lCN +dcNWuEzsDtaj2tZb9CYH+vkjfkXrjvn5iJQXK57QN9RYAOl8pBKYI5ksNaShKlSbUSGHAX4Awj2O +6SQkKwGlgcqrOAp6LVU0U0VDQTwI9NMLiOoBzsHhBmFvn/uxzOzj8UEKA3Af8PsByaeSMRiwZMlD +MFOFGgQC4xlqPp+zrTWB0ClQjlhpRDLNqoh6KFp4wLN7jRiVvlAoZxUxkZZ1++utKCEnyatBEfp8 +TkkXMcfYl8iQ+bxeMIJmVUhzCmLjPAxfoHnGVSyZAHFzKPRkIwuA2jiIA7N7wjdRoOiEZ8JNpmql +XE5R1SqKETwA1sPpqQEWgvaCWsjmiNd64o51qLHQK2gVNFeqpNVaUa3pir2mgpHIibQi4mlOFIEj +jkqNMYn2kwLXMP81syfm6W2VjXKB6vGoTzuq7NGH0mHmgeGBvjBkj70jDn5TOwCFMto8MSqGG33Q +zgc5DjWs9aD2tEwEda6qEx6+HyLZCw8J6S2vQ0LTXdxr3GUgVbEEqdjR41ZN8lB9aA8RcJbyafyu +rNTA84pIaqWG7vPQ4YjPYZxA3AKlZe/vbysUFVwgwNggCsBx4d3gOqBZvB7J45I0tQipCM0C9Apo +T7VSyYeSRpTMOirA0RDHAFYdReFNNNUVCSzrxlAXL1ZvBJKg2yHWNS9wBqBaiAsW9E3OolUEybgb +C4xKrBisLQR7AN08Itmhzk54v1Lz4M5XXEWlDNq9hayKQiwUNKpeZ85jK6HTouxFh0jElBFmRwIX +dY1w6eD727Kq33AQu1pJDUSDWbNU87vcQTStBKILNTsOOzYriFbdgUJexWygygULCKoftRSEg0W8 +24n+X1qpoJCpAXAYeOlATY7NDCAQnrhvhEsgaYDrpz+pqISKrjym3Yvsp04s7TgPEuNEJsBzxE2i +OcHG4py9JlrJtIstocDhD0u51VO5bCQEwFeM8DlwY5RGITPNMvQbsR1LvgnjpOnxNMJGfI2f4ieW +rhNfP582qSNPrfM0zsm/Xia+LNFsfXUx19i4UJ6CxR+J0TxTyVMffuNqznixXMk/0yP/73yv2Wt0 +/voBVcs1F8us6V9r8bFVS28L/ShMWlaNIvkhSmXJKWQQDuXw+AVqhQh0wxk+fsKQE10AiNiYOK9E +yJ2SD/QecJWwFQtaNV1ADUVFRQyF65eEn8hWJn996ZwLdb74ENZb0xPfj6zpiT1vfezK9dXZPBwX +oteBe4oaQSYQhrfX/fKdbc/fURyeigBP7nLm4ZHAVaET0maDWUkUJ5ZqtMyGugpsnLl5T4nX1khB +v9f5vI0rb7zcFfTkh2ebhmqPbugZeMXO3hdekB+eRuFVw+yov7ACZoI4WSAsoAhRNFYua5D7GD9G +ST47R9CE/cHTtHgk3A0C1HFWGJcA55LtTeZnoaFYUoL2MykHDssBAw8SOLQCJpcRcS7kiPAC+Pqa +P1rribpWgM/NqKXRQES1FXSnUgRPGOJrNodqd+qwERBKJdYVwp8gjEaKhrWjWkn43IPdEbheoAeH +MwuhDI4VmCPo/goeOyD5TdQX1OBluZQ82kih9a29BKkNF4Wa+VUAI9KBC8GCwHiqKNkjGBY4wlGF +jpAn4IhaUQcUCHrRGwiI+DmhkZwOhAuVUgGdntxOW8AjqflCPNI6O5fPFlWXD7ZbmZLlBMuAQsVE +mrGoHE9EM+k8MppYeJIPk46pM/AgfriCmk/lXFTuCyVUy6RQPYDKB5df9gKd4gZJEPS900swIsQJ +Xei7TX3QRI6N51g4PxRz4ZdN0lTYfgz9YpN/EXIiPEILiUqV6EKPWgUb9YAqvU/0a3WqccyUVkRJ +CigVOzvbWztX9J2enda9ku51o8YC/AdYMQg3kM2E4xfKUqbcptq70KFLrxq6LoGZz1HNqLonEEQA +2zDRogLxAABxQLPuxVpEAq+3t60EL1vXCXFkcyolE89SquzW7GG060ireKIwEalC1os2qcLlGRxN +tYQNqTvKf+N6HXrFpxIGB7EjfK547KqE4kyKvnoQ0aEk3qLkb8QO65tNqAUruEUTydAn/FcCdAB2 +WNWER2vhSVmFNekmoSQYdb9UniyT8U2KUKhDBvE0qUYRiLE05qIkahxYyLwztJn41fLT1wdTH1Oz +aqyz0NVPbqH2mo5xfrW2SCqwRGg2C9AzpNKyN5bNz//un79h1UhrREy9UDN1ZUMrtuE21hek0AwN +X9HSm2Q0krYkXUPuIetI9riEG8lCjCpYqacOgQS4zpnkMC8UKn9GTs1Wqjgyqgm9WITUZDyPJUNE +cpBiUk0L2hry2VRj/f7g3DtvfvfAm5638Zpr+y6+IFNOV4/MAs0Phk2IWjhf6z78B4Pvetn6K57f +f8FFjk6Xbe+puVSuRN4YnZlQqVYFx/m9xqY9tagaWy5edeW33rb2hudcfPVLXVvCU/fsE54lZmr7 +R27c8t6XbX3O81decGElYZv42d7lq4pWNgk9K9lEG5esDhSRgBqUKrwpWMrqXUwFvonpbTIW2i5e +VQalEUCbBGkRsRNIc0ITiaCc+JVQp0I1ksgl+jcqfkOfKZTWlN22os+uhFDEZh+QXeslV9ioJUt6 +suLMgDtUQ9bPVgYRjAaojI06tyCmikwUvDviNmNLH8uhYpdrtqG+dq+zous5gE5BagnfxuNDuwyE +8MCj4kegjUoGsG5s6MoAmCt42xxox47jCYcI1+ugygbKXLPOs6EQyOUFepFqE0B9A6I1KAMUwROt +CrGBwU2CE2lGgr5SvoSckxsRTN22ad3mlta23fuOoHFENCFLHlwxvGMP4qZwRhFr7mxrgdNZKqpQ +jZqqMrEndapAvaQOdG7VFkTjK+TDiijpQw94UJOXscKBzyQrz4HWxxEoD6jmIBKPPl86m6P6RBFT +IXpTEYOj+yTEqPUGveY9R9K2WapZIpuBICLvKAwZJkrnLUWqlAwoCn3Q3WTcP25zygZaxloM1t50 +abCvs6Cqp9DCEKEF1KgYBlxyIKPKqK+oOgK6I6LVeqpSTKm2OnzQl45EKLC278hsElHuYlJVijqa +SVEMG5l7p8Prl7FaFLXsD4IkQUGkmB1Zl4YwuY46GEfCJssZI6o78UT4mnIolaqngrSiDSTuEree +tyLGjDHBUkGg1VmpoZwxUAFTAbmCmoQuAuRdQpUiyI7Gw6xG61u+7uPVVSPF7blCv760YXuxQMKd +kTxObBuBk+CZXh5dadoKTRtxqXRZvkOtDbS4jcQttMz4s33bOrm165ZswV9VNQrvma69fjRrHuoa +9vyacYnmfprhnn8Gnv46f+uf/OZzjUKc0pqjWbaSZPymcB3J6KVPCFBHti+lsak4kRPaXKLjIHQa +RSvIgyTZRTsYhYycvKCoPsxv4PEB3Ee+Ah4mkVnR5iVZDhmGaEfJcE0WQJ6KJo0QNIwUZUG/dHqt +hdd4c9kX6OPGxnE6tnz+T2KXryEaUrxp1Pb/n6+nHz3Wh3pKlz05kxn85BuCl20AdYrgaTa+ddex +Hzw4kwKfFN4QZMb1pc50lWXqqkBHgq5ftrHOXAMYRrC/5Tm3vJvY1IplsMppk7m7X/5p0d9x5+ff +CDY4wFBImJnVvR/93tTdB54O68V+NRcaMhhBxOJEwpNyJk/zuOQjr117/a7MwvSPrvvoOZYojiX4 +lSG4K5INzdwB9bTLdpAxG7CzQfQs10r+Csj3NkRd3QEfgDzzJSNTduRBL1JCdyKAfOCvgXjF7kYn +A/gHJmLkRNKG1g6U8EIEvWzK1epQZwhdANP5lNsLH81AQg6IUK8P7ge3UqRrISQR15ujbgcgHYpH +kqoRjSnggqIeg9g6AEelth9kWrkkeHxFBG7lQNXrzYN6F98gtwkDMNGjiviyXa5wOJzJ5HDfYuHY +7MwcgqPo++GJ+BSUkiB8rJiGWkOeslgCN4wLqnj10EAqnfL6vFA+cNDhVoEgBi7hwnyqrJoByRGE +jnW7y5WaBqpyDa19y3AXkY31oR+mH8eww+X1eGUYMCAaUKBQqaKkEmnxGTUFvStQm04sK7grlGmD +lmDlyFKfLEWEEIUBSkK+IehFp10h1WmVY69il8GhppgA0pmoiGGGVRQPo0mIXqqWFdxMdzBfA0e7 +EnBUQu6iaspuOZsuxqruVhg0sF8QKa05W01JprsGewu4YDMP+tQWv9keGZtJuopGn8MeAlkNGk4F +KtmY03BXAEMNeULZbAl+rBeYYdMMBoNwnQFfgr5GAF7Kl9urIEMyw6QzTaB/oRoJU4NVRiOEB0lX +R/lWbDNefvD7hSjnbDdkApX5K25H3udAjyrd60THskimImu2ACH5sJsJ8sv5chJTtC+4wxIh6rgr +Md7FPDP/uFWTT6YFsaiyN05xa0tSkOHC5nxjAy5JDS7fPM3pyMXPmn5CgZImQ/ksm4+obngYhALj +eDoHCBaPTIu7yX1l1j+aniWhWk4z04zx5ePrhFuq2070ZVFt9is8FmegAb8/7wHOFz0WK/a8h/kV +RnmOry5pZfybOeTTH6WuIBlozju3Hq8TdXe8v7nEnxEFIvhKdxmSgjCqZL5yspvYu6gwDBuLnvV8 +I/I8oOPPAfpfBCMj+Isp69MICf46V9f2/C19l17gNly7X/+Fey96z33P/VsqJfF6JqcyyIOvv3Iw +fMmGoMPf+qOvd33hQ13/+o/Og3vhjVHZHyCPdf+06ZZa5oOw2p/J4+LP/nFny0r1dPr2y/72J1ve +c9f1Hxd6EYSlHZevk6v+h//iK7dd/H58Cr147gOSXcJWAbfEFZKTTJZz/Kp1S397fAXIYs59ZGbB +ov7ogHISktCJ+CGSlpTbsckQSbaKVOvyO/tinqDPB386lTdypVo+X1EKFQMNENHfQkHyyUallPAE +IbFg7wO1Ax2jgQgMZgZFCzoSvtaQ3SgmoVQ8JNgqXpCZur0IDsOXg/aizDQa1yIHCLQP3EU8Qf1J +uBUIVEAiwXJdMxFTk0233+7zO/0yONjxI3TyZXY1o4wSCgJ2sdRBhtEHzQRYF1xIuwPQHcp0O1yF +UikYCflkGfpo/ap1LaFoKZd3g04UZNUgs4UPXqmiZ3bNrppIpGpZtPriODpK4WvjY7PppAYy9VLJ +PZ+szS7oyMpBO65Y0enzAcZihEMBUtkAq9h88FAZ5qXDPkDGGgcBUcDCAiA8qOrwwslGXJgUEWai +SiYX8F8krblEnIoROU3LXYhRXYGiSzxdSM0TvbZ4jRem01SB4YUThy9TAw5UWXjR+cZAqb67lIaL +5otma22of5DdZbRsstla0MYyq8cMe8DuCLlc7XapS6m1a8DWICJeUpHic2N7Vj3Ql4hhpgor8tWt +ZW+v6mxVbVGlJmuoR4SaIxsZo6u47SrC5NBkNjugT8EAOlRheAh9A99LHN1Qtq4KNR1FywKvafNW +gLVCQNXuRkC1CkeQHEdqEsbFjlznQk83PEv8Sy9Q2kgscQGtKut0U3WvQ/dAe1Nog1BNFNERLUb4 +QSYUMc/S0ZAGhvnCfql4cu3n7+ujkUayLqd+Hc2Owa+QYfx9nYZfbdy/gcqNs56wLnyFCGbVR3Ys +Ky0mUOFfkd1KsoxLhBh9R9A7woag8w1V4Aq+CuhB4nxD1JQb85A0QL9FYoi0Oah+UbcDjJotgGOL +bX46sEjtiRqh5uf5tJIFdCft2vvSywa6tqBtHzJ54a0rrrz/H+BEtl27Ffms+dmkdPW2/paNxUpu +le/k/8fef8BLcl73gWilrs7dN9+5kwEMBoEIJAEQzJmyKIoWRdNBS8lB2vVvtZJl7/NbP/vZ611b +lnN6trySLEuygiUr2MpWpBIpiWIGCYDIM5icbu5c6f3/53xVXd197+07ASApoVgc9O2u+uqrL5x8 +/ufe+wrLyz3X3QKJBEljmA5lcU2bFnkufVsd7mmd4DX1O5arRxbKTvXsb34ma0c/wO94+xIfvfb5 +0/mfxiZC1KLR54nRSpV6dfbusVg++vd/9H/8+//wkb/2vePNjvWGGKYoDgSMbURsML/ahVbnWwM/ +6cNSWI7rM95tM6UjFVgQk+1OsLYdtzpOvw+fk51AWUQWOU8bqY6IR4WDjfpRhLJKwGNnnb24G8yW +vJW5ioe41QTlpJCfOWjWKs06CqbAv4g8V8uFIsJwfskCIkAWo0qwrpBdilxyp+RZuLvqhqUAlLJQ +S4o1xMVYPiph+VyNDMfH9UQ2wUokngztl15Rwqs9pCJQi4WKNAjXrq2tX1jdvLTRuTbYPLfaXWvH +3RjQARgD8GgAcqKZarXa67e0jpTIu1BH8fIlcEcY5How+G/017atC1f6V1Z7nW4CNKD3fe1XrSwv +XkMMJJMT3T5kAgZ2UR2UOG6BX0GV0FJlobFQReVgVL5iTgrt19wfkm7BKhH0jVL7A/QouB3rbKSg +4YICoCd5EU6CrHKcoR/Dd0+RFeWBQxRW7PmXznc31uHzC9xmsVdNgOJdRr1KcJ8taGIor22tW701 +5MZAx43sBjIXgY2QsBIUS3tAw0PxLRg/N7tLoTODcGM72rYGLTvCGVcKCGolrjKzJoF6EHtdx4sw +LKj5DQQGoKfS5g1pA6ovRRpEIUVJGWjxYkrFvwAB0MwNsX6mQSzyWVYfDPkIzOE1MLri+uogQYRq +owvhDdA5Xh9IPYjNkZXCkF7ROCUVVGwpVD5jrDsE9aAn+f1BJ2SqJuZ2xHWzS92Ve566wQwF2/HD +fiiIrhlDeUh6c3EW+c9qmldVeOy4PlZyo1fv+aYpGb/Rxm/ivpeENY7xReGOYm+lEUcs20atU2ek +5CySqGm8ozgaNdMR/kUkSGhNObidpJgA94PASkMKRkoeiMtWLwZ1CpCQcPvK0pvvPfiB1+GJNRSC ++OpXsyHgdL/+ztv+6lehopMOFCLEl97zwPL7XosP2dAh2qV218Fjf/mdD/+Hby3MAIzEqT1wGGkj +7aeIjr3y1Q/de+j1bmhvfu6Ujxh0cL9H7putHGj0W9Vacu3oXdtHjpSryGhERjM0FRMLnTWOQI/G +yUMn/tK73vy931acreH7ysHZ43/29fOvOY7P+PfoB1+HED69HurJ8lvvuet/ffeB2rELV5+/8kfP +jc3v0fe9dq56oHd+C98f+upXL0v5p/yBd2nedfDkX3nHW7/nf/VmWbV45o4DJz/85pk7VjD4Bx89 +efid9ynkcPPEyl0ffgv+xTUHHr3z8Dvu03bqRxcW7j/6wi98cvvMterBuRMfesPia27D9/gXWZJ+ +2lV8g/DO4+959R1f+1pv4Q4iyAAA//RJREFUBuUaUVrITqr+ne953bHX3+vV3JlqcmzWR/j+3IlX +uQfuaEWFTs8edBFr47g9+PRi6FdJF+GeQK9GSXaRnXiiJCHUThcXzBZsFNSogiXCmey4gE4B1kql +Xi4i4BFqGqsHImNnAD1EYkjEpkTgAdZeiaAaIQ6oHMcVK64lftOpNJxyzSlWHJQVtAqoRYvcCFSl +d0A4wTXAFuEEJEQDjwiZeQi9Qc4GKw1JggSQBzpnWtH5/va54OnPnQZ0KoHCE6fVjaE7IeK359hb +nd7VK1vUoG3U40h6vQESxq9dW0UsJ5TKALnrNOVZyGuEuxza88c++uSVyxvLywfgc91sbW+01mA1 +HYiZ3i36RAuPIHVZjUphuVFzu113q1sdxMAuKoQWFCMGOYkVQKowsmCI4JXBlKynQE0MD7wW3PRM +/cQ/yDWEMRN4CIUCBqjab/sXzwerV+CTiI8caRw7uYjSZmuLxVUPN1FaVQsQ3BkAKTpvx0+V46fL +ydVKsU2zdqlueQCgAQMrt6OZzXCpk9RpC0CaKvkwo1dcu1IrF+uVYq0GGwOjj9FtJIfaLqSc7c02 +pBKvUABOBnHDJVQI/0D/wzsqpmyaOqGcTKtymdoh+k12UvaD8SKGixFaY1JDXY6APYfLFgWpaY4w +VlLSG1l01JqoqsI/LOBvYkc1lGpsf30F/TnJIIdEKRcaInT4utn8rRuH/NRNfr51z7meluxbg4Yz +6mvMWGM22uJFFKQNWrTFpSgWfCxCBFFoIDajJ4l6I+kbRNCCMxF8kZVYQWZo9qD+BSJGGRnqITzr +CEJEyYneXPPkP/vm0j3LWNzNeGYr2Xj8b//Yfd/14Wqh+anv/MGL//0Tb/vIP1w5fOcn//2PPP/d +/+PAV7/63u/6cMEtNt25s8984eMf+md43om/9r7D3/hWP/LnagcvDk5/9J3/d/nI/Nt+4G8fmL/9 +D3/qRxceudtpFN965wc/+vzPxa3+mf/yu8FHP3H3j/y9ew+94coXfnRj/th89dB675r3vf/msd84 +femq2wWkCwR2DVdz7bu+7X1H/6e3+ohQqLLxX3nH37/zm95154fftlg/dHH1VPfi5sLtR+rV+c/+ ++C985h/+VHGh/rb//O2NleVDC3ecXHr415784V98099TO6oeKHz41b/4d191+A0f+ZkfXHjtHYv1 +wxv9qx/5pn+z+TSqghC451Xf8b47vukteJd5eZf/8e7vfPQ7P7z80MnFmcMvPP95FAdqVuehDH3h +V36nujxz4P47xr//+Y/gKbd/zcOH5k48++ynT/3qp+/+n96+VD90YfVU5+LG4u1HG9X5T/zEz3/8 +O/8rLjv+p17zBhnMhjd36vnHf/Kv/4vXf8v77vuaNyPxG/ru+QvP/OEP/ctHvurrFl79RhSxqNjV +82ef+e9//19GsDIOYuSeoeQfQ1KBdKOgLvSdUByiuoZ/g37dTU6u1OYqIIxQMZCb2ANHac43bN8q +VlkYOEGYqnid8e4Ik2TyKCg/bgUYix1Dx0SaOTA2kwLJXhXOPPCSABZSQAogYDVaB9QAHMCFUlQo +IVsI6hYyOBApA08ng6aJW0b8sw5AVnrwXSbBar/3+HbV864M+ofvX5w7MX/6/Lnudq+zjdwOGgbn +ZmeDPiJOe3VYR6Gewd8JpQzpDUivREYKCggyEkVCQKlsO7CchghOCeN6HemPPb9SqDaBR9qFBgQr +K9g8oK+31tt4n4pXbPilsIUaH0EFsbglu49sEM/CCwJ4DoMjHi8jgxJ8Sk3oXILp0hEOYNxz4lxC +xBL9gwDdGXhbG0i5RDYUsPcAUrP07nc/9PnPP3b2zLVBdzBfq5cRjdoLXODotiC/uC3PuWZHg0IB +UEGwPSJV5qjnHY0KVaBwgzGHCMBCsSnkWzgoI4LoKFgqglrxbDUsvfrItRARyUgnClE1GibrarnS +hpYM0y9ikUoeTEG9zqCeOMsDdwn8NXSqAYKdKfNIUV/6ACQNJyMtNHTKm9J2LvILAW2ZriMxcCJC +A8jebhWtPnJYIRkjbacPIy1iuxiMgK0lgEk0WcEQTAJFH6REJkmdE1Ij8UaKi/Yrw9fIDgtyUHYM +jVfpVzC9y2UapywrxUgJRil9xdd4PZx0f9eqDJKJIdmfohGKNVXQIPUgk8yEOPFHKhiOie/UOuWQ +WiEyAp2S0j1P+BtR/xT4Kcjuv+Mf/cXSveSLmNKZxvLFn/90/Z7Ddyw9eHTpbn8BkqvVPXMNgnHt ++IGZh++4B0iqSJ/b3r7ryOtKx+ahSj70/d9+6C+91Uu8wAtPHn4oRmkj4CC/7uTKwonz155BKWVn +rtQszEnRQeS9+yhd5Nx5bLawcPnqU5eW7ghc/9Dig01/frM810OqtkjuunOhvb3ue7/9yDdJ4y4b +jy61X/VX33vim98GW9SJlde4db92cqlabN618nD92KI/U33XT/6/UdNxEHRPrT/ZHmwi9CbPF9Hm +3GuOzxQWzlz44tJb7/aqxTsO89G12xb1cW/9gW+74y+/BY8L5V2Ci51H/9E3zr/hdmTmHZ2/u7zS +KDWReO68/uT7jn3NgwsPHRv7/g13vf/Y17322AdeW/QqxxdfVVyp3/1X3q5d9ep+4+SydrV5bAmP +W3rojjf8kw9jBrut7XsOv65xeO7ggyfue9ebPct//skvgjA1Fxfve9V9B+57Q8HyLz/5BPx4teVF +4F/GHUwA+RO8YhTtGd5JmDpSVRaXlQQ8qXi3PFOdR05ZHzipwBobeG4806jUKuDvBeSGl5DiYCEJ +oFiKy8UE8OFM6wbxQ54jCvRZldgFiQWgyoxdnnPqc14D8S+wy8HYCNDPFlIJoVGxzD2UFvQBTlIE +i4K39nqILKXFDbE2ENQGg3673RKCHwT9oObVYCKsVOzmSuVK+9paG7l30D+hiVhlOghhmgzrqNwL +cgx2CRMqvLBABZCSfgUPoZtI7WMJDgiFMG4itQSm1+ZsDeUeAekHBrq23V7voG5WEHjxdq+NrkF3 +RbhPMfYRvFTsIi+iXNh07Q0Ej8b2dgIk8qQLHHR6FpH8GSaItoW8AKcnojXhjgCATO5EfmmEn2CC +wb/eoON0t5xr5/sXTm9vrwZhO6oXkXbvHjkw9/QTT51/8VLNKs169UJUiLbxK4AYEH7sdmwAvwPh +3fYRextW+nbxqm0DqKINizTqTXLoXSAPgckhBbkmG4FBLjKCCEdGCuTm+kYXiG4YFQyOlLHcbkFE +cvEvWFp9sQk0W3gEQyQXOS5YGPzZNCqlJQ+J4YbPMKuCnxEiJ1UoU0eFqkpYCLSsWlBkE8QvVdtx +vRuXsNLQN8wOtVJBHVFLFrkukK2gt4rvJ+WLQ12K7HI/npD9kciX5arMUapPG9MKcwrMCLl+Wbr2 +FfAQtdjvfY4r2iP+Mx1VyqL08UgSLgNK9aTrkNkXRtqilggBnrYZBksQdBPBEjSe0uWNFCkJGwDI +18CNYSHt4YNEUtAjz0RkIKUANmpgDyK/GxfPrXXPbnYRUw/baQFZAv3wkUPveXGdxaSW33Lfwdk7 +zl764vbjZ6lPbHVQNnnjidMP/ptvXqwd2nziDJK+L2+e7jx16bZv/eqlh+4JLm598q9/Dwjyaufi +pd9jqOfy2x9caBzqO/0vftdPP/V//lckZT974TOnv//XP/Y1//i57/+N2mvvObRyb6W+RKrT2cQ8 +X+1fCT77fA8EC0iRIH9iF7zrr73/wGvZ+Kf/X/+PNn7ho1+AptC7tIVAcF0dqK0BN8v5a8+e//0n +Hv4nH64sz7Sfu/rrH/wX0AI2O1cv/dHTY/b/pdfddfTQvTOzS3C5dq/x0Wvh1St/+Cwuu/9vvH/l +oXsGF7c/+h3fp487/7ufrx1fCDaBNAPDFa3Hj/27X7z64ulzm8/iM74H+qV+/5l//wtXXjy92UOZ +Kn6P8JRTl7/QvbzZu7SddXX1Cy9qV89+7AmQnbf9m2/BYF794lnw10ubp1dfuFQ5Mrcyd7zolc4+ ++4VP/+qPnP3pH/KLzcXmbb5bOvX5pz7yX374l/7Rf+ivb8P4GKO4Ud8pDOD/AY2jg5GJ8n3XGfge +/E10F/VPLEFIQOBElxoAlo4VLTZrM1W34obVAuAEEDFCWBlQQPAYqFxwVMLJ1vEAX90HaEppzgP3 +qs0VaxWvnnh1RMOu9vprXdT16AO8reAA+AuxXrBaYF2h/iKodwV1OSJYB4FSENVRKjSqOwPggMM3 +5kKzQ9QPZK9wJjzv9efvWUau5OWzq7MlBMi6UJZgJkAlw5BFOGjuB/5cFQnyyOfwvFoV5QyRz17e +2urBaio+URo6NYcW2SbQdJEW0O73gC+KNb4y7z98/6EH7zxweL4S9rZQ0KJRttF0sRcWNKcltpAR +Ae1HMns9JtmGbns9SXqOg/oo3UKpAxBY3GIjeaUIiYJ47ZIPjP5YGDpyrHAzOH+qd/HFXtABNhvh +vRszta1O59Dhgyglcv75q1Wrgvha+GtRQIr6Oj3ICcobgu0DSn09HGz6Ha/h+Anij5xrSfy8k1yq ++lC355NkFr21kOcCuCJ63QFx04PyV69vrnbb12BSx8iDOgwQOdUKesh8hfUYMoVfKEKh7G91V1aW +AN7bQyEMP0H8MYtmSD0oEhNhk+SU8pUCt8H27tuIusJUMdEfSwU2YIZLkvWR7UkMg1UYhMVuiJzL +Kn3TVBrB7ECPUHCyCrgiJLiirhjFJYjjggFJSR7rU6iZ5PCazF/h9iRLJHbSJdqzGAqVP7ikJrgp +tTQtuqkam3zOLstrFHtwj6wEJG9UZylTX2lr0VOUCzUUCzJuFu4gDNKcaWARASYUZp0AQ4z+xYkF +AxFEofiyM98lbXz0EAB7c0qYMDs6RZ7I+PdeH0a0qx3+eCk47S3zNQ7Fq7FuZpYP4+s2VhEjsJnB +Nb4E7gFQEYQS0B2jZhM5sQCE4iBOEFMG+LfNTtBGChkIRT966l/+wupjz6OYaNGvtJ68MLi2XZiv +QS7fDjbWPv4sHIezbzgB5DYUs5+tLre21xv3H7F97/HP/vYz3/NLx77+zVAOnvhHP1k6OLfgr1y4 ++My1P3oazLxy5xLWbff5K0gjr9956NDSXRe3Tl397ScGl8D9nNnXnZwrL59be7b3734wLFa5w9Zb +dXul2ZyD7wxLDOpD8+ThY1/3JjT+xX/yk8WD89r41U888+l/+BPP/NhvH5g59tTZTzzzH39z8/Gz +R1buPnPtqWC7u/LI3XYv+ew//9nZB44ulw+fu/LM5Y8/PTacB95wFx59du3ZP/j2H4CWiUcPrrYA +pgXn4okPvgkVJT79nT8FR+a8v3L+4jOX/+iZX/7q73zy+399qXnk9OXHn/5Pv/XF//SbXsU/v/nc +49/3m499/68ty/dP/sBvPf4D/B4FflvPXjn/e09CGb2weep3//oPPvljv70iXX3i+39j7fGzR1fu +fvHaU+c++uTBr7oPg7m9vb7wqsPQhj77+G//wr/+vhdPP3epfebNJ77uLV/zAfvShfaZF68888Kl +1pk3nvi6h7/+/Ze/cOrK519wAYUK+s4URuj+LNTLWr34AH8SAZFA2QKUPFgquvO+X2HKC0pfAJvM +qdUrqP/n1SooLAVdCjsZpRzK8EVRWQI8azdpIuojLjUdwHMvLM03qrUQ5Z/W+4MrnfBSK7rQqQDU +xfeTot9xIyha8K/BRMqgymq4YW1Vl8B/POCYIeYEGendBGpSeP7s1agVz/tzlQGKCtrwlK27g9sf +PVY7XL+wdrVa85EggvUJLbYGW2gZ/As1jaEagQvBQOtXKoj6ROe4bmGShRONjgFElyBiB0GYVE+A +P+ih8gaiPwooyBEHc83STLW6+uLVK89fmivONvw63q9cAjx9F7A7qNaFE8IjkG9hCqWoCbMt7MiD +uFktF+xC2EWmRT/oQMOz5utNxCfhUox21I58xJcAJ5wYN/ZMqXh0uXnySGN5tkxLpg3AgSq8oMeO +Hbj7VccAx19GQRGYgsFB4a7sByD7NDkCfhYeVClSUWSSMGJAra5P/CKAn2440QtW58VyeLXitiH4 +AqotcZBiCOA7CDFVGIfb3U3UNYYSDeSEWg0xVAI1wGVAsAakSCIkN0ChRNR4XbPnK/2Gvw1BgKOk +OOhi0ZTwdaW/Eu4u7uUR2+HOdJJ5oQyNhXRDZyxmQZC2WBEcDJUVDdgUWY1ke+7ciLE8mh+z2NaM +3bwUJPpL0OaX0Ov4JXjbXR6Zxj3kIjNNJET2zU53Zso4fswknckv1agqVlNxHzHnyuDfiB2VP4tr +n/olk/6pHEqqDsidMEXNV5ccRuRSI7zO7YQW8vrXUEudGf9kpud+6qOnvv9Xa059u7++9tnnYER1 +YNGCYorQ+CiG09HyndXfeXL23tteddubwD4R8376hz7yhx/+V8f+3Nvvu/0tYTxY/YOnj/25t95+ +8MH16NrGp56v3X0IISXXts5f+t3Po2+Lb72vXp0D7mPr+Uuw3YRF11+qEwr8qXNnziJer7zVuXb+ +lz/16ccvNRcXllcqEDuDfnjyr773fmn86h8+c/RDpvH1z7yAlz7y9geX525bD6688JO/f+irXlMr +zUAbO/S2++858mjf7q1+9tTRd7/6yPI96+G19c+fzocXFOolf6mGR28+cQ77GDUMNjvXXvyVT0Gs +ve9/ex8ehyIIl/7gqRN//q13yLtc++TzmKDDb75/cfbIue3nX/jpj1cOzPrzVeCMn/7lTx17ywP6 +/TM/9fvlpSZ8oVc3zp/+7ccOveleGE7xvmtPnTv2NtPVZ37qY8fe81rt6uqzF+/90Jvvl8FEIuGn +/9tv/uTf+1drg9b5zUtPfOaTj1/4gzef+MDJD3wTisSeOX3h9Cc+/fj5P3jznR94z9/4FkTrI0/D +6QNABgSRIakMFmRgIwkeJhOxxwUoZgX76ExzBuatQR/cAiDbyPZrNmsAp0X8JwM+kSkPdRElngBj +nnRRhN6tJcj0Ls8UmjOoJ1/ob/c2L291rvbijbDcceZifwl8ykWBRkKA2rVSUPZ6BRc6XQy4GeTS +wWoJYNX1lu9AqCpCtWuF7a3+ZmgFl85funDqChxyCIRFRcG5u+YefderLl2+xOTwQYxMdsI0Uf6H +6ocDrwc+yLIQsHBA/VhfJ2wrnLsQ7ZDUj4gezAiRxbHeJcseknWJptgYgbRl31tozpZsv5zUgvXk +6ovXeusd8DMUoZFIEVVhkF6QOOWkUGVpefrLHEITAHIWX0sRZeIaoT8VZIDahS7y6ZE2jFgbqFXQ +zsB/BgiW7TYK9qwT3nd08d7bD2AOWp3tY7ctPvDgnefOvrC2fq3fA8A6tpcLPou0SgouTNBhPC8E +UrA0MRnHAJ1Z8SuNflxHQeaS35spXml6l2ruZsWDKZklNm17y7O3PcRdYfMyBLRRQzKqj3Ab5HQO +ICShV+wd2qZ2A8y/MhS8QdwCdBHmHzbVGIYmZAUxVUMBXLKKI5K8Yb7Zm6QK+yT5YV1jSNgMC0qK +iIbFNFDPAiskutZ+gsdTHU81SWUi18Eax6mrYnmNntLmSDTKmD46qYiNX3Ajtt+pD7kBtiVvsS/F +cM/LRp98K152+rsQYPB6w4PGWKA+ZPJLo0eKssjUfok5FWgbqYgtVg4JyZHAMHBHMVxwT0vVVFkd +/AQ6Bec+fTiJDefiGhLGOwEYJBMYRdDDvTFCJxBL4BSa9xx53Y/9jWp5xkMhvZJ39//3z8y++c64 +FTz5XT9dObaAC9Y3L/3++78LcZVv/9h3zb7xjkqh3j+1tvTuB+ZuPzpTXgrbvXv+jw8d+6a3Lc0c +u7T6QmG2+pZf/wfFI7Po5tZjZ078zfe/85P/4u7/5Z01p3ll48Vnfu73K4/cC9py5uqTCZCh3//I +F184XW8UDiyBvCYzrzmOxnun1xfe+eDcbdJ4p3ff3/zg/ANHZu47gp4M1gGM5hZmQK/7my9cXHj4 +jorfgI145a33LL/jnpo/E/eCk9/0diCDZ1M095pjVat+ef3FF37pD5ffePexpXvPXHmy0Czf9S3v +mn+Ij+ueWgOq3PztR/RdXvP/+TNzDxydvf8IRr1/Yat7eePOb3hL2a8P1jvbZ1fn7+P33Qubncsb +93zDWxeqK5c3XnzxNx4rNEoDWASvbLzxH3x47v7D6GofXQXYmXR1/YULb/pH3zBz5wF8v7Z16Uf+ +5++Cv/ZbfvC7/sLf/mvf/Df/98/+1s+/eO6zYBOFWuPkX/iWN/xv3/GxH/vvzz7/GXxTnZ/xS3Ub +sTFwQ9FqBdOAxMvj9ViMDFYc+KLimm8fn68RVSFCDngHVLncKDVQJqReAkgbyjhUUcOPSf29ttPp +AeAEgtBMFRylZtettrt9qbNxfnv7Sidqx2XLm/X8uVKxDhzUMiyJUG5QuCEIvbjnO234KCtzccer +dADQU1o9ew1Y2L1Ou73Ff4OgAxZ55I4lZAOst7agVnTAxivuQrP03Oee8LZ7/hbQReF6rpU8GFpt +6IGoMsi8hRLyGqEQen1UvwSOKFiilAICvyqVWEEDGi/hfwnah71APETAf4JJNuvVhZmZEvhELwI8 +N8wh29c2gfpTQVmQAWoWMpOPxkUYTuCoBUasG8NQjlwWhAsBlrXXBveBMscaWRUWT0bVLQeZn5LE +AagzKrjYKH6pMDvfWFyaX5lbal3oXHjqoh90Dx1w3/H2u+9/1eEnn3xs9cpqqVCFfIDLN9sdmHmx +yYpAKCCQG1MCEbRCXCG4YdGH3gBZNOUi4IcAQBS07GDDSy4X7RerzhON5NMz0edm4s1Zv1di0n0X +MPeHl+nni2H1acGsTvcrx4DwitAsm5G7HLhHgsJh5LVu9on7AItrIQYWjsRl0V+jpkOT/iCGRX5P +AjTNESiZLczq4RSgRAdt8dAaBR0kY0VTKKZ6iox6acylGXcUPMZxj94ODU4juTu+yNhN0yn7l9MV +0974ulnQy/FyDOva+9xRuR5hhEO+OOzx0MdrGKcggzMjnPZ5PcUqwuZFXmQoGXO0NCEdsrQqi1Lp +cICitrHVCuy1TrjZRRlaJnEwskFYKVpon7rWc3vArG4+envhQL0VbG50r4BAHfzzrwdf/OQ3/ltY +WcGHcAEKA73p5/7OwT/3euRGgkHPlBdLdyzc/y/+IpycuMVtlhbf/+DSex5cnDvSdTqHPvQGD0hg +kT+AlP262w7/hTfBmjV/5+GV+dvPrz639cT5xt3H52orPbd/4AOPHP/6N7Za4bkzW0W3vHhwHllf +aLx8+/yD/+wbTeON0vLXPHDkfa8D2eqF7at/9Ezz7oMNZ/bq+jkG1zSXF6uHKkdnH/mX3+R7CL7d +AALO7X/5rce+Frkoxhm88ODtBxdPXFh7bu3zZxdfddt8baXv9Y//mUdP/Nk36eOqdyw8+i/+IoRj +vIvXLB18/wPHvvYRBHIEsFQN+rf/2Tec+Etvwbs8/h9/vXZwTr8PB/07/+wb7/pLb9voXf30v/rZ +xtGF2cLi6uYFAAsc/7rXalfh8py56xC6emX9HPM3vvYhPA6DWSgWv+k//p37v+r1oEvHT5y89+RD +jzz8umBzFZ5duHoO3HnfHXc9dPz1D29cvIpvQJl6a23CoqrzUBwgTLfDbINEYRXAdu1YB8uFFWCE +szBFG0oOgjYbM/UmCDCq3yJaBvgpTgwY6mLVKc8UK7MlBHlaAP++1huc7waXBskWHI6INPTLXgGh +pIiaQohOXBy0vM6Gux3MDAb1sF3ouhVvptJEYEtvvQuiDFya1e2rEKjag+2w3wNcC+DnlhYaidut +z7hzc/D5ddsbG2Xb3zi1ceZTZwtXUd7FitbCouXDtAkdEMkkDGyNkWsBi2YPxv52rw9KDNBwpoUQ +Ihh5lmBeoV9IoB3CMYa8/hJBcBxgiFZK/uLcTMUvtdfbQTsMO0Fvve3Dm7gZQHZx25HXg2zIyJW4 +6EQVx5l1Sgt+abbk1wF9Z/UHPZ84afTZQowkoh1srZAEkFFZZvwl+gUWUq2WWVwysbY3O+dfuFzs +wQvrttZ6tx2cO7hY+dynPrm1itWLkW52+w7BvdF15GMA3QjOPJQIRsARhFuyFAsJo+h8PwyA3FCe +qdZRb7JYnEu8MoATLHet6J4uW09VkwvFuNINq1s9QL1jLOaOHNrY6KMyp7vWn8WYARuecTRRNYrn +e8lKKz68FR/aiA6sh3daJUTjgBL0qu4WVUqxTulOSOtU0oOolGQfDIkwdEwVgVEdOYvAfhMfnbgj +lUTltMCpDFIEd5Nuqp3arw0yCybKqK6hdErveI5posbuZvAzjTYx3sNJ/9v1c4/xNq6/hck7RJ8W +aLCbOMfH9la87PSXuxFfY+pWFK5mTKJmeek3Y35HGlRFQZRDUDp5aM65QB3TpMqkLAFGZhOQqCVB +mbH4iIpHlAOMo+0AqRPgkSxYJc8gsL7Umon7G8gsC/thF+gea7/1xVPf/avn15+tF2bCax3wxTYM +Ypa18bnTT53/BNIeiqVq//Ta73/wn2594ewLV75wrHkP5VgIySw6YF/95cfWf+updrAF0m91oqf+ ++c+iS9DnilZx+8nzj33zd5fmZjsRIup6289c66114J/Dg3rnNz76V/89KgT0O9a1VYSnD1pPnpts +/PKvfn7j86eQLPf8pccufPTxsIM8vaBWnQVXWFu/eLn14j0HHoF4APhy1keI7Yu/8vkv/Mufz6bx +4Jvu68qjkarR22ibR5/b/K0P/9uNz/Ndjjbvgfsse5cLv/zY2hOnFysHq37j9gdf++q/+0G8y+e/ ++5ef/enfX3z18QX5/o4HH3rk734I33/2u3/p2f/2h1BRMB91dCmxzn7kC9rVsx/9AtxX6Cq+h7Hr +V779+859/NkvXvjEfYfeUCpWNy+vfc8/+8et9jrIy13ven/l+O0vXP3c537i57a3NxAP+MZv+sCh +19753OXPfez/99+//n/+tl9tRR/41m9DfKSidjLgnqETrDIB/GhkQB5AHmGrBVMq2GW5UZhZaM41 +G81KGboJoGGQFjAoJ3bVaQBeB0BsSKu4stG5BMW3ZbdD6JCI+ECZCxdGyZJdL7tlF9UZQwdGUxTU +KnlRpbRZc+NZgOHUWl/cXP30utV1N7wwOdoon2jCmu33B6VWUECpqauDYH2719qG59KveARHtZzV +1XZvPXE6frThOm3yOLwHUvRg8QRTZAELRKDMNOYXwMwb4haAvRZqXKFWr2F1AYezWCJRLvvQwJhd +jg0AwyGwxQ8sLwApDfUat6GiXtkerEMSRBEJRNN44OMlAG3DTQk1s+JY8469XCgsMG5Eah3D/AoQ +cm4nBkzZiQ8mjRi2HgLZaFip1FBAEXFFLOJWKXgAbOpsd7bWWhdevFZIorrvPXjytkun1z7xe5/3 +Ar8JNITQ3ljf7sBnniBkFzWVXUR1ev0AWjASb10EqrBsN7g0ajgDR8GN1rbBOJFYUnZQ2qsG0bNA +m2l91irf1vFe1S4s960q8nS63YWlxcsvXGpejh9qVe674h7fcNyNXjtErCuRlKDSN1ALLExmBvFS +z1rcDA/3rOUBKpihwDSNoSIHa3AfTdjZn+LxyzjcbiSPGDe+a8FFivGXzGONhqYpnyySBdmM/jiN +McqyHTKwPAndL4OcTphfueJLPQL23wSKfW5CRfUdavSSnSfxV8NDtboMbIxSFxaaJKOJZ9FwRzI4 +0RB5tdT/lN9pGCFKqthjuB6ZaUvfN8PsCI4jiQ8MV2VPXCBYofosEvxRamq9Daw4zddRAOUh2Atu +evh7v7V2cuW57/vVsz/5+0jSeO1/+F9O/8Bvnf7h38qSH+CDPP6Nby+vzJ3/tU+v/v5T+B4p/6/6 +W39m0Ome/rHffdXf+RAE7ed+4NfWP/1c7cTKvf/Hn7n6R0+f+ZmPoczu3f/71zXuOvzCT//e5u8/ +02jUvbuXqvccar24eunXHmucPHj/3/rgtU89/dyP/k6E6naoXQVkY1Q89h3EjJz861+H0IPnf/S3 +H/g7H8ILP/tDv77x2Rf8ufqhDz6KSI3nfvT3QDxf+7f/PGjo49/9S8WZ2qv/5gfal9ef/+mP3fPN +XwX2+OxP/N7qZ0/nRt5Gdj8idFDT8fyvPta4++Br/9YHL3/qmWd++HegpwD37LXyLs/82O88hMd5 +7pM/8BvXPvX8o//4G9/7Ld/+2LnfW/vU6WCr8/RPfhRfYuxe/4+/8Wvk+9VPnUYE7xd/8ncvfYrY +AjA1v/kffBhE/fM/+JHete07PvQoXuqZH/0oRJRH/86fw40f/55fRCFD/2D9/m94R/3g3OOf+tSz +zz3pV6L7bjtw772vnT9+1/rFC0997BNnvnC2Vl889sirj7zm7rUXLz77i5+49NkXmK0hYfcM9STq +KS3sDBBF4kkQHSy6hwGnjTrEAEFzolLNXzowNztfrTFAMwHfKlMHQv43iz10W61BC9GVqGgEOg0I +EwdlETadsOWioF9SKdt1GDqhq4GOF7ztQnwVySLV6jrsm2693yld+ti13uc60IeSO6vJPc2wZAWX +t7ee3iohbhX47SCbWGJ1y1+sBEVrs90ro4gWEiRC2x/EvXMbjdiHGl46XOmXHSTWopz9hQtrKFvY +bM62gmB1fQtW02bTb7daULOCbgBeAZxxwAdAF+50+/MNIMN1pSyiXS4jmqpcLRS6myg5MQC6Onzk +GCjwfeAZQPODexTeNgDdRSWABHqIp4QPsQfhC2TdLQIUCsA92BJBB1kVEWBrYKbsAEoudpq1MuDu +rly5Olcvb0FXi+L6bBka7UJ1tne1vXGu3YDOWi5Wl2c7iPtmbZIEWYwoM9pDHi1SMFn+eQA9fs4u +zPbjSogEUQfe1vWicxkoqQUi0gDCDqJYpVGGUr+1uVGrNlGGEaoZ4pvsjW2AoK6E9kIvxKBtzZeu +nVzYPnXl8IZ9LAZcW3hqxvpMobfhOShRWWr3VjrWSjepI+MQGSfguNhEmD7yHUYjlxG7k9YHxYfs +IOmSXEaYeQV8SEp1ETqCGYiQEvEvcVAVc1Lqi1IDlehQwYbCR17Me6USgJbnIhkkUSKonQZipiFA +WsaLB61e9OikfTGxokI9cwRz3DwqcPlC9sRYJt2XxxlYZUnuld/zBx8kZBeXS1KsUmnJRzQGXtUq +9KY0VXHc5TjaGclrlOuVBWggq/msI5AbaBLctL3xlxrp62jHZXb0ELwW02NpWuJMdr93/7+MlsDc +/33jV+YxVHdgjfnHkDVKBHN6yArhIRMh8yWsEeYi8SVy9iR+TLL6ZdXxYE6/ovSK20CKRTHwTBkh +VQcW4g656KWmOxqGKRVQmghd7AVIoorAGrf7hLLc7b1ZWg6zKfi/N3EMLSxohKnTLMJHzycqBsBh +g79R70hUZRkWUZDVJpOtZYJa27xe75W4RNFyU8szTIiIL0Tsn1iCQEdMLK50O+0/68wO17pugNx7 +YWRHlu5ur/ynf/3/fvjV7/3Ep3/5l977D/PXZN//wlf/AwnJY3wHeRZXq7Hf4r1EqGZYJdHaCV8C +PQiEyg3LVh80rOr2m3HciG6vFk4Uiw1EDm/1riD2pWOFHWQOAKUTpYVjcCuBNmfNRQ6CEhc1DzDz +grilJ+r+IRhHe1CUmNIDLe22Y4vHjh5wCgyo95sFtyaBKwA1A3dBHaNWFwYEAruAebjWwEMFwWjb +CwbAg/Ot2bJTksIaSbm44QTbVedqNUbGTNOZcS9WnvndK5uf7c2EbgQIgHtnC8dn1lHy81yvfxGI +6ABVhQsUCxTFJhNrzi8u1691tpHCDv9jvVQHkn33zHothHki8g6UtpC6UKvDdIDROXhw+fKFa2ub +LWLNuclMsxL0ujPNemt9C3klME4ivBaRse12d3GmgXqWSImAiFgtATzNwgtbCEElVI6krsd0RkpV +b7glIyT4x7UCxpwYOwhtZW17rDAoU/52p4/NhjUG5xmi2BAlu41CFsjg6GJUrCOHDzz/xVOLs4he +agDqbm17C2N+4uDB9TOr2xe3PdevL84R6hZ1a/oh6jzDA9hBrwoAKyVusReHDcudtzwU16zQNWgB +sBvFGtedpOcBCh4rhq47APytrCyDoyKuBbA423jtuOv0B7UoXuyEt3dxab+9VL5Yc+bOd5cBP4+V +79hnZ90niuGW48HJWt7qHmyFB7uIPopZx4vOV5G2pdgw5qEilBVLZgTwLKNKQj4kIke8MBFR5snW +QIjgTWQNYrNNKWrw1QxPkw+YcymMI9UIhkjc5D/MUM6zRuaApHkX7NFEtAtgH8Zo/cQ1WmrBHMLr +BNBSdoRyyrQ81PAyPEcJCB8qrFF5yl6sMcfn8uQ7T0Oyz2kl5yFrZOMTrDF7l/0T2YzlywTmWaPu +/y9f1ngjBtUJKiykUwpo6E/pLBpkamNzZfQ3TpHDZCmYanHkQLowFA6AecoELUGB9cjqA0esD82N ++KiyNmTx7HTSPLIvvrhrCztOE4VLcDrQogLSrIDy3OYbKO7G6EBk7YqAQDA5nHgxBCyMPZJasWRI +qQSqNeDGRnXvXsqjp19SXp7xZ2tIXLnwe0/k289/r2xbRNLxPuBvlv5hKp2NPDPE+AfAKEIGGeyH +QMsrAR40cargAc7Rkj8T2J2twdV+vN23wREt5BH2meJNEYCmcqmiQToD+6YDzDSAh4JAgobB6XOk +7C8gkB51juh7gl+vd/zQ/PJcAw45hDPOwQs316wjgxC+vPVWfHUzWW95rBHIzau1BenAhPeumNQq +sAajXLNXq5eKzWJSt2GBjGaAvxnMLy9G6/6pX7/Y/3h/sQOQQH8AHR828POdwTMdqEKlLqKVEj90 +/dDz+q7dtvqXGeMKhRMYoo3F2uZgHeQXMaCwqAH8Dy5wpEUUqwhSSRaXatub6/1uu+Y4h+v1BkoH +Xtl0OkG0TV8mKDcssshEAcogArVY2zm2Ko6LxAbkM6LQWNiGNwCFZSAS0s5KA6GoMnrSWRkBHhsa +bRRu9oPNQRdVHjd6g04fvxGIn4PHcM8ewXi6UI2QVeijhDL/Dw+/exWRsv0enHuej1i2rVLTq8wX +/CagE1gPsgdgflT4oAYeUQBBFKqEvlU9H3CzwOzGHIFPI5C0HNm1kFWfaoCFY5Vxt1yuQuEEgHFh +pgbYcwS/AsgAngIsnsB1tolJBy8fskqC2vpgoQv9DyISqlYzCguOSQiJMKQjjQMcESDtRf6bFBEB +qxDhghhOE3/qZRzfJsPsOsjbnBgpKkXOAakCRnVE/yIelqHAt4YITxC/r+AvphOQr+CXu+muu28k +asRIM+Mario8I2eOkquEY+Df1KEowpp8oy5yapOKfkiwZAXE4fLVzAyRk8gcFBcARI7aBeDDUXko +clCRuDOIkMKIIgMTpHv87Y0mu9eg7GOH5EaD/QPrEg1Y9T+WUZCOZP+KLDByqtRn0nrJBimTmlgB +eXMNHSDTVYMOc2qzp5rhF0gvzerUxvOf5ZEyWnu868F33Pfmf/3NyOgAbkxrtrX00IlLH/tiNAjl ++78y/P7hO/l9gJAPPoViNFuVRxoOTMcKFHym2yCSCtkVUBkrUb8OwBt7vmrfVfSXABPdCS5sDdYG +Xr/jxtvgbywrAVpP1BsKTiJKEBaTZgGuAEw6iioAXK3gH65U6kgnRzwGq46FK4eaK4cXas0yY1MX +mkVEzIIJbG0CNMVuI+YJkAoCqC2xW1D0iAbH0/KrDlxegKpBlmGx7oVVa7s8aJWiqODMFhc7j9mn +/tvV+PFoHoWJUBAZ+fN1+CYLvYud6CKAsxmYgWkmvj2FOHq/MRAg4X4JBQT7PtB0ipaLupI9uPIG +yOawa251rtHvBlD8PRQlvrZRhsKF2KGtYLA9IAo2QoLaIbImkNeOuUbdY7vkFooeymwNusgf8SOE +utCImiAPhEBzzEbgYkB6gREi1daAm5FAiSoondjpIbEXejXjsl2wvkoJGh9LNwHju1iEQZTlCTHC +ALxB9v3i/MVLV0vlcn8QIj1lc6tdrTdvP34UIAxbmx2nUgcvbnfh7gRuDyQ54Mp6EDG4xMGfbHfB +KTYjG3itUBlhzQWTlXLikv/nAvoGyHzIBME2hTEzqjaqSLdBaakAiZxIrZIELNqIgrAaOY2ANnPA +s0GXhcUI66vHpA4H0AFQGlGrbCaIm1GCTFaBfIfSLnA2ojdrHReDoZraDWVgUkuKAOsgj4XCKKWv +BPVEIBUAKJwpKCKyy7rmTUJk9GbzQTessZHmt7+YTIbWGkZGiAVNExrTf/Pbfx8kaEQG1acN99xw +P4/JqcbsJFcPdS3ZoFkPh10dkrlxcXdngqGETu27ulHT69L79+YRe/Oh4b3mk/xHidqtOfaig9fz +hHy9xn1pjXtLFzo1qUpnaHg64/o9DZOMVmd1BPrD1Z0u1aZYNUDy2mRWiHkD7Cjoiw7i/jph0h5E +HYR/KOAD/eR7A/dMHQb05vqgf8jQxeOBQEQQD0nTHmGF2crORknySYzlR6POxmKAZVHALMaKI7v1 +2ETi7RY/zD7s/SLOHV//hsJi9akLn/jsmd9BCYXFt9xZWm7iccff98jI928+UVxqmA1BYzj7lh1i +CsCcsKIC1DK4bYCJEhYjVGGE4lCuWYcKhaNdL9zuX271r/URbeEkbRdVNaA+xEghZDlNZeISgCXa +NtVuTHoYACQG+TQrJd8PUR8XxXGRARAuLFUPHJotN/36gWZ9uY7SSVtbq/2tDVRncAeoA2zqfarq +TuxQVHyAK65oRZWkWHXnaoXZigcEGbeRBI1wqxGFs83a7B2XH4+e/tFLlSeSgwg+BXRo0kESSXEr +9i70Glfjeehk9LXBwilWcIEzIkoTjMGbQRG0H4Xou9tIrcfadEsuDa6AjId26JeBWFjoWa0Lm9W4 +MAA2+FYAxbkc2iUk5XUSxNHESP3oAWmF9aBh9AfmCxRBpnfAfQaonwFLT0BeSOMtqQaLliPCo+bB +hYAMsABwA4NsrevU+14JAHMdRm8j90MiehIAncOcCbdruYriVnYZaYauXykXSyUg2AC0tH/1Wvue +u088+vCDZ85cevb5s3ahBv6+1YJlt49Mp2IBNTYL0DxZgStMakkBfLGR2JUQzj8EP4FjoWBqXEni +Rpg04F8MQ5i6iazm+41GE7k47a1NVslBcByyFlFbg323k2b1TCVZs1GCxakBKMoFbi42OWzkMdJR +qgBSQggRMjERBiP6IlCJaFBnWTPoqVrlTMMd9szOkBWGiDwMFvIUUWy8xFqeBOOj7YbZ0rrxb/aY +mqF3sw+Q+40YnNuGt6TZiUZGCMhIBKzEwQ5lgpfm8V/mrU73NebEFcMFZMoMnxYlkXPJKGqxEWpW +Bi6laChSH6vuESpTfqHaICoDg/a5lLVquYBRkEuK9Q17E7qBBZBLQGuh2CokWTaWN9LvNK7iGLvJ +YwdfIztG3ZFbC+yRtdaFt1GkVHlTGNwwmkmDsNPL+JNmpMj1bE3wIPEH1FDI3By84XHLfI0IzPFn +6KPRA5oKwmo4L0Bymye0rB5hP+heAxynZN8ZkTTva4wGyLqHLIMSt7SmWkHVCRrwL8aVur3Q8I+7 +ZUAtnG9113tWr4MwGC/qw7UE0O6gCB8OiiMB74YCEGeez5Mq1dDAkVKBWMsV32/CM+dBGEKJhvjg +geodJ1Zqs6WZlTkt5Q7ENfBbFJRHwY5+G+CnsPnFcN7CwwabHRABugAX8+2gGCA5cb5ZaFahPIYI +new1nKtesF0pO52lS7/VO/MzF+wrFvISulGviyqAsA4ijgVTAG2YTjVnG1hlVPBB3JElIWY5MjJC +uIUVq3ik0q0C3i6oI+dvI2ld2EDkanmhurXdirajQt/ttgCXDvw3RIyiHJLMLvVAjCiDzCBegBWE +9cQHDG29srm6VQIMz2of+iXyBWEJpFTITWFUHHBedTfICuPAUYWiy15ctDBuI+FxxurVnWS2CHyZ +XjeYmZvrhVEXEbao79jpIRgW9y0dXjhz7kIUdY8cXnrdIw9dvXrtqaef2lrtLDYWN691Ot1BARye +u89D0sd2H0FjgLKLZ/zCLGJTgxgppIDOK0YR8hzBtDBQJdQSTuxNO75WdS+V7XUX0TrIqgRseg9R +ss2FGTB2MNc++tHtgXFW6xVkp85f7TzgVMqdAaJrJAoPDDLpFIuX6uXPxNtHFhvN1a2D3fhIFyWu +WE8K5mATw8cREZt+qujl1SwVG3TIIGcj1gaA6cxTJBCIhKJIfWYp6ovAWwm7ecXXOCQ1Q1nhFV+j +jko+DMd9/ahBdYy3qH6TI9yqIBrNwugX+I/WqxEfm2CFi/VUxA7hrIKJpSE06khhlBXtRyyZJlfR +R0BTKlUsFECAcL6N8MMgGrC6qICu0oM3Zvod6Zf+sZ9rdrgt95Xw3/QUxq92T7gM8RmcW17VIFMp +sx4bNHVWCzeXX0UVM31PvxFewS9TO4bpAaUGoZFmYIYKatpC1tVpxgikcITbveHZEZMpeCSslhhc +Pbc6YRfIzmk4gbLvNFcMnxB3g9g/ajHImUMxhRLwN1FFIfGr9lLZOwrrYGitdgfrbdQ3gh7jJnAx +wikMZJYAGDXUFFn+gCK/2tGJOwJTGwQj8sVyqQpqC7YAimmHh1aat992cHlldv7gfII09xgg1cjC +sFG42iJ4bh8J7sg9QBmpPvF2WTMBpsxBIUTNKQABNGtRswHFMXZrXjDrrPp9q1GrDZaf/W+XXvyv +1w5sVIuevWWjcJ+8I7N+iKYCfyGwwySdlhVfaAZUoxLXNJLrgZlSACIMliEUpNnZWVoVkQnR6RFy +DHDgG4gQCwEmV8TqRgogvG/MSxFjOeC2gVfOxgB4ih9hdbVq88hpLPY3+97A6W/1WFVKmKeK6JmV +XnFDVepMq2UwLotgCdg5iILBCikjFiZBdcxSGYgGQJMtoagkEtmBqwcxFOUjAeVdgc7ouQcW5179 +mgefffqpT3zyiXLJaTbLADQ4fvzw5QtrCHdlagRAX3tsEi+N4hN14KlGCcpooFQVHMpIq0DaBuJ+ +ZGVw5aOoE+p+tAsJAGvgRxzARIBBBbbqYAB8PqDI9jE+2PIu4fEa5SLwZmFEaMTeLEcCsacMEwUT +g9/+vN2vF5y5fjjfQ+YGkA2ILADpU4svivQsGOLqnBEDp8jdurP4GR8FNQDRADYQX5HlScVfssFU +RFHHbWYlFNM+B1sWJj+LaUT2bCrypxfzeUMbqYRU6I6eDMDRzXUdBtXUGKoERB5vKBinXSY+o0SG +tqnZczzOIaN5ua5mRGkvKjH8bSjW58jMCFkbbWf/KohwkIxumzcyr6P0cV/H2IUTFNf8vlt7+31O +3qDqPiqBltmp62P0UCNbeurvwvvMv8L0BBtfXI1ZSj9poWLBMRRFDWCEICflZyaRXEzmqEtTYq8R +tg8kYqSoxZtAExGugp1LbsvRmDIj2UrKZtrEeGacbto0CA0fskbzWd6YIZYqxOtpYnFyfUoHURow +W9GseF0dxrOKW7BtWS9A14yYlUVlIEFWKRj/ZNnMw6Eft+Tu+TrCwOUB6cLQ/Ty64g12spnxdEMK +SpGIBbSj0msFsg4876hqhY3EqgH8x1kpF+cju9fqr3biXi+JOsgfAHaajfQD2PUY6ikVBPF/zdHG +EhFkbNL2pmsfrpTmATMKBgOXYTRYOdA4effx5cMLXr2ImsVwZTGPD2wGWOD9fthrIRMfjASvgiQ4 +6Byw7cI6N0D0CRBPZwCN7S7MFauIcCkhnrK4XuzZ882qtfzEj106+zMbC906qsXHUeAHNuoAw1zK ++GfSRpBomVyWd6BuRyxXiQgTGx4pXT+CNZOMEpAt4Kch4nKxYCG7tSOrldgdcG5GQTLWhHIHemdE +RaqktI2QNUKhgQsSmldlpgJm0l+DyIJqzqxHLJqioYqKesZHmwUjNgwuCS4nlj1kCWAxJFegKCPC +FlVDXFTRgmouNb8jJNFDa0MaEiAIBv1WBekZwObr9p948tnVtQ1kfeAd4R2EF7TRrK9e2hZkHTg7 +WSaEOVXgqYCdi+JaFNURgIwiaCjBCTQceIIl6pwVFsW7MYAxGPkkcOh5UNLxarDeFsFfobMi5wST +VYQttwKjBaWpvm9v9AcVywN3hDCD90X4Fdyua4Vk1Y+rSbzUt2b7dhWeabFbQI9GxQwY70Vl1kRG +sfFJEDwNTbKksXSYrYjkFWDkYyjAVCWw1jAuscKayE9JeVCOps4cSkfCMkUSEtyQIV80QQC6ANK5 +kCfK5tmDNU7x/2dcMNu5EqFqtqTuU1JMzXgQKpPKTHqZdHWM0KeMX5lpSmfME3ZnCvno2h254Di1 +NQpRSv6VD0w7UjBQZZEycyLfXI8Wo2OQ605uXAwZFlUlz6eGVI895LKa1lP+ft2+xslGxwaFPE69 +YkY/ygyuMteiGynwBM6hUpWJYCCaEKoHsPkh2zBC+mIL6IqGS2kL+zqy2tljV2NUdMmOCEf7avIl +ukhUSglUwsHCqUbv1llMSeWteLgxk6ZNTSyQTA7YYcxAN5h1g6QAFGZA1aYycFiSQSUpl53FQmEm +suEbbAHoGgWjEJnSR+ojw2hMaHwqYHPFuqxLj/yK2BU7qm0dLngs/gAobsC09PuHDs/fcfLo3PJC +AQmJgABHWKFHoBSQWLvfAWwa4F5Q6FiLFOADLJbwTiUFJLFbfs1u1ryFOiyIFioP1xdnVwHPOVev +eEc/95MXnvuZ9QODGrIegE4DdDmxv0nFgdQjIDoDDqGNoqDIMpZ4E0EkgO6CgGkbYSXdIGoDs0xM +GOA4LHbIOsGQHKSCDIy+cJPB7kh+R8c4axdQ38b6B71mMgZYSzvobLQDgIATQUHlwfzIp6SVNl2c +1G2pM0FU4L90TNA+C4cnxEbMSaUExx54oofajegiYtdcBOMwOxhFHwdRPMBkeNaZS6v4E7mHhTJA +acp+sTIzs3zl6gZT+qGwt/tWe+D3kwpQ+qAshhGSLspIrARDJ3Xm81lnAkkQsVOOnErs4CdUroBq +S/Q73636AB6CkRQJHcnq9rUAGnHZ6nS3+q1rNbgQ0QRgcJvFs8XgTDlkhUYb5cLYKDg9wB9QIQTh +OTSfM7eLCRsSi0DxBGMNVFWxLXGj0GejadZi/vc9BKA6QFFA2G3K9oajmSl25kOOFZDQjv6JOdhN +EbwVu/CVNr7yRuCmXNPKIJUvinaoDFplCaH6adgFMWtUYlMjiLkQW97kISFoHE43uKg6UdxB8TqQ +TGnGxLNMcEflcxkv1A8qzelhEHsliFFJKm0s2fcpnm/mrsg+7GMONU1lj3NKGyLgSFSqhvIy890Q +a9mfmZS091OmyEEUCHIyheq0JAHyvZ7ps/IMMrMg8APtZyznw3zzsJokNdQQtgD0uWB7qNTX7oZb +XWvQsWBNRR4DTKk0k9M0wHp3RgaiDgTNCjHGIQIrZl1G7syCT3ThmUS1vmBxsX7PPXcsrMxBN3Gq +frleBwwp4i88xOYgD6G3bXVbAOtk+ietnVxSaCe2g7AQIJOg2LCaM0mjGXvNKJjtrlc27OVis3Dg +Mz985tSPb6wAKRUsEUUGYYPlzUoRlQMZeclwR2GWGcc042RDNYXCBDB0K4EpY7uHsGnoVYgyJZKK +aIoCyUTHOZekLngxkApXI2NBq7QqIxERKUlAf9loQxUF40svo7iodtd05WrsrQwg1W6TxibuN8oq +lLzBZsFvkE6D5HrfmkMFDWiIRXBPVKREphH6BKM3A32aC7PN+SJAA9a21je3t5D12KJp3b18uSXw +TxhFlDO0KqFViRIwxWYUNaA1xhY0RaDBErsVypnsZrUlKNYjEyUAcWcFaz2goYILQ8ZBOBTwYWn7 +nJ2bVWtIGCJmIEBuRq1Ualfd56vBmWrcAmaeZdWCBAxyFtC8SAgJEh/2ZgbaEftNS5tjSDGGeLrP +lH3MAwQRWrYxnAVUTS3aRQgGmAzwWAOoLEYRWXa69vLc0Sz+YRDmzptUbzftKMHKH6l3c5Ju6Dc3 +eSjpFEKZf/SerZpbRrq5v26MkxeD1qmYnVSk96Vp7e9ZL+1V49N0Kzp+I6zRGN+y/1A/4JwaK5yo +ieIYSLcTF7n6F2koUJuGWhWRDAE/OYrYAf4EiJNI1WByF6xa0mQuOVbuzA2v8kXjhM8Ivarschqz +p2r9eqoBeETtHhpGhxbS6ZOYMZLdPkxrQkyuqVFZqs8x+FxlWfWEZLrj3s+a8qA8l+OWSwctu42P +GtvlQ+O6COeuxA4WYmYxVmzgrizCdxW7VQSjAi6vnXSBioryjnQAggVJDQOJf0iNBOw/U+ipiCBw +xFrwCw0UQIQnicYspIo37r3v9soMqm4Wa/MzCFHhE6O+M+i43W2nvWV14MYcwDiLBAE6oz2ekY/w +1jCpRqWm1ZhLKrORNx9WjhS6c/1oxpt1lx77z+fP/MS1I+0ymEXLaQVAFYfVk5Y4rsVUQsiGR/qY +enz01zS+imof+B+iYhD4krTjzsUthKQ2ShUAoslMoREQa0lItMD7iaKjJ2i5qIxi5YJ0gBITiPmB +6AdA1AiZkGA5RIWS1B0xOwlihixSWnTViSAxJRosyOlALAmqcNklBI8WkFEaeQHzW2bA0eJiqQgb +Js2tsNj6DjL4YQoGjitAckvV0tx8Y2a+PjPXKJZQtgSMhuHhCDOCigasGeRiVgCUCsQA8DD+CyMq +kyjgKUUwDwJTmVwopfsEyoOnpNFTgWN0i7rjdahCZ3ujg5RC1DJxiyjfiAorDJxivfJy4YofnyoO +LjaAtUfPCtghnx4zVxJBW+JfZfwB4rY4gMRdQMAquSK0ftBrtFH07WoV2nIB4hMLe+F9tSiPEYBT ++TvHHTM2mX7Y1SKqFNbsQnH370DrUldKRjHyH6bt/GkbVmh6SqSyh+95l7llpKf768Y4bRkNUr2V +tqv99efGrxqfppuWUdCVG2SN2UuoLKlrSFdeqjUKM8qZyzOdhd0WsYQqo5RfQHYUXE7I7GoHISLk +EOoCZgEviBhS0EOEHuwswKjkb1TGnL00jfIUE3VOI5rkMzc0GzfNGklPMFx0D1EjAJXQUTMzKqJ5 +5mXY9cP19V0tq3rkJNwsgHu0NeVoENSRvecj2822YZQso5C9ddTyl1CIqY26YCHwD8AXWXMa4aVw +ptEbJmRSFJ5MZsf0ggIuOe6y7degGwCQDCixdlKtF+648/DiISRXVCszDQfgn4xQ7iMXI+ls2OSL +qOUwYDwvOS0hI1gTCaUYkD1St/3ZwuxccX6+UF6wknmAhvftUr0R3/7ZH9p86j9vrGzXESwaI0ed +8ZNcbQVkfhv8kDFGyAhHskZxQskp+po4wTEZ0M4AgwRPKXIK4ytR73In6cGaByeXcZ2IwkgVR7OL +ZBcYSCGlmxJERpcmYmoduAJhq9Q6seJNG9vIUpOWfAF4PgxJST0tABMGECpNthXXLrMyZanhVhrF +crWCZP1BgBtQHpIYjEAABx5PpdrAE2DLRd4JXgcoC4Agr1ScgysowIxuhoUK0jBZ96OIsipQNeH+ +o6YIfVHMyGSDoqDyT+SOUjl2ifuK9PwYef4z/WSpHx+OnQWUVg4Am+4imNkuAUah2OkNqlXgJDlF +INch5b6EN0bEsduAJdrzB43iRsNt+RCLQx/g40Chg9YIC7aQETUUESeL+RsYKMnicMH7kbFaAK4e +AmKBciCZT7LE1DyRWUhklU9qjWblK/Pbm9fcCp3j+jbnl+bqm6ZjX5puv0xPvRHWaNx2ZkPLEk7d +1ZnoTZ429CEPqbtSf13PapKC1ghTai9KoDKivAZi3cS7Psz4M7L97oLA/kWEEVth2texL1+mgVcN +UeJvjEhh4ppkXw9Vt+nd2e31d3uvUUpsrkqpQeoqVw4KKoiISzhymJGAOvV2w4Mt1Cki+60L/Bbg +B8BOCidTnCC0AsmnjPWUvIcccUH3AHQCIj1bcOsgZ3DO0QQGGumeuPPoyqGlUqNUnZtBKiLgn5Fc +D+Lba21E3W1kxUcoa0xjA51MjGmW4MyYxd9Rmt0pzyHj3qmimErDi2H7qxbmS8c//9Mbn/qBa7eF +KxXU9S3CnIeSxBagbaS+MGI/RmUsNaKOenfFHpzq0vxZo5GpRkJ3LPXt4GrQhjUSqYooYwEWlvqG +swFPZ0QlN1EiGZ2Gd4ezNUvONTEMajzLhBa1YmlCk37C0zGolA4lNRQxSsDRqR2ozh1q1GYqsDF2 +u8BnBZx4j0n7BKjBQ8DXeTt2GbRbtD9AMZBerwWL6mYvROmaZDC/XPYqBXFm0kSjWiB6rjDbIpqJ +kCCwxtDbeMI1SBxuQKDS9LowSA50oiXg5/UDIC6gwDGwfVCdCgn/19ZXgdLR7vRQogOqJ14fTy9s +dg9sJcvbSJTpday4LQGli16hgSAe2uwl9dnYVIVLGlsUAm2satmrlQtVBPkA3CDoASIBYpuioJKb +pgGrHDDZUtm2ybM5+XqH7TIc/XQq8heN3qCTPHLkN9r07br7FWYdKkGdeMS+W96hh/u+92W48JaT +2zyDv5X9t7+DcdFm9rMPk1OvcTV6geZmyGcGj4KWsUCxtCFfCiXTYGuRo2lfpU1IKIUEQcimtUFR +kYyE7QzQylZHShvkHpwtDxUH+TCSyeEVeRKfLofxdT8mIU7i84+9cnb9LkPBp+f7sONUjMQg7XyF +GE1y5NB8xI6HxEDCxiA9E6WWXjmiXdA2aOi8DhT/GH1bjsnYth6Vl/EjqLY4HJhbIX/SaIb7oKAQ +K7XodAB8U4uTWlysJQs1dxlo1Eiq2Bq0t+PNHqI3rKDDSoa4AUYtxt2SkXBFqU0VRQ5ARhdd10NZ +QVScQPBK6FSh0cTR7SeW733t3cVGuTzT8Jt1rAILRYkHa9HmtXhrMwASKRMpGHPCEaE1FVgCdq/Q +I/BptRrNRfUTcXEJFsUm/I4Db1BfOv4HP7D20X9+5VBc9wtgil1B8UG0LGJmASOBVEhJZCQHJ/PT +2EXa6qgeQlEVnFeSVeZhYkiE9HLdK1gzx1l8BLK6KcigAV6jXlVpkAs1Q4pM53d8LSmjpB1Wtesh +uU5povgi2Cb2h3hvMTNAcIO1Evbkpr1w19zsiZnVbaB8Q6UvA3/m6tXNUg06IPIoECHjA6YV+nuB +Kq919PDKhQuXYc9EvBPwYWso4xyj5lV44OBBZNdsPXOtcLFfbyOLhXsZDlmYiUsJYnBcxMVAQQUj +1I2cxqOLI0NGAqPXT5JNJzlfK1yuFlpI3U/cEKVnYDnGbdj1QQLY9HqDEK/tK+35wD488BY49CEs +AyhaDb5bjpPFjj3L1QHwvACaNBR0SMYMa4dTGawVaLWUlCV6QAaZlmtJthDjkIShwqItKUEcVc4a +Mmv0YqIm4TIp44N/9WLxNxpiJAonW4ZVQIzDYivAlBDZWPMjOUdSH4iIiVg+st1yLDZPtyZV0nES +xKZ2JAp52ieRYlybHGTprfQhFexoipDL87RIX18fp4nEez8GUV57XzAWIpYnJplZbqoKzoUuhjvp +KreR+tOks2KyGjeaTBucnX7Pv4mhh3n5aN9N5vMap2uNRnjL+GJuxA2n3G0OZJJ0HbBKLX2LQuy4 +uuFNByni9odjHfW+mVNv1tzQ55Oz++375b4yL0xt5SJSCKZqdoDyyrHzfsochbJdr++QLZcFfMtk +cY4QEcV8C56Q4SHP+zHSxxELWS8UAHqZoEBfF+Aw1gDmsx6jPpVOqBFM0g5YTQyUCg2gQCFqC7Io +IRIACLyJirpAbYvuuOvQydfcXQZkwPxMYXYGkSMuKhPBONvrOj1U3hzwMhSRD6BmMaSH+Q9OXC2E +Bb8U1Jq9ZbtyOKrV4qXmEpDSOsXB8oHbnv7xzqe+98php1p1ESuLnpFN0WZvww6JLA2Bn9BIGZIa +jpWxd8jQyTaVxZ5ekMWdp9ZuzpLo+lzIDE9lVSNueW4EzaHLk8zdZkOfqqcG7Yy7rdIwKdb3xntD +GUNiMODkAhROrC2WanO1a5dXO6uDpO1sX2111jpltwioOdRRRn49VPImbKw+GFzSH+CLHl47HAyE +3xBNqFKqonZU68rGgdm55cUZ4C0QAR9hrgQytaEZM6NDhiLtJf/G+OAngJoWAR2HMwQaKjL07cXA +PYSiGe1kBsECiBNA5Q0Yrhmdy1HuQatNrHYXUFIAAgRgj4fbm6EzP3CWe85K11nuIuSVtlvxW3IF +MhKZztjYQ/AQvKrEuVVZJOPOIlmI2CL2UUmFkvk0EUtKeSRkSxg5JR0VQDJSLjfKIjACjYLxjhBq +MevIl/rvde+w69uP2dUpOxwJA7jBtl657UZHwH1UUgj09h1VpVQd1N9Va0w/GDpuEh+zFrirhNBg +Ocm/NIrJA1T+YV4/pDzI6dg2MO4gtR+M0kS4pm+ys4I4IugMxS/dw9OPKXKSyJK6iUQo1jMTM9On +ZJ4qQzpkVIb0bg+N0/RQ2s/31twilFnjl9INm11lxjz9W7xcuTbyr6/2wGlCYToh2ojxmskIIOgF +YZc+AMSRyBgBHKxYceYq3pzllbvxYIvlj1oIlOnZMCo6LJFLiVsovJoS2AYoo8Z3VJEqFwUVOiuR +AI/IyvjQkfm7Hryrulx3mpXq0gKKYSZJFzg6dmfT3d6wNreSdgdAYiDEgugqpRSwjHz4yuJ2udBF +Cd+jvYWVpDbr9AFC5m4fWL7zmV8NfuMfnDrQaiyWq53+plt1Ec9FsyQlfYr6TCMVgTVlXnxhM9UY +LBlwJUkGmsLwzuHWELZnKo+ShLMtrgRhOaYpk46TSjr634llqfsjFQFzv+clIumLwmbQ3cj6hMhO +mYVR29keoCCoFWxErSvt/jqqZOFHMER2BuWpkW4/6MBlHwIaBt/A7UilCfnCSLWAPRJGclSwAXbr +Vq+3Ad2/G29A5baQSyMY33ArMj6I5k1GCbH8OB3/4IuiRJm6iaI+o3VcX0RqhqTaAYW241kthNAB +wBTrQdMLgVWLhYKkUBTQREBxP5wBVFFIiQshQxXo/0Sa5XqXoBsiAiATo1SCr5RPZW06AzMoYoeM +JjVKI3DLwqWwzfAv3YH6WVQo3myWIxlg6vhJ9QkRgQzPkwxmVQYz/5Bx/4uR1ezHHJkxcyUvOY2s +5FYAGhua64fkRZbm8DQ3mF5lVCN9Tra5d6MAwuOn9EqGaORMo1INnVM/wrDvu1Cbvamu3CSzYJa8 +oTbpf/ZHp6ZR9uuYgD2buu68RiV4Y5t8510/sgjkD1nF3OG69phbjdKxSNVw6F8MmMvIYLn05cz6 +NhfnA0amDc8t+t2Qy1TOnPxz91+ya2+wK0pJ4YojkHdKXtO2dmX9mVigMu9Ue+9unRM5AP484YvM +0wMUCv/1C1bTs2diBxETAxQc6iVdwJTB5RV6bgjWCDRUkXcEIx7+KljhULq2aVkzlj0DXBUCpwGr +GlQOMx0uHZy9/f47Kyuz7ky9Mo9K7yD5qFy1bXfXnNYays87/baDrHDiCIJkElMPbBbOzqDkbJWd +bjMoLXcWliMkJDhz/rrfBoL21qe93/zOF2c6CEnxN7ZWEdQIQx/8oOALEg0D7oUXQpknhScVJU3/ +VapGE6rR37QooPAAjQITIY+kmxQZ92rZUakKhbQ/SbkTdAbGVe7kJdpltMep0hiRos5j6KbIk2Id +RHBpEZCo8PpuRfEVFFu2osuJvZZEG2HYgguUL4P4FFSyrlUR+wLDqgcsACQuLs7Nw1ba7wXIaCyX +63E3STYGFUDAn2nb6wMAKQAsTsCNMXdginwpMa3T78goGELKEw1HrzFxtOoN5Eq1oJ/Ww6geAA4B +gQMBfqFLJYRtFouHQTwMO2cKjYVCWQFqbWDFsDYU5leHHVPDFQc4GwQKoTiJAtRIvK64WFMyT8uy +TJA6RNF+SmFU6hHhhx84acRPEG+rGkjNr6k4rVdzU5l/jE6Yj1ZjJ4zAxIsyK+INbu+Uw02QFDXM +mVO5SP68Hs57XV3bB6m7rvb+eF083aBqdCjVF3OHsn3lmmNjYqTzbPHRmQOzFpF+kRSO0nYAwEQ1 +HbBG8EVYQyRgZ2RBCBM1raaq2cTAj8rmO/41dbLG71LfxehpEEpUY+Ara5bK8JxgllMfO36BgfBQ +VdHUKiHV0UM55vggZ2qHfsh8j9nn6xel+HyJrUQ2GY2hRQ/5cGXfnncLTdA2pKsDxQVmPJA3WFOR +BQ9rJ6V0wjfDOg4TKsgZQhwBmDNvOfPgiyi9FCcNH7ngMRK0Dx6eu/OBOw+cPObMzdYOHCrVm7D0 ++cG23dsIW2vR9lrS2bZDeL2QocEsSDI0PX0nAF+ci9wj3ebRQXOhFPog773luWODJxZ+6e9/YeYC +FEV7u7fq+gAs83rtCKgB6qdT/Rocg1mGqc1zOMVCbdVBKImGJu9Ijahm6g3gmKrW1DGVO4JNapKf +mkWzdTK2qCZWQ255TUjuOpmMuCEYnmT2wRgMF2ARSp89QN1k1MO8HPXOdty1pNbzKj3Ha1vugAAF +CAXt9loe4k0hErBOTFSrV2GRQQc88PHEpRhK3ClYxRNnzaptOvZqYkP7D4GkgzdiyREVeWnVJ2o5 +PVZ8L/mWJYGRnWms7JBAzGhC20PiB7DlgPRWRx4kgrPgIdE6LXEEDHaWYIEThakYQGYnTj3MxIxR +oLIIE6xV8qyiB58idFx+o9tBcppgisdtWBCSAY1JRMNEEWLcECIYIJ3IBpEsEkURkoCCNEVU5lmi +jZWLCitMTan6h/lnKIWPcSZht2pN3UE8nTbX45MvBoqRc4K8kBJy3tNKWkMCmC2c6yYwO94wzhq/ +cpM3bs14jLayH9aYSTsjtHZMidyRcGdcU2IpmKIBzyKiIcAXkQjGjEYpuYFVnjk2tJ0xQ8GuGtNL +MSRfojaN5yN9ulAjozrup0c6RDfnnZVwHNAWKkfQuEQHLCAwtTADFDAkW/TjfmC1ItjAkd+N2Bep +VSE1OUiMiGLCu2ErqyJM33LrllumwQ1qDnMV5+dn7nnw7iMnbyvOzVTm5iy/grp+Psr0hp2ksxl2 +tqNux4YDE4EUKCMMpRSqhOqvSPdAZAcQeWYHjSNxc8Xz6vWg5M4fOGhfnv3F/+u54DFrETpO1ANY +NkIX0V4V+fAIhSVUJ1RavJYg1Iye+aU/ZnxSj+PwFlnrGorKoqDEa1Mrm7onRTZU2XE/U7WPa5Qa +UnGW0o2Km4+CFkyP2Izs9aQ0KPoohwhfbGAjIBVxKcidoAJX9RuztfVNVAh2gV0P9ogDOYbYaUW/ +iByP9fUtCKdQ9wu9uNgFyCmyNSC0FIA0x3wq6ZvYmk1aiWjZXFlMWpTiUJhfqImY2TLAxxnZgvKO +STFK6sIa51EMCvsaopPUK+/0idxbKQMCp6I8C35D6qCWhSpXLOiBXz2vAshX4d4cVcAcuQ7AYIke +AIsBlwL7lbn98FlgAUS7lRHH2IuGZ+iE4m6Jkpe+kGGKw6E3GmN+LlR9HJXI9zFXt/6S3UwK2YK8 +9Y98pcWJEbC/QwpK5A8tMZFyPqEGKm6ngaliRDKrUIg3MxCFRoA+4SqJUtRgJEMvWNdNhXJYfQAl +BrjmHhBSmdYlTxaCvPfskFJMm7+psU6T7HyMIYn6NdKTvSUA9l0OMYQaTgbaOdbsZMfHuqpPyZ7F +MUU1WRF4EZ2pRegoGpNowVLFmKZ9kOFRb+SOo2dg0mhTJKALeSFUxiQoxTGKc9QtgEXP+fYMqhJt +W63NqNOzWzDH4VLgnzAoEK48wVcFTIsLIpsgZxzJ40ivQ9wVihsipaIOihxtH1xZuO/hBxeOHfAW +Z4oH5qySH0cdv79V6W8FV89uXTkNOyryEhE/ildjCUZqoWJogM5Uczo1u7NgV24PF29DdhuKJNlB +uVZrH/3N/+vJ87+wcaBWb0cd5MNjUFD6MGCxBWaMQvYCOKqGnjLORK2UYOSSpo6TF5hsf7GY0dqv +aUW6dEVv4/dpbUyJR80WiPgoeajmIf+q0iemvByfHJ/uaSuVNT90Z8gJCGIUK8ZHXVpcFahnwrgn +hBnaXQge8y4QD6CyVWbqfUguQdzuAHLRqlQqg1ZvfmYOWSZb7RbetrfervcK/npQ6iaFALIHlD9U +C0FSI7IyiDtTYNoiPmBPY165RbEokOmIxysfMtxG8F4l8pMnLKRtz7kIvJuGcwrKbQBsQc8rwVgL +9AAWEw46rcogvMP2D26GywO3DkBwXOFEWAeClUoYPIbIAhKPBmosICaN0AfJsRJmJ9tBhjn1QNIs +S5RUWGY7kjckPeEiooNZ7atiXcWuAbIeCxrwO3GDGisLvxF4VY0qwCE+aZUyJcpAtreyVsnzNBTL +bCfenttZ4/QktwZ0PRAR1jgVjTQ7jNRIW85iTaVbGTUeEmrC/44STC7CzDIs/nkhEHuJajBrj14w +ej1xlVJZY/SVubbTV55K6OTCrGWJ0jD7R2Vwxuu+DJqPDunU4/oiVPdobpRtSLzY8Go1+0uMoGBd +aQojfB9S+BDENXeMcqOpL/An4wJSBCx4qaNMKZ5lWlmslRZC+oaMGjNuFcmpRtPHKRdBoNSaaONS +Oo+Q0giIqDl2FWL7AKUvoj7mjnHFTHQAESIbEXoFyx9MnoDdRokGhnKIYARbWZFqDPSH9uLi/B2v +umvlxG2VlaUiQlJLgING1H7g9rYH65c716543X4xZJ0LKVylfjuxVrJssj2oW4MDSXe5Wz1SDarR +oJFsu9FM5bZf+7ePvfgb60crdSwpGnbRGwFLJ20zNHQ4AqL2ASdcaxbpB0HoE9Oo2O5gyRs6ICmG +qO2LJSfJG+mPHE3ZzPiEOrT0sUIJSEsF+FSTGiUp0DjNTL1S/XW3U6vbD08U6KCCGMNLzypQiqnG +7SeeaYGlgTxQh/l00EfZS2484WNYJKxjjPFBEoMUzaDQIGqoyV2ZWCMj5FAWk+RXmiMH0INxppVf +v4EWWMQCAKgtAm0Ca8l2ll1/znIRh4U104Wc1Kh7jSIg6N0aSo8gHkgAxFmQmrIfCUQ2mpknmIsr +paycGnEVyzfqcVSiq8wadYvh/SWEAitREZ4VF6hggUmTAnii/I4xC3lbAd8aN5eqrPPK8Sd2BPZl +UBXvw4hRXTScTNGR8D6tPWRs/CqJMTcoQ3yBpoMKU6gtB85IwJucDYtMddTzvOOfNz9JitOSP3Xz +5M+xp4iQvtfB/SnvkmZZSPOSWTREwBzzXmoQ3J4niJ0gcrHcZeZyFIpP8qzccuqo7WNUpcAwp1e9 +rOByNnQyuwxMOKdc9GaTYn2A+n9xD9UvkNEIooPMuQBZiAxBTOvNRihKXI55lkCMo9gHWgvy4RHi +6gDgtHb3a1915MF7vYOLg9maPQsYVoRktJzOWrJxOVi7nHTXC/BDob6FuAWFHxEbjCA4UGEbcbBg +9Zf6s/dV3EOxtZT0y8mB+bs++Z/OnPrvraOA4STPhiMUfTFQhJL6oDSPI5TBr1GbEwaqKyBNV9TI +JVEPVZIz/JGkUf4wNFUVjWzQzdXSlji4FBuPjlpUDMlODAKddnoSsYAfoJkxGnOXEy/NgZVkGPwL +3x4slpr/i1QW8EXEf8oF9ANymCQpo1L1q5Vy0AtgZ0BdqjJQ44rlXicoApW2Wvd8KH50xVHnRYgM +Ugv3EVYi48LXFjBYUelSlo+nq2KXRcSgnlQ9tJZi+6jlHYvcQ/14uRPM9mButWEiqs7P9grWoOr0 +CauUwMaAnBGWb9S0zdTXO7JRhDUa0U+NpPInJjFFK5Jgd0FJkrRIihrAnVdoPY28l6RGBvxoIqZy +R/Mh1fG5VAR8Q/RTc+xIbSYJwfUTJdn4w/cUHXL0uP42J++YiJgYJUFqGxulD0Jdcuet6MaXrI2b +f5f9sMaMBQ7fM28AVIGLhMSUNJAYQLJLkgtwRDH/0UJE15QIiCkCs1khL+f47c2Rbr4nImveolNk +DU3mAiyWj2KABCyhCYeZL7dCqNV8f6Ot0cVIlZHIMiXGCjaAOQMvXxeAplG3JzX3BogtZkgqoyGE +LMJXBHZYsmE3Q81C1mcHSULWABzLvSCoz9fue/2DKw+e8I/M9xtFu1lxKrDbdez+erx5Ply/EGxf +RcwGgxQZkEqFRmi3rhFkz8XOrOUcsGaPVRYOlvpFFA5uH2ze8eIvtD/z78+cjGcBHb4Rth3kmXRg +MlHST8Q2USgkhkSsnSLacVrEIMk0OCmChpNV6QXeDiZcnqy4IfSUJiNhdGLQUrpKo7YixUgIq5xi +ElIYcX6m4VEQ4EZOc4v62MQyORHKlfuGv6mMIKd6MTnr6ZRneQpiYiWBQ1LG8sJc1AmtDqo8u1HH +QpDuoD1ob7bWrqz3u/Dgiuta1CromQpePnW16zoeEx05PyIBkDVKbUWJZUVqCQt3LETOwYF9qBOv +bPdX2oODQbxS8BpoBikcxcKmFXd8JHgA1Y6WbzYiGZOZ0pbxSA0kVq3UdEK7otao7LN2D68DIGbU +1AQcIGp8Oi4SZU12Jtm31NIWzV4tHdkhbamthI/L7ym1PEwdolt0wd7y+Q0/5Pqo3UvUiRvu/Zf2 +xv2wRqMVqdooNCfHLA3R4VZRmUvJkqiM6hKQsng0pQIQDvZA+qf0okx8kxumn7dgpESuz59KzrJz +zNyiLzv1GO40kf6EbArRSM8UcTqDnh577OSfxnMgLYsNTOLkwRsRna+wWHLs3Y6kgu15qvtYFEc6 +QSRnAzGmTEOr2hYiC51+0kGCfzcOUOl2AIhtImlrpqe4qRhMobE2LHUrVkkEiwALGmd9rnL0/pO3 +ve4++0AzbJb8ZrVeKxejvrt11dm8GK5eSLbXrB7K3MIYyBoQwCBnlVzFbvXiEMx2xoFhrnjUr60U +ogRwK+7B0h1bvxt/6p8+dVevNusWBnF3ZraEwBDkTcIGDFWGaXmEJ6XhFPXiFXsMioUgkEGfSG2Y +/AzbL+ONJA5TxAPhSJKiIKd8YGQnWL644mADBDwLThi0C4k5Ud6Cn+Glk5NMER3ISi9q9cXcaZxa +GQfe6cOIOK9WQU3gMADxhOIBg2QQm7hssCiAQrd5aSvaTKK1JN5IOqs9cMoyQADa/WuXrvl2oYRy +IRLvBvhRWmD3tZ1EvjWCQsqus0BtGSVJ6qCaJmVBklIIg2q4FFoHI/uY5d1mOceS6DaUulxdnW00 +erbXQqYjs37ID9Ed6YxEORnICGPeFAY8XL2q4KlwIFVOxA+IFSswN9JDXsLpE/RXmSOa6GnT5p2G +/+ZfOrV/m+/yOuRucuckKdjXKI5cJG8+nGCRfcYj3q+/1fE79BE5gjP6WTwXY8QhLRaahuLffCe+ +hC2oWJmdN9CTqayRknK+3ZQvCgOUP9Q5LrxORHPDHjJZjyJbEABvE+X8SAQlGkKc43LLH5tjaIq5 +WWHTyBgSwq7CPcaL9lrsILBGwVY2ysRNjp5JxGVwqZBdsgKqfvQUxgmqFGHOWkHYZfwi4n8QDIlI +FVWf6KCjh4loObT7kVqDPgvaGNIG6vXSax956O6HH7Tma0jtR00/VGJA1d14bTW6drl38Uy4drkQ +9EuoxQs/ETlHBM8T4FHBhGjchCJYduK6k8wV3APFeD7uuZ16fTm5OPf7/+qZuQsogwyDxCbqN1S6 +KP7nlNyI2ZPAs5Zqgqi0gQ+Zxw48kn47sHBWcrBg5oWmyyQEMFQHcHg4kWCJP+FjRYQt/wUXFGR1 +BGSiEiMDKaUGBRP7mMMwcdJ/KXxRsIx40p1JoHCe5rPkRJjoz/EY/pGQ/h2lsWGUPy3t4iwVjqTp +C2uXUSqx398IO1f70VYctmC2dcuFEpD4wm4A/kNQbgd8XdJmNR9j79UjrEaNxUpERag0JhH5U5MH +RTUXtRo/wmDQKPhLxeKyV5xP3LnIWuj1F+OkDiQA5PMsza8DK7lYQg4jXkCx6cSGLm7Fofl0hGpr +N8wpSqIRETJvIoulc2jZPerXBHQSgHL1Fkv4zbj90Jhn09CpYcjKLbHH3OTGfOX2L/kIuI9osKnw +V+ojsl9MUpfYoQzQL31eYvqQ2D2zQZQLquFDtpBJPhJPBkMEiZIKyBsUVoXlxzgOVMTUzTCU5Uid +R8RrE6uds+ZMHSwT7ZSzC4xx3h3sCykjl50jtGKUYEwSkKkSZUY+ho/DPRrkphayPBBc7q1S5kqR +QW11UkxXPD4ihwixklBhzBTb5NYnAVB6JbFvSmNEM6cTSkd4xxOXoiY9mqGihjxCWsQcr2KX3ZhR +qVgNg7jdRsH2mJiqfUSOUkRHU7SjEkoagQ8sPgE+CtJUSIBj7dbBOMLA990HH33wztfcXz2yYjeB +etMESosf9ayti8nVF8OrZ4LNq8SoofLioDJST7I1kEAAShtAF0SUTinuz4Xdo3HxZKmxCIIbICSo +0bvrU//qmcsf2VzxCwhypqIAeUEgI3oichlbW7o6Zf2S9spQqr9c0d9I0pkVR/UCikUWnkNDKGzC +eor+J1F0HEnWmybhNb5HheqhgGemQKUF+VdNDeaBcpNsGBPwmMqDw9Ux6TSW+VQDn7mMDaTcE+tD +QV/px/MSVCSG566/iXJXwGQDVqztAX+GIa4R7AyI5N1stzFQ1UpxFajfyKIaDFCPxGuzwpWuLSkY +IsJqakLGl8gL1VgvqRVFwQdINUg+hGxkVDpaoeE7pUru+JZX9IpFD8HJZawQaOHczcw9gfxUDeFe +xBwFSQWFlp1KN5pFtWOsGUIKCNmQAO+0ojctvzqCQlYY7yXpK0YIVwZKvUcyGaU/Wp9a0xnFUqVW +V6L5CESAXMFvBDRI43wF2JlzpVvFRE/qHItWpyK/Pk73Fa308qXGVKiNKE9kxmw0+Z2ns0etd4RJ +Z7s3lTl09ytTV5XSyDB5e5f8ntHGXE/S5WKcCnvQTNhpRomdEonsMK5Z/Vve3dgLdZynUuP0Au2J +DqSMpcypNJC1MkVI2/ezdr1wn929bjSc0QdmT8mmJhtS1R+FZouoKdwRqQagYgJtMpSUjUUhNRBp +qsfokWOKY+tvtwEYcyRkl3EpT7gZsinPvcbNT8HOLexzYtKblfnm2dmwWaGVpLqwWwJqBLUfhPYS +RTnPs7OX2sNuJ7qf0AfsArI7VuEj4rPnQJmDjhiBjqLCAZMDhdQK3jAtcqmBUVk30haB2EksMUhC +Qb9cch94+L6TD73aP7BgNRteHRn/nod01u5quHW+v34m2l5F/UePRkoB8gaFR/glNFTkIvhBUOqH +1UEwn/Tnw9rtTW++EHjJ9qBfmzn03I+fPvXT147bdbwxI1JYogqqCHJkySZ0vcjyS9dLOoRKRpRZ +ievO0B2R0sSaN0LDUmFk9Mvc1A7Xo07Hfg6S9pT0jEkqO90+pHvpPhheJTxMSY3AM0AoGiTWdux1 +UWMkKQyiwiDxerGDUlS9CCU34MjYaiPnJkF1DsTAdRGxCq8rDMqCnW3SQbJNnMmHwszF6UoHAdIR +sT4giRD7mHJsjChT37eKOD0bFgEEJIMvwsDOfAzWkUMFK9hX4+YgXuhFy/14sROUez3o7OuleLOY +BGCoQiaHNFOGKDVsKvtRSymXfLa8lb4KvRUeJmq4MV0N/YU64zKV4KICqQQwPQo64JyyBvTdtWgv +1ngeBydPOvKzMzkr+53+bG/vh6wN19e+eEa+V/tZil9O11zv+L18fZ9qUN1fV3KTk34kGBRrMaKM +aZZLvL/GbslVxpg/KtPdQMvjDHu/4tLYJkqJWaoETGyQHTfdXl8iMIcGMqlyorQipXJKFBjqMe19 +ESQpBi3GSjqwLcIRx9q2xQJ0ACBzhl3gbTpR4IaBRFYKdBeKaWjUPu5FSnYBZAYccdBjBUCUuPCt +4686+ao3P1o9vFJYXAB6uFtyK0nP374cXXkhuHoqWj/vdDeQ6Q8dsRAiIATyI/giLWDAFB/4g6SI +2hrdbmOwdO9idSauFFHZOlxcvGP7M4M/+p5TJ8KlubAWR0iOhZGXIHJkicOaBEMJf8wsMUljDH9K +tfJsrIxxQzSF/GHorHylKnxmkRxpPKftGfVV6P2wAqdql7lTpm9ywewxeyKhCDIx/LN2gBqQiHOJ +UMsG2R2KVgNYVKsdR9tBCI9jucTMfOhXEVR8Vq0KHDcEJq30W/x2TF9AjzSACF/z1ZETKpA2EhMK +4QuyCFzNMKlHjC9FFU8f2N8eSgqXUE0M7RJ8FzgNhGpQnAYwSCTzVPpRPbLmQ+dw11raxrz32+Vo +qxqiiggxbNQcm3J6VUd002khFMO5+X1q0hXtQ+Zd9DlNNE03ZroZxkcPJmWscQ0Tk8xsScoUVAiE +A8GBPjVtetpueuX3P1YjMJV67uNtxRWQ0/YZXE3sMKk2BUx/uhhTz7n6zyfPG+VAI90bC14weoQk +X+tPL9dx3XxuklOOpZnscAGT2iF0MCMPeM4AExHTCw1TYkYVWjtpqst9Y7gpQLjA9QTpBJl9kP0R +0uIhAaATo77QQGoUi+KIdgG9xgKytDrSFCbkD/UToTUWfCQ9AqTt/kfvv+8tj5QPLweIuJmtI3bU +Trp+54p17Uxw6Xlr9UJx0KZWwSQElg4S1QCLJULcDTA2oUkwQbJpFVZ8az4uzAO5dYBqG+Gz5T/6 ++88vrVXKhRiQ2KRwIQrVo40kAHOI3QKg5Ii3OsHPxixcqVFHbWR6pHQ5s5oNl4lRKLNGxDCrXgdB +JxNPm9rVcieptWwAlkSi61RECamypMM/4kmTTo+Fh00u1PwGEfsAjc+IYuGjyLMIJyf5PvQmolf0 +hvZtqxV6gdWo1GF+HgQBbAywM5TKqHXhDcgbJABJo5S1B+rUMEMi2rcYqWm1BMSbEwH4G2WQAXNa +QGFij+WRIaIxOZRqG9+Zi46sVQzRHCYJ8HKSahKvdKMj2+FiO0SqjxipRcDgYArUmwoa8p6m1ozA +DklpMjFEaSUNUR+lVzSRKoyDlk/KDdGIBUUHU8J8xHieYpdQLFB/qu6Ul4Y6jNvC9itbvzS9SVtV +YjiVPry0nfjybv0WsEY1YaX0gRsMaiLSwwdM1QDiAjOXph63dpTyltWb5Ig3xLOVoubPyW/GLrju +PyNWRQLcHioDEzW04BcKsK9qvB8hJaeb+jAy4CpSjojxl4QZ86walFHQ0W4w6MWDHhKykyQQhE0W +00A4IquuE0JOsOSoZICPuk4bBWzd6N6HXnXfGx+uHV62mrXmgXmUZY9aV93ty4PLp4JLLybXVr1u +UAB/RXVblsU1FI4uLxQhAtcFsfOdwWwhPlLy76x1l4L1cmeAOvHx8c/8u9OFjzorpcq1wbW4mnRb +4OOI/gE3hbgPog1YFXRqxAatA5od+qsywsnP4pQ0P2k6ubrTDO/Mr86UL8qMqj3W/Jt6F6mWMFRX +ToIz0ALNWQI3wL/iaJ9yTm6HsR2kb6MaFxz7LPmsDzLeW3aKT0S+TS+AcIps4m7Yh+EbxRpRQBgq +YH+AmHGBoJNQIZjMqTnxG2OsRQuMvoppLoA7s1IpVGvFUhleY6w2g/00ZGNi2VRtjHxIQmAlKlgW +CtQ18TgudqPD7fhg164gGMFOBjA7SFxSToMXw+nQ4K1xw4ZxKVuRiGN1kZBTssO63A3X4R+7H9RQ +RVAxYAX6pwZSvBTH+MRlpoaX4mH7blPlQrPmdxQfXyJJYd89/JJfeEvXg9HM6ECigxEpYyykZojM +3prULRyI/JzeJF+8hb263qb2wZKV1EtSulBxeB/BHcUCxpUvAQd7H+BJTLUTPEoGQZTdQgX/AdXs +AMXG6kfU+7V4DV3naVYDkxrJF6k+4n/bYR9+ytc89KpXv/Hh0kKjMFP3G7VSpRx2Np3uerx+bvvC +C4O1a14v8qNiHPqDkFZACd8nGgr+P6D/BwwawaNJsmy7t3mFg7ZTB4SY24wPn/7x1Ss/v3a83Gh3 +tyrVstX1igEKKlu9AqorhQVw28Tto7rjLlaBPJObNiC7KJ1COjPeabSq9HGG42aMOb0ye1bGiU2Q +W06WHAqVU3uWuwANUllnkjsSVBwPlk/A5ZDvCs2HbIpUKRbu5cRavSDu92ZmqsWaX2lW+9EA8pMy +LVhhwRFF+2OmIzzLKMcpzAavyAzIYrFQLnlFsEOAuiHsClMuqRMpeRdzEZJcWQzTRX4IlEnmsVCJ +R3iwy6qLkVOOC6jRCCYGKwcKUc307fmOhTggFG4ZwNRB9PAs0gQ64B6L1hAr4YMaJsOlScU87dAU +gVDBjFTlJCIdcSJgf+a/o4LU9czGK9f+8RwB91GxhmRI1kbGEatI7lQRVQ8RGI2HW3c9RWcYXJjI +iM1GrweR90mts+ZIv0fEeuNsTw2FY6Mr95rT2EI1Pip3jgTwpZtEd0uOKg2fOoyC2HMq07C0LGBs +TBsZyrhZF9XxpCEFODLxddiN6xcVswnIpE62PJQvZIRIKyTald8LayCcJP2FLs2LwsJSqq6TIV0S +VLPUsgq/EMDYAAtXKTszvleGFtIH8prNUA2YUgeE84RVjGndjNtkUn8JhTQAzoJ8gILXx4ey/8ij +Dzzw6COVA4ve4lxpeRGlrKJg29k4718+HZ59Lly/7EYDCQek7xliE4dLchmlBlaMUscoZBzORsli +v3+745ysVivdXrC+3Dy59VvOF//xEwc3UCwSZabgVUSdyAhqSw89o8ZE8Bn6HAmZSSOjmTaZdsOk +WApZlqOYDCXLQNaxyUzQEDBczCtSBUaGVCEzJXZHx1p1y5H1pQOaKaLpj3mZbER/V9/aniswW/u5 +TSDPThecfDadkxeR58viyOzl+APcDuyTtk3fLTZLiJOB8bO13qlCgoInsssMY92WBJERVVcKYrAI +Rhl4Rgiu8R2UJIOey1whtXybKCfZi2K5ZfqFrCsaPeUyXWe6bgW9AEPHN2ZhbGTRIPdU6l4x+zO2 +SoC+S+GEdOuQoPB10vWeOmzJBVWUkDBRzWXU2Bl+wzWgWH6KeauGYgGP4ARI2TG9WE3aJvZKjb4y +qUrTDGUzFJBvIHqtroN09lO6pq3lrBXpSpggGuk10ludf7NWRMkdbST9S/9rSMqIS0iGXzzOshbM +ijaB6WZ150ifjGxKa+WDyAjGL2tWowmJTSnZyCrlA+QhqpJrbGtKT/ZazWYfDjup21D6I2XcJAj6 +JT+m7jjtwViEan5Wsi7u3ZTeYmyAAjYBvkjUG0RHAA0OuNPMw9UlKUCR1/3qOfqTm9JRl45hQvkv +93IYTCVIObPw0HO0v56nq3wXTj/WyN4WH71Ylo7SasPSRFjOTr0oi+CWQRIMbbrgUPTXs1nKgCg6 +JiOHk6AIW1yP9EniR1rdEI6BtL5CgqJ9RbAahDCG1gB6hwDGQJtAYQdBLJNKtpxsIKmGsN3Cgovi +7U7Re9V9J+5+7aurhw86iwuFuXnU6APoarx1OVo9G5w/5Vy5VIC1FSQKFRRY3UHgWwQgGda+AJnq +XgQ1coBaf3NWcMz1j5QLFZgAB7XSfHLG+/z3fcG5GJdKyGtEWiWqSTDPpGO1sLk8hIMoX2QSQQYB +LhvWUA2znVNxxSwlasDZTlcinDt23A/6e0aqUkKhM6VtCS3fabnoU6/ryC3/3e7LyLTKoEaJ4n8Z +HkV2RIACMAfkTvWjqDPobLVR0QJSCKcflkwMGdYKYHOiEL+DczDhkewR7kNiPgAaV9Kv6OpjfUWJ +5CQ4Q6Z6aRcENoLvznxCxUJNRWIxGysDw4EHA268HFqN0KoF+JAAx0ECYNORS9lqOqZSv4pczcC+ +Z3buNFhBeKkINmkpFAWC57/sqpzGMakx6jJaSrkMu0s/a3GxIfcb5XmGCQ1XQbZMrmtid784z7nS +q0YFLMPcxICcrfAxHmyuSVdr/k+RCobnbkYW8+w97G37IV/Ze6ayRn7r7LZRbtFI3qJmboRjT9r6 +gLMlYdDiNme0tZFwdLqnTML+3kRlxvx5XTO0v4e8fFeNinBjEl1GSWU1X4dgMbJdIesWi8hOS3CC +h0Ah0JBVCUUUWR4Gg2LBKxTKrlt2nSLoJNjiIAh7UYRQ/x7z4lTF0sAeLUhELC4pTYwLQUzuuvPo +697wutljR7ylBXtuBhqlE2wVNi/Zl17snT3VXwcOHAIXXTiyUBTDAdYpwnkYLwNix2WiOAOtUjea +S5IFv32sVloE9GcrdOvl7m2P/T/P9z/eb5a8gTuA15PxJoKwbtiQkGEzPhNbOa+rZeLFrZxgytyq +nMhWJ02dasG+lc/fuy3RTvX/GPko7g6s9qDk0F8IiUkyXpH4wsArCiqO00esJq3qMK9TwULNzA5U +/BRfTZ15KcSjLkrNIlZiMAx7MdbmtHPM7aGaSDekZJiyP4ieKoU8AcUgTFPVSz13ZjnyfClgqMMt +Ml62O3QNDOWPaVtmxz2lhnfDIK/fzPPyTe2+nzQmkF2vfLbv5/zxvPAWsEZqF6RXzBBieQ3WiNGT +wF90TTAJd5yfXu9w6v4bOdNsxSx173rb/FJdTza/S6Ru9n0mCSg8spxjOYrj3c9Mr+kHiuwA7UZC +OOqnwziGf4H5DSolU4YEiACpEwhUBFOsw9HIeBa4m5jLyIpYhGAgD2NUCbK6CcNGnGsk+5dsp4da +i651z/0nHnnbm+rHDsaz1X7FQ3nhBGnnW5fDS88Pzj8drp23gw7DNWFkD0BobTxQMtWlVi1NCwz/ +QYSrU3HiOWdwpJjM4c+2XbSrzaOXf337zH/ZWCnNJH0gz2GF4UhLPgtfFfORSP9qKZs4rkeq2Pda +MB6uvCqYj/oyWkp+ue+76eu40AhNuceM3ZwassivWT28NbC7YdTqz880gnjgIIcVwaVSTJjrwbGR +wNBF9BW2sVNAugyko5DJ/dC9tHKOwM4ONyA+803FUs25FAF4eCJVRE8YSxHzI3WkeQ1rXgPzKIrL +YYyUR3yQGnbgjZzH7N8x65A4AGiV1Uk2Sig/GEuK/JSGCu8yitKmYafKVCdHTOzDAoCguY9Z1uek +NiDf7PSo8dIC45UGrtuCsPP7jPVo7KIxQ0W2P1RR4Tm5WW6aRF/H8v1KuPRWsEbhi2py0Q9S1UBt +/ZqJLe6H0eN6B2fMcLDjn9fb5pfqerVC7X0q6VfDfPZ51Ccx3v2J/QuSBfccnYIIIkWATsFj0XTE +mGosPlKgQ6lyBIy0CoDTwHyB+QwvFLRB6BHkqqQ3FKXRArgjbiI4tbuNqMWKf/9r73nNW183c8fR +ZG4uqZaBh1KM24WtK8nl58Pzz4ZXzxeCLkx0ZOlMMGAdRbqggTFPbyOKZUQBMshhw606TtOPlr34 +qFOvI4QD9XgXe4+Fn/ve5w4HMz2k4iFUoot3kbIuSg2NbUjj+JVK7WCDNL+oAVl0qFsx43mCmJEg +kXZEdxRf5shavxUPnRCDMqNl+qixK9IAUWaYwCTudICXk3Q3W9VKBYWoIeY4gHSTZA3mxYJxJTbc +xu0w7kJfTFBqhciAdNoZMYZ5KAKAzCtZptOQfFWc1Y47PMUSajAFyfcgXQmuLGN7hAFCccR6g9Yo +7kFVAQWt1aiD6f7WcJtsC5hNQSNVltUhH4QvqhaZrYidxn2PX7krZamb6ZPPJiMqEzYn9tjEQ+T2 +3c/MxXiTq2IqOR26nWXVq4VGbSvm39EejC1aNZL/ST5uAWvEkiRTFKpFB5X4JIyfV+3+03PP9zEF +43nSQ8vLrbb676MzN33JVE6vTzB+pFEv/ajPa9euyCNM5LvwFBIhaJAu8h6KdrHsFisE9XJ9FFVn +FWIUAgQaHE64LMEXmf8BaDgRa/gM4/hPBuBoRffeh+5/zdteP3/7YXu2adUbCOqvRYG/djl68anB +qaet1Ut+0IePWVQ91TVU66NZFLyR3kzk+IBAAq604rVW/AEgqOe6RWgThWr7cvmL/+SL/hfiZqUW +B20PQYTqMDKHuIoEH1mxWihmTIrwozL9rdvqYjjN206HTuwvC5NVxq6JR0+JCLGjdhFRp/0o6EEb +RNioi4/4VtHztF4x41phDLCSjcGgy2RMtwsYnYDJHRr8wjlM/aliWVXtLZNOuES4Skyck3Hk4QlA +kgMzBZNFipDGx6jkTIUSVgOWhh45ZNmPfaMDawZaPW0UwMWyKjzV6IPpltlxU4gElR75z9mXZISp +PxNS4A3t8n3u7Btq+0ZvygP97OFEvNHm/3jeZ4LH8ob60c8ZHdYFqUvUWLDwJ8tqs6C25J/Ts685 +uRrtgeWnAE7mXm1hTNk3PCD3024EJn/7jp9fnina6dG5wXkJOrHjNp58zrgZKosKM35glrRn9B8I +ACyrCLSvuMWiC1y3UgERiFY4QHmNOEAkKUylQsEkco8xfpoPwvx6mkCdO++5476H76sDB67ecBr1 +uAA+2nW2L8XnnwlPP21fvuh1e9BOQfRC4sxpNIRZFfguQqiWFDBD/kdQSQYzUXDYieejWjlueX7c +Wzr13zbC3+wdqy+sd7dmHXfQBR8VqjuSCGRe1xin08gN0rNhmGAqP8mcqdo4SfAkxk8UvfTXPHkb +H+dMGmfCzI7UM6fWjnD0dPOM6v47ktLdtoBeLIGzyh80BkUKhJER8oR9mumn/AxrgQUMByD5IUHf +6VuAjutstMpI2idKnKCQim+NxUnQMnDhXQSuRrCssvhYVECIlJQgkypkEudkjEOaU6hwATKxCqmj +Vlbtm0aDKjflsstFuJCADOkAFT4TPErggPw5DPRLtXKG3abx1QJTMDRNmafiWZSXUuPr6PRJkI0M +ohGVdBSlQ9lcGsOuKpFScUwInrHqZsmseWFrN7K2MzEwS0gfLfRUjmxlZXftSDBV8ZsIyTM3pQrr +yJOzhb0LXxxui71fZIx077ZK88/eu8Ebkz5eAhK7Q5NI3hhqBYTtENKSiXxcQxrLRZOVBrhzD6le +iCwolOhmgAVhMJheJ1AhEuZu7BLGsrDDlIyShHzQxFg3h/M6+sPkxEyabffJVMbXUe7vSW1j8l2G +PdmR9N7YTE4YysZ6MpYMk8YyDMeAE6lQxsLhmGnOagRSfIoQXglKUZR8p15GVQyEKsaiMjrhgDwQ +mh1Zo0yj56McU4zgRtRO8Ureva++++G3PLJ4bMWerbvNRfiSUYDP2noxOP94ePYpf/1qCYWOWXmX +DYhuoEWepSNEdmNBXmQDxMVCt5Z0DoSD24PC4aQxD9uuFZaObf1mcuafPXu0V2yjiJkT+W28hwdg +MxOgLySBUakGJMXIYFk4h2o5slbZe0PxDAXMKJEG4osqLP9yMCULhkbGlFpxJdMZoAaolH6lGnwK +cTZUZvUiZVbshoqF/D0fHK3gZrLF0tNAn0tv9BxzBWW7I2cTloqJklYhCKeWL3i6OAF1ipNg6MrO +pLiF1tfECPdRUKVaBcZ3f7ODisYoTmKAbuhWJH4P+CogO2BzRU4jJCiJ02cgswrGmW9QqyiTLghS +NJcLJ0ZGSyGCUgapTNFQ1RROQZuTG2XAjDnaRItiAebYB39D7ALDhWRkNLNSeLMU+FRbtrHpsk31 +fBo7g04npXR8KYKAMR0S/c5MBQZeVoPJj0zlJBENjSWSPlGuKwPco+RNd/ao6XJc2RwjQfLWw8nP +SXE50K5Uux3CeOUyN8waS7mj6UN6S2ooUhKekwWHF2RCQNZ/swDF4Gd86RnRGq5UTTpQ4q5MYhfZ +cJzgDceKtyu1lF3AXbfPNm6MiA7fYn/3Xx+8uMiEiiMleyM1rUAZ0IBpE6+9v2ffzFUTZpabaexP +0L3Z1iVmpSgIrJNHPEmn6NoszCR5/6SqnguoFBeVHGDDJNER4gOTKAIzYkBI269+zb2ve+vrl0/c +Zs/OW6UaVItiuFnaOBecfqZ76lS4vgGbKQJuiNWJ0PwBIAMA/Y1cHngM+/xELoldAe+hHZaiQTPs +L0TBsotMf3umcj5KZl/wn/uexxvAWHU8P7T9gdt2/S4TANSqZ4Q0U4jOZETlZF6zc3cwr+4932ji +pcNDGXs0XkIi1EbO1DdHfy5OY7M1jjwpdaGsxHyTeMivkPJkKGKspxpushOToGg++Ed4iRSggNTS +HkTbHaiBRYDaCCHEBVAW8S9MiIzTEprXtaP1pL8NUwKAV6VgGU2Yov4TjZXaIfknJQfJnSWCIOsk +ip5HaDwD96OuagWc0XPsT1MqWu0BxtqE549TTGG7aRxNxnj5ehLfw37wdUdspkKBlRAL5ICw95RD +3IjQvC+aMe6N3NdNr1z05TcC032N6mYYMkUNVxMBUUNSJ48beE0j6+WiMLXx7EyD0YaWhBt4ystz +i8qZN3nuo6vjYzY6YGp5EuLIzqjaJqYkzdMGe0KdQitCEYwkK6eJEkIIZC3ZqK1YLDulMgr+kVb6 +KGEUBPc9cM/Db364vDzbLZW6BEcBZurA3boQnXsiOf+Mt3W1GCHeA9BtNpghY1wBOAcwuwSWWgS9 +ChSr0GcgtyCDo18OwpnIPuA5ywVvpXK6vV2eOfHCj59tfDpZDMoogYXQHT+EoRYZHxIFyYWI1xgl +gJnsKgVrlTam1G+HISQLfHnE1N3njzxm9JTCz/DCRlTXeUJnH9FFEP5UKLp+yUM1Yr9U8Iue58Ng +qnWzeDKOhnnFw5ODlbJGnXr86Q9sbyuwNnqoyAGDuiKJa/0NsE9iODDpn2wSwVibyWAjGqDUVctK ++qwvJdWfiJQq8QQy0JqmmhtyUQYyjTBVEaYt5vyciJSSHVkCo0ytpNpSAZRFoLZkikv59DCVIMS0 +INxUYVdzPdhtiQjnzZEczoLUYk813rG3UZ1rjPqNh+pO0IFdTJpTRmicxo4uj5HVkuvutGF/5fe9 +RmA6a5RIXw06NUsNsTaMRJUIf+QTE+L6Vhx5E9Pk51vxhJevjb3fZT+/TvZ1bHuoCWiPkxYfcRea +za02GZHpiexJtQPFNpiSobXSGSgI5NsAKJswk0dQIkuu0yhXaiWwSuu+B+969E+9rTA/6zabbrkM +naNkdYB3Mzj7dPvUU8nGNR9Q4w5LaABGBzkawJTHGeJDSL0DSfmSquxGdmHgOJ1C1Gsk1qJXmPML +tcImLKnuUvsPB6d+9tJhpw5TF1JIBFoa/JRArlyCQgc1a1acTEqalNnJMk4ptPDGnRmksZamTksl +ySN2p5dhjfB5GteZnZJJLxxKvHiiFOZOycWXLBr5IHVVpeSpMeKQoI9RS30PzW5RtBcMvRegWJVl +bQ2s7cAJiORgNG5D5g3x55iiuRh1ruL1OGIhSBbdgDzlgV3A9QgHJB7JJNU0BI88EuwzrV02zDfK ++8R2+ZyFskvsq9gHJEuUZ2om1chYQReR8FTj6dS6BanlmsI6l5lmWQoHlQkXJqq8PFsWuwhQO26o +m1oTYy3eVFuv3PwyjsB01qgLTeiUrDzKblKNTVYh8VfE+Jw/bqD/O/HCEelTtivLz2bnDTzl5bkl +3dbZ/r6RD/vo6viYDRXqlAaRLIihn34aSTSjYZQqI5FwkOPIOhqgNQDcBCANkXCsALn1UPpA/4jg +BlYGZtm684HD7/6Gd1eONEtLs4VaFTWr/GQz2XihferT3dNftDe3nADINglujLoB3VmItAEGC+9G +ZWOij/GxWDGobeUm/XLcnrF6y0604llIZHTjziBpXGxc+3enm2dh2rMRLAJrIyrydh1A3qAsFhYc +0cRA4KjlCHUUm2lqShUpIac17qw5ZvMyObYjasU+hv5mLqF/QkJYspO1EKEXonwKqjsVnCI0dSAZ +5U+idBt2Y9inyKPm/VVRGj1g0YQhFO/FKGDJw8BGdpm5Y9kIs2n1kw7yTMmdhXGq+ohpZ/wwDKOA +dIAzGlPXcpztKNkO406YMCQH+A0MzAFjBZ/Fv8TLIQQ9bQSSuCW1y8gj5dRqKCMq8JhGPOqpM06o +oRho2J6I5hLkowzP0AZGR2RuHSOh07iaTTVVTIMrkM7ZblqjeBgz5AGBJ5BqyTmHnUoQ5tDHjcc3 +0MI8do5v/xtYPONPGcoOE7Ql71+8gSe9cks6AnuxRl1+5sCqkDUlwq3IbiqC3RqN8ZUJmT4C41rj +9Qz9cH+nWpaE5cCNGAdh1A+i3iDqgykCFk7K3BPuEglvsbXVbh+788Bb3vv6pJFEdWiRFSRdWEnL +GVxB6E2ydqrQ2ayjGIPn06IHwokWGCnBekJI06f+iZyAAMSS1BdA1IEX9MrhYN4KVrxwyRlUrXbU +q5eWzv76+cLvbh/2ZjvIGCkikwSxOl4fPlHa9wBbQOVWWUDmQBLKlDEIE/VoTBjG4jYyqmNEThtK +NYsdx3+nxT1CHvWulEPLHyO7ZoSQmjcgDNGolAd2CAs2+SLATlEsExE1UMBzp6bXKeSfpuNktFKp +8069Z1aEURmlCAZ7KbU5aEboRUmPqLN6gDuKmRfslymCuMu33CKM4w5KahagZ7bCpBPG8BiLhMNs +DIRZaVAMFUe1O0gOBRm4WD71lFKt5hRxZvRMSUqmZYr9QnPSybWHCqWOrchDQnc4b2DDOvoqCokx +k0MhZElzJU1Ujr5mXlPcRWvccRlM/3JkBUy//JUrvgJGwP4OJIGPHmM7TeNSuRnE0YM1BznRxCfp +tkqLiWbNTC67qQtRbSD5Y5cNv98xVZs+90yObuxHOZjqDNghZnW07/vr+cjrjt0i0S/DN9WPozxh ++jiI8y0OCAHN8ArQRCRL9PwAaKVexWoUnRoC/UMk3idBPw4BNk0wcc0FJzA5/YvA+r7v2Ds+8HZn +vhk3m36jDlpUDduF1sXw3JO98y/Y29t0OcI+i2EWvwzoatAPwoHkMBK8BuTL8awCol/jhtsrD4Ji +MJiP7buK3msq7oqDKMk4mTn8/ImP//XfPn7eriQluDuDdhchtD0pQ49gHCRIokAE1F4xq6aHKMTi +0yQlhBqJcpL4Xcg0KbXoQizxwdAURVTntbxQwhqptQEIjbBNvB6XKQKKxD1KbrvGTUpIJOstYzTZ +DgSGjOamNFrdh/IXgZ/wKJpq5dCgJxUyxUcmugmzYYbHpB4zdY3tY4tNJ9cSN5xbZtrh9KDGiVFw +4Gjktp+xMV3+fIIUSZTKxLIC+qCas8msWRBLM0zVNiGlqDgD+EfEGl3AeqTKbipY6+U5qQI4vnq1 +uZEJspJCoajijJBP9UaRNyAEMKRalUXxsnK6+Y0JqlQPtf5fD6JAUQs1lnnueg2V5U8yhRq1KjQk +RRSAkSy/1SXwh/8zCoa4PJU3y3SPMmN1iI7dIpU8zUM01omdyCWCZ+QoP1Up7gVuTHWbtA+mAyoe +pMeILzadBhkkHQrTiGg+uQVgor/kXeSN8n0wXv6dpMfsuRMf8pFQ6SrY/er9/5KXDXNTPGxgKgPS +S3+c5g9zTDeoijgmsp6urXEWtv/+v3LlfkcgT9Xyus5+7zcrXlJphNxrXAIZAdFPiIaqgM2sxAgW +xngZ5mYXwBnBzyD7O87GoHfiVXe88b3vLh44HFcaDF2NwmK/5W1c6bzw9PYLz7uAVok8j24nA4eE +AFSYVGEcLFT8YrVUqpUrqE7VLHszsTNvWZWoCAtcFYmLTny4bCMq1fVafa/qHvzcj3yh+VS81K1G +8Dq2QY2Rp15kMh54N4gmnJghAyAZVytZKIyuZV0mjejnBVJfOfVACZHUU1yrNP/Lr/iK4NoSuGJO +3MjMFqnopICx5IlSXZdI27r4xcWpKXoYLQF9ksqGbJNeBYwmeiZ4Qw6SREu+B+VPTq/oe2IalWhS +BP/izfAcE8WSZ/TXNbcTdrydFccpbY6Z6cau1qGFRZT8PoFyH10N+htO3GX5FUQ3O6glQIgPSYEV +dkSgd4bsGTohBmNymDSnVb3E6ihWPDVdlRxaMS9IyWyeAjQpiqh+kHLQwlUyMq2f1JeYc2Hos1Jj +gCbJmGyalNXlEiH0ladKw9c3Ny/H1XtQhXSYXo5u/HF+xg5a4/jrypIU+wllLxIDCg+GTzPQY0Jr +nBywHUJ1RlnspPCxP91r7FFDTj/UGnOXpKnGu86obOodzVPDW7Q44h6HJodOI0sjjeygJUy0sR+V +d+yhoExghpgsEnqoAKB0xTgpJb5vVxCDwyyLOOxZQQsWNkDBIZIG6XGovpdsdHr3P3j7ez/w3sLc +XM9z6khhtCM3bDutK53TXwivvOi01koIkYzhTYSxE+2SfbDLTGYU2ZB/8LHU5bzeoIK6iihnFXaO +ef2ThcKxQlztb9hJwVkOfz85/XeffuBCpWR5G1AXUJOYiAFQVsDTvELA8QZzC8EKxYgvNgzWiVCt +MQ0ixC+I8GT6QkhSrUEc/GTsfqpwiJivPE3ZGoNHaPejticrnKsbc0PzL8sgUrGS5FCTvMTucfmL +sVJVBP1VtEGltBKSIhcJIHYQwHObk+GZ3zmi0t2AuDm5YKCnj83+1JU8uUQnrBfsuOSFUKGARLVQ +cBYsb5GSC9YOvJWJlPsUKDiTUgNrPHMd6ayTMchXHdKtw+9Ur9pJziaRYWDDyIhh5hiqpFmMOa1R +9Cy2I2omT2HWsh7FACDslzPE/J8cF8QUwOBvbDHpXpO3NIf6QM10pvom+HZu3+6oNZpbzMLIq266 +OJS/p0rePrXGfMekkQliMNQa9SkUOrL53VFrlPGQi3NK6h8brdHM++QS3/ObvNY4nTVqzINChJsF +Z1zT+hCsfyO87fHQSdY4lRz8CWeN1zmnO10uUYxinxEkNbBGqEyoxVEEzjhKUYHSodY7rKlW2AGK +GCgay1gN4rAd9E8+cNu73/fu2QNzXb9YqNUqcFW11tz25e6FZ7rnn6tGPah89BxSYYT+BZBqg/xN +r5WaIgU1hEVz3SSAPbXqd6pWZ8VK7ikVDtmVeXu90Anc5sG1g+f/7uPN54KDhWar04t9H1FAAC8H +Hh1iPVj0mL4sODGJLME1I/EY5G1iDpNwRKWD2OGsfEUVQ2LEhAApqg/UOvVgEa+JbaQZ5jFr+mpp +e/Fg4R/N4aOmJGTcg+SnUKKGlLNEsOGdygB5meDEKmS6EmohOCn/C3JMyyhRo6zxBub6lrDGqVsM +LwQmBw0Rr0ntO4qKUbLgOEueX4czVKAjULpTrcfMIoGsJLkXGjTMAB8ZyTwhFw6WWh1z3DsvKiIa +KBsTMXWK6ihCDCc3ZQwy2mLt5byrJCQxECoCq0FV1qMAkmScQNsma8xkFH0e1fn0wV/GrFGUlVFe +KHHowyNbhfrVK6xxn1tsCmsc2zCAF+OmT0s/SDyqOvV1ge/AGifZ3tRvjDCVe4Op+3ant9VofB67 +aI2qEuxxaKD7ngeVjb2O/fV8itaYURPzIjmBd5/TTLLEIH9xlECkBycsWF4RlarcIpLEQWygMnYt +1tnow+PIOn0wrHbi6L4HTrzrfe+uzTfBmKzZeqHklLvtcmu1dfoL2+eersYD1BgipBj4D6keahAL +8aKZUUR2mQTQVPg3EUuChwblyKq6WzNB63anfLLkLyTurHfFDZZ6y+FPrIXfd+VACe2RjgJrYNDv +I21PQlALYObCqMgMJT1Bs4hE5zOskVHTVBiBygS9AFY5pYOpaVR9oJLcQOuxEEr6GlWaY7Suskbm +KrARibHUQA++Cbia+VXWFO4Sqi2JlvRbigkW3keOpfRImIFZgdlSJAGX+zXrXLbS9S6hsR00dUNl +Hr1pj9pzJTO2ir2HORgvhncEUk7ZsWYct+l6DQSyYrTgFGaqPwO4xK5NVAmxWUtJDSl+ZmiFvLvy +URlhZV+ioI8ubxVEso0slnMRgCC4cH7Jp4VicBrIOikhpbhxafjqsOCP2AYyfc8wTvUu58OwxM6g +pEP7o50QI4CRd748fI0KwDI6cUPWqDKh2QJ60Su+xqmamA5UnjUCKG7c3ThG2Y2XUUMS1LdrsOO0 +NbUvXd8GFG13ZG4nGdL+GEz+uaRSKUVIbfHc15ldfrg9du/u8OLxplKLjegqI8dYV6+/5zsEGWYj +qjKI+TMjuznEsN3ehdXXyRpFxwehQriVZyFhwy/AGia+RpgLe8zlVl0Jf6EM9fE7Dr3na79qfmXZ +KrjlWs3zEq/bii+di88+u/nMFxaRtgHbJkJjWIuYLIv8MGIha9on1fLGoh2CiAIlwnFC1DUuJWEx +6teD5JA3N18ozHjnnH63UJp51r3wH88d7sIPWWQxx8CNAL4SAzTA7aFXgYuygUiQBGAAilohp84G +shniY3GCZ+MkQDX+BYRAZONfP7B94JzBZRrZfujg9PA5pL9SPoMngmKwngirilDJRK6KoKcZQyxf +h8SWbE4cqPQp0peqCQJSlFQWv2Qgih02pVCkz4x3ErQ58eGZmg3Ut4z7TaiVMXVNE4z2Y5OfsudG +bLY7Xjvmaxxft4hrJQ6RhfAsqM4iSsD56wKBAR5lEGdEYMHubfJmpS2RAdS7Lfoc3wIvbwQCpdlK +NLjfhZQYn7Bs0iwlIYuB4jOFsnNpaRgWpS5RH2UWaNkWHkmjd0rJjE1V2ZueKYnKeUuobo6E4bB7 +aggfEjbtbnZXnsfkqF5eHN+VFI46atLLhhM91KBHJmIoaWVzqKFDo/M7VMx1iEfeYeTR2ZMzCpxb +bHk6vIOXZ4zcXjfhzw/O9PW5f6ay86BfJ1fC4/JAcfZfh11EDlHI01ArWUsariT+cVkfu/RURPlR +PjfhQhjR9nXqrnvzj3LTkdlOI6yyeK3dukoVYrf30E2QMddd52VkIelN3GKTrzhsYULamKK6ygCN +93OCck2ZeVo86QYGH4hR1slCAn8RhRutooeUtcTpR8kA6G32oGs3So1et7896N59/+3v+rp3lxfm +AaDpAGfTiRrxdnj5zMbpZ5Nrl7xBGxgsMMQi1EaK8UHZCgVtlQEbNNyiz24EeolHE10OhLPoBaW4 +Xe16i/HgiBfe26jPxb2Z/pVk8MDgwbP/9LnS77aX/VLYHyBIJVs1WFFA0THDJ7oCLZ3MDaDHSBmU +JslnEr1Y21hECz+y1iS1ErWs0REgNUQZWSr8jXY0qbWEoCCqIFzh9P9p7sHQd6DwoCpCMCFUVBTB +OTPtqJvTREsKjdZdk+msujaypaozKk7NKaxx7ILJtSjpfiNf6/JIlTERCadVvNmB9o3uXGVWxsKs +Sx0IuEz6oY5YL5WXC+5MvwfgXRARgOsAJkJFB6w4TBqjvagqm6FRg4zywB2PjPFk5mt9I3qa9enm +X1E6hbNmA6Ubm6dWh9U5SrcnLeTyW/plyhdlDymGHJuTMTTBxtlg5vrKSOn0MDZYE5OsbWtCyzhp +M/OeOS9posgmy/inzcuJeZhPSReNiWs1NnrzbGMy3nkU9Y3ol939d/6iQ7rHIWHeN3uMqmt5Ncyw +xpt/BCdO/HoaSzBqL9Sn4Ne96HP2kiMRqmY95UYga1qHePKCmx2tW3z/dckGmbS12wcZ5z0PEiVz +GgJ8i1/o1jUnBFKT/kWBHK515N5bQWh1gSJdLW+2Wlu97r333Pamt72xNr/gFOGNRAxo5Ifd1sUz +2+deiDZWLRSZgp4Vw/+XwIIK7x9OlGBkokPq8ROukwQhyhyDM1K6p9kVwAIlbzBf7B30BtVBt2Ff +SqJG89jF3z0XPLY+C0gVgOVAO6H6aQ4Jqk/FYsnlIwIMQF75L6BgkHjJz0j8ww/8Eh9hzBMFRG9L +8cJV0JFgVLHy6YkBNj6l9Pr0YXKv6tm5ZZAS1ZGJ0VWiNpWxg98rLUsVDt1EX27Hjj2f7GRevNfh +xoRAYe4Gg/ZgAMXeKhSIMBDBnCBZFiIcZIGqOwzQtK9kZGXAzL+ZlUknaHgYoV35ZKp46mfOfTro ++iGbAkNEd9ro+7S8fblN5Z/4/pjguV3GYV98cexe95EUupg/GPFuyA5lDWVVtc29GbPc/4afpAvX +rzUqKcpI5nD9yw/573dfJznjap4a5j/L3tjrzPXcvFZ6y/C5Y/dPao3Tl/LEvh0fsalcXq1WYBkU +4BGhSo+QQEiDxTlIZwSHc7xSqz0AY7rn/tsfffvrl+48PvCR541rQr/fStYutV54Mrx81u12YY8E +lgpYY4QAUFg7GdkC3siKt1S1qc2BJNIDB6ZJVbVgD7ykX4wH5ag367RWEveYW2qG23G3aDcXz82u +/odTy6edqsXIVbj0eLPkNJiTel8qguhXsG+qP1OTDkVvMP6k9GKJOxWCKvKAFhQ0oTFGq+Btqj6q +eiG3DMmw6ivSvlBanVoKFal7LE2e00uGypuh2GmmuZhOtSv5xZ86uvY1/3vNcGoKzF2jaqNyfnUj +jKyhyYU9VWs076BcRU5yRXHBcOY5AVEB+qKPNQVMJaa3DPsjWhTJh/gxjKRgGJSaXMfPjLtJ4rRY +cGXWskTSlACZh8gayHQxw/mUJ+sQSA/SMUlr+cp7yE8puRuThrOJm5wkU5pDSZHSnWEj5qnUUHN3 +coUYBi63DKUvvWh0n8tfJtAr9xTT3phQsPv60Kove9Ix7f9O0kHa+UmiPX3VTrli8gVuusnxQRxz +zeXluunPGq+8kbfIc9Er1ZGBSWP5RoZ5+hNekiuGRgZpPrN8ajTcPocAlw1z2nb6TCq49zlV1p5y +/0syODs1CvrDVD2wRn4gOBx8jmAdwLMRvgi7aH+rhxiS+x+++83vfdvCXUc7xSSCAmAFfnfLvnim +8+Tj1uWLRcTFIEYfZlmAow7AEBnQIvWIGbwD0FTEqcIhCI4I9FWccAwCxabvOm0vbHn9rVK/NRvb +TQd45ZEPrGp7JTiy8aOX6k9Yda/UKbqAWkV4h2ZTZGfOTcR5ZzgI4sEUx4yckn47Azku1l1CC0iC +hVjKhKspGLZSIzGNZiZIwx5FtVPOZQiJ+aw0YcjUDB823+ov2V3jy0E1nfQCwxfpIVPSvD/1carY +s79FdNMrOfXVZY+TgtSMzLE8VAix2nGyGoaot9KDMASTOFy4MGsTE8lhlUckzsIWChO3IC3rB4kW +ljRG+ZA/9UviMshd6ro28LJm6MSgraGqEnqjbyixWTpnkmkzYQjU8eeVKYPM3miMAWQTt78R3s9V +Y7Own1umXjOVxuRsW5mRa+TD1Ef8Sb/A/jbkKqeHcZireKmCm6RojTqfpbDQnsfk0rwVvsZsxWdc +MP9hKJXu0bVRT/gNzn3u7cxrSdzBy+5rnNp9cgTGC1LdKzL6BoUXWfiHcSfgajR/Adntgdfc9ejb +Xl9eaialQrFccsJuobNhXToXnTvTOnu2ALOqz+xIAsprLWTiwTC5GyI9WZXaKPkXAFMRFwOV0oaL +EZUdOsV+2IziJcs+WZq9rRHMRVeLLa98aOEPKxf+z8/f2ashlCYpJKU+QM7J+7IXIv2DjTX/ghh0 +xkeqAC5Q0SmutN6GP6nESjAq8XHkOuqwDNcRIx+D/iWCluqOyn8O0XAY1SPJG0JtNW2A15ORMoKG +vkaG30peuxhlJQKWMaosLCWOdu6XzNdoQD6HDvhJx+HYOpzcL1KseK9j8pZxXyNnZTzCbqzF3RrJ +sw3thwn8YCIXEx402ohTEMUos9l07TnXRdhqGTFUopFJAUCZLqmBqBsVDkj+K85H3j6c79xnnc2U +vWmyTerJN2Miv8r86jzL6LNunnBETKVpWBDx9DKqrykyjmlQfONpF9JgBZV4Rn1sed75iq9xKtXZ +c429dL7G7LHMiOUkmsW9T5XJ3D7qa0ylXnU+a4MaSWAEqBQuSyIOdjhfAr17t/HPFETu1txF3HFD +zKTrnb3rvv6lEAOvuxMqLu9xirlRrGqIqRdyBgcd8w5hmWQBJJaefvM7H33wDa+pzDcAg1pCTkc3 +KG61wgsvbr3wZOfK2QqrFtm9vtXrx51e2JdEsDAC6iqA4EI4FPtwWCKen2EsMKziX2TsRz2WiQfw +HIL6k6jsOTMFd8HpVJMt1+3F1dpa5fmfemFlswJgGvLW9gBaI/2XLNPB4B1WscIZAn91eDKFQimW +Zn2DO0rRQBXiyMkkx0CXRQpqIzRd/oSiwRvzK9XQP/NVan4y+mI6GZlRKvve7IncBbJlpB9K7TX9 +N3tWPkItM8/cwGSP3cLlrpUPdzmnBWFk5GOoXkz2yryS/iBjAGgfLCSat0XUAAxRz7M3o6SF/H+p +wwNNUVU9yZYREL60BDE0SJEzWD2RyONy4gP4TQZHrh9YtACOajIz/JnWLh4WeCJYOr8XaF65RYQe +wT1CN02+jaSo6pk2soPWuONbp3aEm5+oV1r4Sh0B+1s9RZcXC5asfgn/E+mJ/5LOjClEY8Im5bZR +C8akD2OqG3Q83m6E32YBqGPsMBv07PspbHo/WuOkKD02tzkdmo8T7jMm46vxZq9jX4g5Yw2MKxI7 +PEI7YvpD0sNai30kNJbtahmJGHYBtCpINtYHzWbldW986PZ7b6sszluViuUV/GTgbVxKLp/qnXsm +2FhDogfy1sAAYSplMKoRyjWCkyYFBm1y5kn9BK0GSqqrBR+sktWvxJ2ZgXfYc4+X+ydceznetoOG +f7T2k73Vf3f6Hr8OrbVExTPulhCrAzRz6TU9RNS/oJ3kX9c4B436ISxSeqEkTCi4WaiqNdIpb4JB +BOYGX4oOIVY4ozWSzYv9DaY/XfCKm6OMTai/iYOVuDfRICWLQD3+EihLRig3pl5J0xkTxaoMJZsn +0b00NnL0mPhiJy/gyC2Tptm0jXzbwyHMVr75WZUtmpqHzU6uZPHYGpVRr2QgoNGS5U0oa6G6I6bS +mnGdpg1gXrsMUwWQB6FWE9wPKEwsqizSO9NIxc1ohJj02WP+IWP6HooXxiajryMeSiPFc3KVZqkd +nXMieUT6vjRp8GJRcLNhT195YtTNc3X3qqKsR7af82AxGU0zfZC+yQVD7VY/qng0nHf2k7tUVrX5 +V1aw3MoIVf2Uy63M4saoAY+bubPJ1Z6IkiyDlNMfxoacinju/U0Lpp9mUUwhYWNreJc/pUvZQMrg +DJegrECdv5s8cit9oi0ZycnJ3umJoxiqKdwkN7yMudn5ugG00dFTJz87b/KlcrebB+4yUns8Nrvx +1vVlXy2xSztFE+zr5uu+KD88u66l4SiBvIJ8wzfnA9KNAhCMXczrX18fzC023vned9x+313VpQWv +XBH61bO3r/Yun9o+93ywsS7ZhEm714cCJ446iaSACRGhp0hqI/QqsVFUQWCeRhIP+C9K4LKqX+A6 +XT/q1ZLBnBvP+1bd3476RQTDolbHr51fDN2ONQjsYMD0D8TKEidzGF0jLsSJwVE34pC+pApdynxw +j2JySkeH38razcLxdRea5axcUJ8kJFXOlO6kcTdCbJQNU/lWCoYzg8jRiBc1qKbNSD/SUNf8djFh +tONTOba2hZqPnmMDMtZAOjLpnpWrZciG+KXaokmI1w7mo2Z2Xo6mE6lqbNRhdFe1VYIPQsSAa9lK +tpJoMw47YmNXwm5GQ2zT+lkkj9QvqPtc+dbYKVeOKNkjs2PWgd6usoxMR0ZqxT89XC7DOc3F8eVF +r+HL50mJfjZrZizUIb1jUk/YlTaapSMzYpaa0A+z8tInmZaHL5z1IpvLHSmkaTQdm1R42oE2Zu+1 +96K6bgK11w1DiSHdDnr1zrNwvY/ObYfxrXS9TWXXS0HtFDlBIwGNNKFDmzHIjNjc0Ifr6J+hKDpq +3IO3cASndmMnipPSzJElvFdLt6SRqV2deoE4Gm2pMgU2ptws3ljrLB5ovPGr3rRy35324mzSmIk9 +F0ka5a3L4Zmn2y88O1hdY4537CHiBpZNpGdIcSnG2EgpeqR8AMQtPcMExRlRtAihN9DAaE9Lor4b +ddBiJQ5m3WixFC0W+sXB+iBuREtbv7pqPxfUfb8H4DeURUICACgsImQDMlpzMN+e9tNcuKp8Hh5q +7heaZahhJqfLtwQ4GBrYxR81dCbluOGwibSplCylFxnqLrIuyfuQk+6g+02dkZu5gMp07ki3hr7m +DieDr1K8NgL2UL/jNwZLXf4c68/YI/jn1B5LNBT9r57Tc+ytJNlO4naCjFnY7oGgQ8gAWqHEpInB +UyMqPZASIEZRg5cY1HFFtBGol5EzdRuoJGbST9XjIxx+aDwWkSVlZimvUfZ2s8etaeVme5G7f28y +cwsf9Ce0KfchUE+ZdeNfVHIj9gehMjuEl0zyy/HB28eeGr9lhMZlnD8LPc0LHTczVVP0atlpo0rx +jUghmf4yzlZ3EOHkbfZDhSYGeYdx0Hb0X7qCWGLKLnhuuVRArYr11eDAofk3vP1NR+85aVeLfqMC +b14p7nubl3unn+6efc7e3CiQ0BUYZEjAE3gLEd5CFgnGCk1SMjS00roAmRKe1A4kI5tGJ6S7wd/j +JYNyEM4k3krBP1i056zVeLtQmiudql/5oTNHWsWCBbNu6NPsBcUD6y97kXSscyK/Kk86RqKf6ZLQ +5F61k6jgnelzXLFGqhO9SJUWtZSKmpgqqKLBkJimxqXsRrRP1iymqVTdYSdU6VElSINxZLDNo9PX +GFHdpKnsF2M+myor76Q3j0z39At2Wrdj3031L5ixz3GFERemmRfOO4AApLw5R1wZNUwRBkaBaf98 +Mot4GMOq0bNNGakdpO0RY4xBRpW9qaFY2rGhccAIS+yQaPvspoFeUPlHbhwxdqlwNfaljLEOC/6R +1cFDH6SrMJuGkWU7Mjm5P5RPG+OsGjgVGkPW87A99iNreuhLyMkmprdZJ/I9T7uWdlhomBhV813J +LcPsddLfpwtBu73hlO+Vi+TGbYyS35oHZzN1g71MbxtJ3tC0RYMfIYTDSOTKL1ORe1fpdDepdfT7 +6+lxPisjm//raWCPa3cQjEe+IrbantENO1bdGGs1Vyk8XzV85PMtep+9mtHkeASzsLB7lKxvhIdO +HHzr+95+56vvRUlGpC/WHKsWbLrr59ovPr3xwtPh2mqBEawe8viB790fJEGAkJu4z/LuoiaGlsTd +yJ+wncZUFgdAJk/cAfBXbRcViwGL0i1EvWJioYzVnJ9U4lbcjqzCwfDg1V+9WDkHquAO7AiGOILI +QWOFn884u/cckqF1UZTHIa2S1aIShxypFY9MNP1sHDXKokbUz+yWlIEpy9FrTGumTbEq5yxZjNHd +6TDaZV7JvRWTne82aT9JnyTlpGfeFKyfx/JhJM5zNENmomNjT8nxRHPpuPJqKBvdzrDVg0e2IYEl +Mc5WiqJFX68g4AqpETBbJP4Qh4+nfAZPZb5HdjLxI3dqAU4TDKyKozqGNcRGKFhOoZdQLbJo0XtV +VU7llx38HzmPyNh4ZBgRbEhbm3JMyiPyTc5KfytMiNomtk167mA2mOiqIviNGB6mvc1Xzu/T6PrU +idvhVSVeIBM20gs0Kno3oKAJP8jkDrqBQc2Y7+S9+cDUG2j5S3DLLltkKD6N06Ap2uyNvAJkbRhB +Ae0GSLf1a4PDR+fe86fftXzHsaRcqNSqHrjZ+pZ9+fm1J/5o6/QzhQCVg5FiSHBUQOSgSiLS9oOA +uDa0lBJslIWA4Wfsx3E/oR2VZwTYVRaBDB13gABUz277yXYpadWd3kIxXCq26uGVuOO4FfuxbevX +1o53ilAuCC+XRB0XOG3oGm4npo6EGMqZBqOqvmhOw/fyAQjGZDqiP5EGSfEPw+qMipkOnxn1IRNN +WZ1pZKeFPBRIU9aatqYawfTFPzbXOfZ6HdM6lWmNtUWeAQ0+F6Vp9K09qUTmUjGOlWkvB4YHdHrY +25EH4wH6NnaxWrZicMekjdVCTowJF74p+qTkwzDlESYJBpfKqQ7QvU+tsmJAenW2zPCrTVWmIrN1 +pd3OaXvXMdSvXPrKCGAEVO7IKE6q7RpXzs7baChMiZFrUg5SIT5/7m+sM4YylomRKQUj/REhjhvK ++CwkRUFSqqQwjghJaS90J92I7LC/no9eJQR6QlQc0YAneed1P0hmDe+fJXAoiREUGDEcRrEfFypu +KejGd9558E+9/91LR5aAa9oD0rhnV5x+ePX09tNPhBfOO+02UMRRmyMICHGDG8G8iBwaAfuGdlQU +tQCqHDjYILYHoT0QbjZA6E0CXyRwdXAD/Iy4JO76Sa9q2XNJcSZJ6gjGwURUmu2Fcx8519ywymKg +IGUEFCtlf4GTE7VRgWzUDqaoKiOrUkdH7WTGDqrKI0miqnm6GKkniE1PljDhesyt0q6sA1ldioNg +TlExU7uZhv4zVz0NTZT1ZxahehxJjxVFYEiOM1VT9TkkFU6cKtzyPSb8aaMhM/qIMeFgZEOlHcq+ +HF8/Elk6hC3N/ZwN7NgixPPGjEMKrJDKLGlaSsalVUU3mHDKuWAgRxkyp29ZrShqsZSYA8s5cmtN +qWIjh2slaCIgMdJXAjYzLpxXsVK9UJhfxvDI7yVwSxI2BItJwnDAd4UNm0rW+VdOWWlGCPK7UVed +NG++TsmaUTJpnhQYwnzMXWoczjt99tzE6SP1PbK29J5pQsjwln0SilyDQ9KXH+eMGtLFYNwH++rG +7h3IE9g8Mc8vtLG79/Pe+3zjW3yZ/S1uIbMdGeFLTUnpbI2mg8kMjRoW8sO9/96NCPsS4DdEzt+5 +FR3foRULhIdZ12lnikW/j0yDGKWQEC7Ouaazg1jTuuLBL/Gn8pGbOsYAECbb0gCDka7mXTR7ggNk +rU3N7tBsZ1iyGBCoXIN/xLVyEUE3IGmlchkMrNXr3//g8Xe+983N5dkAFtZipQDI0c3VDp2Lz7pr +1yDGkxXSnYg2xIXD6FO6GGH1lMgaye+XCFB8YEK9kYgg8bPAEyJfHZ84cv2St7XgJCtB7bakcUep +tejDdTnnLdQ+UTz9vU/fveY2u8UuWioyLLUEQiq+Q7qiBN5GM8FlgSVIwczIhQ6eQN0YAsZeqkCU +ciqSNsy78kiopcy70JAN4ZBCzQcSzY9WJOWfTTFnH7U9aNbDQ4h5J2keRABQ2q1GVBzqpESPEYVr +uFGaxSG9koRz5Wdmse3syJNX4fvusQRp+RT/5U0tU10Sex5S2WnkGN/aOcu1XjemJDOgRrxaRFkS +SQK2eg5HgJrZVs3xZix7QXCJYGnHmAN5XhwWCuFHOAp5z1S5lzdWScd8SHvHoU3HQ7XDUbKDbnCD +Sw9Vctn1yHwi6ow0ybF8LGWa/G3GtZRqpfgJym52gXFJCi5K1uehYV9fJH0jHRyde0rz5LFc7sL7 +xW+tmS3ScUnekIuzAcliQXTpq/CdDY5eNsQ04O3q0JSHmD7LiA37LwW/ZV/rcuSFo6h3ewzizj+J +C8McWcbddbdyAzfk8mdu4O7hLeMp/8Mhnraqbuqxt/pmbhY5EBqCBYBkZKGyxKJSGju6QzKf/a3u +x5e6PbP2GXoYF30PCfnIzQcLbA3CtU73zgePv+dDX71w5Eg/Yu1EcMvyxtXw9NPBs89Y56/EqBkc +xvQpEuyNel8YOsjip5oIBZGhNyxyaIozUeFTXyNMaLgVSYlgnCwU0Lftrut0inavFMczXjDn9ef8 +a3Ebl9W2Cld/7vm5C5bfZz0jUDAg6zg0w4GToY6UA/Ms6ygqCqsQKq2dPTSxGv0pzd8wnCXji2Iq +gFggODdaOlHcWCQiIg8JjTA1VZR0DjmTWSQmHlsoBJNVRmir/MnIEuVYaQilmXisviFfFGVXfF0m +zVyDM7NT1GZjWtzNqa0axZdkWRn+nv5HBnBck8xUK8N8TQ6hXAppI4jg3sb4IjirY8WrSXTFjjqQ +yRIEQhM7jjKIarSyI5V/mFNtAuk58tPu40HRJ+OauzNFfQQONSxzSeR0tUnT6/Tc5C/JDL3y0Jdr +BOxvdlDHTy0j3NJDNpmum0kRbB/u6Mnuj0eKj1Of6QZPZQE5osZQcAaJg7Dj30atNgj73e4A6C8o +DyBFjVITXWonkW7tJVFmgt5NjT93noCnDY/xz1PHcOoFhuKb6BDSGpZEsN1+GPql4vYApizrdW94 +8J1f++ZS3QfvKbulItL31y+1LzwbnH3B29gsRvY2kjKk9CCYCqiW8COyKMGQI7sSyGjaJRGJSqZF +wRneIg0UZXYA4lFD30K9YuCjxrNOb8Wq3lko313ozQZrVrTgLVsfacf/eu1gx6tUCz0UTXYcVEJG +dSyPsbOIvCA2DxoAXiHlaNIs/quYN3KIgiFBBKLqZQI1v9Yi2yK2M0PAeBkFoCfFDadFUDWDAXDR +Re+R1BBOD6s+AnuFmrEY5bR4I02ponGKQiCNmtRyVTElbJVyWaaScpA07ZLfC0ransfeao2s0Skt +TNUI+fx9XbRXRydZ9FjPlc2IC9H0WDzF0CJBVsQmkMRVO1lwCrOopAnoXiwlzrRJixSaw60yti1H +NG78SGFGdSBZEGbGDUPVF8Cu1yenCqd8lztSlWxkVETjV1MSPqZao3EnmeWU75tCaeqxi9aYrs9U +a5QlYbqd3XhzWiN3Sv7t9JFjWmP6rF21RvrkJ7TGm1s1f+y0RiMjGlPEOOfQod373JsQvHS/6j7g +RiN9DyuVkovyuVLWVCg5FruGi9JSIPRqL76om2nEn5D3LaSfp76OGavcvSNuJ+3QLRpS01vZ3ORn +UeT5boc4beE73vOG977/3bVGDRbmkmuXEfiyebF39pmtF5/tbKz1o3A7DJF0j9AbMEX5lzpiP4h7 +YdKLEgSm9hBlIx8QmyOORlEZbSkwbDlQC4DUFTgAE096cGGWrahuFWYTv2m7Vac1GJSTRv185eqv +bNX7XqlY6VuI34HpUtHgcAziYGDBb4kESiCU4gRv5inup2HhTCmLDEAVxe5E8Sn1JcuVLKnIypGE +qpFZFtMc55ofNP1iqALxs1HwmLwrayFd/MPLaOTTGzWkUrIBRPvU1gRVh3yRnAftSDEQ4yJVwOER +71zeU5d+3mlZjfmxpq6yW3DBTkt91AmqTGf3Ewh8RvnTtAqRYQBlDzQ5/gJ7NWJWLXsrjrYQvSUV +qGW0JE7VMDmdcYECSE8VwswpgIcy7PKvdlB9f/nNql7P3DnW7eFK0GkUa6oqjkO11Sj9KdNVo33u +3Megjw+XeJdVtEs/7qOVPS+RR+Q7vdMEmeft/p8dH3HT0tTNvtuXz/0UgvaWYW8FEdc29k4AmSIm +7zhkSvegJOLf/mBQrZVQxZ7u/WFgfV682peX8Va873Rh4pY8RWVFO0ZqILIWCxCci36xNwDKTPCe +9zz85nc9mvj0FNbK1WLQDy48t/XUJ3vPP+GtbRQTN3SKWwgxpQUVSRqam8GTiKaIu5HP4IjyjRUS +XpwaJMR+wQBAeEVCBqn+PAdVi+1B2e7NWuGiHc5aW0m/n1iFdmn9I9dqT8TVYrHnDrpRHyZvDwVu +Ec7qgFAOBhGYL2y3DpydLAAJ6Qa8HTGxLAkpVTx4kt/DEyoeUPBUpGLiwlE6iKUFXPPMJMtCHKoA +K2U3Sm66zkUzFWPecOUr4RzKTmNB0Qy5UTld+KnopySvmq7OT8IpR2T53Ta5GgD3PvdDIKZtJz5n +73NyEaa8YtTauGdvNNJHzMiqeYkjWry15Joi02yF8boVohB24CknpRE+cO2Q4QDI36E8oVyKqjyb +SkU+hVpVY1Hm5RWgNR327Ey1KCUjO2wvwwXTN5siI0sreku+uf3My8Q1Y525oTZ2uOmWkZBb1aE/ +Zu2MhVftd2/n19mXakQgnIq+KK4mKDEoAoA69j7q2Rs3t5Is3SYqyKdbiStedJARM29qR7n1LzS6 +LW9ECBjrExp0JERSFWPiiCMKxSutd3pOqfSur3rje7/+vaEXexW/Vit316+F61fa5091L572Otul +0PIiN0SgKb2J4IJkhAxABZukx5GIqOCCPIkWx2IUhMGhZ1FOqYGBn8DL4DEEk8IEoF5xv2yFTded +LfZL8Wbcdd2Ke9m79gercz0fwn03QjkrxwdCCkpH2gxijFCmg7E3gKwmWrSSP5k7jo+J+jE6nVEl +wRfVHymlqVIdjuqdIKSIj1HUOIl6FFQnCVHVlBBRFI0SKXEchtTKZXkmSblKNAphrmKlSwm3fGBr +xhkqpFM0JTU3KNccn6xUb9X/irV0VOZX9UcU3vS8JXaal4goD9+PSkZqATF2URP+ITZmJm2wD3BF +txCzSgckXba6G9WMmZbpHB2jVGvUKVO2N+b8G7PN7Lhp9yZT6XoTjp6+xkg7mRF2d5KwI4vdD99N +WS/fQyc9+++tJ0A7tZgx/pvm/Vnrt4CyvTzvvp+n2H/J8tPrjFJltn/6bX4Ed2txIrBtbG2w+IGx +ze+nUztfw4D4nIceTisGuIFSCz3FFuofX5ktlsuPn7pQoD8DgYbcTWAhsMFB0cB/VNikRU55I0ia +iqNC8ST+cb+y/x7voYaaPQ72alqcKvqR97iMzQJ/SgpI2w+tQQ8oqUC8sf1NK6rNlN751W955E2v +g7XKLVhlL3a2r4UXX9w+82znygWnHyCXMOqbNAykKkIjk3oXLHYcQjtUpkKEODAaCcBhQUZ6S7TW +Cz+5TgL/ohcHGH4ftjPXKrrdxbi3EjTvrC4ebWzW1zed/op7pP9fg82fu3Q0cUoxcjXUjAzoOur4 +PJFkwywHHmDHGH/8JOY4mRdNwBAEBrmPKeIcUhlbrXCrJlEGDXKs8F9qbpIJzhhUparMh2NKHC14 +JjaVbFLpMlySoqPIBfriaEpzNiSvXC14UgJQykfgYkQSKVuFlEAvqJh5ZapEGaWtT3s3PCasMgr2 +o4dy04ymmA+TsaMTy2k/5HeEVO1tHLqxfZmJMmMJpDIAw/9jzjFsJRQoK/gzqJMdICDAyK+yIcWR +mLrxhN3KDjUHY5UFxX6vI4tRHPqox3eh9Ec1XDkkeGeoDPCTPDTbv8aOkBtFw+vTjvD1h1OZNTg0 +QEz8mk64DJxahjPiyQhxHbTU6KU9NeRJf+IqvpEIVbWQ5H3YU6M690H5jZvKjEd+XQ+dsvuy1d3Y +8svumvou+2x/FF58nzd96S/TmRIPgboERFeQPQmrDZM0eoN4bmam7LpF2y6i/IPjIhaA1D6KNNWR +rA/bDtxSKpVDKZLIaV00SnD3pTd/6QeDmxpJ+zA9gnkUHbu0GfSWFsof/vNf/da3vLZYcEp2uYIU +s85a5/Rz288+a1285ndgo4zacbjNulFkilAbaSyVsBSoj4NYIcIReqoFhsQqRrrFNG0JuYd1C+4i +8z3rMhI8M46LCHsNi03Hb1gtd6tjhd6g5p4qXv29K3NtZ8atCG3TUZZMDSUKbFCUfhOPys1LnVAU +Q6EBRscyH8YGXWx3HAZ1QZpURq4MpUepe4e/Cf3RSoKanYbPWhxDrueXoMX8gNwfnpKoHiEkNY0m +5SAM89NT1qaUNCUMKRHbD9NSopwlLWTqiVnkXxYLbJ+dSDXxMb1PZxxyAuq9wFCAMUay43oYbMJI +4XkZfo1YbkZZuITG5M2l4vOdYoIeG7hsDrLvlWjoIYxp+HqGngxR6NKf0lte0lnJxm1o2Npl5POv +sM/JeYkvG44uHyRW8JxQOmn2VkvNV8bhPgCSYZhNynVEHr+e1ZCGP+xlBBqPUL2x4cncDYaWCHVU +yGIfapSV3HFk3gu7ThgUPabjgWJCaEGFe4lXhOxKEywtbyKEGoPqyOunpRFGbV5jjvq9/9zPqyld +zs40qcl8IyaeKWuoWPK7/dD1CsAcaQfdkycPf8M3fT2KTLUGHTCgAtJVNy+E577Yfv7ZwcVLTgf1 +E8EZiV8TohwUYliQzh9ZXUSoEkKcafgIsWGtRHgTAfHF/EVJyBcvmhAyRJNSeiDACmrpuTZq9QFU +2ioiBjEO5u3KkbK/7Gz63c0wPu6fuPSzV6qf7R+AtxOmVFIjg9ZlChKJ2RQiiupbIvSJWYsjktqX +9E/Zc6pNGh3TGDD1B5VmeBBAwLisZBJVzU3FHdJW6b9hilxJzA1XbGsBfiL7pyhALVajbMSQq0qk +fI/P0Lr5jZhFxbtGYiB6qxJdmdg8xUilLZW55Mw+jq6U4ddk39e15Cgp5vZsKkWOdeS6/+QI72c1 +55UvuT67i/ZTgWzDWEldT0og2HcSHUf9iOKpKmq5YeN2GG5AyeIbHZDJl813M9XhR8lw2idTW1nM +ueOKUY5y5zW2XONqNt75yLHbdPNmwnZmSRKClQ6ShPmkeucObCPPv3PDmqq7Kf3QiRW2NCafTRCR +YYsSrLjXua+ZH7aHxvBH5uDP7jZ26+xRQrJzxG9kvezrmS8dfx3BUM3t2J052746+zJdlJl8dXTV +cEFrme/ajbLb9AYNb+sNr56561h0+6FktooUAZyI+2A4pRgx6d4qFrxaqVhF8V4PJj2zj6aOw/4v +2M9g7N3a9BZsq98NPbfQDYO21X/dG+77y9/6jUvHD3eTpFD0GhUnar+wffqzrWeeDFevInAlcNxu +7AYhwkld2ATjwO7Fdtt2qClKJQ1JaszqaTAelfWENfRGcxyZv+jgFKQY+A2R6W8nBScouX2kdje9 +Qs0H1lwrSWYKC/0vDjY+vrFQrDp+0ocXOCuiIfGhKnrJwW2TqocjtqlUZdTcjDRvMP1vpkcqWRma +rcSko2oMVwhNZibQTPRUY4YVF5eg/aSA48ObVKk1VgQaJWihFd4pnBUKtMKbsVSvUYV1trLdPn3y +RnWWyevJR6b4Gicv2IHI7cJ599/4fl5l72uwu4CRRJ81Rt9zIs/BCtngoo0pXSGjRj0Z4vnIzpEQ +33Sy96DjXAM5WZZ/DrV5MQilzuzMupfXGk2Yj/CerJ2xRvQRX07H1Gmc0tmbvd80n2e+Oroc7zRy +QPn5zVK7L8mw3xJfI0XAfO+HpCr9Nm/jvtH35MoU4VHEeUnYoI2MkF5xrVSYrdtHlgYP3D9z2/Hq +1asX1zbtp59PXjxvXVxjECRMq55j16sV3ysipKQfBJ0uIiSRMaDCDuUdmWT9fFOH6i57HIZgT7km +j7wxstV1l8JajKT+2Pfe/o7XffX73w3/H9hZqVjw415v9ez6qceTq5eiq1tWiPhPF+huwAFgbUUp +LxUmzgAQqUx4ASoqtWsAlcDXKCI82mFtDRo3qVdB0BfdCII+4w0RNYNkfRhx46gIldHqzyXRwWTm +qFc+VNqsDDq+fdvg8Ivfd6b56XCZocMhAmfxXCJP0+gJh68qbsS+oZVbrJ0a3Ql/JA/B+jOMRr7U +/cZkfiVeYvrWwAXhriabSrVG1fBw4jraZcVrKBg3ClHNvEzqwUIukawpETqqzRjNmKZUuQV9on2B +7BO6spQZsQmzl48HMdZaRSbUBcR/R5bQlO0wYh4wJF2iV6YeI9fkorKzG0cuGAsImNr6vi5QDX9S +a0w3gEwWlxcWABJvKZnGcSGOUPS4YTtl/IRJIvEY2Xci02QH7fFjW2rSxp69Xbb1eM3IbeaXDGdK +Znz43OyTXpdK3iMckbVicl3LrtG+ps7L6/M1Zu+JsdDPQz1SnjUU1GTBU+tUUSI9jIK7JxpOKpMO +ez/VP5cXL3ZbDCJGjq03fUT6oInaZ5PbYV8rbc+Lpr7LPh/xEvkaXwbRQFSEnDxoIt1AQD2rWLEP +HPcXj4azB7fueU3hjjvd48cK80277jsHZsr3Hj/wwMnDzWq53+1sbm1ttNrIcYDNVRscm9t9juPN +XXbdwlS2qJUK9IKgUPLe/c63vPu9XwOkEafg1qpFb9BtnT29+cUv9J57JkblRUrBQAlPev14MECY +qN2FERVORvBIVh8W4sGLCPNNHgAyxhRDCQEVLyATDdUHQgYqP5J3sXA7Yb5ccOMknkn8BnI1wl4U +Fe1a+4ut8PO9ldgPgESOG3q0keowK0abSpQSWKEfaPPOzIwUszJJSy37QjeHGapp5EKqb0rLxipl +7Kiil2YKqVEteZWE1NDKTvXP+EIMOZKeUNNkdXqeqe9Hf9eYWaP/jpLmMX1mqpA8Kn4ZWquy9pCw +pyt9rPHsz5tbfbfubpmN3Q/IPj6A6xlcBa5C1gLvhudCwujCsWFbpSKCyTAfmnefnUMoJA16urFj +8r4xxpbnLnnCcqMPvJFuTo7djbTy5XGPbs3Uu7/Dqvjy6Oa+emF/400rSSox36SyNVVKhnsLe0Q0 +KTj1QSeJniqKUFzxrWbVuv2Y8+gjyatfzUK5tm/3N+ILp5wrl/zVjdqFa/YLF7bOX2shY0FoPSgQ +olbRBAghNAFqMIKeguwC+CsZ0XPj23FSxN3XRIxfJNYmc4jpDtiwLrLLWYPWc4GMWqwW//TXv+u1 +b3tTG4mFrtcA/sjG5cG5Z7dPPxWsXvLoQ7TafWjMwEeFWxElNZjCiAGUxERmaEiuIGDaQJYcgcIh +F2TdYrJBsAEJVDERM3goPItJ6LLonoN0i6IT1oh2kywHM8cL88v+Ga/VrpYObi92fvhq8zOdelxB +hohnBQUUgnd8Opi4awgHTaWZhJIgt/IBYavM21eVURk/15OsKkKnCOc0XNIsVw1iFZ4lhFNUQM3X +UKeg2DwV6ERCT6k+klNTd0RSnZpm4V5VbgktkwA6AlHN1jSlQIBYYT2mMRmpeMLMNYBrj2Oaj1i7 +PEJ7je9p2KiE+ey5JSbVpqkdm+zzmKKplGy46mSashkxgcIqquj0pKZmEkN6D2EGoFYozlfR60Ui +wlcBa0uzLVYD5Zsx56ecAIW+sACwQCxMGumZdkO5CwDlKiJI6iKj54xCZmSdHQffRcHu1Lq+I2PD +A0Z00YlWDID4iFI4zpSnskzj7ss1IhHZORE89ZRnq9eYcHXRywHHf753kqSmG8D0h4JrXrmUqFW9 +xTxLQ4p44LXlP0MHp2lbvh0OaV4yYDtptIO5MTXSaNd0qWSLUNdefvFk/df+mD2ceyvTT7Ee7ei/ +zS5IW+Ywpq1JrlQm8dL2MyWAea8dm/vtJdIa9/n0G7wMCcSCAY1hZDAEthES5UiqvCIKxbmhjQwB +RGZiOzHGMYgKfqU5tzRIKp/74tU/+Nz55y5sdyIvsnxoNwo1Tk6AgMsE0KuQacXEp+DSt0BWuMF3 +nLzNrFdZiAMYgh3bK7irvV59rv5n/9zXPvyGR3wXQFz9ZtJyN89vnfni5VNPbK1dTAooCRSv9RCx +i8z9iNGndCXiX3gWwTPTExBthEtlgmPmU2QOBNO1VaWi4AC8G6KpiTWS9msUZXSsrht1iuGgHPsV +r1IqtAohOPSMVe8/3g7Pd31kTjrkgmStiPNS7x0VUW7F1OBirF0shCvLXy5Xo5LixJHAqpFKgmKy +NHzjNZTtAT0DlBSPEMQ8JuOIFTTlpPJoUSDFuCq0Q6NkiRKH5SP2WILpGJuoPFmplSgxkrUiuXpK +y/IBBDt+3sfcjxsMTMiuiiEaAT9VVBQmnT/28dzxS/ZuQdXYLDSUElTmFNSpIzO0Xc91kSwFyQwb +iZgOAF/CZ/6rP1kopS1/4w98wgcAHmOm4Lfu2fYm1hVWtVzO8JwcyrZEn4vtcPLdJmwuGsUqcAGp +vp9zHO5jOM2k7ziM+1fVdeUMGcOodXTkXfQV6FdAGhoPCL96crjkxIExo9hhPAjCkLjMOVS4SwZM +5BIRM+XzkICNSDo3sD5yt2Qikwn31r9VPRzpg/Zk4ky7qrdkp95rhIN0vljdHM2n/0rFzdRYqDtx +dGb3M7k38Pb70RpHRJidnqFa400dqU6wayPCFEXAIFimuIAQAp4wab1mxYeryX13+W9+R+3QbY5X +7lnRoN8uP/ec8/FPbn/hyfBa2w/tAlxr8P17knHAPARBjC2gpL0rXED8V8Z9uafWOHUmbkkAFfm0 +IcdY7Ey9R2kRsKitXv/47Yff9afeefLV93o+jJoA9O5BTdx68dTG2VNJezMOezCiAuZm0AfrA6yM +xNTgXyT1U8mmYRQfFJpL4yMA981SG6KhCaQMM//IBcBpUICPhjBQMxrDiDkDEBPf7lXieNEqHHLn +l/y5Ze9Mo91zvUObCxs/slp+Mlq2fSCIe3QyOQAegFKo+1UMamiAYAsS6CqbiPknnFPVURiJKrTQ +2CJSVSaVmA1fVFlTStaK5AueDY2XohGVFi1xRcMwXkG1xjTgFmPIdSA6iaQ/in5J7AJ+oDNMsln0 +s0KqQlBgtAh/YV7mTS10MmfN/RhumTElUgfqZp9yK+43DEj1eNUBU42Bw4NvINaovkgzOK8RlZFJ +q/KG/B+GmtSc7wStkcIR9HucuAJ/lhK76Vo1Srz4gbm0aE4ChkVCUDVJJB3+N9UbJkcMWmNez5h8 +e0LsTqFSQqRzYolRUq5nJCUSQrtsjuFDDTtJ/QV0ocrIIFFYfA5GJSTng+w+bERM+RQV5QVZTYFK +pD4iU6SkyoLRLOU61RoFvkLVTfXHjSrF07RGVQRlXzJGXAdHVUbtgPhCJHp8F+VPXzrVvk2Xzbto +4xMlwVlhNjUApG+NJoYArSb4PKc1Tk173ecc5rXGfbLGvdeUmr5u6tgPa+RWYYCkVACC2QGCBRL6 +Su7RA5XblyvHDhQajfWVQ/bCklsoWFevxJ/45Paps94LZ+31FgIscScsN8B0pN2HIR0xTDBIio9q +Va408Ib+wOsA6JOWur02xd5mmXRJ3yxpI5FXFcXIZxSdOmF08t7jX/ehrzt829FW0LNQGzHsWBsX +1559CqzR7feLMLomCcJzUHsD6mBvMEAhDmR14sR3CEcSFD2G1tCkBTshfiWPlBgTGDXpmiR3lIBC +AWpgTiNOB7qfcDMLcTVARx00Y+uAVT3izC0VCw332WJrsbTk/17Q+68by4NygW7IHpLXoNAT7R3p +/CkgObQyAUJlEji1NrGzgjVqYI7uXWHRsgdlNMFKM6VNVcmU3Eg1d7Hr0KBKRsYRo8NQzKSmqL1A +kJuwUtEeGX2jXkfJ61dLqcTvkH5IHUFa1Wl8Nt4v2mCFM5I17s22dtZy8pvjK4c1Klkc/itSgVrP +VFWQv/lvZlJTMmoIqNypYBH8NkasmDH7SEgUJw7i7axjNURIVRAmXE3Djyh/hjJqYsKerFGCulJz +oooeo3bqfPDULqRKJQFzDBlzas7la07b1gJxl2vBmJWFu5j1LT4EdS/IcldumnFQESMMa8xIjQp/ +GcMwnGmUf6jcoEZySrcyepohLB9UPhy+IM0pe7NG2YBmAciqV9VEZSDpfKZDmtcZG3NdGNqxTJoZ +k2Amw8dEwBoy0XRah6xRmam5Ri1O0xAhdpnx8a9vgDVOkZRvAWuczluZjAhShekoODaK19cqleYM +UjDsom/1Oq1Bb3txxjq0Yh864MzPN/s9/3c+eunyqnVty+72aQ+Eh4xkmZiNDMQs2EnNjw8vW4cP ++9WKvd2yz5+LzlwcdMkhhslG+xzT/GXTieM+GoWvUbUf+vwAUoouu86DD979vg9+zfKRlW7QqhUK +bmd94+yza88/3b14Ie60Kw40Y3EuJm4/QEgqeCFtplAZ6WtkaCqdZyZXj5/BAeCulUpBVCgJ2CZe +VwGe4brnoyXcBuGFKWv03aBuh/ORvxLPHnEKi/5W0dooJAfW53o/c636aXu2iOpDbUR0FmApo+ks +QZCqUh1xNIK9iouRGpjJVAQlUGJgilakBQ644Yz3USXUjIIoqRQziww33Mai/AkVkHoOknQh+Yip +u1FjPaSqhhRuJOaD/KtfinaI/abtEDyd1RnFB81ZIGMk5AErhOw1f9Nn37DGvRfBNBq8jyV085dk ++kFKH0np9EuXE8blQkO1KA5GmxByKvNtmCprxqmcTyZJ05iYB6gaEsoKsQIxFhRwKzwPkcymcqbC +GQ2ZQUYrMwo79nayUof0NOt5dpmIgnsfhjUOeZu0mNc1p7JGxSLnu6YELRsZ4wymuChjx5UkVF2v +zNmN4YPIOqq/qrPcvGDK3/L8RjASJZpa/uUHuVxOfmDBNhEYtGWdjn2yRhV3cA9fQSShdD0w6liH +OjvG5yXHGvMTNOy8Zgbkjsm5lm+G8/tlwhpTmrbnopq25qZv0mm2DsUdQj4eQmXsSqnULBYL9CxF +G61OC8GaiV/wSkszg8VZq1joH5hvNBqzH//k6Wvb1kaPyiaz+0kSsVABd8JqfmUvmatZb3i0/OD9 +9YWF0qUrwac+ee1TjwVXt0mxd9uB099E9tLNDwjijFKQZXrCwNcfefSh933ga2YPzrbD9iDqVLe2 +oounLzz3ePviuSJeDWqOg6pPLJehOKhgisEgwtCwUjF0YqRtiKlQEbGZkECSj5FgGKpk91NrNPBA +St+wrKEmehbwTpGURrBW6HdFN5xxraWkvBIsHnIG894LUb9ebhb+KBr83NbBzbJbgGl2UCKWDDM1 +wGLgSVLrKFPXdJcxDko98FQKCasq+y1lhILBk+or8qWYP3F1SkiEv8hApzGvYiPlviV7M6g9Ek0j +tjrK0eJHFCsrRQQyW1OaSpyplBkQekseCZKNAiMAN4BsIa5HWmW127A07M0a1SG31zFhIZmUtacu +vzxZ1Gfd8jAcvq8SPmMZ5fxJ4TPDGiWWmGbnIVlM6WTGHeVWNdJzHPHqrMeBhmkJh8AqP4Ttsp2U +Xd+PbZbExnINIJmMmE+nskaa6vOqBqWuEYFeE/z3PLjo89fkb6Eyp+FFUwQjRoHl+Rycrzo+1J+E +IyJGVzaDBqDwe1oiRJjjQdux+h/0L34hw2dsWcIl5SdlenLANCT8kDXe+K+JrjbB5dkFQ9FS7s83 +Ig8aHpRitH+pdouPcIOqPYBrQ8zpZoXsyBqlOXiQtZ9Zbw2YMZm2CAT6IXdkeqS5xVwzNOaRywvL +15cQ4UJQ9m76uC6tUYSgidyU6+3DJLGYuvnHHqFw/PA8Oy7qh2Mp9eFQfPhB766TS+fOtj/52e1W +r1D2LN9DgXiwvuChR+/+1Oee3tyCodRX9GkEChAPDFQxRgEAq+JZx4/Yf+qrmq96IClUB0Gn+ek/ +7P3Kb26cusxAUOa+cfa1CJweJutIs8hv5iBRzoRKsdKQZMBTzRAS6CogHaLaQQzwKutBq9Gsvftr +3/3Gd725UHaB8ubEncHa+fCpxwcvnl5bX8elnuv3GaWD8CQmZvRZw8IhOxwgHBVWUxS1ICAcrlQW +KDkT5C1URs0eoaVTOkI1lfSdKp7s8wJiU8MBdUcqgds1y1t26nPxgWXHXrEvlaJBoTp/aX7tZy/U +ngoWnRJQRhHOiuqNJTJb7mN6FhkjJ3gzAumGSAyMgSuBd0NTquw5MkvZeHRx8gO6JYZlbsfcJAjJ +4EjKv+pB1O0t2qFUfVCgO60xmdZilGrHJiMFZlIBvpHqxzTj8C4BSkWxLJbQ4t4mTTThwkat2HPu +uZ9HWeOQzt3Mohm7VzWC3DHJGse22A4MmKOSa0Qood4ltFzSaISow2yg8R34lVEjiLYhVSTiohRN +FWWJgwUhlBYBmtJxl6DLenTs8jlI5DFPQytSIZPMF7gTYR+ZpDWn0HS8AhZrH2jkJH5aQFvs5XiG +2AnTwMzxVyMzEWyHdEAmHTRjt5AsZxqbSFxZZuqOsyRknE6c/KEqmSwQRfdhozp6soLJ5JgKLFyQ +G1wuo74oPJJjpSuMHFLYC95Rgu81U0iZB9lxakJURqI7OA03ElxF9TPS8sHNr+4S/mHUScPls0HI +GNXI6+iKUl4ofVJLAL/RkCjptvJ5s0KUwUtUEKUZo1OKqUdex6itqvimE8SOSvixjJ6EJZPW0FtG +l00CJBJSIKwNepUYFQJ1hkllogsbaiCJZpAIJCmb62Rk110vf9FX/oqMUFXbADMRkJ4XIl0/OLRi +veVt81/1ocV3f039ttuQhh4CGma7F3UCZ71rXVnv1GfqPS4hLEmWa1YYzDRaUspWcCVgi7vgLUIB +EY/JdSgx/7dCCNlxk+30paw+UWiksIQkkniDgnMh2KofnPvTf/mDb/vAu+MyaMx22F1tXzh19anH +r505vb25yQJRcOsNUGQxBggcMADADaEpBkHURzAqOCWBbFgSkZoQ9zYQyclBiZJKsZ0Q23jtAYyH +WJRQnhiMysBU2BjB5Ohy41iQN3C1eXahzELRRZT/Khe6sQUEuuKgGD677V4IZlwEADOphqndADpx +kgFYeeopFBnP0HMlv7oBhZkJSxNSIB/UM6VioX5pEN1UcE/vksAZNXjy4JUmYUPFbWlBBCP5IJkb +4sTSGByJUjapjqJJKz3BgEi8Un53ZUxRe7jHaeii2KONZ2bfy2D/Fwo1HTn2f2925W6c3jBReQZn +AP9ge7BQJvkhfcc4IV3ytF2fyTxOAYWpC45XQhScg4IvTrlslStxqWIVHZdful7RR1EY1NNGdRzE +knuowlJADCsrGxcKIXCUov7WoNPHwi8VWH+T1Bhs1UaVGOiRsi3Z8VF5wLyK2mlGd6whlNlMZW89 +dqX5c+L+sfEclXakJ7nTqIkpU+HP/3/23gKwqmvrFj7uFndPSCAJBHd3KE4LpVDaUmhLW6hSF+pC +XagrVJDSFooUd9dAiBL3nBx3+8dc+yQE6C33fvf73rvvf293N5ycHNmy1ppzjjnmmMyzJOiDgkK4 +5aCeckxcovHiJzoCBCRCn0zklYl9Ctr9cjEK0QJyMU8h4CuEfGqyKuKDdk87nE1cMQkuI37yBXTR +qX2NhO+X8gMyMON8PjHcfwankoVhsdR/aQhyo4KrxmCPLg8zFpnQuWCjvgJiIVruYKeeeIT8kHtG +q6kA0B4K6MBwRAEBD6uCUOyHKBmOF4JIYkFAIvCDWi/GT7C4RF6URAfEHp/EHRB7gSdIRRIRRhib +22LOf2CGL3gcwWkVFCDuQMj9L8yAv3uLsGvwK4IrDrfudNi56XHtwPgXj+Of+oDrWCMyHliyaQVF +D3FBpNaflyMPT5E6LbYzp23ljVS0x2FluMBw+sPCQhubjLhpDE2luca0QikEJwYPz48kZWgoX6MS +oXFcazO/pNhWXum3gInDDiTo+F0+0fbD+2dO5jrnQssN+xjus3D/4ShR2WGw7o+6z5t97i5dU25Z +eFOXHrkOl0MS8IitBn9DhaO00FZW5jaayFtEfhHwJRooovMIa7UI4TeUiUEc3O1Dnym4jazNYXu8 +SLaLEFpmFVjTZ+aFIXiiqU5LDPnoZDzAkeWYozBv1EAKCxUP8jdinVCs9GsjFAK1yABFd55YapG6 +D5pkVYEwkdTlp+oXYN24zF7yw/2AYNra3nNpQ6Iv0lNcgpqmH76zbalvuyJc0p/+wP6EB8EWjcw/ +ZtEHO+A2Cxos0ecyiPSTw+vodLjcKvcJ9KdgJhKnSa4SnSmBqJynRNcByCpwVPpa8vI73qV/ccz/ +T7486Mtftr9/MSavGoLBHBQXL7Q581dYVy4C4DK+XHkAt2EdZPUFLPShBxQ3spID9itbvYRiso4w +eFR+AIicVJOEqJZCJSMINty6yS4piyrxDOwEzCABGWKeGGr4YiyKLMeND0R+nN1gLhSgQ2qn5Pyj +aKA9aAi+4x/M0Y7XhLu7l1/4T0T37cBR+71l5Mw2Ohn3GD9YTo4TqWC2hJW4MHMCg+CFjBQZDRS2 +COE4SIH5CPBYhLosVvGCSyMWATCiv6OHGxlUwvFhgcQBP9uJwopLjJ/BFC+HbMKHAHwJzSE24Vge +gjkTLLANZnmCuONfewJts5DDCmiNJEwdHxw8F1aXw913hBvcfWcYAhsEuKE4KWiPkElEHR3IeGCF +QI2FrgbrQM4qTjhfgeW4aBeCN4KaOghB0OkQOY/7ID4KEFAWTbXrCAkJSOJKmdmJMdCNK9jimqG0 +Sxz9e5Ouo4bqdU1ju5n8t77zajj5Lz7sum4brbGs2wbNMCJuoOUDDKFRWHzRlX/RYbBDdIPiQ1xL +qVBmd7lCQ5UOh83l4hLSUMPG2kcxDFsoadnGuAKD0+nwNzf6Ki75SkrcjXrk6phpZCDBlS7qP28a +r3sujMLXTl6iO4uoDkuJSCJBtsVvB4NGIpg6eeTMW6ZExkSiD7EUbTZMFmdVqa200F1TKbHZcRpe +dJIKCBwUIFLpBU6UyhOpup+1XfQixCYmKtBUAlhYTESKB5yVoCvJLgxLpnHgBvPJWdTMsn6snyLq +N5AtZDZKxucp+EIU84eIFOEym8hnBpVJHOYtdXtPOELssIjokAwqMJTAuGwt7CJWSEY34BwSbuPA +L7J3QbiIc0SCP7l5TEOfAQUMlaHqe86GtgUHHAePq2bjjCVXkdlm6VmWMUjDYeUoFC4z+JRq/8kS +BpVRWZjIICy6EkwC28f1pSIgMfh1/5Z7eO1qHgzL/q0pdfWbuSD3ilDmypdc66wFTQPnA3LgXhuA +Rus45RVZpSIzgPhJOCqrwKOfrEwR66OfLYjYKAykslta2tlLgxZRCgl8rO5Y+IRiZjNo4aaFnLT/ +ua7TYp4IZckiYBskwsA8G65mnWYgB8tdL5UUdOyCA+dyBNluKRl8e4WPzyF+3Khsf/D39+RK/DkY +NTBjz0wkgxaZOST8mZUqEkGbe4YVd5K18MEYMjNJZYtUxMhdVHZV6U3MGLF/mSdBZor5JgzgZvA2 +H3G1iOkZw6mj7CzUODCniFgdgFy0ENgQNxgYkkJTmetyRqYOO3P98SlMf+GKPWgCOaPFQabcrWIe +D2wWZ/LZmOBsPR29APWq3K/cKKGbTecTTKKyak1W+MoZezKBtFb7KcBEA1dq2EpNbvAc4k8ue8fa +3rAqO9BL8Dw529z45Bpks6AYD4J9W/4SS/gvTK+OphHFG5fZUH/5WW0Q1n/hiy6/5arsCzP514mr +rvo+uhJYqZB/YHcYFxE3O0TOV8tFdg+vwYIeSxLEinCZ/G6eQqJx+WxxsUqv11lf72QzAjlIsnm4 +rahhIJgADeTEPJnIH6LhYaxCMkZv9NpcLE5i8xGD+sqs0X9zrrGjX0vXAkNOJABYjEyhUiWfOn9m +v8F9MXjsFoMGLlVri6G8xFJV6myp4zlsUrHYCdU3D6UVHegtBTMAlJPaUrJIEZYSsSMIOPgD0W0Y +SMtFh8SypEwm2T9acRiYzyxQkNzNPHXA/ERawQSDuyoBCEKeCFWf6QKKcF5ItEwYKmoSOO1iaaQt +1LKjiX/eHekHrOPjg5YKaIelFJE7IOeW+XzMi6WMDBXnM3COVYLQNWCqqm18dqaJwy1VZLHZP9zL +gmVZbRaWAz9ZzoJGAi0SOBmOrBVUAGcZZo5rw+woCLNcSw24Cgw+Za0ZOayYPUOYM3sLp5CEtTnY +uYoC32shu7+eEf/MwP73TeO1S/k/871XH3HbQOfylDiqduebVmksbSxL1h460n1AQMCFKIgFsdDT +0ombwJZEpCWYt89FomzKc9lKxpxsS3UThxJeG0YihilGLo1UkoyjBJkbnUcFUpID8KAfKHwsYk7j +c5i8YFu42xb1Xnky7dVxlx1Y1u6jfbsaHucgjLat42T8m5WuY66R+yLuZNuiN2YeaXAz04JLwgYS +FzvCBuIZAlZhCjC/gCcirqKe3mRW6dIhL0vYFon/c1X9rNsezp2qR1mZLZO2AZlADJ4SYUwgnXOU +a5ZSRO9V1n0cE5cuKIY59allnFXWm49uAcVgwZTt1ZnT4OlwxphuGzsvBpJyuUQqt6I/tCEHXJhI +r6Lgjy4Diyk47DN4ZShJyM1ZShkyt5uBNITwBR1zbngIgTfB3BN4LhA4vVAogWMkhMeE5KkT85KW +foogg63K2ZvYykWrANfLteP2X5kOV+YaYRqx7P3d9q9ZsH/wSf++aWQpKwwWZMTo+vsAMVD0jQvu +xX3BNROKJQGvGyMF+Qx4UXy/SxciEEj59Q0OVPsxjTB6p1goAWEFCCE2jFy4X+SssMFs96KjHDNS +HM4Ha8BOvs3X/B80jfhoDC8MdqPfl5WQOPWWG1O7d3K6nX6HMxRAauOl5ovHPE01HoOVF5A4vXwn +JoHfg5pFglL9fBcINZgeAlglrDNYcHgwikRSxUpP1B4KETkbwOoQgjJpLCbjKKksdGTuGi4FibZh +QJL3gcsFEFXgQUoDNlIb8If6w+OluLAOqbtJHPBKlbqLYv6mFqFNIhfIxT63SADlOSxnxMMjPgFd +SAGKNxgFjyXbiQpFGDdzdWhxutI0cvMTB9PWO4hlGTgXmP50eTgGgyQOE6ADRrzK5iDriRGUCKcG +k8TnoDcz0g3AZBoKFGWSo9BW48EwXZhGINLUupEdOkJwTgydFXgGA5HrToegHegwETqswB0W07+f +ddf7azDz1uFl141Nr7XHhLNzkUFwgSeqGz6S4VwUGUCtgaYBufzchvQRpYFgBKkqRyiQ8aRyvxRj +wy9GfgLLPUhvBPXRKso1/cKgAuyAB4hn2HJIZEofyDbA+71MQcLrljpdKJPxigV2kcfkEZg9OoFU +ispc1CuRR4cMGpxemrBtNBx6cNX5Xm0ar8HYrjKNBBz96x7KtaaRi7YZPMh8AbgOnBlhESBdCBZy +UZhFgCm7bgAbWRhIsTTBhwQkUmkU+YyYKKimQn6Vjg4nhYkOn4Gza+TyAv/BCHXClwcK4vEKPYii +mEwa3+P0ee3wrDHAMYGxlOERezldKzKH3CrAjChZqmCS/cqRRpadvpsOBhE9F6Vyej20DpIhpOgv +CKMzfSOalBRE4PUCgUxCipLErKOSZlhSsI6BiaJHJ0gQOBnASNxMZvgfbCQt4Yh7gTQJ0EkIq1bA +aQmYvUo0QBeLvCKBySeyklIE66GHSwHvmwZQEOJirAFi6PxPmMZbrmcarzdJ6e9XW75rIsLrLijX +/Ra2QOFCUq0cw75o5DEHEsplXP6QAS888HFEuNh4TirlK1Ryg8kGVwphO7GeaM2GqCc9ovFC3cTx +IcwgkHlgI58LFdl4v8oRafv1ui4m94KOJ33FYww0fDPsGoYYc6aBWEI3i9/icnfvkj735huTOqW3 +WIwylKm4rc6KQlPxGU9jDc/pckHUhi+x+3hWNyr4WVUGxllcGK93NiokrNuPuxqNNHwwhWhKkFB2 +u2kkIIUTcWYlHGyja8ZBcAxiZL+SLBxVTrAdLb0IFApIeXZVwKv1q8L48VESvtpjkHnNQqXcpvHs +1GsveKEMh8kE/VnqKc3iLQQU1B2YeZFUp8GuJrH2L+cVyTSyeJKZTOZlsr4GdCDki3JeCUcnYGwa +Fs8GbwIdMztyjovHpgaLgMkDILopSRlwvTWY4CSVbwZrHIlbRKaRGk5RchoWkWRj2WXBA84zYm8J ++r9B4Peam/rXg5aO8G/H+9Vkuqs/5jLS3vEv/yLKQhfvKpt81fcEZRfp6pLhZ2o2LPppk6CjFRJ3 +COs4UkFkKeHQSCQijwcEb5FMJscCJVKIArJAQOnzKj0mns3kddl9xAgDEQzvQy5fJhSESZQhIrXK +I+eZ+EInBgcE5x0CnaCJZ672GU0Snw0lsDIZzx2QufnxYi2v3KKu90XxJCoyDkQt97jAEgsWJ7Ak +AKvF6XBBOHCNWx45Dzg4hdtnLMeH7XhnaPVmVo1t3BuvayuDtYEMiG3zJJkrx0wj2+BxIxbEDkvI +R1YRSViMNB7qryUItKkQiLImBDHzkGl0if0eScAt9sI5gMsO4WMMc9hPjEJA03KxSOGXSt0yvlPo +dqMsywlI0ulBdZbTIfPbFQGjwNrisbgEfocb+JlA7ZdozTJ5aUDtkTrRmxxhFsWRjNxHwSSb/szQ +stHN1kvuatHiyl0PwryD16FNuI7dfDZLQZxC1TJss1Ckkivh3uBdHuRRtBK/mm8TOs18m5WH0nBm +vGiCBUAegiegEAh0IqXYKfEZ3Qr0mHfC0hLdwOuDiglPKpSDf2OzWVxip0fn9UbyDTyXxUMZf7WS +p3bIBFUCST2WF74z4ELvdAhdsniRxgab4D7ktdsqXNgY4Oo9rtiunpJ/4SkGAlcUb/xPmMZrHdi/ +XkT+q8/KwtXOFgt3bu0zgTt17ldumuBOazRqu9NhdzrBdMIYYdDcFQDyX16g/+pxtb+PWYS2ycie +vezVYPK44H6JgOb6HV4X2HuYBEbonUrFvfv3uHX+HLlCZLeZJcKAx9jgqCh1VRW7GxoFTrBKxQ4P +3+EnENXp9aLPFPRrBHdMkIzssfe342B1dVPwWz78BSMK4Q41yCBRNpZCo1nAiW9x/lqwLoijHgYR +SqJIB9M7pExDfyGbBtYhzVMZ36LxCXS8qEhJTLjErHDWQcXEq1MUi627GqMd6JtMxoSj27A1AqaR +s3kwcpg/lDcCWMRWH27sUkqdvGSGsrIKAG5hCU5LjsfAXssg1uA0vly7xqVQyMIFpwFDa7jOGEzg +LagVR1Ej85iRvmCV/sxXZVJwBFARD451b2beNBer/nsbd+//ZuPuwt9v1waF13vHv/536hLF2A3M +E+C8W0LPiDPBcW0I9xMJxBL8QOAGGqGIcHu1IhzKw06fQ6kTumIEZYGGFqsRq6VCo5LIlDHxydEx +8WCWeDwOm9nQ3FTX2NhgrLUmKmW9ErJ5LXga0vieEkdNg8AtT0hM69J7VK+RqZEJjY1Vp8/tP7xv +q9zk4Rdbw+2SUBA0AL36hT4nyNQMSWDRAQsfOfj/8kagUNvGubTccOqw0Sva30WOIbvXQcPIWcfr +3X0yXOyDWVzMXTL2C0so0heSuw0uJrqLII8Gq0AFwZqQEAemq5hScmRDZXxRmLhVaK30Gup5jga3 +y01SimR84HkgIQc3gKQ4MIfdjiSZIlMSH+0OU7okfo/L6bO6Ff7GEMNZc22TTRIbrYtPTIZYtEwq +b2yobKwpFejNIVV+RSNUJVW4TAjWiNOE0c7qk2iEU2RAOQRKSbHgq90ccrh6UD8fJ0PZYhhDKiBn +VpIYsqghV6qVOFmExzwJ36fmO+TOpkBrIzjxKGnWiGWqEF1oVGRIhE6uRDq1yWCtqSlzGOstTW4N +P5AainbnPjEyQh6wthAGOkQ+pcVsdwvtokixWeZs5NvqHbyMzgnJqdlSqay4Kr+2pCzaKVLk85Qu +ZKOdKG6Ba8CiYJwVyBbUdg/gHxcdc14R81CuM8n+ZdOoTAxTp0dr0qMpgdy2SWJCDRdran489I/m +31VR43UP62/mMWjK6uRIbVaMulOcOErjMdouvLSBez0HqaXPH5Z196gTj61u3F94zee0HzNnigIK +hTJsQLo8NYJwi0ittaypfNX+K6bLNZNBmRCWMKmnPCVSEqL02d328kb90dLGg0VXzbF/dAq4gLrM +OHWXeGV6lNficJQ21u8+bylvvPx6AjAJUYFnJQULSyDUuxxh4aHzFt6W0ye3TGls5NmU/kB8Ta3/ +zFlLaTnPBLILlm+Bw430odDrAfWUrCN2wR0TZWN7Y0xQxp7Ha3xrjXHfWWilwglgWURALXCmmLVg +t4RZCGYoWJyN53AcwWvLYRJkwQiQJCcCsR4l2AN+acClDHiVvJBwfmy0gq8LNEg9LX6hzKCRHLII +Ljh0WAMoXmTWkQwdWzpgFNl6RoAaS/ogWUnPc2sWewFLVLFIlRXBcc9yppMAWA7cC65YnB7c5RHP +uGks18hFjfif6yPFhYCUy2JRIxMLwCuYMlmw9p86fDJ8lZTz+KhpYf0a6b1tpvtvBug/8afrmsbr +f8Y1CNF1V+2/+sy/PRCcMGWIuAvM/BTONBJDgiODEANLhFbgCBaw2mOn8gEVtJYEWqlb66oJVJdZ +rNqo2N55A7plDurTY3hGUmeUwbIjCYZwkLKvaLm4++Bv63/+3NPamJuQhMLjoppas0w6auKN0ybc +np3YOzgC/eiFqf9t95dffPK6rN6hqOZFOpSoN1ag+zbuvBcOGjON3J2iEX7F2selFdtPmDkozOqz +Tycz0GYo2RuveDf3S7uxbL+SV5BuuAUlCDFRiBVEF8gBDKZj6R6RLDibjpgSEpEAtSoiCVBApVLm +Aw9eKvJqeGapq8LRWAuynVodmZiekdwtM6VbbERiRHhUTGyCTK5weCytRlNjS8O5iweO7P29+fyl +oclZOrsCpEJI+F+0VOa3Ojr16j559OxRfcZEhSV63dCTlqFaKr/k8FufPVNfeCKmUaStArIJg0jZ +R8bFI6yMCAdtjkWwJceVppHzT9lPKskA3ZSoQ8SdAYMYllEiB5VY5HPy7UKd2KxyVfpb0Js6JCou +J7dfdtaAgX3GxkVmSDogkR7o5XqtTm9zQeGJLZvWH9y+MzVErnOr0ZFA7MPn+mVONOjx1sn1lWJL +M8+b3rnrrbPu7J49MFybhMSL2dT42Q+v7Nr8fchFodKEgjNMbsp8kOOAWQ6sneUu6T5z2WiW6bz+ +/PorNwjv/uuoUaSQdn58StIN3eEzIgrRhqvzBmbhO5x216WCap03cOKuL/7yK5kXfF0/+J85Wh5M +ct7rc0LTo/HqotPlW386sPT1eb92W0aDkoDOQM4jUxS9U+Mz4zaPWo7A8ZoPveKiyCO1XZ+ZGZKX +JJaIWxuNx3bnDx6eu2vya//oUHDiPV6ekzguz2ZxAPIQS0WmFkttRWNMbKj5Qs3F1za0feNfl/zj +AmYuGZ8yvY9ISmWUXpe3vrq5prQht1d64dsba/441fa9xEuh/8VClLbC20rOSLz9zttjsxNOhekB +B7udrj/XHR43uWfCB1+I6lqF6LDIE5j9SB0IwDBCgIPB7gSY2iXZfccNOMKQ3SdM+8/CEgCNIYgV +WRt2sSiNzeJFGjFMZ5hlsQkdDK4FRAi/bBoJrST0k2o2OKYTlXsK+B5FwKHySdSB2EhRRJS8SWKr +p4p9raTYL9tnDLMK3YxTw4mLsoQwpV047gxdCBIAoLHLdGSCrjYWY3pNsJsGRZVcZT0zj/Qe9oCh +q2ySUkkLjTP2g2Ch4Fdx3FQ2IViKlHinXA6DE0SlImIyiiz/yRlLmlSMaEMlnowQQqaU8VRpDb1e +3HDdcfzP+IXXn7vX83mvfxjBDOnfvZCx8umMKcEVzCWSfifHmaQnhB5ggl4JeMkyGZZ5mACZxx/m +qxUZqgNmcWTkyP43TBg4I7dLLxC0XD5TY3NdfW0lcNeoyOgIJKWlWp9XCP60RMnbfWjde5884Q8Y +TWa3VBp7551Pjhs2XSPR1TVX+rzOuKhEnk/u5vsbjWeeeeI2X2kD75JTZ1XIvX6F0IWWZuDrUFaK +DWXccK7FdMdz62gUg8+zv7cHjsxBCppE7r1siLVZRc7yXXlj/uJWtg1RVEkwqkOQLkP/EA2T2JbE +RCJVC3EACh1SORgxMgDLISKLxO7SCQvszTV+uyo6rkfvYYP6jO+c3D0pPJ3zANs3H3KIVPOAJ1xl +tWe+/nbFmb3bu6YkocHAgfOFPq1q4e2PDBswPSIs3uNwoY6NFJMFUqfTrtHyV/3xzrpv39FVerRV +iLlxoYiaQ9qPjIfGdK8obmQ5gmDWkS4FEa6YW0tmnrs2VH1BlFJGUsWNh42XysQuvs0pc4rjFGax +p9xh0iQmjBo2bVD3SXmdB8CncqKkzNZotOnxHTp1WJg6QiJS2e02qJKArS4SOd5//4nDm9Z31sTJ +rCKhG3S9gNlh8oUKLnkajQrpkJHTb5q6qEtidzhoNH+prpH3y84PP/rw8fBSsaIFStDgGWJWI/UM +shKcDWIesjYBlEBpN420ZFw5PK61T39pQf/CNGJZ7//9YkVsCFEWXN713+0ZNjbPc67abbKF5iWH +9k4r/XZv0Xtb/9E8u/6Quu5U5vGiR+T0eHmWRCGtOVLCtziE6dGfLf/52W/u50wjtvQ7hsuHZG34 +bs/cW4cfvu2jv1rF2q+AQB6lHbruQZlabqho9hnsLS7XxXOVWTxB4UdXnEX7h+AKdH9zbtzATHwR +fC19VUt4Ynjx5ztiJ/fa8TvBlb26JJ5Y/AUrvfqLlU2XHd9zxVxFlI6qslxeq9EqEQrLfz6kG5Xz +1mPfv/zN/dvHvOhHjQWNOuJgSSXSVtRkSIQDhg0cP3OiOib0TFjrvh1nBg/JTiirL+8UDdAg+uX3 +xc0WQFVmr99CiVAB1fDDAQSVD1DC/bPWH6u4/dGp5Te/4LGhRTGfQalU1MhcasqhBhFUGj2Misky +apzl4YYOCZpwiwIl45gCDiUGWXcMKqiiuMGl5HnUfp2OFxMpkYbya0ROk1+scWg9B4zyi+4YidRK +xQ5EruHKqYLYKIOa4NS15RpZ8ycas/R1bT43lRbSG4MWM/hnwqjYOsbMImeu8BiTmzjp7D+u+JK5 +wO3dNgh25Ur1CSbluDawuQQvUxzJgahB+gEHriIbw2R0ghpy7aHGPzFa//FLrmfVrm8Xrwpq2Ff9 +qzabWZDrfBXiAC4+oEWeM4WAWNtNI1faLxUBCJSSjL+QJ/ebNMYiXotVx+/RY9QtNyzt0WUomnEe +OL1r98Et54qONjTV8/1mJHVdFmVKQtz0KXMGDBgbokpA2zKXt/b5Nxf8uX9bbFzao4tXDOk5tbK+ ++Jv1Hx/J32k1uNPjEseOmjFi5Kgju39f9d4buhZXoN4lBYToJxanIiBQIHHFkmXMNF5OEF57G4Ln +zHmDHVbE4FoZfDc3CTjcut1RZIPuiu3q32nyBy8ZpcWZQ0eJRs6TYAQlYp0xZo0Y9b8SsRwXziuy +enVeg9Z7rK5OFxs9sP+4sRNnpaZ0lwnD6qrLTp07UFBxwuYx1NY0t9Q6+nTtMW/+A9HhsS6HC7ZI +IpPsOvnj628/iK7sNRWm6OjOjy57OTdloEyqppUKGBGReFhc7fHyJc5Va95c98nb4U2BEL0iGDUy +qioWBxjEIMmC2Dgs39IGTrSVhbRbRzL16AtL1ZUA1Um2GoX4Ui/alas9vCR5sbUJeprjx8y7ZcY9 +KXGdbE7TnkM7T+cfK6kuqjfUNBsNcD91ckWsNuqhRQ/3yhvi9khRUaZRKH/a8Or3b7/UKzpNZsNC +KbBD3zqCX9Ra7eFJ5tyyZMr0u5Ty8Pq6qsrqcl2YNjkpzmhs+uHnt3duXqMtk8rMWOOAiaFEjFrl +wJHiw4XA9CbCXJBaSLfwr7zb684Fbhx0NI2oa6S4IfvJqaHdkx2N5oNzPggfkX1gV/7QSb0O3/mZ +/sQlkVwSNTir8N0tWNnjJnTHHY/ol57z4k3AG03nqyP6ZUQN7ey1OtxGOzeoYJPix+cB4vaY7JGD +s3SdYi2XGsO6J0cPzrJV6/E8HgsgUW1yXDUMES+qY0KOPfZD0bublTEhqRO6ww1Ta+TNuy9A5Abv +6vny7H2bTkbHhyoqWnBgGKJSnSqBDkmEw8h9gQ7JcKqcW+AyF42UpkY1Hiw+fMcnYX3SDp4sHTW1 +b9lHO9ytVjac23a2kosVsl4fLYjtk958rqr0850xw7o8eet7Q8d2P3b/NzW/nxz+9PQ1n+9I7Zmq +0ykN56qvnY3kWHx5F18hxQk27itosjm3rTsczeNfeP33lGl9jx8p6pyb5CyqdzYCESDwCnB5K+il +YuHEyWNnzZ+tDdeWSvX7jp7vlB2fdfyi+7c/1Nv36Q4e5xstTj8fthHxolck8UVHCAbl+W8Y4D5b +7rJ5NfdNO76vILtTlLOwWpQY6ajVg13JehTD3RNpe2WEDOzstthdBiunFyONDoke0x1j3d5oDO2e +GtE/y17X6nN7ZTGhsWPzsALamowh3VPwvKVe74UkOZVk8QM6UfSgzqEZcSKvTaERmIWuZny8R6Go +5vvPW+VeYpxhBWKRBxfv0SlySzlBTcFfOPifLBwXTbbBXW3pSRqXQSJO8DM4UimFPgxyZUxaVmXB +7B8nwYWvJioNvZVbs1isHCSs0VmTuEEb7krWlC0NHHOHCYawhhvkqjCdKaaF9W9ZxbY3X886XvdL +gutvh3+u+5arXhCU9uKqCv7B3i50wqj3rBSPcVGD9eno4oKqJpFEDlliccCjcZojHGV8My80+uZZ +D90154n0+B4n8v989/vHvl31ev6ZA3x3bWwkBG+8reaWyNiwQQNGDeo3KjkmXSKVmp36Hzd8sWX7 +b0qp5uHFrw7ve2NRxdmXXr3v+KH1Qqs+TMGvbSzZfeKPzQe/P7RvS7RQzqt1ycCKFgo8oKgiyeUN +KBBFcRs3sJhZ6rizeqA2ti1XCM/qTrid5VC5x2TJuOI8jsbFAa30M/iAYSfBnVMduLwHkwE0junj +2WGwHCPTCWLF7yR9gB2lGjAmKHFxyT3eWFGhV39W39S5/6glD7w2/YZ7wkNT8isufPLj2yu/fnHn +odUlNUdPFZ6rrr3UtXPnEQNGpyd1kiFRBxaTIFBYfnTz9h/Kqk/ojbb4+B7Ln/qkW/pgfLbd1Xyu +/Ni2vWt+WP/xz79+WVFfYvQ17Dz225bNa0UNliiPBnRf5kqyi8VCZuZtMryFEdU7wspBd5a5Sdx1 +YudEHFqfBMxGOiucnQdK8BHii5YmdUL6/fe+tPCmx9TK0LUbv3/786c2bFxZdO4w39Kg9XpjFUrE +CG53tcXWeu9dLwt52p17tm/ft7nRdnHDhm9QPxAti+G7ZH6h2KH0nLNWmuWSubc/cNON91n97o/X +rFj+yf1f//7Fxp3f/bnvx583fnX+2KlIh1BaL5QG0JWP9WtBTMqa9ZAjT73VyQ1vHx1c+BscKW0z +qN2laZ9Sfzmhrir5F0p0yuwnpyAeOr7oM5/Dk7V4dMGpS9E+nv5wSc5LN9nrjCeWfgtz2Puj2/Uy +Ufe7RokzYvbvyh+8aFTabcNix3U7X6vvc/84c2G9o86QNn9ol2enN4oFPRaOSprV3xUfGj2sc+rU +ProRXQxKadLgLGWEOub2YRm3DK7fds5rg/J3cEOKLv22YU6j7dxza/FUzNReYZ3jW+talWpFy+4C +OIt9P1uIEDD/SPHAUd2qVh9wNBgTp/bq8yEdUo9FoyTskHpM79uWSuQnzhl44mRZQnJk7cYTec/P +3P/HqT5Dsgvf23L1CsIuYvqC4SmTejaeKDt+79cQoVP2SkVmONzhbdx9AbYcabjEodl7Np/qNapb +/ZYz117Tzg9MjBmYaSpuOHLnp4pobatMnJwe7Txcaq3RZ949av1n22+8e0zhu5vBWUfyDoTaFp8v +Oj56/oI5YyaMQdBiMTaVxPi3rj3Uv3uy+Id1QptZ7HLbzQ5EglaP34bhMH205LF5pozEi3Zf+pju +hp/28kO0vMG5LU2mvNF59sQYW1JM7KQBTX8chwEJGda1xxcPRIzufq66tfu9E6yFdY56Q+pd4zs9 +PqNJLMqZOyxieK56aI49XBOZEROSl9z5sRlNImEunh+aqx2SY9dpotPjmk4WIukYM7Tr8DfuD+mU +bnKKOw8fYLp0CvI3Vr9AaVd4CxySOo8c7Dn0u+JENNl6RT+5sco9DppBZspoinLlT+wPzBi2W0r2 +AWQWuQeMTMcCRDav2/iHwVWMQm9m1TjUtO0xKYnTi9ta2nIsuWCqlQwn0X049g3iXDd1nmLmlmJU +7mivF2f9EwaKO+u2/Qqz1OYqBFkv7S/jkrMd97ZS5sufxBFnO+zcRfyHe5Bd1eGAL0dAbE3HHrQg +bDHkqsoZ14JZElbeD7YYZPtRl2NWOvVhnkKvSReb+/gD70wePg/25ectn7y98rmiU6c6KZNGd+7f +JyVX6BOcP1fbJWvwI0tenTH2TpVC66bQx7f72Lq3P37RaHA+ct+rN4y6rbim8NUVD5aePpgXEpfF +S1DW8RICulgAtq3OGK9cUOsWm8XAb6kqnA4Uyx9wSrguzHxTpSQlwgnwZ9UOnMg1iehwizqzUlQ4 +wRWq0zO04BPjmhARZtNYhQXB8O06Yx2MAzeGgxb4mjvORdl0HGRlmYlmCnC4VuzqiaUSCbwvpVSB +RIJHHbBGes9Y62oDkpvmLX38wbcTo3JqmytXrnnro5XL849uT9AJ0hK1LodHKgi7d/7TD9y1PCtz +gFQh9QpcSPFafM3Lli86cGQbBnFSZN7yJz7NSuqBS3K08M+3v37hw09fPHpgV3N9kdtRd/rUgd07 +fr14/KDO7o1xygLNLrCJ2QwKlvAGUynka7JmrBx6zDAjNvou91ThCvyDOkfgCkKpj0SOJGCieiOF +lzyWiMzMp55YMSRv6vnS/a998tjPaz926vVdo2IGRGfkyhM7iaJS5FGhUjVKzyTi0Btn3AfQweqs +vn/Z/MKiXT5rc1ZcjMKtxIF45K5mX2uDwXD/oidmTb7b4TC99cZjf/72TRTfmaUQx4D11WLTenkR +PomokS+2QTgCwTFVKNCKwC0inKcdTMy0pWdoYHAVtW07nWAQCmerS5DHcO1s7mga6doADERC0Wd3 +2ar0MIEFR4rR7Cl5YvcJh1+IHJip656c9dCE7i/chNKBQzvOihVSbZR26oKRuL7wi/QFNYkZ0V+9 +vTF2Wq/MRyblPDihtdF07miJIlwN8c9TR0ssRltodrw6XPPzJ3+qUyJDB3fGi6uKalUpER2PTCgR +G+paG6tbwnul4Xl1egx+qjRys95MlvvpaeooHZ6pKm3QRWkN56q6PDSxx/IrDmlQv8xzz65lTA3C +2WDVBozu6rE4wromISEXlxLZtO8iF9R03FjyIpByyyA8eW75Oo/NGdYjOf9EaU5eivFkGffK1nNV +ienRFYW12vSov1wbQ/vRMZ956kev3aXtmVpwpjy7Xydno6H3B7cf2HRyyYs3V289AxoaGoY4BHy9 +z5+RnXnfg3ePGDfCZNP7nCaXwlVbUpeVHa85fExoNvt9glZnwMaX2HwiF1/KW3a7/KYRVr1ZGx26 +64+TxsJqh8EszE4ozq/kmFIxmXE/fbbdLRNLEqJ0Q/Oynp0D7SnDxWruvkRM7Zd01/j0O0cZ9ea9 +m0/hvoTlJukbTbDEabMGZS4YHXw+TB2ek6RvMG1be0geHYI5H9otrc+yW3DBEJjt3HBcFRnp5Hud +3oDcIxa1+lz1djBO3V70aSfJG+ZXM407qq1pq0Ok6mQK1ojuw2qduN6P9GvQLScTSBoeZKKYYg+x +gUhWmqI6FilSZUUwWciaEnNiqkFJVS6l2kENNQCZWaaSQ28Hw4/xD0gyBPXEDE5mC4OLF3DwA05Q +hYmDg1CbGL0cUYEbD3+zBd1vzgm/ZmeDjxyFy3twBeXW0eCKfsVz3Op8hdUj9StOIKR9Z/pNl3fu +Sv7NHrwnHYOettgoCHsTls5uAy3vtBwyMSmQUaFNQ6Xo4CBKJXJwJv0KeyDCU21vTUrt9fryT/t1 +H2m216786sW3XlsW7nFMy84ZHts1WhjSajIfKjyblNNr2bK3enUZs+PwpqVPzV3y7MxXPrzno6+e +dXkN82+aO3XcvFZTw6ofXqu8sLN/SlKUN1xkUYX4I3VGTXh1aHJ1bEiZVmNWy6Vyau2IZRmSkDhG +scAmRJ072nmT8CopiYJxTlJkJItGRbJBaRYSYgmQPhntVCzBRMuoCpO9jPRamZIaa7LGJNnIoDLe +DJdqZUkE1mSGGWHuprVt7cBrEEplASiyfGynbwxKo4kAwVB5oFcltkUK810NFrXuiec/uO/2l+Si +kFP52x974da1376eIrAuHDZ4bLd+hjqf3aa5+/Znbr3xMYUkCtDPyaJ9r76/9IOvHvz2m5dM+sLQ +EFmYOnPZPa9mJXd3e0xrt3zywssLL+z4daAsbHZMxhhhQh971CBBzBBhQj93XFK9TNrsgynjCT1U +GUqzjfwB7rzoqpIzwapNWC8djhfAiFjsJ0kL4ELhdHDtIXckRoGpHFNa4nKo7Q1ivTgi9OElL3ZL +HX747I7lb9x3cMf6/vExkzN6dxZmqBw69C/HNPKAbi/UtBj8UfGdBAE7WtS5feb0OFG/2Kz+oV2j +nRFqvlKMah9RU6GhbtKNC2ZMewjFJ598tvzorp+7iNVpDXHRJVFR5RFRdWGKMpmwSiJzKcVyKLH6 +UTUpCZBWL8qHsGOoQmPOL6VbQAeMoltI8eH+cqJL5BUxKR4m6EOUP4YiUZEnq+ANTg4GF3AE+47L +O5lG5MkRn2G4INcY0jf97MmynizlZmmxgL1iL64XqOXWOkNLdQtGIILLi2//4bI4zh0sLFt75PBt +Hz0Vc54AANGkSURBVMenx1QW1SJs8phsxtIGfDxoLBAKdrRYmutamxtNpvJm/NpnRI7pXFXjjvxH +35ofIhXrjwcND3c05tIGgd2jjdQhfAQ+GZkdjyflarnJYIsZnp00uiu+12FxiCWiptMVxDZRy6x1 +re2HdHb5+kPzPmw+UtJ+btW/HDt+56enH/g2YkjnI7vzu/fNYKbxLzaUgiAetSB5XKUnq5yXfP5E +WUbXpJbjl7hXi+RS4P5KtdxlCILGHT8FRxuSEuUw2ri3R/ZOqyiqBVOm30d3KlMiew7pIq9oLnzr +N6lMZPQ4LX7XgCHdFy9dmJ7TqaGxSuS1ePU1FU79vu3nug/IFB8/j8XOgYpegcRFUqh+34xR1ogw +e0WD7+NfMX9xVJ5zZUirC7okI3UaGqmt++1w/dr9Wd1Tm2pb1V0S0x6dln+0uOKXw6cXfhjH7ktk +/05ChcRS1QJiEXfYxvxKqUSU2yvN0Wjs+LzhfKVUSs9biqpR2d3z0ZsLT1/SV1YjcMjpnWasrjR6 +HXYMI5fEUecUgRAErwwCFgQQs4wNh0G1pb+ZTWTP0JTjAEyMPqo2pEoO9npm4a7o/8UU6phDGARm +OyCcXC1bG9+WI9tyBZnBqg0iKXJGl+OqcsUo+CPj3iIxwcRXWBcOlnQNlqhwJNggxMuohX+7X2Wj +rvyVm2Pcdvn0gzhwh1HD1Xm27UwMssMenKJcprh9/8vB+289SVeI9QmjNqacGSCpB0r/Ug0jag8g +ES+xO9WuGosxNbXnS4+/nxqbY3bUr/xyxQ9ff5oXlTEwoVuKMlymsJnkjZsLDvDDkx98+KVO8bkH +T2166/1lp49uMlSf3L5zdV1Ffd8uQxfd9iBaMh4/uWnf1nXpuugoUYgcmUTkguUOt8rqUdm9Sjea +N6LMjZQ3UULJl8oECFklWAoRloEnCYIQqWxLoNshJuos/gDkEjUR7DFQP9hLyI9yT2ATI03a9gCP +JJQ2DW5cdpCTvmM6oZxxbAvgmUUMrpQdrCOeJGvStpjSa1iIwiRNWVArglq2UCzX2CV+R5igzKnn +KSMfuf/V8f1vQkps78m1Dz97V13hxZmde0/LHtgpOvXQmYt1et7ddzw1Y8I8j9fsdBtsvqaXPnju +xx+/+Xn1ynUbPvfzTG6T+45ZC3v2GOHm2dbv/vqtdx6TNFkGx3TJkEYpjEKdXaY1SdTNsEUitUkq +d6rlgRAxX8nMAhN04+w/leQwzT8m10Mby+J3xB6D86CNoEKibVCPF2iFPIlH5PJrXXavZ+60O3t0 +GVvXUv3q5083VJaOS8xO8YbJTUwbXCzjKRQehcSnVTS57dUG46CRE/0CNa7W3iN7hH5FhCghXpbB +t8oCEq9L6r5QW5Hbd8httz0LVZE1Gz7cuO7HzMhYtUcF/hIDciEuIZfxVDIUxwZ9F6olIq8FSVCq +jCGpPVIZYv8zKT6mT8dCXq44NwifctA61VJyz3CQFpe/p/vHTdWrNloWDPmVMclRRnAduyZquyZW +lzbEJkf91m1Z7e8nYlOimo+Xnl++rvKnQxdOX+qSl1L4+U5xiPLE3oK4hPCqtUdoCaCeR36hRFT2 +2c7GXRfOn740eFTX86/9ykdM2WjM6Z5S8/uJ43sLeg7IatiRD4u1+4Y3ji74lJMS7rgdW/BJ4bNr +Kn85FjUwE+dmBWVJLXfY3Sk3D8DLmg4WXTxSjKSd6Wgpfr2wfH3lT4fbD6lu65lrFwnYKnxLxJCs +ghNlSV0SkJ78y4UEngEsNywc+LF4QWSvNFh0XBd7nYF7fVjv1LPMWMJ+X/sJfpcHbxfLpciGajNj +Yb/jkiPx9pp9F2vXHL74yvrz7/wB2eVmaNtIZTfOmnrXfYsjYsObDDVCn0Wor2s6ftAQqoQ1BQ1P +2GJw+oV2N9r0iFyegDs+xtM3RyYT2T7fHAgPyT9U2KVLvD2/AgltaU5KQ1VLj4FZVR/8CvkZ9CHJ +7JUuS4vh7kv9L0cp7GL3BetB8Vu/V609ePH0pZGTexd99mfzmfLzZ8r7Ds89+uDXlWsPcc8Xfvln +y7ny86fL+47IrTl6MXpILgJ0jVYe1SkF6ffBwzMq9/wIQrAHivl6oafaLXawzlHwQZkAONNkZQaS +/ueKC4P0Bq6ggjw16iqDIUhka+7FDDAlRJSLLIO8IbIFXNssbmt7cIWhCH4X2TiuZwZ7fVuNCgNZ +GGDExH6YOBwTgaNWz5SzDJaMM3w2GLBxd/Z6QeP1/86RZjvqmnU06Uz3FbHyFQHftdaWFT9csbMI +/PJ+7QuueeZqgOTaocsF8BRYcC4KEzFh4SOFjLA0sEteFa9R7LUpQ5c89GpCbI7DbV29/tPvVr3X +JydlQGa2PKB28TQ8tbCgtsju5S9Z+FSP1MHVVYXvrnjObqq7derYWyZNiIuNCtFE3X7zQ1p1amXD +xS++ejtSJcmKRFsAuRAKOFjISB2GwhoYP4SELGxFGTkzhCS7w1gtiGOgAIPGDRIU0uPIyDqS5WMs +SnAXhJBqBWMILT2COxUVkkw3draM0mPW+IIto6TWRsa1nYrLQqiO62kQbuZ8MybmxCGpQeCJPY3n +OM1sDoKm//h+2G43WM/h4iJ7TZPI/+jTb40eOhtMteP5+595/QmB2zy9R6/siAypQrvtzJHjVVW3 +3v3IzMl3mi1WB/rNSXgbN60pPn5y4fgJd0+6cXS/vo5W/rhht0wevwDI/2+7fnp7xdPJalnvpMww +gdZj96E/awDZXSmnRo7zpQYAPoUA1oTMB1eFwwzL5dC4zQVoT6y3p9+uekBCN1TPChMlRj9cJ18Q +l5IzdNQUMAK37fquqehU/4TMOHG2RBQKHRuFSg2NHh78eoUwEOM9WHU4sUvKgJ6DRH5Jk776zy2/ +xUXqVHJwaGyqGLlLZS+or1Tq0u9f+FqoOmb3sY3ff/9hRkyE1qNVeuVoLBhALChGqZBUQveWmPLE +leUwAAkCROxk0hjYwawe1cywnblFtBPEzeJFzu3hWGZBvJ05BpxAF7ufzEZipQl2zORmCplG8GtO +LP7Ksb/QXNooUsvCorSt5ypx14vf27qpxxOmglq8U52XVFvR1GtETtnXe1Sd44CsqsPVMB7yKB3q +83RhaoSG+Cg1LGtZQ5d+nWCWYCRyeqXXbT0bOrAT0L+49GguUgTI+ZcmCmZMf7oCf4qf3R9retVP +hxHMNTUacUL1R0psjSbAswhfmtpCw/ZDKv1q919+IJ7E4cnDIUEqaD5dAbTzL1+G4wH7Zu+mk6DS +JEzsAYAxPTtB3/YteG/44M4XT5dPvXUYcpx/YRp9/pbTFQe2nu76yuyYEdnnj5f1G9Llwqu/Hrj/ +84LPd+lPVaPAv9Hpik1NumvxHTfecrNMIrE214uszZaS/MrD+/U15X6ZNCI2VHCx3BTgmVxeuxMU +Jp4nKd49fuBvPx6Af2A6UiQc0f3QrvMZvdKNJ0p8KgXqUmQKqelcBdShlLkpGPxOvUUYHYL7oglX +W2sNsohQ7r4YK5rAdNf0SC3Mr8rqlYaAMnJYTlNdKxYKS1m9tu358l8ORQ4OPt9aWRszpOuf64/a +bW6nzVF9cHf5+s9NVpPfJ5LYZb5al8joR5UWl8hn1ojh90EBcyYzwEYamT0uuKNWyqywgkBO9gIS +ymBqrhTOkSobe57Tsmlnx1BqkIlHstdwloFroMFGMXslaNz0Mqa1jNeAr06l/Wiwgo9iSCyRcVj/ +KZZfBILKiQFxo59Tc21jb/yjNeKK5/8xq+WafGE7ZMNJC3G9mzs82YZ2sktzxX4tWHsF5yQoqvx3 +ucb2mOcfTQ0Ka2nV4CiW7OV0IWCSOOlwZJeEHpXIopZW2tx3LnkyLS0PckxHTu9cterDHhkJmRFR +fqdFIZGHaxMazM4z+eU3TZk/vM84lBp9vuqD2obSkX0GJmuTmi+ZSi+2jB4xu2/vcRgOm7f/Wl91 +KTcpS+HVeBxY/pjwKnVZxXKuQAUC1FZgEUmLgOtdBKxMLkCbY55E7BeLEEl4qZcTXtgWFsIkgCsk +w0/gv/RYIId+nRh+Oe1YViXsAdZKWNN2S0lGl1lNAl1pJQ1CdKyIgWTbyAxSn1C2erZFGLB73KDh ++piyOJuQZ6JzMlAXCzIKg6V8v4rXJLQ08Vy3LnpgQNexaC5+oezQG2894dHr5w3o30kcqtKGldiM +uy4WTpoJkHmWXt9QUHBGjoazfOGeLb/3T4jqFhEfJtYWFzSGabLmz10qkUiPXdj01ccvdlKo+sdm +htjFMotQ6VMpRVrk+iG0JkSXOFKuQsAP4WI3CVJxtpFzBVi1JRdAYuNCqA7qf23hMRPCaQu4cP7U +bMwr9pNSn1gEYDStc/fIiNRWe/P+fXuz+Ilxkvgaf/UpZ/6FQGGrskEa4tOEyAUqwa6zJw0B3m3z +748OSTU56z/4+DmPSZ8TnaKiomNnQCasthoqja4Fi5/slNqz0VXyzXcvQ545VpoqgUQgjk0Cqi0i +XNwgtLbyCYCksuMnU0fWketGQm4Ta2gSlGgnN4ftwY3hCoSZM5eIQmj2CSyUZLg6uTZtE5Pzc64s +oQly901FdcUrd0SPyD5zsDCvbyf9/o4V7vS+iB4pbpfXWFQHO6qIC0FSECUKYrUstGti+bnKW+4b +X7vhGF4W3jURRYSGojrkL2EkugKdK6gN6xIPbw52tNOikT0/uD12dNd/PF15caO7RnZP8dhd5WuP +AFDd9N0emNiLb26MGNipBBBibCgOlXs7d0j4rmBRxF99aMzonOO78vsM6aLffxEs1n/07VXf7Rt2 +Q08kncL6ph/bWzB4bPcmdgUwerq9Puf06UuDxuZZ8qvAgP3LTyj7fGevgVkAHiOHZ58/WQrPoPHY +JcALAZEY5fwhfVMmf3Xf7a/dN3rycIOl1qAvl5qb7GdOtZw+7qir9OVlXThaPGhYZ0lBudsjhM4D +YY43DuMtmxs6uhcSt6YLVbJBuXapuAJn6nCFLhgfOro7kOTUTrHOolpFZqI6PrxzbqLhSJEsNpS7 +LwKlTJuTcIndl3ruvnRPRToZ7gW8LHmYWq6Quupbs5+YEd49hZ5vMqGqm3ve2djafemM0PQ4PB+X +FH78rXfkCmXukueksekCp0RmFLprQMIhcWkiigqhesi19AgaQroJLM5jFYQcJ44V43IWkclTsbCJ +FV0wTJMFixS2kKkjmToUVrKCCsaRgZGDYcNP1loSF4d2qmMhWWv8hPAyuNyogmJ/ZcQc0n6j/CV7 +L9UHBwv82QuCgGuw0IQj7LVDoFcimFcBmsFfr4BCO8KiVz9uB2k5YI5bfrgPuZL22HYQwVpN7oCu +a/au+4K/mWTcn9rqbVg0RIWsXL0NK2Oj5UQkbBU66/z27N6Dxw2Zjnvg9Rne/fh5jYjXPzVPZZVq +1Rq/zGsV2faUFER16TFr1mKFRLv3xB9bdqzqm506IDHLZ/Kcu1AaH5Ixd+YCp9td2Vi0bv3avPRO +Wl6ozwGoDimcAE+MdR0b8/NpKQ4IpAGQEaF+TLsMVhDtHaVChRz6nOjsCLYs4ggxbAXqn2RANMTY +qRckLKKc2UW8QCbiy/BTChsplEnQIxI/RVIp5OjwGDpteAxLCamXYGzK9GsoVuZ6UjLYjd0mDl/l +bhjD3IgRScaHs45M/JBK47nkJX4CPEPJiVUnKHbqewwfM2n0PPRp5gVsX377ZsXF0wsHjwkxgV7k +q3E3bzt5JDWj6/yZi3WK8J07t5iNzTixsqrCqoay2OhoZajqVPn54vraW+benRjfpdlU9OU3b/oa +mkak5unQ2hFNcERSeHtuEkkWQoSIQmo0TmC7l04HRpaoSey8qIMi60rLWmxyNTlBlDho9blIktl9 +lnpk45XMP3Bs+gVuCuRqApm5PVHvZGhtvVRZHBUdYxBa9unPVklNF+36jRePNYcb69SNB8vOVTU5 +5t60dMKgORab6Zv1b+/au75vRmaSLFnkEilDQuqd9uPFpaNuuHH4yMkenm3rlh9K8k90CovTBZQo +fsSdgnMBZECO+w/1TLJnkGPixiXbgVzDN0K+GcpBGAlweJB85BqZcBEy+SisIwhnJNsDZy5/jHPn +7isXNXITk7mtHNe1fbtcago8MH5ar0M7zvUcngMZF5Bf2l8EnNBisCWkRBoZJmm6UJPaJaHwRGnG +HcPT7h0TkR4TIpFUrD2Cl9mMtpiEMNOJS5rcxKL8ytSuSS6DrbqoLiktOjQzNvGWQT9vOJL79LRr +Zy3sVurNA9LmDur2wo0wUUUfbUMw57Q47n7uJsSdyBBBnzY6MdxU2gjYE8fWfkj4rr9ZAyJGdcUZ +9R6eW7+roOsLN/7yx4m//HaI3eyd8U7pJ9sjBmYiOI5Pj8a1zn508oAfl1jVMsRhudmJEOXp+Ant +KxO+HbSgfTPeLvpgC5K1ZoMNygBSlUKdFWe0g2OgHvLqrT/+tK8qKVDfWOZ2QjCppPzYntZTJz2N +jZ6sVMP4YQf3FHQelG3tn2ddPDswa4yvd2dviKrqUhN8AgCtyCDGL58rU8mTM2MVkbqEmYMQJuIS +aUKUQq089fEZRgMIqrriDzaZC6pxXy6eKAVzOGXxOHZfxBXrDsljQ5C21IWr9SdLNZ1iYDJxOyJ6 +pqfP6B98/lSpJiP2Un5lUnp0ZF56xvh+EqW8oboFoUzvB5fG9BvgsjvsDhcmh6DZ7zcFpDRCueZg +FKJRiNh2D7gk4jXAPcsGBovQ6cpxgm3BXGOQbtoGILIUIeOhcDTUtp2RUIgjQCUZtEMrnBUvBskp +wdcHQ1UuHmWGljVnZnKprNiRrQbB4+1AC23Lv1yR37vWOl4NdP4Vjsm9izOEHZs/tA/Uv/jYtiRq +kE97JXB7XSv3X3sBwwgZsMSJ/HG13YQs0k+U6HnVgVaPdd6MBSq+JuB3f79mZVV5Qc/0zIiALkIa +7bb71BHykob8Sw1Nt952T0x4isOmX/3Nx6GyQN/sHKzFBY7mPY01M29aGBOVgTV2w+bvbIaKGHW4 +yq9EsTcsF0/gBM2G9OeIYhPgwyhCjZdsFh9wGvx+QKQSuJwwZpRNlOE3OjY42jKZGKYOYqQSiQyP +KS+FdZUIO2QSKA7FsomCezE1UWb2FbYQtXkwXULYSxYpsiaIHNmViyUYmMwF9twQ4RLfXEaWjWqu +3iO4oDL+Epev48wnrbwSiUPKr+Pb7XLFDRPmhkhiUB+4ds3XRw9suWFQ13CbHKicJ0p5sqGgyWq+ +d9EjidGZ1Y3VW/fvjElKBhh67uKZenNzWk7XGqd5W/7JvCFDx46ZCvGrb3/8+si+Y+P7Dg/xRfgg +kSlGw1/Uw3slaHQpQ6sOB+k94gip1SOgZimqR4hUxWwJt+6zTtSEPxIL6TLViNFwOmzMKQtaDL/A +BcEoNPTB5yIx4woIQsNjYGBx3xS6wDnToaNNJ9zq0BefX/vEI1/wtdGnjOXbSo+WGMx3LXxoyfxl +bo/1s1VvffbRe90TM3okdxP7pA6hzx3B319eoE1MmnXzArkgpLGu5Ncff0gOjUzShgpdDjn5SbgL +MvRCJjSXfF2KHMkgcgUynB47l07GPeZECcij4po8cuE7uTgsnqQng2Oa0ZQZ8ZtDwLlmQKz8hqGq +zEW/YuPaQPJixuQO/OYevcXRuVcaiu57rZib9fTU9pRJRN90cPQHjO0OG4A527jt3Oy7x5YW1WmH +ZIWmRPIbTcfv/xqfigjS4fL2GZ6Ll4Fo2m1AphkEHKNNpVM63ZSQA/dQqZa5zFcDqrB2WU9Nq4sP +9fVNx/GVrj1czkTp3IBku6ei4iIaYZ/e0n1wl6juySPWP5T9xJTIfhnth3TVuiBWy7lngKZqUyII +kCyqg6HFdZVIRdd+O/fipJl9uz093WhxdOmVJlXLe6+YGzulpzohrHPv9DCD4/BtK+matn1Ce+Kd +LjamhEjY+elpYMza7K5uA7Kg5jPy1wfHrLq355DeDz/1EE1g6ONAx6GpwXn2rPHAbuelAogHusYM +sT2woM7gAHsWSdnTDZa9pS1nlDrLjJHaMb1z+3Xi2xyDx3U/vPU0Ymik53CnERFW/XK44NnVSqwS +ICtN6a9JjVHLROeWfYP2NE1/nuXui2Zw55CUCH6T6egDXyKK0uYmO+yuoRN6wo2ABCOQ2Fa9BW5H +1a9H8fywCT1bT11yutjzLRan1bFn+eflx86Pnz1o0/f77DaXqabm4Or3zPWtfJPQXG5G/TV0f6Hj +g/XUBaU16hZHiUTWVoGDQ7mFhGYZ7BvBUmjgCFVgiGFCYj+AKjXU6aLNiAACBihEdpNSDQNfg92L +ggqZLBTlSDYcj7I9BmV9NqCkyPTCUBLDFJM5bXGKRBkVlfFNCe9l3Yl56GLHolLivzK1RTa4qc8N +SK30OcHterlEHAQyciCR/8Od2uuwnfroscKyDmfxD3itHJbZYb8K2iUr+y8aS8K3g8Sl4JS/TDNh +8RCtMbhmiN1R0U10fbjiJAXGsm8gKAbcGncjrzW724DBPac5XKJaa+Pvu9f1TumUoo4meWu5JzQk +0uBx/1l6cuTwUWMHzHTzRJsO/pJ/6tCNA8YlilLQ5Hp//qnUblkzZ97qtHlqmy7t2r6mW3pCQlgk +sD8VTGDAi072PiGBK2z9wnKHbguI8tAph0ogsPZhYRRIYESFAFsVUrFcp/BHCFojXXXhruYooTVM +4lEL0QxNqlSKFWolxD1lcOrESpVSFRbqCxG2RtnrIlvtMUSyJEFWYcAHCJdiSmorAOQTmU3WDwOB +CSwnSD9YlEHmIdyNllxGhATjkWH+5JO18Tro/gTRStT2IxEGbStSVRN5oaqo41+y64cPnDw0bzKG +WmXzufVbvswOjeuuyg5IPc4Qfo3Vc+TUhZnT7xrYZzryeT+s+QRVJV06D8HgrSk/HyEOJGp1R0+d +c/hCF85+TC2LPXHhwC+/fj+oU3qsMtztdqG0g6PYwkCCKYOBDKl3aHcJxQqxVCWVK1UiCR5JpArE +UzgF5GQZZEw7syTUMxEXlmXsyFJSuNy2sagxWNaDW4K6Z/gRXqkLM1VJbSIhzOWIDE8d1/8ukz1R +Lk9/4a4V/ZIHO/SNDnvLpbJylTj9jee+WTDnmeamS8vffeDrDe9PGJQ9NreXCr2AeBZpjKKoqaK2 +xXb3rU9kJnb3Byxrfv+subEkKz4O1Sq4B7iIWCYkwF8kfOQc+UKVQiRS0D2CFaQoEcqa8IkAFpCX +BFKVhIWNEhAlRVI5fCd4QiBvIeYkZXeSeWUwMrtX5DSx4mVGZ2asLfo+JItxY9EymdzmKzZhLpMK +g5wQTyHzlTeHOTymqhZDcUPNmsOoHeRe6zHbMWRs+dU1m09jhtprWs3nKrsM6uxpNpevPgDCKmoB +8TJnk0kuFVvPVuFlzgaT2h+o+OmQ6WKtOiE8LlyTv3y98WxV3tBsVPRzxe/tG0omUm/qn9ApJixc +XfHz4eK26kOBUlr25S7U0YvD1CFxoWqP31wN6bbWmvXHjOerOh5S+0eBSTT4xyXR4/KMpyvSF464 +1GAYPb1v0Yo/rJXNznrjX3572zk6XHqLu6xRbXObag3AaQ1HSsu/25f/+u8Nu85Dm4bk7tknlLy7 +xdFkIqSO0mA8FABg/qiSwm16s+1kZbhG3lpvwHGKK+23jJ+ZlJHiaTXm9E1NLWy0Hj9hLyvmN7eg +0bCLL4IcoMMvVDe1ZoONZbGFKaRJSeFRiREyidi477z+0y1Nn23JGJITZndUvbpWnhzVOTWq4p3f +6tYcxCpuOFoSlRgOS1C/+XjBi2uht4D7YqtpteRXdhnY2dNirvzxYOE7mzx2qhwFcxjrjae8qXrT +CXujITQ1OkoqPvPU6uaTZXCm3ZVNFdtO2FtaQ1NiIpTio6+vbqqva66pSM+OycwIt54/UnJwU6vd +AdF/XzPMr1fmQe8SrpafSjaY20Uu6+XHHBrI/sLS22Qi2UW+7JaRueAIDvQPF2RybNagJWyzg0Fq +DiU1WW6TI6TSiOVA2sv1E1ysyaU223+Sm89qH4OS4uwIrgtF/v0L/lom8Mpp9b//tzaaIRfVBLe2 +HGuwUQS1GGIMDa4xLTxpwq4IToXjznPLPfU+1+hJN/fIGoRbvnPvur3b1vVNSYtWhEglMrFWLg1V +Hyw4U9agf/nZj6PDMtxO03sfPi/wmscOH4GV7Fxp4YGzhU89/npadC76xa3d+OXWXb9LoYrj9sll +CizcHiSsES0KIAUHAJXalLM+oow6TD1tWVUGpZyoQE+qEDlDfGWShnOC1gv+pgKnvi5gskndEdEh +AndAI9GgQQVwNXhX4jCFUec/6q4+JWw+LzBV+mytHmhS+0M1amovyWXSWAUiNZckeA5cf/ByKTQK +9qRhirysXxLl7PAuKB1D8FMMaSgMDQpKidXCtWrisncsRqGQ1CX1tQgdJgl/yvQ7OqV2xwfuPPT7 +lt9+GpSdGynX4eI6Zf7Nh/eqI+MffuilEFV8eUX+G2+90Ldnz6H9x7k8xh/WfCuRu+PSYn/8bdeM +G+dNGXszQsYPP3u55lL++L79lV4xmk64oZYGXQ8mXsGg8GDEyvioNJugJ05+IhTkKBwm84DLiPQE +8VZgB5EHAced2iFexk+4uoXgKGGQMlctCocJRhcNoQIqgZnvUkfH9Ow2COn87E5dxg4fNmnsvB65 +w4sr9ix/+9mG1qbxI+c88dB7XbMHFZUde/vDR//c/1t4RCBCro4PjYP7Cel0K9+zcff+CVNvmHXT +fDgihZdOfbDytdTwqCRVlNyHQpMAJMoVUhUqjklMiI2C9pY9VOQKl5sAYYLfOcUH6hOKsAPEJ6RV +4N6xvtvExcE5E1ZOmUnmsVCvXgHXeocI9awijBNdZSYRTAUSeA7wZj73TPvU5d/MdC7/EzYEeSgn ++DePBDnO9Kem7dl2ZtItg7VROgC8FasPlHy289/82KvezpUfkOBbkN9Ik0gik6NdWqvTkZCWMP+2 +Of2HDTZajXZTo9/U7KossVaUWisrA3YbMAPYK2ii0viE3im6rlHnOnQKJBFUtF+DEA0aTnHcTJL3 +hBAik3ShlBuFRFSqTNV/LDYjk8LiM8rhkezTZdeHBWJkC4LoUJt14mwYJUso20d5BOqCgcwduueg +SFnKg+aFSBkIVYsidBK33NfsR0WrhG8UeUsd4rqA2gcMLFhyQR3aAEqx4o3gvKIhSAwd1nyK9Zxi +ldVcQo8qIJlJ5BS6go9ZMRWnkhpMc1wJbXA2k0MmuceMgEpUT3q+TRIWAz+Y8qQOcIynQ4lGru0U +cXaYhvhVruF/77j4T/w0zgUJOiJstDDkjxAl5NqIHYqFAyEFlhFRQCrHWggoEwImfKPSVWCzrfx6 +c1pMN4/X9tTTd5ad2Xfj0FGh8hCIINlkngZL6687do2cOPuZB9+HoNy+I78seWL2/LnTclIzhO7A +ex98kZDe+7U3VkklIQ360qk3DUvKiFSrROXn8/OSUrLC0gRmMZpx8/hWj5vvBj+KFH4pnkdjY8AS +iONx3GBjwOn3SFzNPGOZrcWnUaTn9uzVa2B8bGJVxcV1X3+WLpbn8uPUZik4mXqPQRSnOdNaed5Q +L42M7tt/RN+8oTqV8tTpPb/++NlgdUq8Q405hu6mABqQSgAhi7ACL/QVXWj1SpOQinsQJrAGwISC +kKAaKXQyrguOh6tyYSEjcXy4ekoi+OB30tMTO8J5pSKTUaN65711iVFdXW7T4mXT6y+euHP0FKmJ +L4nQnGkpWbVt19PPrrhx4n1un3PFuw+sWvX5Zyu/H9Rvut5Qt+DemxLTwEjynz5R8+1XG5Njcvaf +27T0vtlje3ftH9PZUWGWiRQOoEkuB2uvQ13K3Wyj2AdacNS1lYhmMEWUG4asGrWkwin6XDA7TIg7 +gGwhCSRCY8tFhbyksEqYT3A+EpBOmV8WaPGR0YVUnUPk9keK60X2QFT0Y0+t6Jo2BPg1N9b3ntj2 +yqtLm5sali15eMq4u+XqiFOn/3z942VnCs9mpOqSYhIKTtco/fxpY0eFh4ZvP3w4/1L9ive+7JUz +Avf5rY+eWbf+8zsGTw0xily2FjTrRMdckOUCDpybB/LGAJRcIJRTp09qrQV0hwqT2WqCtYWOHSsk +W0oAQDGqO5v3dOdI14N6PFKCnI0r1u4LOBMkNqEqS8uu10PuBVsmqIchuWT+H6hlb3AT5l5Jy/nf +OLk7iuP8lw/D2WxGYqLXzP7amJCWi7Xg71T9QjyU/8aNC7w52gKMBPjSRPYSiQxup0HAmzB13KKl +dydlpbTqa52tNUJjrb7gWOuFU556qArbIX3q8Issbqjue11wAD2496x+kfpxI5hk05ChgRz9hETD +aTzTA3qGuisHs2jMoNF4bsfIrwbL28I0zjRy5oZ+BlnLZCBZix36Myci7BeDe46m0FA7FOoIlOFZ +xT5YcYFDIgABGfI3TjSioXHJBX1cY5v2XAt9OMfpo8PiAkKKHblOT5xF5mwl9vbaIjooloukmPGa +nUs3cqaR2UL6YJZEZIxWIqBSjIhLxF7DMNU2eXFi4rCdix2DVY//ZtT4f45tvQqV5X7lVkBO+IYF +iIz9QqUMVESBKjagVYh+AiqxQejwCiV3L3wIo85sbvjmqw8yU2NTU1PLGmr/PH1wy8l9BwuLQiIj +n3rwlRB1gs3e/NK7j5SigNFQtefIvgMnjtXUeZ55YnlqYreAz/3zj9973Lynn1wxa8adVfVVZcWF +qZGJaLkghXSmH5AdV3VBnERMA6SRVWIFtS8GjKaQeOX8Gr7prLU6plO3JYteXnzz83npQ1OjuqYk +djpVcMLc1NApMknk4olUQl+k+Iyt9nBt1YAJMx+696V5Y+/LTOieFNMlLiVhy5FNarsw2q9W8KF7 +R1k4FCRgXJOmYgAsSHgFFAvCQ0SoQRET8pbBSk/SD2FzjrX6bgungsQW1tWZrDddPREynB41v4Xv +ikztNHXiPKFQbrI0fPTR8gGds5IU4VCVcSi8a/fs7DFgyL0LnxYLVPuObv7iu9fkCt69dy1TyqP0 +rVU//fJJeKT64MGzt8+5b9TgG+0ew4uvPek3t47u2l1lEYqRFcAKwtpX+WAznODhIKgG/kwpWKjS +yUikToZjEvMkMqFU6kfEJ5b4RDg7gsvhfpKdJElVnCNMK1s+2JzlZivnQrEqB6YTx7gt1IkVos98 +iUrSZGjcfeBPv8TpdFqqG0rXbVn9whsP6ZQhbz7/7rixC/0S/qYdnz764l0OYatSo5gy7LZnHl7Z +tdeAHYf3lNSVeiWu/SdPzbr5zqnj5vP58mPH933w8fNh4YHY8Ag5GMZ8P4j5IrRydIkkUHbAEcJX +RzM8npQk2wlvp5AQzR8B+dM6CJVQED4RqMOtw+oFCBncKqI6S8Q+rlJHCneBo6ejcxWNK2R6uB7l +weJpOnPy4LGGs547WIJmPHs5auwInrf5/f+NZuR/x0dV/Hho39QVv/d4/OCc9xv/QZn/v3NcQRYf +UzzBag/xN9QVNbgd8pjwpQ8svv2eRUqNxNRc7jdUIadYuW9n67mzAb0BUtbQMfUE0HORZ/fw0GEK +DxyeAPowOoCs+ugnepW6qMEa5SXd0Joh34mCRdDQ6FcW/TB3kHZOZJT1YiSr8pdCLh1Pk7MuRCUI +lsLT2wmBwikAawDnQkQtQQEaYWQpJcj5QP7Xg8aQPKSEbHye3iex8WTUPYoQS87KMYWmYP4smAtj +BowVMnLljMEKP0JEWekFFw5jY/k+9jvZOeKiXbHzgr9i1aKd04WhjiWgBYCPyqUtWcFGkLBD2SCI +f9AOQ4ifnLtPrgZZR0oxBg3rXzFCO7JD//7xvzN0/he+t6NdZCSTYPldEP9jtPag8gtD3cBphKQL +60LEKigkckeAFx4RhQGCRbfF1tRkqY1IibzQXPXT/u3e8Igb5y966L5lzz36flpCX8R8rWZ7iDr9 +5hvu7ZwwtWfuTTExQ2+esyQ3Z4wXIhl+f3ZGl1efeTM7sYcAWJkbGrxilThUAtJpiMyt06AE3Cpy +WKUON4RTNUIdFDhEQq1UolFJA0peA6+1tKU5L3fEG0+vHNRrTEVVucthwoU0Olv1RiNFSFgdI+T2 +WMkZc9X5mtrpU25bfv+KnIRuFQ0lXj5oDb66hkqX0abA0q7VeDVSo45vDOWhHt+nROEc3ACK/zxy +kVnD14cGDCF+u5rvkVMwDTOJkhHU9EFnBUaUQD6C9XCtiPlJqitE+yQRFjSVgFUFPwSDEwn4lOQM +QMoej62svNDldkSEhKMhAKhAJy7lQ+xqzqy75KIwq6X+101fW7wGtU6nUaKiWtZiajRaW6prqhIT +Ok2eeCMiot17Np46dPCGgb3jZGFut08YofRESBxqAcRhBGqlAt1oVVq5UiNWKYWhGr5WHlAiekbh +itQnBZ3cB/DWpQi41AGHJmBTeZ0qH/oEQKydcv0BUFnbNi7xzCU42gEG8nwZMwCFG6DDOkQ6myRL +Fhrmsv34+fKlj024//EJKz9/afigIV+vXN2v32Szq/WdVS8+9ebTqSm6kb3zXCZfampeIBDatcvA +KVNvLq82b993Kiklb8bUW612Gxx+k6k1Pb2LQKzaeHbnBU+BWwtbB0xcIVPI0PMYt8MtJckHGYjI +crlArhbKVVKZUixTCBRyYbjaH6l0hEncoSJvqIQfLgkoA0KZUIKXitQSiRpvdCsDNq3HofN40P1F +F/CFeH0aN0/ilvC8Si9P4RNKGAZLKBS1DQoSqjpOUP4cKhO9YmtPSPwvnMj/J31VW6BFNsYrBDTq +swQC3fp0u2PhgtSMdKO+hWepFhhqjJdKjCUl/lY94ZF+8vDsHmqADruAaWIjdICHQmAE9/SAWkoF +iNTC/Bu8CjljMi1AAmhtYYAqo5m0resAPqhWm7tZ9JN5t1fZQu53Loyj93JoZFvUyH4FBQ3cBD/8 +TLjLPimazPmVcn5kqEyp4dkEzhawXbxSfr0gUOySG1GvBcsEJTuysSKC59mnsuwfpTlYTMhwVC5H +QLaTQkzKzwRzkYwMfvlZ9h4/LTdc+MoB1FexXjkotcO54e+sNpGdNvsbFURydSCkFUfIMgwkLh2V +fwSbGMNTZo3Z/+/YOprG9iWv/UnKLBIUjtgJMRS8cqkfnBdUSYhEaqkCQmfeCMlFT218cvarb30l +lGqPnNr+0P2z5t44af+RU+rwhOXPvZMc0RmrOa48ih0BL8C5QndtMYB4IF8oa7e3CoRatUzh9biE +EEmhxQf3yPHFqhe/+/qDfl069UztUVVTUqEvrzCZmtG6BeQLSSBSoUxVRydJwtROIocE1OI6gflg +5enkjN6vPf21WK56/8sVv25am5yoTYyPNrXqL529MDo3L0MSAf244/bKzfknp8y84+E7X3daPC++ +tuzAmYM53WPCFdr68kpTQ93Y7B4yL7/O1lxlb7T5XAq+NIEfEmIVCB1+e8ClF7vqhBazwCn1CFV2 +kcoqkFh5YgdPCCoBOXHsf8L9uRbbpHpDWUZwQ7CCk+4O0V0grGeLEB5rLZ96x923zXvY5fat//2b +Dz999pbBo7NVSTan6+3dX/UaOeHFh7+SiUO3/blq+buLVbHQ/0lY/eFemTRs59Hvn335HtjcOTc+ +cO+85SZj3YLF80T+urlDh8oNiKCklS5IgV2wggJvawlTqTJCEiMVoRaXoxE0GGuL02FBFhJwd3RE +rMNnq9dXQyORlFTFYvjWLWYDImVFQKF2yCRGAd/sQzMOYMiELON1TNGR3FScHVcISG038F7Ui4oU +qHhBYSmitFCpX+dvkRjMEsfZiooRY6Y9vvhDhTSCz7e98ObDazZ+3z8jY3xeL7CAP/lzc2Rmr9ef ++TBcFWOz6uvqS5EkCgmJjYtJQboU3Bmfz262Nreaax559nalyzEsg3paGdGxy1DaaNPb/D4FT5Ek +jo/khwh9dPx8IOHAiUU+m8jX4DPXe8wGt9XudWIEqvj8NK0aNS1KjwoiEuhsbBBYWnitduiSgKvs +E/HQww+PnDypUSg3CCUW0KUEAKXR1grOOhWKMWQVsOtqz2VZb2HOfwyg+h+yZGHcUyPpthptXDRW +9AUjwFrxoqRWQu0Skbf3SYVmt4enUt1+1533L3sAuo9WExp3l/vLz7WeOVJ78bzXagMLGQI3Fpff +joaLyCMi0MHK4RPA0YaFwXBhGXH0WiGcAxA7w27I3uB5LuihAJHoCZw4J6e5RnKkZKLYU8GKQYIE +OMPJ2gC3cZM5NkybrWEhJpkyLk1JACSCRT85yKgyg9SIFyx9hRqIFtVVuwWCJo+Q51TxLzklzT45 +aaPjkIgKQ3qEDKYlihfL2nNZvGCWkXNAWZBK5QEAMuiVbOfgUzJqQRvIsAzKoHKnxkGqLCS+vLOC +eO5X8gOQP+HyhtyTDA/iHAyWMcB1EwicjCGF4mesDzg84GZX0tT/dw43dq8ub1ySNQhEM6z+muqX +q4/2qrdw72XMBFKHZg/wJVzPZgIDKf1LDbjgcjHeCbEUOBIOUfUIYUREhHSjAGWBkgB4Lxqe3m8N +qKWTJ0yXCNXHzp46fmR3z84JRYVnR0ycNazvHH2tmecD8dyD1ruNraV7jmwxWJqNpnqD8ZJJXw+t +r1BVpNvhEMt8Fwv27Tj624bja1/4+NE9e7b1z+2UkZSy58yBQyUXjAFpcmrPUcPGDR06MiE1w+hy +FVdW2DxmdPQNSBVOme946RmJKual5Z9CT+f5Fx7at29rYqrC4DCcOXNa7Gro26lzoiouRKgzuO0/ +Fx3N6T7mxWUfV7fo73/2jsrGC+np2ktVl0rKCmGfx3TpJwnl7as7e6y+1idXKRPj0GvQatbHCGMk +Yk2dznwuUGdSKQZPnBTRJetEwyWyBo1eNU9Jk4s6BCIipEWUWKGwiswSYn0ncQCyJZjlpOED1DEg +81Y5DDkjhnfvNBjEot27f226UDwwsatELdxQuqfW6nvt2W/ClLEma9kLKx7TavkSj7RTXK8Rw6YA +/jh4/M/9R3bHRqW/+sRnWGe++eW9vZvW3D5ycohYa5P7d9ce+eXInqpmm1yrDYuJMXp4Z6oqTHLn +oZLzZXqzW6hURUSqYmMvNOiNYlOVsdHmFyujU5u8DqOwUe9zdO4yZPzEm8ubq/XeKp1ALDMARAd2 +yqUjyLWkRQdmmcnSggCDAQdGPBisQBR8BMjC/QB12Ov2COShoSeLi7Nzxrz09DcBj0Isca1Y+cj6 +DV9P7tlzSGJ3FC+iRkAqku8/urtOXzViwFhYfaksNDoqTqeN8TtxVXylly6CnR4WmhiC6h+ZZ8uO +jTHp0WXm5l0Xj5XbXHxVTFhUrCMQKG64hNMKU4cKnU64bHalv5ZnO1F/ocZtU0dre/XrPWjAiB7d ++2lCdGX1DWarFdB0QC0oddWXecwujS4kJtEl8JQb0A8yTpUQZZZ7m0VWrVLubbHI/DKXEK1HxUDG +RMg90tIL8oZw+jNPt8+0/3vc6H9hKSQsvy06g3PIEnWI49jqQmuvEGk4RH7NDnfnvn1eev+N6fOm +1dYX2hvLhDUVluMnqo+faq2qh8oo38Nz291uB9oPeJ0unxNKN+AzuKC+6+Eyi7ghbEeGmVLKWMyY +kFiw0QRXQseyeax4oeNy2hZDBSPFfy4BRisyS/i1lx5ySzBJtMHmMTQVZ6tkOhKOgK8VBAkUEpuh +7uRFgxzi/7QFphw8yyG0V27BMI4zZPSy9hoMptnGhGnarACzaSwFeEWRQ8eCh/bijaCxZd/IulNx +uOflDCUr/giaAsZK5Q6BNZy9Ipz+F0bC/66XtpvJf/TgXziwjkBZkDQcfDddfiofYwovnGVFio0V +gOFeo8TCagYjAn/zQXQXzFKUE4qF8ovnC0DiloMy6nagAYDXZ135xRv3Pzj/vkcn3v3w0CWPTx4/ +beyx00cwYmUqxfb9W6bMH//Bu89v+eTzWJN7zuC+4Vr1toOHq1r9M2Ys+WjFL5+/ve2Bhe/ePn35 +4ws++eqd7YsWPdloctn8DkmosLihvNXtW3D7Q8kxPUvK6/v1G79x7cGfPjq19vMDi257wGKR6jSR +EoWkVeTcW1ag0sQ+u+xphVheVVRy56yFO9ce+/bdPas+2TFw0HizyOOL8f154ezFJt/Um+99f8WG +79/av+KZL0FDMoiN9hB7UXONLiT9tce+feLWbx5b8O6EsdNsXqtCLUaFEZUJUcaK7AZZDOZzsnpG +NnG4VlRctR0BqviJWgKJ2Y4EPcYjJS99Qo8YXU5N9cfPFd84ZUFKTGfY1u17thcVnB82YKjPJdbq +YlC07xMLGlqh0M6fOfN2mURdXl+w5fdfOmckRyeGtXhNGw/s2nsov3feyNee/fj7Tw9++e7xrz/a +PHzIhAsXqrv3GPzSs++s+X73t5/sW7li6313PVpbbQnRpL/wzHfffHzk2cc+9Jg14aqEJx9485aZ +T8++6R6BSOPwwMQruDLjYI8R5lgFvds2kIZN8GDOhMvse+BtKoWltTXqsNA75i9CmY1SLdy685f1 +a9YOzMnunpQD4IsyQk53TmpWr5yc3fu2txjN1XXFCxbdePT4LhIJkTpPFWy8Y8n4xQ/fWFR+WiBQ +pGb0Dog0BRXF+w6fzske+OKTH2z47vA37xz/buW+oYMn1aKVo9wjitAADi1uPneq5FRm17xXX/jg +qw92v7Tk1/tmv3/PrDdfXrZ+5Vt/xmTmFPKbLgTqqly8yZMWfPzqptVvn14054PJwx5c9c6un986 ++sHza7LTBgH5B7KOHoqoqeWLnGiODDCAH4BMJzUq6bj9P9N4xeWghZe1f6Cmimz3gUXqJyV7XClY +LwwmlNDoQXtSKmfdecdjLz+T0TW1tvIcv7XSV3mx7sCuxsOHXfXNaPQAeSWr3YM7YYZ1RPrQy0cn +Yzxw+dCwAo8xgnzosOj2gZtKgSPwGsSUjJvFpRgZjkqMK4qwGFGOyvja0J0gAMJFUX+ztYt5ssAz +GE9yM5wFx1wbOx5Y9CgiBhtBJuSrxaj4EVoCfpMfGWwxT+/mW8GahtGkQn1WYE8rNmcXmbnsIBnK +jFZbcwxmuGgKBo1xkFIbDAy5IDGoONouPfoPHjDIlEVALLuKr2BSqIygxLEIGTENySyUMHIXjb6a +s58svvw/yT5eN/XJReR/v19WV2571DHpyOmfMJSZKieCjXiDysxU06BFk2qDzYH6RJ4gLETjcTkx +fvK6dj55ePe6tZ/LVCKVLhSgicPpQuOD9JSUiBAFjsfuco4aO3jQkOHQPm211X/30wedsqIW3Djl +2dsX3DNpKs/m2r7vhCQ09qWXP39k8etd0nsVlpw6fHynvqUe3EE5Tws5OnBHQLi08l2nyytGTZ0z +ZuRsY6s1JT5jyuQ5GuCPHqnan3zLjGW60Izi8hppiLLIUXu8tXLxXY8kROTYLY7hAwffMPImQUDn +sWviw/JmTF4YEMt+O3CoxuR/4qkVi297Pi2+t8Mq7hzf1+Lzm8Ndhfxau1I+a9aDvXve4PVDbk6H +EjnMfJFcTDxxBFIieI0UaFNhHKt/ICGV9t4ObP6QT8HYMajygGxBZUUlCLAQntKFxVr47laR4Xjp +2ejY1FlTbkfeorL2wrdfftQ5KT45Mra5pjkhIZWieJ6/svJSl7SuY4fMdPH8WzZtbiipGD64X5O7 +5ed9WyvMljsXPfHssvdHDZvFlgWPTpGYndlXIYt++P5XxwxeIBUm+DwyLBH6lhaHjT9/7kO9uk+y +OQR9uo7WKWNUsjiVMtrp9Ccnd/NAhw1FjTLOB2rrMcKVNjK5h+AGKhLqGDEQqDoZFhKm0WPzu11y +X5W5qUe/kb16DMEHGC31X63+QCsV903pLjQBVvcpFWKn2cZHRY1MHhqi0mnkZ8/vddjrM9IynQ4s +oZ7dhzd6+EAXLqx4+6E/dn/91c9vCdWeyjrDvYuXvfjk52MG3Oww+bxen0IS16fv+Cary63y6gXO +PRfONLv5dyx+5OnHPxnaZ66Ypz18fNelqnyXy4jrnhiXEZkQW438s1P+8osfLl30UkZCLkq3x/Sf +9vwjb8eGpfJ4yrTYoRlZg5wChQt0HqUKpThCmH70J0GOiOUcr0pI/T/TeK3/zcU7wWAHfDA4gG40 +LQJTS4zMHL/ObotJjl/63LI7H7iDJ7BWnTwgqiu3nj1xad8uQ2WpDAl8idji4RncPgtPiB6cToHY +SeYQ8KnQ6xHiAWyk0wdVc5BuEDvCLlLsCEIBJcfxmLFsCEqlVkmENbLAi8MNO6zvbO1vyzX+RRTB +BXYMQQ2yb9oxu/awjUleUW7QL/aBmyqR8tTIL4DgHaDiXh7ktmwCfjP066jugisiZDAoMZA4aVOy +bVcEilzpPqdXTa9kYSJ3/Bxyw/F0ghwZasjHYse/39sC6GCbRlby32YUg5JyZDhBtwFxD6YRNQG4 +dCwg5swikfH+f7ZdgTh3RJ+Dg+RqgmpQ8aWNksPaFlLIiFiHOIBMG5u1oaCHqDfUiRV2c8uho7sB +rSVGJ2l0urKa8pgkrcForK0qQPJHSmIzYq06/KlH3vrxiz8fuWvl2IF3+Cxxk8berFKGCXiuQ4d/ +r7x4cmRWl9z4VLVO9ev+XWv2new/fsr7b/0wqNvIo4d2PPHMrTPuHLvi4+dKLh0Vio1nzu9Yv+Fz +dJ0JTw49Vng2ND5h1o338PgquUIulgb2HNuw8c9v3F5oAtuh3ZqUnGb2OB1S3s78Mz1HjJ0wfD4M +nlylabU2/Lnvp0PHdwskfpvF3rdrX4VAp1SEf7ziy2lDZnutLpdTL1PwzxWdMvIdtTxHfoth/PQ7 +J024xeGxFBgPfPjDc7t3bpYJFU47LKGc1Y23ddbgDAfh0ChsZF0caPIEEwfM7aO/KMXSypJijxsz +R5zVuZvDxz9bUXrqUt382xbHR2ei/Hjj1m8trRUzJgz326wehyU5Po7n80gCPmNd7ZwpN0VpkwtK +Tv3847dDB+QlxIet3/pHg819yx1L75z3CHz1pQ/e9uf2n7wBYjdZ7a2gnzoc5sq6EpvDDGrmqfO7 +Vv20OqdL39HDphgtDSKUQfJMPIHLZNLb7RamxI35CN4x/CFM6bb+XCQiw2B4xsHhNkL4We0Vy8CQ +OgQVPwh4rR6DXyUeNXoG0CWf37dz/7a62sKxA3qLbejxBk4SsE8HRpNCrSi7VBOui5RLZPV19XfM +XxgZlgpBhdP5x9ev2zQwt8/EwaNLLh5786Ml+09sQbRw951PLrjlcZlU9/hTD2745UuHA4R4b11L +k1QnsEoMB0sPBrTaex95Y/7c51S66E+/f23enWPvuHvG9p0/m5CSbaj7Zt3bRw4c6Bnf99PnNgzv +OctjV0D/mVSTpJKi8jOrN35eUHf0eM3W/IpDIjUk0N1OYORQmvDKQGdlywQgPiweV4BL/6GmEa2p +IKaatXQ8NEu7vTK70x3D1Vf2d/wfW+ZwoVhTvaDIEj2mVRtCnRA98PjAeBs7Y8Jzbz4/eESvyguH +rcWnQo31dXv31B094W02QM7O4HCarC67HWLmfo+f7wzwLNQKk+dwQQDVz2BVKtjgqdS60T3inpyd ++PL82IXj5dlJzCKSIaHUI6UYOeMRtEYdokNmIwniCVqkdvLN1WshR9ZnasjcxuUfWf6PzWOGo7H0 +FNg3ULv3Q6VSLUbU5bPxfODHip1CXotHaPWDq0HKNdT4MAjKciWIbWAqV2XflhsMwr84TiEppbJX +tkO43I1rh2Ep4mRB3t/vQYMaTJ3hsnC9G7lyfrporOiTLCLKmUBx55Js1G+pLYfHCQX8n7JdF1Dl +Es9/t3NBM3ff2x5xIQEXO3IPqEcw0xJjDRuprTuD2ITwpJUCSZhGsG//bw53S0R47MBhow+cPvfH +7qPd8rLvmH8XIDlc+6OnDry38pX8gmMg4IwZeevzj3/21cpN40bNEwlkLa1l33y9MiZEMzizN1yt +TYf3bL9YOHHugqcfeT9cGbN123fvrnz4+IXNcoX1oaUP9us9uaW5+e03Ht//x699cpIN9ubCysab +Z9+dEt7FakEKovmlFYvve3DmMy/dV1Z1Cv1AoBgK7qQLychLhcgY3XPLEiinyeTSi5eOP/3qvfc8 +OvfZNx+p1ZfK0U4YQ8sbAD8uJjpy1fdfFZ49LRJjYrb+tuV7EeivBnu3vNFzpi62WluXvTR38ZIx +Gz5+S1FnDLdLRU5U9UnYlJEinQgfAoJ2+DgGp7Y336TWgJzECnkZkLrz8eOUSmtj5bl8qhnrktEl +KjL6yMnCtMyuI4fMAA/9xIUdv+1YFZsW3qCvKa48L9XwScpGKLHbHPHh8UMGDHN7Td+tfkMsbBw4 +MPuP7dur6t0PPPD8/OkPlFfVPv3ccnjV3br2lwhCCi/lb9zyo19Yd+/DM7/6/k3QNmEjf/zx+7q6 +honjZ7jcngsFx0RC15mzR8y2Vqu92WBqRUq5rrYG9Hc11GOoFp7rX80pi1L9VpsIQLBwEKXOVNPC +OHZc1gWKtAaLISw6qnNWb4/DC0Wa02ePaAXScJEaqgg+MYoJsQiIJSp1lbm+trlu5ODxgMTCdemj +ht3k8nhdfv2P6z5Q8Ow9YnOztGm3T5sdFxHmtcgWz3ti7rQHUNb55DP3lpeXjB0zU62OKa+5sGnb +OplWvP/4RZk2admTH44bfkedvv7Z1+/6+ffXy2sOT5g4cMqkOVGRycfOHnr1nactzuaoSK1KJVn1 +/WeAKzyotpb59p/e8Nybd6947+7F9w166fFpLfmHwlHO6EQPJDDewa1CGkmK46YKaDrTK6zhf5xp +RHOo7MenDl/3MKTasu4YHtM3Q5QZY8qM7vvF3fETe/xPL23c8smxQThKJ9gwYglqFnh6lyshJXnJ +E8vuAeNGLa7KP8JvuGS7cLJg60ZLRZmUxNMCaCbAcwv5TtQ7iiRdM71+IRrHwJ6wEgLSxfaAg4OW +5UmRWd88EL10SvwNvUUZcfVx4amPTCMaCavrJ4YJgym5KI0r7OMq9oLMkzb8lF7Ubp2uvDTgGkYO +6SwEmZtZsI4ZQayJLODgbCTNckJWMbTB5oJeIaqG+H5UYIPkwzO6XXoXNWVEBRqeoADxcnhHH8wy +fQyzZfwXLvhjZpDV5rJsBtFCg9gfO34OtGa0HI6Kg3depbd9za+s2zsXy3Pv4gJYCk/bI1KO1cO0 +UtmhMlPd5kGwd/+fBKn+2yO93VG6HApcpmZxxRxMYTkIBjLtUVKcZH2qCG2X8ISxkdojxw4eO3NQ +LtbOnD0/IAsrumSZNXNpfGwvKP95vM1vf/Lwh98/9eDzUx98asbKLx85e353Zuc0F2IVnm/NLz9e +KisaM3o4dMTPllzcefTkuInTl8x9XMuPOnx43ysfPNUaqGg1WycMnJrXaShu3emCs1VlpTNG5/XO +zD56pKhLTu/xw2cgENGqhM+/fv+Wnatyc2PlShSyaQMBKTyppuY6mMYjhedvGDuza3J/0ABq6i6+ +8PID54v2pXVSR0RDcAeCKrym1hqr33Si/MgdTyzec2JXYkonoUhx4uyxA4cPgO2v4GvvvmVplC76 +k1VvHDu0LdWnHiJL6+YIjdC7NURftNMKS7gi5HrgHyO2QK0QN22oaJcqAKGSTgWipNxJ1Rtub5Rc +DjnG7du24iJolZFTJt7Y3OS9c8FjYZoEf8C+fuuqRndjjbV17fY/9587zg9T8TTIcoEMpZo9Z0F0 +XNK+4/v37dk+dHDvunrDvoOlN824a9LIW6wW04F9W3JyOz3/zOtpqT2Lyg+/+vYSq6s+PiHS5fSN +HDZBKdUdPLHz2Kk9aemagQP6V1ZVeD1WIABHTx6z2GzoA8zSioLqymJo/PFdLtRkMLVSTp2Qqeay +lYQ50KzyFbIGpG5LRgMIDelfUREgrJ83MixUK4uAB+/wWWqbqhJDo6VuXCY/WjA6fDCOEN2TnKq6 +AHm13t0GYNno36efXKEFcamg9MyB/TsGdc8LxTW1tHjN+tILjXOnzp85fqHd6ty7f2NSYszKj1bF +xnWqqSl+4Y0lzbZixKEKRcxD9y/v332sxVn/4WfPHDz6u1Rh12qipk1aEh2e1eqoWrfxo8yksOzc +lGPFR+YvndTqrAwJkamkvLPnd76x4uGm0uIusoRkW1hSU0RKXZTmkjrMFY7EuUuIbg5YjIlnyfrh +XQ0s/WeZRmjZDF3/YOpN/SCWB71QY6MRMqrmbecSUyKfvfOj7GWTsOL/O2sGo3xcwQakIJH8PgQc +GPQY3WLkOfAYErpohgMWKp4zuh0mvn/SjInPv//ykHEDzQ0VzuoyT1lh09F9DeeOuYzNIOd4vHxw +t9Cr2uL0W9ITAt88Ln1iLn9wNyckHZBZ9AWcfr8LMhSYBJHa9HcXFhXW+Yy2C09/769tQUcq05ly +puRJGp+sPQWrYWfpTlbczlJnXAtVFvmxgcsq0UBNJrooMUZZMwviDITmpYza8WzvN2+NG5vHyacF +OxqyxkfEUsZQxxooCripnJECY1wWNC1QkMCHGOk6B5pZgJ5m9Att4AfgEwBAcK4CldjD9sBes47Z +NGEoa8HE/Jmef7CynxlOSghSiQopdBDPiHWVaiPIMFEweG7kvJGaOCcczmRXOciVenFzattM3gkT +k8oTQWCij6G/MdEbEkFFj2KQUZn/QcpeSPhAcZbqX5il5rKj3OdesRMdOLhfA0j+JVzJBcrXCdeu +G89d84I29yY4tP8eKf17C8/FiBQLoNrOL+fDilBzIS/utUCi9KOzokArkSgdSH9jNfBJVUIt347m +d1KfEHUKAPjtKBqCYydwC8UuXrwyXCf2f/rpi8WXjvRIH/rWsz8vvP25sSNnupBNsjU98eLDp8+c +7pmT7MAi57n45mdvrVr3o8FoAccVsyojPRMelcnh0Yucmw8fSknr9OiiZyM1cSVlJ1d89LQs1CKS +KcM1ifNueghoZ3VT4WerXkVqqnOnvLJLl+pr9HOmLAxVJKCu5MPvXtx7eNP4kSOkAXFmcpfYiDSc +Y4ulpbKmWsJzi32ymVPvCAjkRmvDm+8+fqn89JgR/X0OX25a1yhdIvo4HDt9gs93h8tDagpLF997 +T2RctMVi/f7HT/3SFrNFtPj2p3tmj4BF2bHp5zx1WKYjTOMUSW1SpSdUIFDRLCENTSQ5ANPLxb4I +qTsMPRL5Yq8dORYwA1wOvt8i9Dv5fgAkUow2sDlVPnFaiOr4yV+37/0Ov8+efvcH7/2Q27kfRuL6 +X1ev+/FXgUE6bfS82+54pKrJp1Enh8hD+S6PQqTu2W0oIrzvvno1Ra7JTcvevHN/do/+s+Ys5ot1 +YpFqwpibl9zzVFhE+I+/f3jXU/MLyo6M7TPIbhD2yBvRu8dgHs969Mg2i9MWHZ+lUetQO989d1h5 +XenWfRuUWkmULlkbEmVwNp08tyeUL1AisoPUGuMBYFqxNYNoFFSNAsCABOdpPpAMCQWLEtQRU86H +6p89VoFdFKLBEEb5vdNlhYCNSKSk5hcBr8IrCLj4Ep2uwdR67OD5ceOmZmR2gxGMjE51eHzWQP03 +698PVcmzYzpDFNqp9P9+fH9iZre5c5YpZOFA8vvlTXpoyWvqkNA1295ftGxGScnRrOjE5iLLLVNu +791zFCb3t2ve33ngl6QUdWurb/SwqT1zhuD4d+3aWHXudE95amdVIt/pVslDZ0+br5CEmF1N733y +jKu5podOk2BURzTpFHUyYTMIHThPSDrTeoAlEDcIxR60VJBT+J8KqCJe7LliLl8hNZY2lP9ytOT0 +pXWf72g+UQaZN4kXXG5pU1ULGm7gZcBa0VgRD3Bv0YUjaXofSKcmTuk5aM0D6YtGQnAu5cZ+kCxH +8iRqSGe8GC9DXxE8KVLL27JWLKSQitRZMYnzBvZ4c64CnlCMNn56z7CeqXK5QpKXGDq1TyBEq/cH +QtPT7n368QWPLTGE+c61nDeVHrWc3ttQdkEfF+6PjBJ5Rd6UJPuAPA9YzB6/CxNpQO7GVfuscInz +y8kokvAVNdHlhI50w3MP/3kuOkpb9PT39dtOO85XokmyvbKJixEh/xE7qmv8hODZ0TInFeGZ9FuH +KRPD2863L53v5F6DfnogfeFInAe45nGjuyVM7Mldk+hRXf9Yvc+iN7eeKe/oSeCjtJ3iMm8Z3ueV ++WK0WEfHU50mbWLfsM5JmcN6D3/2oZQJY3Q98kTRMW4f3+vwxXTJiu/fhWoJyHnk4k8uWUhTh40j +DjwN4n9XLfFBtk17H0c29GBd26DX9vwjS4C37SxcppdwkWe7zWr7hV7JicMxxit9ZtCSks0KJkKD +MC538sHDusri/LWL9fdIJrP3/45vFnzvNRfqv+Ezr/oImHwIYXmg/U4enhAy3mI7TwpXx2tvlFhc +2SpblsQR7dJ7amVK+EeURwIO5pQGzEKnXRzwiGVur0TjUneNiq2+dP7L718tKj3YJ6/7koVLISkn +lQpqKorT4mN+WfX7Fx9sGzZkVHmlMbNz+vSps8JDIhvqK5qaLw3sNzg7Ny+/sPhcWUFlXfPC2x8M +12aBdvbV6nccvMrYuIiacsNtc+/L6NQbq96OfX/U15QN6tUXqtLnzpd179uj/8AhKIo4fPL3H7/5 +sH+3LlFhocUFtTeMm6VRheP1G7f+JBJbaitabxh9Y1ZqD5RV7jv0x4F9f06dNArSxobGwA2jJ/mh +V+Ezbt+zQSiy62tb5kyb06tTH2BnZy4cgnqnx+cZNWTCuJGzUGyMT1N43AkincyNZRIXjecE9COA +QWTaMFT5A+fLh6rHylh7Y7agItptixOYefYQTThsM5ZZjxAyrTagLVafBWY0Kz7dY7d99f2nJ84e +VSviJ46cHaGJbmmqLK8+N+2GSe++8ONDd70ybsz08Ig4pUodqglF5RTovqie2X1sV1HJuf49u1dW +VhrtnptvuT0qLBl+BomG6wR7j699ePmsjz5dphA3zRg5LEKoMNcZB/QbJheH6vU15wsLtDpdWGiC +Wh6Wnp6r0Uas/uUTg6UCTefSkjtFh8YUlp0uLy3SyXRwfhiVjgNQOWiFJjE5VAwuZrEjVytGM5ZB +L8wJJkspcrndmF9Op10n10UlRFW1lvjlBre3VSESh0eHNDhqd53crdZqpo29VS6KEQekqOtWKeVH +T+48un9HdlKSRgStd39RTVWr0zV75s1RujhQ9iVyTXhU5OHTOx5cPuvNjx9x8YrHjx/cVK/vndd3 +9oy7pOKQY2f3rl3/bXp6pMXgjAlNu23OYpT3G02XNvzybVSEKiYiwtBigoty643zoiLS4Fd/+e1H +RadP5YTFywAku8DyILcalh8yBlik6RwZbIWNw6LaSMeXp9F/UNSYee9YdWyoo0qPNhfWssaWRlOv +AZmtR0tRJaOMC2mubY1IDFcmhI/b81ynx6cEhnfu++lCmMMRm5YJhncZ8OnC+NuH79t7ocvdo/t9 +v9jVN63fpwuHb1qmnd0/4e6RQ35YkvH0NHfftC5PT+NKDpBi6XTv2An7X8h84SZLWnTS6K6dF48e +9N19/P4ZfV+d02vlHZmPTZUM65J235gxc6e9+MHLmRNztwjzj7gq/ygvPpMRUhajqF0w40hcqvHe ++c1P3FNxy6QzOZmOmaMccbHilcvk43oXn6tUhWoiXlmguXEo1PvgcQXzYX6erFvqqaPFKBDmyyS4 +hR4bKbNbShswBKNH5o3a8WL6sum+Ydl9Vi7C8+r06EE/PRh127CKUEWfz+/qtGjU8I2P8Yd16f/J +orjbhuF80TAkZnQurknm41NxTYb8vHTYxmVJ0+kA1GGavh8sSJ83jOyrUND5nnFTdr2S/exsS1J0 +yohuGO2p43tP+u5Rec/OI5cvTB4/Yu+fF5ImjDJFZ3W/Y6EqNFKKno/3TMt6aGpobtK1K3cQc+4A +1XImquPWnn0MCt9wtKCgGWxLTDLTx/oSU7qRIkuKGJkb287NCcq/MfFpRjlvL/nAwh9EmzsQjtqP +gTOG/5Dh097OiiVOg/v1Yr7/fiP2X/3Evw8uMe8hPkrxAMXNVMoqV/E8slZpssvfyWFLNbckG+ri +jbwcaZNID1EtmUjuMrhEoGgQIwND1gmSoVfvzwhPH9w3Z/3GP5547v7GxsoAKlsDKj9Pl5038p6F +b+Zmjq5vspSVNYp8uvk3Le2ZO9jlav1w5aubd2xUyEI7denUYqk+mX82L2/gwP5TMB5qGssOnfgz +JFRedLFp6MDpE8fN83o9rZbSn9Z+rlZoOmWmVdbX5Jc0T5iyUKmNcnqawcqJkov75nTes/9AenrP +oYOm4kbVtJzftGUVOIYqZTj02NCJymxv+uyzt3JyoqIjIv/ceGbciBlds4cilDl6ctvZ/COop9Bo +QufMvBNAr8XevG7rt/Wm5lBN8j1zHldKdCcL9h859meaJlpmlUDLDUPPC0EbIXqNeKCGgfUTDSvQ +icQtdtjCLS0ZrU0Zbn2CrEbWwo9z651N6Hch8itIlgnv8wvDxdGCepHGKBuc27mq8viSh2+uKj9H +9DC+IDIq5YmlX65Y/suwQVPz80988cn7tRXl2ZmZIZpIm8MiVkjKas5//cUHSTHhqSmppy8UxiSl +Dek/EQdkamlYv/Hjx1+77bHX7tl1aJMM2lAmd8CEyjAIS4rDtbEYyK0Wu80LHRFPbGSURKT0uh2n +8nccPfUH3Ai5RDd8+AQ+z73l99Vek0kj0kCbD6o9nFlgJOXLGWgu9ci0u1mTb/ob03nkkvcekVaq +s7YanS4jAnofT9Wz5+RaK+9QWaUgPrbe77/QXPPH0T3FpmYwSXO7DAp4SM1VKAm02Gp/X/9jSEDa +M62zDwQnseBiaVl8VMrAHsPdLjTfa9645duHX5i37MV5+w5Dgx4gnqymphbQxJQZtwuFYV6Pe8fO +tUKh3mWFKLX0/gXPxUVl4eh37d1cXJoflxLq1HiPlZSkZvYaM+ImAHOHT2797ZcvusckSBsEcrMS +hduIewmTI5CKW4CCfNSO3P6rJuJ/kGkM7ZeGgzvz1I9gsGh7phacKUdPYPTo6PXB7Qc2nVzy4s2e +FkuPF3Hm4F34dv5+PCQztscLs+CSoLU9OjRpo7RTF4yEDUD733NHSxThahjaU0dLLEZbaHa8Olzz +8yd/qtFCK4D2AqI+79/eZdFIs96M3sh7Np/EZ6be2N+oN+/dfApvDM9N1Deatq091HlAtyVP3uOS +GU4LanARZYLAlnWHeTERnonD8eJdf5wShajEmQn6BuOWNYc8ETpeWrwyPryyoBp9gLFyKWNCodsE +GQfQakDJodjRH3BUNqFZ9Jdv/pr92m3iUHXF6r27xr/Qcro8tGd6j1fntp+dNj0aoSGsdUhKZERc +aN/huZA87rx4bMfzHdQvs+73E71fv6X9XYoonS4xvIIdAJ7UxIaK5RAJFvZ5d0H2naO58927+WTj +xepO80b0fXyW2+k9vP2sWC7VRWqnLxrtcrjMRtu2nw51HjqcZ6K5UFNaLwtXt1uXa0KTK6oyaK61 +VxSwl3LLd3tPDM7+0a9MGz1YGckct6tqNpj0QdvOFWYwEbhg8SJBsIwiSxIE7ElWztFxpwxm26uD +xZVcm8f2PQi1XgGQXpf/8l81ZP/N7/vbsg2uRytdY4hDElgO6E8gskLAwe+1qNX88OSp0x564dFV +/SffVOq18uIURpFZIHKrefxQt1BjgyKgTcCzClVOcYT4Yl3lUWrkmTzr5jt0ukiv1/jH9i+eXnHr +ii+XPv/uwoWPTLn17ln1Nc1L7nl6yujbUAb/w5oPf/j5e5vTAkCHYnqRu9lcP2DIILk4BMd05OQu +ZEgqypvT4/s9et9LakUkZNpW//SlXl8Pptq+C3v2Fp7q1KvnsKEzxTzFvoN79h7Z1XdY14KSS5WV +pgXzH4gIS3L7zd//+LHN2WAwmIcNGpeemAUtjZ/Xfa63VPYZ2HX7rn1qbfSChcsgnAnuyWffvC9W +uM2WwJhRt8ZEdQJx8tTpY79t26bRRTyx5LXEOOBJnt//WBWwmaKFWpVPSeEUOUzUQQ16LQChWd4A +caTPqfbZQ0W8kOiFi9566tlvpAnZxWKbIZHXIgXHXC31hAt9oS6RxuJ2KEJERq/+fGmJTKaZMHJS +aFiMU6D/fuv7733/+Ldrnvti1bLn3pj53Bt37jm4TqMSZ8RlQFQIgp8ej2nVN+86qkuHdcmwG20t +emfXbgOBNGJyHDz8x9OvLN19ZKvNYR3af/xzT3wZn9Ytv7q42tQi08mUSgUubGRYuEykq620hYfg +OovqWypeWbG8qbHea+ePHDirX4+Jx8/tzT++J1atkEBzElOJCxO5BhZcDUeHjfuNeklDIYkT7ODa +JPs8IDa1VFUVFp5UyOUWq3nS8HGTZ87Ymn/+w+1//HQKq+MfzW7/ffe/NH7krQaT2eNBEskLqdMt +O3+5cOrYkNweGr7SbLJZ/R4QoDIye0SFp8rkmiMndix78fbTRVv1ptZOyV1ee+LbnMyRBYWVmtCQ +rt0GQK7JBxHd80cczkBNte2uBY8PGzQTWhMol/zy2y/lal1Rtf5EVWmTzTtz9oIQXZzD3br6p49F +XlucVKewi8Ve5H+pV0lb37kr/u1gGrnO65e3/xTTCCQwJCXKYbTZqvR0p3unoYsvUP7+Hy9QpkT0 +HNJFXtUq0ikLT18yFtRAvCi7VxqEGax1reh3D4gcDsHZ5esvfLIdYCyWBZvFgfaQjhZLc11rc6PJ +hLaRdlefETmmc1VwljrdPVqYEGYobSh+aQNy0cAzIbFoQesrSVAzz1jcpNaoeg7skh0VX5V/7IS4 +tfBUmaimEYhDbu80nsfjrW9FE0buKnqKqqRSUbfeafzCCuMfh+te+QlfMfyGnk2rd56e8VLNV9uB +sJJRRGMNcHACArSLGjC6K3jF677emffpvQK13NFoho+W89LNODtD29lBEqrzI1Ma61rL/zj157Dn +zz/6PQ6+4/nmv7D+6O0rYyf36nhNGk6UnXzyJ9zUoRN7ln69e+sNrxZ9uTPrrrHi+PDWsvoLr6/n +zrf5TKlAJbc0tDbXBK9e8fdrK3/dUv3rb5lZ4XAm4nIzBWZ/SGxoYlacubDuqlHCBYjtRYqXe2Kw +0K1962gmOUNIWGiwdVRbYUnQcF4myXCmlCmqt+9tHFSmKEu2sMMLqL8xKQZwcvyXd45K0HEPEgs5 +eiHLil4beP03W7D/sY/7G4YqEWmYHpCL1hQEPOiphLQ0HgQ0WrUz4NeFJU2fcE/vLlMXTHsmL3tE +s9XmD+WZFGa3zmeTuO0yry9MaAp1X+RVbK7cc7T+fEhc4kNLX5p9w/1SqWrfgU1vvvvknwe+X/nD ++wBTL5Ye6Znb9e2Xvp439V6DvvG5l+584dWXu/fOGD18rN1ja6ipR/LJ7fUmJybT4hvwNdQ3GvXe +nE5Dnnnk7ShdKlTKCkvPVpa2rF21bcToMYfOnyzT182av0jMV7hdzoKCs26B/2T5xe2HTtwyeyEq +3KGxuOGPLzdu/kah4qsUsdNumCcUqO12876DO7URsn1HjxZf0t+39LHU+Cwfz7zyq5eLLp3TaDWA +46ZOulUiVlTVFC9/+ZXoyOgnl7w0MG8csojny44cP7ijc1ii3CtFEp6rlRJ6wTpDl0JIjYPHyHp/ +8n0eOQ8Kir07T5jUa0HfjHH33f26XaGqFDYatc0+RatQag1ILGZZc3mU4XffuR8bzjSq1HNuffzx +R96RKuSffP3ay288+sXqFe//9PLnm978eecvBl81GMAiiTw5OQervFgm3PDHt79vWDO8e1YYT9La +0Oxx+5MTU0Htg8vnDVgtdlAf1GP6Tl5yx5uD+80NSU6s9bQcvHiiusWMhB8OGpqqIr961pSbJ425 +FSFec6uhqqpexgudMOS2e29/FsZ9zY9fOlsao6AvanGAZktkZMKK260jo2NR6Q4J6tIDYuignSGK +17iEPTHBRX5+uFLjNunX//Sp06lXSSQagfrhO1984en3UrLyYtOzbrl9yYcrvp8/afHFM/llxefF +yF+JA9X64l9/+S5ELE6LTNTXojWR12gF2d8dH5fsBQgBdrxMCGfMaZePHjLl9ad/GtRzdnRMvM3l +j4iKl4jRfBedQhy1dUaeL+LuBS/MmbkUImJoPLV1x/ZJU256/bX3fRLZheqqAYNGDe83EWvt7uMb +zpzYnRme5Kp3QnPcHrAjrwgVQ+qnwgwkFzSS437FFiyEa5+y/zGdN/yB5LmDxDKJ/kSZRKsIH5Fd +UVzfc3DnhqOl+r0Xq34+7DED0FBpQ5TROYnwdSJk0kPzP0a1fJHRkpgabdt7Eb0hDSfL5RGawmZj +Trfkph8OgKK5bc3BafOG1mw4Xthizu6e2rjmiFgpS7l7tFgqKnjtN3lcSIPfHxmprfl6j7myqQYi +Dj3SNI28jNSMTdv2D53YS3O+0uhoOqe34HtRMooxkqqV8Z/7Ej18TxkcOT1SpfvPwgAfLW8ZPCbP +9O1Ou8GsGNb1VLWx36iujV9styGDSIwXok2yuIdErt02V+uhoilv3Lpp9T6QU3LG92zcmR8zurst +Qo1viWk7u5q1h+W9Un1OT+NPB9GxEr0kG3fkQxO6yEDna99bUPnz4dgxXe3sXe3X5Myy1ZGDsgrq +DTiAkk92WCqbdOkx6YvG4HzPv/mrIi600Yv0qNZ4pKBwzV4ATWXN1sS0aHthgfHQMVd1VUttrcli +yxs76Ozh4viokLCuKYBNSt/bzNUGYmOpBy43EawJ4Cq7ODPZFnIFS0PaDWWwAJ/7MyuVYqmMjm9g +yY0giYfMWTCRyShTALVIP5Hxc4PG7wqsNagWxNizwcNo/+wOX8IxW9u34NG0/R782/+CLOD/mLm8 +XKtBbgEPCxtr1Ide6aBqUVrJ5RKZhdF8frhs/LjJbjtfo4nLSs88VXCkTF/dKnEZVG492hfLnBcd +LResNY18uy4+feaM+5YsfqFn+vDapvLvfnz//U+fiIwTKuWK5OhOzz34/jMPvjt5zC3xURkut+XP +nev27jkwZvTE++59snNav5Pn9/30w6cx0ZpGo3HiqOnJcTl2myc8LDIzI/eOW5emxHbxoNOCWIgq +yUEDRkeGhh84sqmstDAnq8+iOU9opKEI3SKjQ4vqyg8dKZs7a/Hdt6PoTf3n/tVvf/S8NlRsNdmH +D5oxecIi6LbKpcrc3Mw1G9daTJ6H7n128vg7hHzJ2l8//eSzFQOHZDY1mXp0GTpz/B3wTgtKT1U0 +VC578KmxfW9EXkooc3+66vWqC2f6hHbxG11oVsSE6TFFqQAIytpOWETI1QBn4vucfIdXI3GrNCNg +9Y321OTUELVq/5HDFr/JpnQ0KcwVotYyf0uhtYGviRg8fNpDi1+dMHC2xaF/47NHN/zyVU8kbToN +cfn9Zpene7ecME1cU3WgT+6Qm6bfIZeFbtm15sNPn0+OknaP7cxrDqAjY2F9VXb3Pj1y+mAhj4hO +yOueN2PU/NtmPxCmC/187bu/7/jJoHcN6z1q2OAxqcndIsPikVmIiYufO/sOKV8H4xYTEz/7pjnT +Js0bP2q2UqH+YOVz+/etTw4JV1mlSDkjFAahnjjoLIHPWomzrCrX/ZH9S1w/qnEFLI9cqpQ6l/HF +coHGo/Dw1IGCwguN5pZu3QfI0bxTrMnJ6DN19M1Tx9zat9vY6LC0P/5YtW3Hz4MHD1eoIvlC7+er +V5w+tHV09xylS+S2kK4rSrvLais75/bu22MEVDO1IWHZOZ3nzVo8f8aDYZqY9Zs//XXbdxaXKT25 +x/jh09GCEwYyPAyU1LmTx8xxObzoL0D8Da/ghvFTLhSeOnB8B3o4P3nvm4mRnQ3umlc+eIZvMKf4 +4pRWucNlt3rR/EzoAcOMqaXQhlWYzpp+ck8EiRM8fsd+jf8pUSMOt+V0xYGtp7u+MjtmRPb542X9 +hnTJf2nDifu/vvjRtuYjJVFju/654ajd7obGQcnXu4/c+amzxaLOS6qtaOo1Iqf0q93ciqPumlhd +1gAk1lqtB4ad0yu9buvZ0IGdivMr49Kjm46Xxt4yaNPPBxRqeeOhi7ETehzZfT6zd3rhkSJdr7Si +/KrOfTImDxpTD1C2pkUmFogPH66V8TeuOWSzujxWB3/jHs+zH5saWu2dEgvOVXXqmdb6+1Fpvy5N +da1Q1DWWNTm9AmmPTs1NJpRYWYrqQJiEMipGIX5Sqynmp2DMmSuazi377rF379j608GIvhloMB45 +puPZ7Tly5xeanqlb1x6OTAw3XKjlbAiGsbpb8HxLvtqDj+r4ruKvdx9e+Kldbwnpl9GCAwCbqbgO +FyRhzuDg+R4uihvbk863V3rT6TIMUF12cm15U6/hOfV/7kHrF5gfLBf1BZVRSZGmVlvqlL7gCTcd +L2OO8+UtmLvmmKNt3Bk2x4LeGBeKsTCQQ1dZGo9ZUDJUQcopfWCQfgqSD/F8gt9CUzZIXKVr5qVu +XUEgloYzx02ldmyQrcJaxiUgidTLKjku7yy93vEJiiw77JyObPseNFh/n8D776Dg/E9ZxvZiDUqv +knMBR5/0B6l1HUs5og2jCI0HbJLK8vqfNn4nUkKLyZee2v2Bx14ZMn1WeF4XU5iwXuq0qaXR6V1n +TL7nifs+ff/FP+6c9WyoOn7L/tXPv7Jo1ddv9ExNSdYkePXixbc9PrL/TQGPGowdr9uvkGunT563 +7qftzz/5YU7GgPM1J75Z9Y7IY8tL64wm8U2NqOAWoklN55Qe825crJVEYm6ixAaKUJ3SMjVa9xOv +3rl99ya/VzV9/IIwVZQdCnUOS1pyz3tvXv7+C6ueWLJCo4zcsOvb11Y8E64Sx4aHWGzi6ZNvlQrQ +mQHZFV9SXI8XH/r8jed+nD3tQb29/oO1L7z85jO5CdFDU3sI9J6EqERIWnic/IzUHu+/+WnvrP4Y +GygGL63JP350e3pEmJ+yD8h4QCHdg9aGEG4k4WkMMa+P+ucy/XuFUy4x+M+XHv5uy2eyMBlUkUeP +v23JY2/EDRpSESM9JjHXRMgievSeO/vJD59d/crSlXnJPXYVbl3y1t1bt/zQPTS0py7K09xoaKif +Onr2Jy9ufO+ZX79ase7ZZe+pFGFbEIJ/tlzmdfdJ7ulpEHkdUjffB0yqrPSCE+150FJCFH3DsHl9 ++4w3W4wfffbcqlXvqHmCBF30vDlLlyx6LSe5JwTbsA0bMO74hdMPPHfP4TPbUKiokcREob+0wPXz +5o+37V6rk6jCBWEih1iCznOIujx+pS8AtNSBaaSUu6V8l8xrk9pcUgdfRq0gIQpP/hW0IyF6gOHj +87BYM4CK/xh5bFSo7o9Nq594ef43v75yvHBfk6nOYG0sqT76y85PX/zoweVvvjx40NikuCzM5TOl +xzb//mNaRESMMsppADMMNH7IKkEsXXSp9LzTYxMGvCHCkGkjF/TIHKa3Gr7+bcWb7z+iEBgzwyMM +VQ1AsNDuy2uXTBt/68DeYy1mG/JDPEjzilT9+w7evHv9O5+9Ymo13Dzu9s5pXbEKnDx2vO5sVbom +zmcF/oraG9RjC1CNji5jVF/GVUODMU+aqVQSwNh/bfrNbZKK3BT9TzGNOJSyz3f2GpgFfkrk8Ozz +J0th3mAR2xcSiAAgfxaXEL53xjtilXzCgeXhvdIieqS4XV5DUR10ZbhXhndNhI+BZ1AHAvvatVea +6WJtWJd4AJj2utb0+cMie6ciGdl0rjJuVFdI41YU1YFJPOSpWYn9s/D5Qi9fX3gaE0KhlMlbDRXd +Ev1xEXg+ITFC8PC7HolU9v2LgcwUWXYynnQ2m7AMQUdZrpD6GvRRD0/jh2i0GXGI2vXHS+Luv2Hg +oTfUPVKxwMNjkcaHxU/pI9YquOvffKa88WhJZvcUavYIvzcxnDu7fTPfkdDZPZs0Lq+soFqmlruN +dolO1fmRySDZdjxf3NP2a7J7Jl2TifuXRw3MDMuMxQE0HCnp8uDEqcdfTxqVS+ebXxk/qqtXTufr +dbi6LBgflpMYncuuXmWtFGLp6KpHO8nxt1Y04QiVsaH4WfXDfo5Nc+1GyCczbx3NCdlEjm1K7+Jy +lAy+JCoIEyVrq2VkJFQ2VunFXFqco+EQQxU7008no8n6SrVp7DCdVGphyn7CNKKQKuj9Bqsp2zmt +V1m7a3TVmKVu2/+nLNb/4s/FVaRCfgZKkyoveQxUA4SLBPEtNCuSGvjhHtHGX1Z9s+49OXp88UR5 +GWNeuP/rj5dv/OqVfd+8fvDz13Z8+sq6J+/7YNrIO0NCxL/vX/nQ81OefXFRydndQzMysqQZYY5o +r5lfWVdl9jYHIAUBeFuKUgavQKzEvWm1Vv22/dNXXrm76OSOoZmdoryyMKlix47N1Q3npGr4io6C +8uMvvv3E/tPbJHKfV2w5cHrLo8tvO3h0Czrxga06qD9QU1Fx5YmNe362+Bv75vWcMnZKVd35d796 +6skXlqjknjGDel8qbAGzNCejNxa5zTt+2HX0DzA8Rw2bOrjv+FMX97y64p5vvni+d0bMtF4jQ0zS +cIHy+Mm9rbYSqYyHtNypI0fP558BXcUjch08tsvV3BgjUQpdbtT9BdBS0kNNbiFvDL1peBVoPI8S +IEqIoPjdJ5eZ+BFu+6/fr1j/6wd+nkkiUE4dsej9J9d/9+K+NW8eX/3KvpVP/7J0/gtZGb0vVZ58 ++5vHlr+y4MLejUPVyf2EKZJm1PxZnTwPStSlvBilIjYjq7eVZ/3yl1c/+fRxgaVyQGySvMkrsrlw +GEKrOFYbeeLEvn3HNosETghx2CGjWvDLI18u/nTDO/EqUbJCIfa4W6wGTBm5ShXgO6qbT36+9unX +3r1n577Va9a/fbFsp5OHSb//g8+f+mTls2qfLUEaFjDASkArgYInMcS0ndTiGO203HaPzC1RuKCF +heqNgNNrJeVxqk1lOjhQyaPZTDIk6Jun9CoVrfJUVWL3yMTac0c++fSp+5+46Y6lY++8f/x9D930 +3ucPfbH63dGTxowdc4sf1Sw+2xdfvilwmHJikl1N0ADjMw0wnrHZGRkSeerM3gPnfuOJ3AGU6vld +x87/+dbHS9977/GEEEn3mJQ0ZXhz5cUdR9a5RCaR1Gt3N27YuvKtlY/b/HUimadOX/Ttzy+tXPmE +01SFATawW3+BxFNrvrh984Yoj0AKCSCfF1IrHlxMqjaDIcRywtYSMoqE47Fe0GydCi4CjODeYePf +jM40/zEbKjGih2Z1eXTye0+uXrj0hpMPfgt6qvFCDQ5wyK+PLF/yxUtf34/IXq5TIit58e1NYKUe +2Ha6C09QuHI7XoPSjrx3bz20/WyawytPjth+onTu0olHH/g2/r6xJReqx81FHQwPKe4n5r335s8P +SxTSlrrWnz7cct8rRGNxml0/f7rl3puGRpdVbVXJUVk1eHp/4jQ5XQ/PfueVb+5HqxaJVuky2czv +blDcN2XdVzvHZ0bZDhU6J/dHRnDYzAF4cemra1U3Dtq79cys+8YDsncabacXf2qC5RbyRvy5vOhc +pbxGf/Gt3/F1IAIO/vmRrRuPDx+cfWT+h4N+eXT5ks87np3b7HzzqVV3PjpVZLSHd0+GjWw4WCSI +Del4vtdek4L3t0TP6r+v7QBwlXC7289XX9f6Y9v5Xtp1SpkWf3Dr6W5R/JY/90D6EbWbJpDzrII+ +t848nl97033jDYW1h25596pyBQ5Jbd/wC9eOisM7WXMpBptyejncRnWQQUN01QPulVd9JoWabSlx +brgGI9dgFEqBKCuRJMPGcVkpKOXKO/6v3dhFDGrcBIFn1m2EOlQL0HMTvWCVAqHf7RBFqRrD+U0a +XmpmzuCBwwb1GaGRRomFCvSUwOvdXnuTpfp80cnzRafPFRyoKa9R+/nZ0ckJkjCFDd18hLwQ3qmW +gjq/UajUgcYMIRgXCPHwVXw+MayK3WZosGhE4gGZOWmqBISsJZ7SXacvJGR1ycjpZrfbiovOFZdW +JqYm9u03qKmh8dSxI0pxoHOn9FOnSp974v0bRs1BQvGFNx9cs3HVmEnjFcqw2vrqqqL8loqmPt0S +xowYdPjY2SNnqr/6dDMskN3TMnPRyOaWlhHDRolDlOUXi8vOnkIZRL9u6QM69REY4JL5yr0Nq/ft +SuiUmJiSLvTJzh8rffuVlbnZfRFcLr5/mqChpq8uTdIkgDio04suUgGPy+33eJCpwdIJtIe1iOcE +KqAUi/aHTke4r07mkyZFTZi8YFivG1SiULlYDuAR+TCLw1Bam7/32LaDx/baaptSJYpu2lStSwmJ +cRNApxBevrlYmZR+w9Tb45NiS8oK9u3aeOHIqWS5plt4QjhP5zY50LQUFcUBQZhR6z7cdNKrlvTp +MUwhC6lprjpdetRtsPYOi++m6YTiwl0lp2Xxyam5vQBuN1ZdKi8/29yoT4lRhSgUVZca/SEifoia +Z/F6aq1dwqLD0ELEAhuIohyfzwtpEpxdwIWoCTbJi/6FAvQvdPoc7jB+vcgk1IhA5dUGZHKnUGJD +DaAEuDduLbI5iNUQjwrlQrEKTZNFNrG7WWQ3eEw2txHVxHhZbYtRG97l7RWrVSihU6p//vnjd955 +eGTXbqnyRE8TKmM8bo8L9aw2GMhQ/qnmM7V8W+c+w8PDko3V5SVn9qPOIy0sIV6RiA+z823nmoua +JeauA4ajNLH60sXii/nNevfw8QMBrhaePVlZUJYaromPiq5saKwXeHXxWkTDnkZDckCraBbI7Qok +amH8cUcDJExGNdLAzJF1pFo6SiAHG3HhXxhNLFYgA38Pu92+aP1HmcbuK+aijkLfaDy+6/y4mwdx +B7ltzCuORmPuCzeVWOwGvWXMjf0DBvupR75HwaJ2cg/Uipa/ubHxYBFemXxjv8hbBkH0sPS137q+ +cFPBuYrEcM255eu6rph3eHf+hNmDTj67Jv6GnkcuVEbEhAwY391qsP/8yda7npklC4jqmvWAarsV +X/JXVZ4bMvDE4aIb5g4Rvv+drW/Pw/V2fYtl7E39oajf8sLP4qwY8fwxmAf+tftcLWbpneNOHyme +eMuQwidXY+wmPD/r5893zFs60VffeuHZn+2NBlx3oUI2fPPTT9/24YtfLj686BO0T824Z2wgSqtS +y0ve31L7x6mc5bOuPLvV8VN62NMiD+44N+ve8eFRuqKvd8P914zpivMtW0HnC0Cw6wsd32U78ej3 +Yo28y0uz13y+Az4BuEInn/kZfsbxgho633HdTS2WdV9sX/jUjSV/Hne2GiMG5kGiqmX9Jkd5mVvo +a/X67G6h1yToOmV0frM9p3uKe/Op2i2nr0q/XWXGmMULWst208jw32Cwyexkm2lsy/cFU5XtwzCI +bV42a4gdO9o4uHysZ2VbJIqQiBPgaTPBwejv/xrTyBXPXLWxcrQ2gWhmHamkkVRaQL3ku9E1QyLl +W20S0P8jlc2h7qZQt1fsgMCMVKpWyNE3VgY/2mq02Kxmt9PhcwnilOERghAND9GaROTwy7EsInOp +cjs17lp7k90NvQW0xIX6NpkOrCwSP08l4EcoQsKUkUKX3G/hKeQiYai12tlSrG9pMBuB4EWHaSMi +tNUN+pZWJ5DA5NiI3LysXQeOy2XJn3+yTixRVNcVzL9jKprvIXIxm7wqtTJeq+uakJiZlN5gbf7o +p19unHff0kVvSMSyfcfWP/HybeFKidsAkM6tEujSIqOyU9I0EqXP5JH6pU6vA6m30lZgJbXNAvOl +Ksui6Usffeg1hIi/7vr2zecfGhCTnuwN4RvcaDjsgpVAMwEXEFVog2NHUopMCBUjs5o/EakFiawK +py3e2xriq/PbPFL0IIxAGTRaOTktZqvRxEPBh1sQIlVmy8LC7WK+GexWSAEQ6UUiDlh5tnxrXZPc +rtKIHUaXiq9I1UZFCsIV6DhsR0jj5oHvCqzELccDp9ZRYq+rgsSklx8SEGcI1AmiCJ0wwgLJLZWv +wd9YZq83BRzIsCv5CpVQEa+KgggMqjktLrPea7J4HTK/NESg1fHVfosfhg9SilTBQHk6fJefL0On +cuAy6A/rdMlt6AzcLPEZxaLY1OimpmaR2xTrUIcaFAoHhPkgcEMBpESkQC9Hst3UPlImEMrEShUE +wDxim0/nP19/qbzR/uLLn/ftMREyfvtPbnryqUWdNdKuUZn+VgHEJ+EsE+UXCRIX2nh6Qfu65Gps +8hhwUFKXOFqmjFFEyJ1quVeHwe2ROrxhjhJTRbXViFSU3CtJjQ6FBEGNwWp1OGJ1qgiJIlSs9toD +Vo/NJDUandAvEYWI1VJTQNASkPpkXg8cAexuiF6i7xTyNHQv4e4AUKUcDNuC4ipAVmgpWUWiLMHt +PytqRMG+Oi3KWaUXh6vAR8UxWi7WouQfD1BCkHhjP2mkpmV/UePBQiCoUFWNG5eH5bxs1X5CjcEW +j1CnzuyHZy6t2h87uqsqJQImRH++ussjk0QhitJv91rK9Sahv9eDkyJCQ/O0nbzdNE6XLarFxb9U +bgzhu/UtgQ070GHAOn08xUIbdsMk+7Qa/vj+gVCN+1ih6UgxNHoCWnnY5P5wKut+2odJFHP3RCxS +Nd/tRoNRLNYpd4+VJYfrd5yr236WlnIW/sDr7PzADeGjumGYx6ZE4nkXJFbd3rPP/ty47yI7O03C +TJydVn+gsPFgMYQDUHcUf0P3iJG55vyqum1nQdxVJYfHj8sDssCdL4yBLFyFayKJ1LYcKGxg1wTf +lbV4rDIlsvHPszU7zsEXEirFWfdN8Dvd5RsOd7l3IhhoZb8fbLh4CatIytBuMp+3dddBnpRnFXgN +Lp/bLgy08oa8sDC/uKF3j9Sjt7yHZeKqNfha0xgME9mI4ig2jDxzuTqeeju0LeScoQ12d7xsGukR +l53kNgbKXt4IFKHhzPV7ZBaRCjba1ebYH0jz6Rpz8f/fJ645V5gnuvxMIpVdQ+pHD2Eb3F56yoNC +NMhogk1B7AOvVxrwh0h5qPdXCKFaYec70SQNaxcCBCzMSqFSKZJDNVjqAoVfDjYW1c6ATCiWAfIT +KaEihyBB7kEGTwKojdBy5KJAV0E7PthIeOIklyKReN1uiUgk1aktaFEacKFMQQ0LDKUvjxddgKFD +rVaryqzlP27ZvXz5h6PGzEYR4YoPlm1Y8ymadaAvmsgjkqJdpEwbQJ8/nndfyfHT9XVvvbumU0pf +h9Pw1PPzL+UfnDFsiADVAFhIFWF8n8wBNBTrv8uFtswI5vAYDYc9MsHuhuMlDbbPVvyWktyLJzEv +fnhq44Vz/aJyxU0ePtqLwCRSExwcLxi11D2VmUa2eJJIGplGokqCqQqJVKDIapEgxG+ROt1KvhUc +MZ9QKZCqkBd0eENESilfCjFl1P2hLZcFWtc8SBAp8A0umcSm8FpEVqfDKedLwuQ6IYq8PID6MENI +1N0NSS0SZnGjcy/yBBB6tMv8kDaGpoAYVwOXHoZXyRe40GJVDAUd5N08bpdGqvDanBL4EoCEPR6p +RmOHGhyiXgFgUIHN7gKvBuaIh5ww1Q5TZz0kKYQQwnIK5TK5XmI2xDnL/NbB42ZMGXN7qDbGYTeC +CXx4+8YYnzzCqlJ5RFTFCOEDQHbU6Jj1sEWCU4jenhRswSIVWMr3lhcuuv/5u+Y94/OAIlt6z5M3 +usxVgzWpIgvMII0LdKhFCyOXpVUBlJMvRUV3AIQOBa4yAGwaYdQdnZSXqWDE7XMgFypWaiw+q0QR +UPNlHhOCzoBco3H6XDgWnsPjtCELJFAq5D43wGKZ0eIFJdVttUHRADfUDRIjBFcQILpA82UtABlX +gYwFS+dwdAeWraGlA4vSf65p/DfXMZiLdhyvbY2lPBeUkOAK2gJohcGLS40ZP23a1FtmKdSySyf2 +eZuq3HWX7FUV5opKn8UGKpQFxThuHhLwdmQGCKPGogyAAWoY3EXkQSGK435QEow1RaJrDseciYVS +gR01mecYNywP17bIh3ZPCe+fGTaki7WiyXSirPFAMdKf3ClzSvdtp8+FQFcV0F/NLW6/VlztRJtF +6XAJWXkCBVuU44PtgDISaUn7UewGHTwpRPECcUoktNFzi2fgec0uv88kjE7sNPiNBXaD7czD37ee +KeNkMTpuf2ka27FTboS1MW6C7+MCPjpN9lEcYaQjst9OEQueRQdAlXsnM4f05naD2y4IwEBc+hs1 +6v6/eKM6zjaOKi4v0zABhoqiDU7GBNWtKLH1CUjMBlYLBg8wKPXaAOUEKocCiZAnwQLtBzwIQVBg +bvC5kaGXSyGYKECaKCCF/h4MJ7QEsYJRAlNCHQqpfBEtsIE20hjAPPN7JDI5YFwHGlaJAmKJFCXn +UE7BO3Wo44O2N6YK9U2GzBO+xy9UCn8/u1cYEf/hW2tAaGxsLr/rnknINo3O6yu1wArILFYTtdQT +K6xiy3fbN/YbfcNLT33FF0j2nf79qcduG5Wakx2ZZDNZUFjiQr8bnJQcTaQ8ggAxaqhlog/5Vb5Z +7Pzt9KGxk+c+fP+7QoH88Onfnnlyfm5YZKw3ytdo50PpGL1mqAYPphHRIxxCBBeMpsGIGyS4DpeD +iEQwYkKZAGX+uL6+AFpbSP0uaCuQ/cJMoj5H+EbcCuT+vRKh1eUgFTX0dhKL7Fie4Zj4eWh9KpSI +PTBTRPOhOkpEZdDQQWAJ5TUEpm6pG7VhgAeBYuIK0V0FGsr3iqT4YEC9LhFfjqpLpAIRugEkJJVF +NCT3umCM0eXOy8ONkkHdzeY0gV0KDVtmDhwUNxHbDT4OE4P18dUSmcFldEaKKiXOPhMmPbH0daU4 +1qR3acOkdfXFDz8529fYkOKIUtpw/AKPBLq5uMCQh4ZqrBR67jgbidTvl3svOWrP1FeNnXn74jte +kIqgkOp4/c2Hd27/oW9KepRZEXBJcIgotUP+EkcFxAet+dwOgVIa4nXZ2QKD3h8oIoVCpRuLEmUD +oZ/ACdP6ZRiZTp8dChYgBIklEqvVhHGHAjqpRI7rgpvsdFilUjgCfIvFgYaiPvhgGNMeHkYAg8XB +oQBtAQ4IeTocP4K52MRXxeKMKIP0tdj2A0QK2lfh/yhA9e9XNm7B/YeBAWuW4YVnQlOU+rOSmBgG +tEho8notAV9MQszQscNumDMjNDGu6eLZlrIikbml9eJZR3WNo9kowTRFlww3lLXRUhE+i8AJhhq1 +iCJeJK5o0O1g1elcoyXmfLFKO1x9Eh0hO8LRUBmvhB1tm55ax1NjutjEjLp8G7iKhssbHl99ou0m +tv1VnI5Tx0iLq7Lg6imCtoiT+6ZiN9JLBCaEPhpwNoXwbMX+KCxWqHKO1Knyci0tFr5TlLdwEspG +iz7dWfzZdpwodRq46kCuBFhpAW5LLnIHxvQ12qwge6Zj4MmdM6suuLyxgXn5a9i45cxh8KowBXNm +/jgjGMRrWe6x3RD/P9PImUZ2fSlQ5EEiHMsOzyMG45APBZT/r73vAIyyPv+/vbL3IJMsSEIYYW8R +VFBxYXHXWVutWK22Vftrq1YtHbZqtbauutq6rVZxIirI3hBCyA7Zudze6/95nu97l0sCBLVa9J/X +87i7vPe99/2uzzM/D/ZoKHsBYs2lOH1kh+EfaJLIX4OiRTy1KsjzkO5Y9dSqIOujHTIkQkrHgAWC +0CDVUDshhiBHl9njKclMpqZzSHWELqiCGRI/oFZpcCUBJeIgVToom7gwbOcGHXh/qQYW9A+VTJus +aXZ2vrBh48/vfHDlKd/HKD/81D3/eu73K+bNy1IlKx3IK8T1IZNC5tcrPmneVtvR9dijr+SnT3Wr +XLfd8/2aT9+/ePISrVeBnDVkyGm58q4DKgh+E3FliDNVGLCGQzH+7aZ9e1t6//bIm+NL5oDm56bb +LuuoXTctIVdr1Aat4HB0wDAJ0zA2UChhPsJF2lTFNoouJJJi0kCVvoBCDeoblQe9QbeMLqX8Btpm +UYQAq8stw5athGHZDaRFx8J3Atsi9hPIKBoUlPfJUflBqfRCWKepjcqoKAFHFd7cPpdWA4mVEpWw +syMWRhev8hObBQJKcRKihIhFRKWFWxBsBEAOEomZABWOOeiThJ8B1JRQKnx+FxqHLg+BBTjjdcGa +DuAhFKAIbbotL73zqVV6v1VrtSTKVJlj//rX12Njc1ByHQVpkdzYb+u48ZbvuJraSr3ZsXY0EPTq +fBCpMWwYP4hZIOmljMxkVZeme0tHTcnkWT+78aHi7EmekOuRJ37+9BN/nl9clOqMUzqwuGPcyNMM +2FAxBaXdPT6fJjYG9VSwb+qpoBFicAD/6AYYM4NKiAkAQ43W63NDLoBqggLagDiVTm932xEbhYqa +4Gv2eLyAZ7cbwh0UWrUH+iFiCaEle+xE8+NX+DxEr0wF4zGzGWyZE4R1mvBuImLiObCdlRm57J+I +1w0fJ5ZBNRo/hrwW0CFsbtImKkXfiww5EpMpEo/cqfAtQ2yUG9Cbfk+PzxuXkjj9pHkXXHlxbkGO +ubPF2lbv7qx3NNfZm5vtnb1qlPENqJ3+kAu1hVHbKCBzw9FADng5xpM5rmEeFzoigg2QEkRypEiS +wS9TqgHjn3gm2GM/WxgaI3t+NKRIgZoDIDeYiOEY/RCNi0z9J8k7DCRUFoCgg7ZHsQokJx6tXugH +WN0gYUJdRmVQp5XF6uQxOtAmy8t/cuvGjw6kZyaljUnJHTem7om1hx55j+CHJLojerUGXWCEi5Bg +kpGSVrjAbcYxJFpEvjCAiNF5hoMlg+ifFMBP2RvsLA/LB2h4ZD37eLrxW3OO0BqF2IFnwW3C6mOY +Ckxo6+R9pIoRVHiNAI8sG4PKVIVboKaoVmGkiBWLQIy94aK39JPYyUnQ4U9hW4MaSj8q9CzKHMfW +RdAKDADuBtQIkw1qoEoGIfRrbfGeNY1b1Wn5zzzyZnwwtaF7z6pbLkgOOM+ZtNhlhGaDK4XCqIRt +zap1Prvjo+WnXXHbD38LW9xHNWt+cssl8zLzJxjyUWA3BMUQNpGAHTPFqzT4A8Aml1LpRtCQKqTv +07jWdG+ZMePs3932OLI9P9z15m13XD45MaXQHKtEoWEwQgVdftSagV4BjAUwMjSS5Mu8YkzfRAIf +5YbClsgdAusn3aMo7CU6ivtGbLJSmadwwJrYu2gRMH8TK/TiCIuDPDIcOkq7NfUgvaWlRJ3L/ct7 +N8uMzAQuJMKwICR9ndBPii4RRdl4j+IaPmTaohBu6Wcp558K7XkdKd7mWOekuctX/+LJnu6ef735 +RLeptby8dOuObQ1bP8v1a1OsMbFQGNXQ/aAmqNSyWMg7avDkaP2eGE9n0LKn53DVjDk/ufWP+akT +PCHvY//49Ysv/j5frx/rzzFYYynmBxgFlQX6HUzW5PBjQJJS7zFHaYlLPgCW6oV4J+6b71pk5Qqp +mCsOiP02fGvQM6A6iwRNgCh+Cz8mrKYix5/j3GlfFvZx0clH0DRCoW+q1kjdEZV7LjZfmrLC/wS4 +4Xq7XKgI1galBSJAvH7WyfNPP2NJ9YxqW1+PsaE20NNmOdxsa653d8OP7sR8RQapG7TLqK2IgYNd +wx8ELTEsJFgUwjrNZbUVFM0dqRFIgoY4IjBIYMkfiwmIwSfnRHgABqtaEnHvwIfDx2nELVtYFI8B +jWRH5cukbqK8bzncC6gqg0WNrKlYvSIGUhwKnKhkRVdfqUxMUqnVer1m310v9a2vE5oZrb2w8naM +66H1G17jhFdiZkf5Gof4AMVOETlYE5Q+GAKKAlwZGnlgR6Hx6MMwHBqlzYVFCPGayYEICLEtEihi +g2VMo/2dNyUxDmJLkl5JuCfie0Q7EkbiH2y3iJDiekeClJp9cvwPZSVBvsfnpFJSiI5aDY0Ddc+0 +SpXeEKdxKuztst6P6/et/vW/Fsw4G47BB5+645WnH1pWVZ2nykR5Cw8xX7r1IaVT5drQurczoPrj +vc+NK5rtC9hu+uklTXs3nDp+iqIfuhIFxiL5DX42oBgiKYFguAavxwN7L9IvWrx9O+ydD6x+cUr5 +fK/M/tNfXNO49ZMpCdk65Ci5Qn5E8MCFAgUNEhyAMQyNpE2QXUjyelPXcLgvnsV/UveKHVw84wQu +hiY+EXIqWhC+AyEUUvwH15CJPqSe5wx0fAE9F8E8IRribcQ4xKtSeC0iZwlUpoNEenHRol4bbWOE +p9EuD24BPKZqBOU6M2XN8c5zrlp13QU/eW3N83fcc21iPBhsZEkafYpLl+zVkpqv0KASJbRQTUhH +RZx1SCx0ebXOlqDpkLXvzEWX/PCqX6SnFfkCzqdeePCpp+4bE68bE4hNssYg08OOMihQ4CkuFAAJ +GzXHhXLdHGEE4juR5Hm2JYnXUcAv0r6E4hF+Dm+/0r9oVmx2QESCR46gIhciyzfQXgQXrADFAYfX +4FHAn74N0EjaSZRqQr0GVytcEihOJFOY/X65VpdbVrzswrOXnLFQ6Xf2Nx2yNzW6WlqsLc3Wjg6F +06VE4gtMD6hNLVcBGt1+JcaOc8hDHqwP6kUyqPJcJbsGFT9iaAwPD14zEgq5RqAiH5JkNjCW0UAg +GRSjVabIkEePVLQH8cj7oVAIj641CmjkhUXWVL8a/iAqRAVoNOhkCQYYt0CJ5XWC1jWgisnOd7Xb +rXu6DUQqw+tQSB4DEu5Rd+VoaKQtILyzRvAygpzRttlog2oUjEq/Er0R4B5oCyY7yKjWONA/Q7ZX +2gujNhShtoktWegc0dBI+MbcmBJiCtWHt9foRiL7svgrNRY+UzqfmFUYGplYTDqNxHw+EL2BSA0k +BmnkQZ0ctlaNAhEYWjg7/QZHKE3+/q5Nk2Yv/uXPn4yNSdld9+Gqn15aGBtzUn61uo9aQBqmw2OD +WbBLYXpty5Ybfnjn1Rf8BAab1z98+sG7b5mZkzs2ZozN7IvRGjwuC3TLIOqrYR+HjgBTKoI+lCqb +zKZMU31Us3PmshW33Xy/Xpv09obnf/3T607KLojpCShcGrKdIpIIkTegSSWmCUr5h5aD/2g5M6cY +qR1hJQaoJUFi+IUEXGGAHPBvMDpijCjiVCTzhjeH6LkdWVTU+WFNCJKL6G/xoRik6A0h2jwgrTYe +u4j/jJri+UFaL+G1UHElyOE9CmscTk+ZOyXQkmybd855//eD1S+99sTDD/ysKi8H8cfxoTT4ldGX +8NxSpWYINiHE/sstSqsnJdCncnXazT597CnLL732gp8k6FId7v4X/vPgE3/9XVGMId2TmORNUPsV +/aZ+GEfJrga4oqgN0hoZ/hGVIzKiuf/Ec/jySJiImpDoDdaWB0EjzhdM4UIKIE8j22RJJSStkTV+ +khRISeURJNKRyI9GAHKg/xmov6nQiPnC1grJTBeJchRyHRk6dJo+lwPlvgvLSpavOGfeWacoZI6u +5gO2tvpAe6vjUL2tvTPgdAPlcDZ8tA64cBEKJUOpRTliqGDTh+MQLltyxFNgGrpSYB6lDnCdYeKR +Ejl2YrCYlJ7BR4JMsbnwpBS+MWn4pdMiM2FEaDzi+okMJLc/AjQSlwX9LN0BvA14oC4fcrPUqlCs +Xp6gV8MKhRxcB7wRROAU8vf5VU5YW3EvFFFKeMoTNvpHh7/mixi0S0t766BTB+4mGh0jTQ9ojeG1 +Qv0Z1QJtMaPQGN0hw4ZlCKoR+Ik9JwyNTBgt+Xi5pAKF5wyBRqn7wyAndp5oNVLsWRG1X6guZLJh +NUpscCSwkWaKPEl4phByAk1DhYhKHBqqn2YIqv2OJOf+/q5Or+yRP79UkFsVlFvu++31H3/y7qLK +aWMCqUo7/GCIKfEpYzTNXsvHh3aOKStffdfT6Ylj24w1P/rZxcrurhkpxVozhFqop8TvAh8dyrpD +RVXDy4Z68jACyYL2eO/OvhanzvDIAy8W5VahfNJtv/peoL6uQpGhNRKXEqoug69OjlBJSvUT0TfS +Qf3Jdy+UDGnjZkumECzEJ5KOw4IBvRW1VSN6EOYt0c+Fu0zA1RClMdJvYsPGM+NupDMjoCFWkeQW +G6xahTcctlPxZfM//GusNUrYHYEc8hYHkHth1/ktuYGk8vEP/+Gfh2pr7/jp97xd7UWJGVqPTuZW +6GSglYYBDlxKSnDVe9UOZ6y30Wnqcdkry2dccs6PFi9ciZvtsTXf/9effbL21XyttsCXru1FbTyD +2WuW6YKIcOKkEepeCo0Nu28lWGPzWthcIc0dyXIq9TlL5wL4w2qJeCEkAfE5+b34ICDkREWqAkv7 +NpvEwygcgdIoe560qHin/GZCoxC7SBoSwBORwsIcY24ZktaV+aXF515w3tyzliaqgu3Nte72Zmt7 +k7OtydbUILcgFgcSDFwXITtqayLqS6O1ebzASC+RuXGJXjlKXSJ1gswS2FTYHg5NhQI9gaeCmoXY +FHjT4SsRXSpZM6J3lvBYDqwLvm5JfByCfGEElcbpeP4ZyaBKsWkcGMR1DWFNxSTX0D6i1cDMrIzV +YvNColUAVYv9LoWrz6ewyAwEoSSfUulr0jAHOvxolzQcGiNnRu5RKiAuJIjwEf16CBBK61/M9TAG +MO/hqNZ41KkxxKDK2zSrgMMMqmiCthuO3I5go9iJxTyMCDeDRB7+EwBPugKx8QrxTHiXw7s5AzB8 +jISHMOPIdbAgIoFWjVBGnw52GgTgq1Hmo6a9+wfX33HpObdgE3vu1Yf+9tdfTcgtmJRQIjd6VEmh +3pCx09KDCNlGS7vdp//l7X+bOe0Mm6P9sWfufe6VR2YWjC1wj4k1a3ywCMnsQW2gzwsKbgR0KhOU +OgOFBckdPmenwnHQ5bn02luuPesmT8D20GO/fOuFx2cnFagPe3VBHWxGyNggsRh7gZfzHjgqlbRG +SgajjVTojaJP+DZZmhBEowIaw7AjvRUutLAexIog5egOSNBR+3t4Vw6joKTLwB1EnUyNCzlDvI4S +FsW4DMAng18YBcMvRexDeBFJ1w9TAX0RkgwSKfxaGDxlPlue155quOUXDyyYcv57n7326J/v6mrd +m56MMtdBHS5FrfYoUNTcZwv6HD4vkiGKyqaduXTlydVn5CaVhIL2zXvff/TJB1trNudpEuLtaq1d +a1DFIlEFpAI+pIsgahhbirBzktmTFVnJ4ShwPDzlxP1EHeHZKd1p5GRxSsQuKoZJfJfOEe5M9qsJ +6KSFEPZQinOOuIrw+TdDa4wsdTHmZKThVUi53uSIZgUaCl8AawNBcEpDStLK7181d/nSpER9R+MB +azNpiu62tp6D+xHVi9gwlDdzBOQOIjWFgwS2FKSqIu1HCY+thyzhRI1EswZJh5ynwZ1Iv8Fao6j5 +wMPKjuPI7OTZL+QzVl7Z4RBlARD7xwAoRiZr9PB8TdCoRcwcosxliQZUmcMU8psgLvvlPpvM1R3Q +uOWx8Ahxjk8YGmnKhTXjI04nsTcO3kKHKTRSxVRuIJKzcTRoHAKTo9B45H4f9ulxQiMZ0vjg2JKI +E1La5cn0FGWVleZq1FYS1hqHbmED9rqI1ojYZ9hTlaifjcJTGrc20J9i606w9ytlMOD3dDuvueTC +2668W+1J2FL/8bV3XIJ0hjyFvlpflm1I3utq2GRttOLyrIijVtxw1W2XnX8rQmFffOvxX9734/Rc +/xhtQpG/MMkCrg6/W+dqCfTUBntlSXHIFdbYPamKxHgoqorQ/vb+Uy+65Kbrf5cUSFrz8fP33PuD +yqTkhPZAljqrv9cklzkRroHajB6UFET9CuwBQZhVKSleckox4khaI0kTSH4K64uCCl+sfdbxwtBI +GgvtD+FVIX2d5UIx54cv9ggoCD8YyG6FMsq4KEAxMgb8o8KdGaXOR9oM64q8VUUOLjolQF0KkuPI +jFhQ4KoDlsx+S6pcnlR876+fHltY1WdrfGPN42vXv9bVb3Q6AwG3TKlXxCbFx8enLJy6YP60RePz +Jxs0WbiKutbtz//nkbffejVNGcoLJsd3x6mdWr/C71Bbg0GXzq+SuRDHjLhGsniiV7GNSloj9ypd +Kgni4cwu7sFI/4j+FPcY/bkQ+CRQDf8T3mR5ONisRxo/ZWXQgXsd2j9HWlQnKDSKiSY0QqFYYShJ +DScNhjpP1BxCP0LzodA3YutWGP0g3PAXVlacedF3lp1xGnKUO+BTbGvwdTQ7Ww86W5qcPd2o7wxh +EIwSiGZ24jWzc1JCLTNVe4lGkHVBTmGiFCEoS6QgsvtaCmoSZiVJZQENtxB1YHEVA8ky3QAgRM1j +vpsoN2TYGT90ZI6wWoZZFIe439gfyNmK4YOqU4RD4JB3TXslXRsHhGvlfmTnqgMxmlCiDjQAKkQT +wuJkhrwc0Hp7PEETapsj55HkDlY06OvsRhLrmVUPTtzkX+PVyokqdIeDVeBhwt/Iezv3WDS+RqPr +8XpnR/6Zb/UZw7t9yCfEZBPZa7CUyN6OgOtBO+yQrwy36g+ZqJgbFIQq9i9MCEAHfJyIIhGORjY/ +ghXZoI83K+2OLI85TvXDG+/ITM4Dt9ek6QtiVMnbtq2/65d3Fk/MW3r+GS88+yd/d3NZbhYIhuOz +8lddf6vch/J+8pMXnOZy2/7577//6aE/f2fFmRMm5D312P1pAXm2Jw2cO7akwDbTgVOWLr/5hl81 +N3c+8/JT67Z9bHdbshJzLz77mhXLLkRu5Zq1r/31yds0VnOeJz3ZrNcHFA4wQyJiElw+oAajFH83 +SjnQhBb09hTrz9ngfGfoB9EzEZ054vkTf40sQaFhRk+0oUubls4QbXyoEkN5qJGD43+j7NgMjeT5 +Cdsdh3xbfJUD6AW+8sgM4LeASOjXSLLXQ5tHPqfO6xyjaNU448cWXnvNz+ZXnhKnz0QbDrfZ7jKh +b3Q6QyLojci4SkePuaWlY/+ata+89e7rMq+l2JCWYNcrjCFEHSNNE64ppE1wJT7eUaFnkO9PKpcY +gbSIzkeXNnivGyp+CVUj6hBjIfkaSVmh2DKRmih6m/TIsIlVjAcD85GVxeiWT0StUZILBDSyjxhp +qyAzZGMNhbqhAz2IsNbp9MQW7+9zOjCli6ZULT3nzFOWn6o1aEzNh7xtzebmRltbm7e7C8TBfqcd +2EqIiHwMwKEMOUVEkEQZioBGOK8Qg8rUEHhw/iJlZYhJxzHbNPkEYxnjjGRukkwtJzo0sutZABsi +EzTIt6LCfSDqSEBJdzUxlqADrT55yKnw9nkVDvjbRSVvYYOjm8YKpHUkzSiBjuERYlOrmFXR+h9/ +f5jaGD37hAg8xKYRtbkMPleISdKkiP7TF9CzB7f8bXs3IjSGFTvaiGhzJFHHz2HxEQuqCO841hFl +pZNGBXjLu4+YGMK6yq5Gyo5DWSOZyqAAdbg7Vtkd4yqYPvMPv3kemzEtPLnM4bUe2LstRhVTNaUi +pLb86u7rarZuyM0es6ej7dIf/PSi029ECpxWoQn6XQ63ccu2jUnxiTOmzem3NPz4lu8GOrryZKkq +v9aod9b5jT++7YFT5qx0uzxavfJA047u3p7cMYXFY8Y73b3vfPTik0/+KdjfWahMNpi1YBLQEjeB +C5kGAaTSIc8LOfAIMCDaO3YWckl4hkbIzFzWkMVf3oqkzjkaz8ZAlnG4F48g9Y60RZNAGjkkj7+0 +CsRyIAPWEIflkEETZsQwNNILiqVhiCVTOtnGMCpwBiPGxo/0wbhQIFlu0nntsZpxE6csnHVqQda4 +MXn5Bn0csl+RjGi29qMs3p6DO/rs7dt3b21prgUE5vnj8kH2ZwUzNerggAsBagiF3BAsMfkM7ajC +xUhiBqdfhUFLyqAgH8nQ/WI4NA65OQkaw1E4bMRjCvQoaIxWmukvbNYe8TghoVEgECuOYhYghxO8 +tvgELnXk/CI8xKDSeGTyNp8bWfmTp01ZvvKcGfNmpKTFdjQe7KrdpwYjamOtu6vbZTT5rS4YSjA2 +TkpVVIJ2DViIAFMwUFDwNL8VTBdcSpCGjFNC6Q3n7BNIiJwmzKkICLDBlBS1z6s1Rokw0bP+WCOF +K4geSv7lqPUihFWOlhn4lGyfTLeNZ9RgodxtvMSCgMUZvYlM3VAM8pLQrSoKOXKHZC6v2tvv9ZsQ +GEFpxWRMHYBG0iqEZholCA/ZgRlLB8+5EaExMoMHlr/URPTGHPWjHP0+wl4w4qz//+0EVuWib1ps +7mJ0hMBDJgfJaBfBthG6KRJUIs7jQeIGJdWRmmZElDI3EF6o12k8co87wefOVqdVjr/5lt/mpExQ +wK9N9Zb9akqu9G888MFba57csW5dmjImLSWpprtp/tIVq75/n0aWoh4wx0OWc6xZ+/pLbz7d2Xog +K6hOs+lUfo3NEDysdlYvXnbzD+7SKtKIpzO8yPbVbHr9jb+u/+w1ncKVbktMtiXInMjv99KKIFYr +OFngUQA0ghycq1FR3RdOLRDQSJZRliPCPRl5EaU1DtpyWSsa1IdD9eyIGHH0nh4OjeFzpUUhBQgd +Y6wYGnmg6UFWN/L+kguYQqQgzoCYDt2kBbURRSrrgZW6oCfJ35PiaQ3ZnUpNWlq6Ug8Hi0Kv0EGj +tlrsTjvodvuCdn+KUpkqi9U65TEOjcaBUh1gtAO9kQpaOLIYGf0ooJ82D/hswkmEETrviOIoXkiG +6ah7GXEDkfQoBn/RvRw1KOmLoqWh3T5gXDvWDD9BoVEMZiReHBs4QqrJokF8EwoPrH9urz9GP7F6 +4rmXXDR9wRxQJXe3HDS2HFKb+tytLb21B7393X6XG+SHcEvgmx65AqxvVBQeCiPqysAvDL43ShpF +NBvVmsHvUYkW9rPjp3Em4yRDizSivMwwkaRYTyEUfjOgERYECjUFNirlIQ2qZ8m0WkCj3EB5ZpTQ +5UYnIEGpF7le5G/HZsluEsgCUnVFgkbOnQwbPHh8aJuIBktep59nZg+fm0NN0EI6ioLkUWj83Mh+ +FDNU9C5PPvcobDyenxgCjfgKphi1IbxEcMdxLiNtxPAzIhKH2Ur8SrdN5/CmyZyJypxx1aecelF5 +6XTIoH0OY9Phw+s3vH6oZkPAbC8xZOdos8DW1hvoanbZq08+d8EpZ2ekpgKxLDbTnj2b9u/dUF+7 +CwJdni45xaqJtyHZTm4z+HtjPa1B54IlyxYuODs9LQ/+rQNNNXt2fnZo7xZvV2tKQB7jRD1Vtd4f +g2XtBzMLOLNCqM/EcQcUXwdyDxSkIKObgEZaB5JBdQAa+R6lqR7ZaQdP3SG20iPt0f8NaJQ8GseE +RkKdcAAteaIoPpk9jWQBJ6tcAJytSKtBpBQRHulxtkfh8cV4oYqDGNaErC4N8c9RYqpChVBVbUDl +NzpBuxoXjA05ZHLw44HHlQxtnGsP0cIHoh+6JtIE/LDRUU8S4Z7YYymyUbxiRBvQ+YbexhH92cN3 +GAkLGQZFBIP4oaP0ynHpJCciNAoJV8JFVkSQIAo6IiJ+DAVMPrffoJuyaOZpK86ZOK1aE/KbOzss +h5sCxk57e6vx4EEoi6AxwkR3e1CSFCxNIfiPPVR0TO7FKKCYI8YJCwF0kQR+ZI/gyBq2yYcVNDga +yXNLzlvWX6ViNDD8szs3nFvzBaAxasCOPELDR3RgFQohaIjWGFbmjuprxNIntykTPzM0giwfIdWx +agVicZD7bwv5XBATbCpvPypRQSkHGybxG5IJiUwcTLQHg6pQCkmjHi53ETcrX9cg8tIRhb6jQGPE +ZCRuONJR4vOhlqqjr4Hj2eG/heccZ7dHQyMZNXmeCZ1PbC6D9qBh/RTtwsEf2ajAlh5Ob+etl4yp +nLsBkIT/GhWBA2oDatfK7UqfLENu1cgQ8QIKOy/I2cDs5ZSlq2Ky1DFxQX0simc4wTbl8cW7zDG+ +fab+YLLfjbACn0ztAQGaPFGlydbEJ3oMaqNc50D8qyqgCoLe0BHjcmQ6u/02nxaeAQSho7CVygCy +U4cs1aVJscfrzQZvwIP0AyA5BaAiw5/AgYLQuDA20amA+gqASeAm8i/YXccxBtJUJKQJ988ANPLO +cGyQGtqlI1lZKBEyckTRSUaJpCMZB1mWFCEQGGIa4bAVlVVgsiWRZg9rKaqJgHcUlEWwesvUoFsN +eEHQ59Ml6P0KJcxsSBLF7qkC9zdo4uQaEPKFVDpwB0FqxpahBJkp9A3EMwYCOsQvgEeMfLXMYQ75 +Q5RFZLscm+IZGUUUh6Rf0OYypP+Gz+QhbsIhJwjDsQSNYdllZN/AkcbsRIRG4uUQpCcUd8PxOKCW +Vch73W6NVn3q0iWnXHRe8ZRyT9Bv6ur2tB8OtrciANXV0mzuaPHabDSlZUpnUGl3uohUTAE2W8pN +RBAAISYsqFRPmAoO8NgwCSqCTJiBTMh9RL9IdlTx22QyZ+GG1jrJQdTjkYXBctAJ7msEvzJsqhD6 +wHmC2U/QGIpF9VW1UqeUeeUKMyrroWuMiqA1qPGBEZFsydjg2LuCNcUCATE0CnspJXlGoaPYSQU0 +UpdFpGnaLr+IrzES4MP7LdtIwlNXcnwOMh0P28S/hVj3OW9peLcPwTlhkRahlKJt4X6JmFiPAI1D +0m+P3u1hC60U9yGQEXsv1QLWyFB8A8zccqXWrbUT2ulDCi3YuFHzQxt0x6CMH/intYEYmRu5Hgiz +A5W2LKALBuL9ZpkJVbY1KNbklulh9vMr/VavlgiLyUNG1SVQvMEPljiFRecOxvgUOgCcR+0DLiZ6 +bFT02u9zgw0dZYyZ+tgLNYaS+n3IUgKxLIi2KZ8R8X6iIANRqtC2zbgyGBo1SbFxRZnqlBhrzWFH +a9/RoJGNrwNbvcqgNeSk2Oo7RWkgOhggBoElVidqGxZnalPjLNz4cGgMtylJMENcGJHWUN3WZ3OF +f0jyNZLwyqYv4TYV0gtWukoLfMN75JpCgsH1+RE4pVbFUAkCLGqVzK2EHxZ6I5uJEBQph26NJHDa +KFEoGFS5VCUYISDMQYeNQ+llMjIoHvA8glITKEr6t2RUEipd5BgIw4la6uLKKel2yHEkXXAI+Inp +HXG7DPvrcS2n/wE0YqlIUMPKB988cbRJI0YLVENht3IfqDLA9tnjsoCSXZ8aO3/p4pVXXJFXUmQ1 +9pj7AIodsu6uQFe7ta3B0tnms1lh32Yck8MR4QTnMZMD+SnWhjiTRPkRMp/SPoGVwLSIGC0RZcoh +nEIvYsY9oj9lFGSbKXPcSJYi/rrYa9gLwcMgMcKx4SIaD4TWKR0cVBQeWm1mUua88TFlWcZPalGO +akTVB0vL7xyorhk9vBwfS4uQ5XWa8eQeFWn+iLshQg2EmHJyIgogaIMxGrlOA8ZjEhjB/u+AZ8AR +DHb5Ufj7KCstorhGG1QjyDew/vnr0at9qBQNwD3mxGS/V5R0HC2GS04d7sTjmt2jJx13D4wsxBxH +n2O/ZeoLGnSSo9gpIbRG4jGBRwLE46RHIiyHYleJ4B4KBzZhKpAF9nFUhoBpTjD2SDHPaAnJ94jk +Ie0NmzT4OYLIWyaOZKw9nEmkJ0RdxcQUWJkoCREwsKMcwhxWMIXYkXNLAT8KCb3UJTAeMz8ZfS7q +A4h8cGZQ4QobVPmMBLOwl1/s4/FlY4puOj22OKujvtNqdpRNLuz497b6R945RjcjYSX/grkp88Yl +lo1B/fCU7GTjzqZDf/yPvQncdINU84SynJKbz4gtzoxqfHvdw+9GNX4EnTR9TpkuI0GfES9OU2cl +K7VKQ2F6bHYStouGRz9sfXXLwPbDUCyM3Cz/czwWuhgs8RI1A7EGkgMSDETgG6CUGwS4h0C4xxIr +J7DxVVCOJlqjXZXWJUYde5OwjtI/pGeIUogclMoBTRTxLqJpJXOqUCukQ2yjxz1hv/IT/xfQKHow +LCAwGZlMCw0eVIfIucV6QU4wCtkoFe0OizJGn19SOP2kuUvPPaOwZKy1u6uvs0Nu7lX1dRrrD1lb +WzzGHmSqBvpNPARUkyxYXuqpb/dZXOQjIMY+6Ihc3AThNrypEiAjkKe62FHX4e13stdWoB9DFw0Q +mqJBFISfpDKSofW/CY0Fl8wbf92ph/a0rH9318XXL/3knD94rc5jjPb0v16DmpRbr3vSWt81/DSW +QmnCikhvXCiFFjHfILnxMbl524DVI3N6kb21XeNxaBHLhLsDcSqKaDuR6xsIGSF8h5O4j2/iHcHc +MdLcFuB9jEMIGQMLJmo3GIXG4xuWL3LWVw6NlBoPtn8RsUqhrJzQQTAoWV4FWZ2KPFosXTFXrpRJ +C5sELHYi+BBpcVwSQezQtFwl4xyLuLy4xcnSTksfhi14omfI2cWfEjYKUKRsZUJE3quFHZVBMSz4 +EoiUrFqWcXr1vx59F/XVp88vr6s5vHXt3jseutrx3q7m5z49YqenTC0af8d59fWdn767S61R2cyO +5oPtk+eMu/D6pR0vb2p47APRPuSE0huXZZ0+dXjj9nf3ND23/mgjWnr9KXELxjXsb+vpsRhi9Tit +pa4dIUTtzT297f0X/nBpnl924Df/lr4u9jYRhkPjQFAlseBC6RNuYeGGFC9IQRzg4hkCyyQjR2t+ +QENhcxahjMLLyNGMLLvgNVuUBqdVDNEHRlQPvsjM/qLf+R9AI1VfCBtzCHQ4YRYJtxgQDcqZQRBR +q5BfqklImDhj6oIzTimZM1mRKGsztvVb+5Rub0m9zdfRZG+qtXa2u8zWwNL5/vOWyB963rf1ALT2 +wOVn9aNYeILOct0D7DsnqiDyAZOMKapHEXak/vAMT1lOary+9uI/HhUaWRWMuBUBq2L7YCVS0hoj +vEOfS2sELk68ZbnL5oJYF5eR2L2zedMVf0GrWCFJVXmJFTn6ooyYvFRPn83dbQmane5+a9Xt5218 +Z2dCbXvDPzcMH2spaylMc4rLJFGZtUaqswEMZJWx6oazdMU5oEHYfsvvtSi7QXNV6fEpQKgHaJTb +wRx3XA7qo002GtaRLKhDOAGGNzXEbif2PXFaONU1kkPyRWf96Pe+mh44mtbIIZGcLED1kTk4kiLC +hCpJU07KsyT4JMc2C6hsXZCCeiJ5tFxtgfQVMSsoCJsnB0cNCGWEQBMaH/sAON+CPmGWx4iJDXHu +XJRBsE5DhSSTKW/ckozMW7u08aN1fUbixAcu7+izvfWv9ZfdeLrS6e1dsyNuSpEzTveXe16+56kb +dt/8d9OeliGdWvDdhTkXzX36/jdnnVRZPrmwd1O9IS/FkJ0EdfOPt//j2p+vCG1tOPjwGjQ++aEr +O6Ma73l7Z3z12EjjO296xrSndfiIZS6qKPvZ8r/f/+aKKxclZyfjLtCfGgQR8OG2uYw7m/fd+Qq2 +kSHQyARFZPni3iakJOAKu4bDogpy5CClcP4rbYOoAzlIqKVCZtTbUi8RNx8boUXQIqNw+G+CQJy6 +lIdWMrMNdO9XMxm/bKv/A2iE4Zlo68nyyHXDwD6v0ugMBhvqScOFCyd9euqs05actXx5SkXBesum +oI7QrG5X85p/fnrj6kvT7v2nsalB7fUEPV7vJWd2lxVmloxxX/1rXx/qg8qVv/3+Z7tbZk8pNN3w +EPsIKfSa6fo4T5EsJqTRF/zlhq17W2dMKqi78qHh0AgfR3x1qXl7A2JcBRZSTEoUNLIzUkyUz21Q +TZ1RXHr7uZAfjS9sbHt7p2TDxe6g10z+/SX6wjSn04MSql1tfZ++tf3UC+aOKUxPzkiEXdhrc31y +8QOubvPwMaeYGRLKgYbhTDWKsWWWesx7OBox+VWyRQ+v2r27rbq6YM/tD6IyEG4AQXlej8IF3ie7 +X+2B6/xLzafjgcboypRH/jGxqsLHUaDxCJalL3Xpo1/+b/TA0aCRd1vy1AtLXQQOo6GRfp8rR7J0 +Fc0WQzAXtjGJ2BiOl2MYpK2WQzlEDAD/RwoQb9oCDskOKziqGDZ50cL/Fbad0plCcObfZPMR/0aU +1lhwwWznlLEfvLb5hrsubHzkndZXN4vemvLgVS+9tS0mTr948th9d/wjugvzzp+VedG8e1Y9fvuD +V9s2HKz745sRFyMk45Szp9993WP3v3LrJ2evzl4y0VUtNd7w8LvhxhXVD10uGj95ctHe2/81ZHz0 +GQlzX7zxmQfeuuD7pxy481Vg54Bn8WhDGdYaGe0oiEBYUalnGBpplMKlxwSzLlf6ZDOqKKISddD+ +zYegw2ONnR3WIqZGwsWIEkmfkBwTxkXR0gmlJg7ptmhoVE4YKFfy31goR2kDc5DSaiCGUMIidm2V +KxA87HE51MpxVRWnX3jB9+748WkXnWeNd2+0b5WDuqXDnuj0u7SKTW9vn3fWdM/TL6JuJuX8n3FS +S2nhvx77cGJuov+NDcSPDwxs7crNTbH9fY3f4qRUDZJiMMnpt3gEaf7jhbeJTut+7B2/xRUecrH8 +5PETCyY8syr9tMneLrPjYLvYpIWdkoRLKTSHzTVs4gnfpTRxeNodbeOmFspuO2fd2r1TF5TvuvsV +yqDkduF7n/nEtRmTCzG/PS1Gb3131fKpH7z02fKL5hs31ffva217bev+1W94++zc+qAHyccUgM0X +KZCWE0wo7YTmNJXgo/tWhJztPXkFaa3/eFfu8qAJiM0IKQN3gt8lQy4LlYqV9iCpK8Jb0lHfig0v +8uDYxBG+O9LfI6mT4XbC2ypT+YjQupE0069w8n5rmxY74zGO47lznj8ipYc3VLFsxCE+YulJiDu0 +t7IrKsosx1DGIQD0MT9ICeEoGCak5mp7VGmZ7D+8tslTwlVyKPocFZLRANVql/7IZ7AeQ89MeyXo +rcSeLqUr83XwVj0An4wYbLRFXd1+R9XVJ884qXLPLc/0rNsvpia+kFCR1xsMJKfEJfTbTTsaI12U +OrOk+Mdn3nfjk1fderZvW0PdH/8TjQHmPa2Zc8ebvb5Yg1Ztchi3N066+uTpJ1Xu/vEz3etqIvtJ +YmVupPH+7U1D+n/S7y5et3bP/KVTel7Y1P72LrDijTxAQ7GNNy8xMvyK35B/izqHAY5tyxLOCdiL +PEQPUrcLohTaagdUbf4riyuiY4WXkY9oRBwy30a+ha/xjPN+9YvIr30d0IhuRgAqqMkgICJM1CEL +2EJBTWLCzJMXXrTqugtX/aBy8UIgprG5bpvrgNKgTt3Vkb291uowaSYVow5KQrxOu3mXx+L0jMuX +/XDlJ//ZnpmTXGA0Ovc2AxplmcmqiUWOjfsdLZ3KjLTYhZNRH87bY9FXFsRMK/O0mwKIQQuFtDkp +sVOKLOtrPC29mPxKgzbllEnarBRvjxVBapkXzvto66GcvNTOp9b6zA5MFjWYkU6bolArk2aUlP5y +JYLHYrKTCV+NNqzk9HnlMflpjpY+0Y8RaIT7PW1GScb88X6b22t2iD9qEmPLf7Rs12e1qQ5f54f7 +pPkok+WvmFGwfGrHhoObr3388Otbvb1W3aQ8s8me2G3bd8+rvZ/UWg60Y1alzyzJXIAGXV6z5JiM +yUvJXTbZ0WXyoWhaWlzy+BzwWvisLlxz3jK65owZJVPuWKlNjHGZrNlTS3q31Ho7+6g4tladNrtK +k5bs6Hb47UF9YkLB0klYYD6LMx0hA6XZtsbulMkFmfPGOdqMR1t4YseLPo4mFETOOR69dFAjUW8i +ITkj/srXuIK+JT81fCi/wI0Nh8YILIZhUtqLhSbIe6ZkfpE0OkI1gYVsBSVRFI6BsDGdlTmJZ4yi +O0RevnBucSgNPwklhmvISe4uForDvi5O5afzaZPm//GWU5vFJUU/i60cK671+U9bnvsEDo5It2CJ +ld9+7qtPrb30xtMRVoOFE/nTpD9894k/vTVr0YT8eMOBu18erhtp9JrUacX9fbbkUKh3Q23z8582 +P4vGwSUjHdgoKu44WzRed//biJyIHo6EsuzkMyY9cueLy7+7MKkiN2l6UWJxpipO5+mzHwsjw8tG +XA+nc2ABU9Sg0A2JGlYcNBgSJIbfCLgceJBwIOCRaxIJtYO+ygfUHynulKpeDKWnEZ0eHSD9BSbb +V/2VzweNYX1FmlM8s0WwPt8nkzBKCW4wk5JqyMHB7NXGn8B2oVFqoCYilrQv5HeplSWzpq/4/lUX +3viDxeeeWVxR5nM7euoPuDoOua2Hu1LVKBoV89ybtvo6Y1m+qnCMpcNoiDOodtSA7TFw29WaWP3e +zXVzl0yEyujpNOmuOVNz+Wm7zO78c+ZoUpISV53TJJcXrJyvn1sRnFvelxifVpJl33ww84bladee +Vmv1lJw/u//fGzOuWlJy72WG6aX2jMSClXMyLpybOLnwzWc/PumcmQknVYA8yVCQOv6Bq7q16qpr +liiKsz5du3fe907pCASrrl5sXLdXGa8ff+8liTPK7DVtnh5pZqMrshZXzX16VfYpk/Yd7p92w1Lb +gfbkaUVT7r+84MK5+zbXoVsmLpuSt2KGKl7fv60RPVN60+mKWG3Nb95wNPeiV3OWTmy0usaWZTs/ +PQiIwgnZS6rmP3fDmNMm7W83zli11Frb4ew0lV1/6vifnGkyaPJmlXW8t2fOo9cmn1w15uQJMp9/ +1gNXmzTqaVcv0RZlb/hw76JrTs2YVdFs8kw4e1b/extzvrNo3C2XxFeVeRMT8xdO1MQYJv3ivB61 +Yso1i/NXzvLkJGcuHD/27OmJi8pNMdr8eeO6P9h7vBORbTXHeIhJMughigpID3Y1RjcSZXWRvnU8 +6Hq8lzt63lfZA8JuNzC2kR0Cn7P9U5hxBp7JUkqx1BQgKsrEMIhJMCnIxjj6EQHk9IwHYxz7EQlE +OSiWIu7oVAouIJIfEWIgnrlcDidnMDmkiL7hSkbUAP8UfVmoq1LuBm/kYpuj3iK7BTymE39/2abN +ByumFqtbetv/vZXBhuy7mYsmxM0et2ND7Vkr5+y87jFgVWS2R4jXIUwrSjJdTm+MyWna185tipwl +eqDxyX+4ZKPUuLH9je1DBgltFl8yNzEtYe0bW/fuaGzpMjsT9HlLJky8cakyTt+/vZF6bfgyDIO+ +ZOYRgCjCD5ktTnxHRFhwt3MT4gTRIVHgyKZrBkXxLIaDe5C+IvRFToyOXHy0iCCZFL7K2fcl246G +xuMJwRA6tiRX4LVg+hYPkjTgk1MjrRfGOQV4l0BFAUa9oEoRE5egiYt3hHxtAUez3JswNu+iG37w +wAtP3/PEn8+88qKsgmy309xes6t77/bg4UZZ6yFn00Frd39XczcyemW9pmB2Bn4yNkFvNVr9+nj/ +d8/RpyXik5ZDXYnpCb6aVsOPzk85d67N5Pjs/T3xxWNSL11sNlrXvbVDmxIXV5Fn7La889Jnqsyk +jJ99Z8zKuVac9sEen9EWU5Gnn1dh7rE4t9erNWp9VlJcbmpzTZubcyTispLzr1xU9ouV8Px99sFu +5E4kZCSc+70lHqcHnsJ3X/ws/4qTRe8fru/UpVLwtBC9MxdXTbr7QpVW1X/gcF5J5pP3v5l17ozE +6uL47OSY1Dh48seWjdHG6fEWtPqihcTijP2b6uIrcsTbhOqxB/a2lE0rRpw33mYtqZpyzwVo0Fgj +NTjmnGnlPz27/Hsnd7f1/ef5T+QaNWrfxRekrXlhfVJ+RvUvL/DwNasNWvQPrhmN4K43vr/HZ7Il +luXGVJfjri37G0H3nFKWM/lHp/d3W/ZsPmRIjYvLTt6x+RBuMLkiJy41/oVH34srTP+Skyz668MW +LK+8qIe0/xzxJ8Ob03/xekab+q/2wIDYwma4MJ5IIMOb7IBSyLa4cBKVKOOHMolcBwqJyLDwcOFE +mHqkB31O6chSIVxm/sZbtrJGDK9kUOUKxNEHqTbMas1qDoVvU6QOha1zIB2rjIOOMByHNdBoaKRo +lwmrL+5wuk1G+8TJhQd/90Z0H+ZeNPe1Z9add+XJDQ+9HXCCa2QoSMFkk71yzkdv70Coaj/F1ww6 +hUB39UVo3EyNj6393X+GDxDcLluu+luB03f24okrL17wnSsXnbx8emdr3+2XPuApTh930+nCbxp5 +RLUgfksyHQ/IHByiCEQMa4AS3vF4cdIFIjUGP1DVkjpQ9KFUlkEq1CdKQTF2Du7U/+pU+zobGxka +j7A1hbtapMwTpwRlECLYEpkwVOArNi7Bo1HX2foPOa2avDFTF8274Z7bV7/89FX33FZYPd5s7m7c +s6330H4ENfsP1/laa+x1O/p3bbWvWx/r8cSlxHvitD6lQldCmKGP1VtMTuXUitg5lfgNj92FeGjH +vmYsH9SP8fbbEJQ1Z0mVt6vffbhPo5G44V01rTqNqmpqUaCmFUWmPUY+bXGVd1eTfmxW3f42wK11 +c53svR2Hbnq8/lf/Qv7DwtOrO579aMvZ93Wu2+/s7O9r60MEHX7x4K9fOvjQW/W/e218fqrV4kid +V47omKTs5LxxYyy1HWKokGlbcfu5UGcbX9645cqHc4qzWg62Z84uO3Dvyx+cfp+pqSczL7WwIvf9 +Zfe9e/pvDj38nvhW79YGdGDWsknibfq0IqfN5UKEap8NDU74+TlosOGlTRsvf0Q0mDWnzGtx2Dv6 +07KTi8pz7Q1dMJwe2tlUManQ0WO2R13ztt+8WP/mJme/zWV3zV5S5app0udlHtxHd23cUWddt6Ph +9c2m+k4MlgN2WqcHP9rb0d/bbbE09eLt9EWVliNFx32d83L0t74pPRAFhQNYIgLVpIfk3hPaIasi +hIhIxyAgQ6IVajv4UDL1GA/2j4uHj1/jGdTgkYfPj8RHaowe4pXIHiBlkT2ObPATihqnNEmvw6ob +fR6l64Q3OBoDOhO4WLn64m5ZqHZv6zmXLdy56im/E8kkEciRJ5SNQVJWVm5q50d7BaFW9AOpnBW/ +ubi1z5KSkeAn8bRtQHyApqGUV62+sCfc+I5VTx4tldnZYap7+L3dtzy/8aKH1i1djecsm2fVXRc+ +cPvz+edO+7wTRkrqZECT1MPwC/GWpBGAYPSDqaelh5QCI2zbgxoJyxZhC/bnvbIT4/zjgEYW78Uc +EQ+S3Fivpkp+KOWtUGhDcjxUyLsP+VGqq9Fs9Mbp5p128k3/95O7n/jznc88tvyqK0OxcQ01Bw43 +Nvot/VqbKdDe5Gncbz+4016z3bJ/q7nxEGjB4+79W9qTL4U+2uKeWIrp6O0z6+L0iN5ULZuJC/Dv +OFi7ua6iKl++qx4z3f7AK87P9n/2Sc3EmaW9v3rG/uZnNTsbl5w1rffv79n2NO3Z2TRz0QTrxgNd +v3vV/On+jZ/UVM0qtW6udextqpoz7m+r/512+cm2Xc223c3avLS925sgzRk/3g8jbctdL3a9uHHf +zkZATtMTH/S8t7P16bUdb25te/qjRcuqN72/e8zp1X6PXx+nd3X2i0HMWzln28c1Y3JTD7+8iYQz +WpxBlUYFNn94LGIyEvBhz9YGZ0c/JjcJEWxW2n/Pa/Ft/Qd/+yZew5Fg7bNlF6abtpFjv+DC2aLB +1pcGGlRqVIcefb/+qXX7tzYA9XFmcvXYHZvrKmeWblz1ROOLn+3f2Vg+qXDf399vWbtz+x9eat+w +b8vHNVUzS007aq01TVWzxz32m3/nrTipe3vj7t++3rV2H+5x3uKqfb95XQ7dtNtcObnw8Bvbtn5c +Uz17XNfxW1PDnoYhInj02+FT/Rgniz+dGKtj9CpG6AHJrjnYCCA8eIIIhV4L9x9pFeJZqvrEWiKh +IuokHvsBOZjUP/FgVXCQeshvRGB6pPZRFC4KdJRcmLDcUpA8+TKHPhgvh4IicDGuOGv6szd0BAJd +Hf1nXTxv89V/tTb1stVQemjS4vC7aq3KeaRIckShT7jvYrNeA6vViisW1d7zSrRyF1eSMevZ6zsj +jV/zqK2ZaAGOfYj+dneba+9/Kz5OH5cUg+IYmkRDBHF5gxnpYN154BF+C6Vc9DXhI0zZ0Q+MaPgt +9HBy+1I0lIiJ4hCpYY+RbuXE/fvI0Eh7/eDrB3cCJy2Bw0KJmBfoiV65HGEbXQF3ID4mpWLslTdf +f9/fHrrtL/evuPF7SWUFbcbulgMHnO3taqs52N5iq9njrN3tqNlp2bfTemCfu6XB39kpd3hArKhE +9n9NK6J2XEtmY7Yr3t2ojdV3dpqRIOXYccjTa9m68VDV9CLvzjpxRepJxTCc6gxaT0OHYsLYA3ta +y6qLrG9ujplb2dPRr1Ep3E2ULG+oLsFpeoPW2dDpaumxfbLvzIvnP/Lrl3N/8R38NWZaSW8PNWI/ +JGmB+on5yJ+tXlTZ9vTayK0bt9RlFKabTY7cs6bDztm7rT7yp5jxObBkxqfGuTpNyFjCQk1MiTMz +8wXyf6HYlU7It2wdOJ+N0XKP0V734DuIVcPr5Kq8vdvqJ1YXwQ2Jb8WOH4MG41LjAKVRDZJLMnlW +6f5dTUVV+a6O/uzFlV2tfZAx7W19CRMLcM1TF1Ue+MeHIrsxbbJ01/bmTntbj2nz/jMumf+Xu1+e +fPsFoANIqCpoa+gqn1nqaDUi27JyanHHO7uT55TW7W0ZU5xp3Npw/HN2pDjHAcdDpM0v8JXjv57R +M7+2HiCVSiTLhR8w0pH/ip9hjsOLsNImdmvJfCf0OQFqbKY71gPVcAcMqhHLatQLciJKLNbsyhT+ +RKH7COKc8BvSWymXi58HmzWjOk3olHJ9dnLlXSsnP3Ll+s0HoRHOnFy06fJHrc29EiiC5ycuBqWW +9dkpXbzYUcVisFFTnjKjdMZzPzpkcrzy5FpErkIjBKwyiYHCkJUy4c4Lpjx81frNdWh8Bjdua+yL +ZDrBdISwviMOpUijxyPnrGp9ahx2GwAzh+lJ9ySiXaKP4e0owLcHakjoN/yMt/TMb4m9iozWlCEe +/Yj0qdDH2T/J54tnagF5Y4MeX9tU/K//0MjQGG0woeEnclNkjVIuHYQ9m9vV6XH3qxXp5SULLjnv +lod+++e3Xrnoth8VTqm0OxyNB+tNzV16myfB3Kdub3Ds3xpq2q9qr3Uc2G47uNvecMjX3au0+8B5 +SEVMONOI3ArVldrxhUGXx/fOVkDjf55ZBztq4PG3VVNKYbVIzEr2NXZg1FWpiarEGEOcPthvi7/u +7JgJhfAXunutYKTSJMcCCEPdpswfn6crzFQnGnCa32jL/fVlBauv6n78nfKidFCzatMTNemJCaXZ +mADGrYfybzh9zme/SZhSlDSpEL5Ga1071L5Ij2N5mRsYaLOS8Xz4XwN0FfrsJPj/oEqq4vSJE/Ka +9rRc/MOlna8TV1Ni9dg92xomTC3u3XToGIOXOLsUbj8AlXEbYZJhjNSgOk4H1BQNtnODmTNLYHe1 +tRrLb1n+5vOfwlTrNzsn3Hp2Ol9z/6F2Ea6mT09ChCru2muylvzksrI7rmp5/t1xJRm465j0RENi +bFpVHlytpoMdYBvYx2qoraY9pTxHrVUDj8deMo+ynUaP0R74/D0glP6I6k/kNtJDxMhIYTIcLBP+ +E2+sx3iAUxL6CjFLIll/+EPs0tKDo3gih9BeJaZCYZCIPA8Kv2TjWCQ0RpZ//qyZz/5wR2ffqrNX +281OaGaK1Ng5L9541q7VeCz97K6T3v4pHvNevBlRpum5qR1NPbqU+JxlU9VxMWgnpbpk0v2XF/30 +nEd/97rJaLtl9aW7b33GcrBToFf++TNnPnt9dONKbnz57tV4nLbxroVrforHvJdvEuhYevOyZVvu +mfT7i5Hyj6QviMsIg5/6yFUpK2f/9kdPXXHz8oYnP/4CA0VCq8gmFYluEuRGZ2sM6iJOzpAAQWI+ +EWqTFIryrTL2KCthCBdBvKL8khRcI5gM8AdOneOUOWIA53ArmCIcgQByIHyyUG550awlC8/73qWX +XX/taRecnzV2rNNssfab3VZ7wG5XOe2h/l5vZ7Ozo8bVWWdu3O9srXd1tDg62wMOO8pkgLQmGFQC +FsmsUjk2MKUsUJrnv+JMzPLAP94JHGiRnT47tzgzpakjUNsamFtVf6B9UkFqsK4dY6Qpz2/V6eHf +Hje/wjA+1+PwNB5sH+v3IYGvOyEOxHOliybqS7KDJvthXxBwPn5BRXu/w+rxJUEci9X742OKK3JN +H+0JTcjvPNw/c+XcxImFYG5zHGhVlI4xG20pXWYTpRZJghgWTuL0kqZOczkisGsP1z/ytnBuY53F +l+cafQGtWplTnpO1fJomOS4mENx37yv4K4im1r61fd5pk2sfXEPnc0CbkDDEAdkQsamZp0385O2d +sxZW9u9qVsfotBkJaBAFh3MrcrLOnqalBkO773kVvHFxs0rqa9oWrJwTn5fa29ybkZNSOmdcSmWe +qdNsMdoMvaaeXQ1oPWtaidMQg86pmF/Z0eewuX1xcTqFTheKiymqyO35cF/SwnJzn9XQaowty96y +q2nBWdPaXt8WLEqHq2bCosr0WaWdH9dAzjie9XZ8OQCDl80gTVK8OYJyeTy/PnrOCdgDw6ZEBJCO +eLFDorKGv6WVw3HxDKUiujli0OQXg9uVFqYEgsIjJNL1wiGFHFI5KBiGpiDnYOJReOGczAvn/N/V +jyDnqrgyPzUrydxn6+8y7dxU9/EbW99+7tM1/1y/Y2Nd7c6maYsmND79SfLM4j6rc/2aHZUrZlbf +unzcdaf6xqZ/tG7fhvd2XXHLWekB2bYb/m5rQq6XaHx21oWzoxs39dlMR2m85YVNEMpn/Om7d1z6 +gNkfyF1cOWnV0vSlk+ypsR+8t2v3lkNQRl3bGg8+8t7QyJ9hPS3lrkQtNuEoGxI4GnXaUKiLnBnu +bil5lcHxyLh44kelRvfToOSNSpS85qoKLDJRlgv8rCA6VFO1EqQhgqkbhgNwHxKZkycYsIaC/cFQ +Ymbq7FOXXHDtlefd+INTLlgxtqpSrtFb+szWnn5404Nur8ds8pm6A33tnq5GV0edp/OQo73R3dXh +7+0L2uwqomenqtDChopCGYHcDNP3zv+ozeYszEnOSna/t0X+0oc0e0+dkZWT4r3vOe2Cid0J8TEJ +MaXzK2LOmBVITZR39dsLs5rquyqmFbs27Hca9DkFacp1e4M2l3xqaWNdZ9G4MZ0/fxalvD3FWc18 +WkK83u0NFJw5PaYwY0xuSvvD//H2mGMXVm3+eH9ZZZ7rUEftqsdjK3JlxZkFJdnGN7Y62kyS654D +khMr89pRyckXML+6yQ5eUxGOhyXn8c2/6cxt62srTp+SnJNir2vfccvTAZdfHWco+/4ptbuasmSK +7rX7eRaStEHCRtidNu2v1xScO+Pg3pas/LSSKYV5Z1YnTM5vfubThTefEWnQdrBj24+fRUFRZDFm +njrpwK4mhI/bD/eBZK+n2zK2PMe4p8WjVuSXZB9+awt9rpCnleaqx2a38F3HJ+g9nkDJadMTCjKy +c1MO/PEtQ2aSpjwnb2xmxytbCi+eBy+m3hvofHd3zvKpzQ2dCKbdcdu/jq3mfs7tePiyGbIvjeLi +5+zRb9LpEcNTtAdMSkQ8TrfyyO7nI4QDcSKBlOTBoHisSGiBLPSE9N8pf7jsZ5c9MGPRhJ/84YpJ +FXkpZqe+06JuNY6JM5QWZU2bN37hmVPnnjapID5mz89fcnWaHQ3dy+76Drw2n7yza/2anXWIYFAr +F58zE8SUzX95v/7JjwJurxgxaIHVgxtPpsbNytY+bjxz2rxy0Xg+Nf6is8uEe0eS9Iq7L3Db3cDm +D17d3FALaT+4+Ozp06YW1z+wpulIRJLDJ0h0Uk2UVBAdUzwE3469SIeIt1EZO4NSGL5JMzUaGuUr +kFFDQVJQMEhoYjM9lfSEeQOBXDqdziMLWZxOUPIBILMyUnOKis5YsaKwuCR7fKk6NsZutljMVtS6 +RFUSr9fpdttkXpvcaQ+a+v193cG+bm9vtwdROfb+oMsZcHs4/YgMHmwdgRkV6Ms8Liq5+95VPjUF +ucrXblO88CEXGVb4z1uo2NPgr21VzKyQzZ8UcnllOjWK3Pje3Og/cFh79VIMiOfNTQGTLWbpDIhX +1lfWI+bNcNVSDIjtn+v8/Q6ZTpN43emY89ZXN8EtF79ggqYqX9ZvNb69zbq7CfaCrGuWavLTbR/u +6f1wDy5Mm5+WvmQSItBa//EpLja8oohqY9Kj1+6q65pWXbj1kj9xNM0Ax0d8RW7O+XOUWpV5S33b +G1sQYgCfARIT5adUJabG9z3+UfeGg+E5QrZoNk3QMfa7C2KLMrydJoVeo0yMQf/0fLAPJydWZuV+ +Zy4a7N9S3/rv7YJxCqaocauWxuSm9n28//DH+ybecT7GrfH5TwJ215hTJ6IAY93LH1OZNGVIH2eo +uO5MRN52v7PR5/WlzKiMLcsHv0Hb61v7d7TqUxIKzp8ZcPrqn/sYWSJQRvGL4J0af+tyTZKh+elP +LAclt+s3aV6PXus3sweOz+rwue9tZDQd3GQYGmVYDlUPXr5/Z9O4woz9d75iOxK5//CrAVFq3sVz +Y4ozEJcIE6tle2Pn2v2AtCGXgcYnPXiFaHzfnS8fsXLAEW8V5qW8s6oNRRm67GTEGXhAL32wI2pX ++dz9M/qF4T0wiEP1MpRMo0QhIBQFKVMdBzgDFEoIOS6vB3wM6hhDamZ6ccX4yhnTJs2dW1Q+zuVF +9cOQ2+F0Qf+TK9XQmhxWecjt8/R7rN1Ba5/M3Cc39gX6+nz9fV6L2et0IhSNie8pqBXwQ7lLSDGC +tkruRYgqiBnjejRJ8QGjlUOtQYLKnwuyQ5ZmyHBCpH3M/kTvyRkm1U4UaMOlp/A/l07hhGD6JtKZ +mHKRvZlSurBoQOJGEuIPGOZID0Qb9Cf6UUrsiUBjyuySSX+80mly7LsVtMLN/PUo+qsojhjh5sDl +Vd65sk0Wqp5ZsvbU+6KGAW1SbNgIU5M0eTr4GiS3H1XYEfwTorJkmFscQg29BZU+rN5UaV2GpEet +EgFSAbzBL3lQxdwV8KM2tw8lqpjNmZOdv1nmjtHFPNoDX2kPRPQg2DwRN9D83Iaj5VEc7TI4b2GE +Y+yFs5Gkj9oan7fxkRoe/fuX7YFB0LiS2WRh5AMw+BUKsIB7UP0zGDIkxJaVlU6eMiV3UnlWeVl+ +YZEuNs5utdkYDr0+N6KsUHLN5bJ5LBaNz+m3m/yWLp+xw2/q9vZ0hyyWoMvhd7r8PlhhgxrU46Y8 +UQAC1V5D2AsDG9MeYjbJoT4yHzgpU4wJTNYArZHJKwQDMO3nxMDA9H1caooDzXCalLcUhiTO65WI +h8m1TFTjDI0E+sS1SCB8XNCoyUrJWDwBNPZIb0BZNaVWfehv7yGjQ0iCR4TGuIL0xAm5+LsqzjD+ +hmU4bdvNf+/eEB2DI7TGEaFROuGI0IgRCxATkygVyS4YtEqhwyQ/qJW4XqVaTZy1ZLVGCVd30Isa +zy7Ux0Nlb/4K9ysC0kadfF92PY1+/8v1wImmNX6Zuxnq9IxynXyZZke/+/X0wCBoXKFVI63IQ3W3 +4GOUxaYkjZ88aeLUKVXVk7PHjk3MzMCea/N6PbCVI9IEOolC5bWaVSGf3+NwOa1et0PuMgeN3X5j +l9fYHTL1Bm0Wr9WKRCUgIp4AEkxGJNJk5H7s1hRmBv0HvO6UewsMI0iWwQYJzZV2anK3MzRSBDhv +4aTFSQoha41Eo0g5U8x8K1ksCFbDRAysPTLoEkYCDsNa4+eExskv3PzZ+gPpGYmIQMsZN6bhqbUN +j1IJ06NBI9hzFr552/r/bEfWY2JGwvhpxXVPr6ujAJzoaM9BBtWjDrlcCo4dERqZT5zqsKLOBvoU +LmLUntKh1pcKTtwg8rhgFvY6A3govAoVaIpI92QhgWST4SEMX88kHP2V0R6QemAUGkenwgnSA4Og +calGrklITEtPm1A9efq8eaUTqgzZmWAgA2DZ3Q6wTGiRrAFIQ8WGQMCD9AiPQ+X1aH2ukLXf09ft +Mhnlth6FtQPlhf0ms8zpCHngnSTPJICNeQ3JPYtqUqwyUoka1JEmqyVbcAkiUQ6C4rHDqUZceZis +oIRq0AjxHxtUCSNZrWQIJOWSK0nzAQWK0IpPEH48/taRoVFYKVk1FdxGJOnRFfNrgcFCI5SX/u4y +eXYSmNUMBs3Be17u+ayWvsXfPJrWOPufP7K7/TqDJiE1fu89r3a8v4dH/QtAY0Rr5AApPiIGVdYa +SZmmuhdsWqbwdZhKUTkWNaKVCj0idJXoVTge5agC64H27kAoMEaFrKkcQU+9NAqNJ8ia/P/5Mk5k +aBRbw/Efo1rj8ffVCXjmIGhcs/pXxVUVxRMmyAzxPl8w4A06weBExOkBTdCnQK6cHBX+gkEngmhc +Abct5LYFHUalzRTq7/H1d7v6+3ymXsAkDJ1EbEH8hBTRyxWhyG0pDj9nFhGFoYigFhszeQEJnAg9 +4ebEl9jyydk1TAXMFESkbpIiCfzD+ZT6KOJCiTGR/0C4SsBI9WokFyODK0Mj6aBhgyqhqeCGF5Xa +2JbLKCi0TFGiml1wdCEA7JA8tjI34PLYmnoi9RrR2pBBFWtbQmmlMr48BwbmY3vvJVX36LODlMVB +VQyl8DppHyFTNFmkiS2YEoihhFN5HpkypFbLkNGiV1NUFchG4IEMeOUeOBrd6FDyRYahWphSJY/m +CThNRy9ptAf+Jz0Q8ThyVYqoIALmxTz2JY24rv8ndzT6o8fZA4OgEVkWXoCa2x50uTRKuU6tQSsu +r9sPvdDrDXrdXp836POoYDh1WAKWnoDd6DV2yUy9vr6egMXsczrk/oAaBAhCp6P4Z2WY1pdqbQtw +xB5N1bsIxQi05CEl0TUxWLLxk4NNuFYwc90SbiLjkRVBwe9HyCeFxpDKSOgk2KM4MkdUBQPU0TOh +JkWPMu8G2RkFAJNeKHimwgqfBI2sLLLplcuFDEAjrQRWq1hJpNLBfAyHRkmlkxzwQusdIV9+xCV0 +bGgkAzUVGuW8IjJYk68xqEBZZ0TvyhGAAyYgXBWifNHjqM4IXyOM5qPQeJwrZPS00R6IBKyiKyJg +Sb6dYx4jruvRjj2Re2AQNPY2NyF2g5lPoIX5Aj5UuQVUeimo0eMOuh0yW6/C1icD6ynia4wdgMag +rV+GhH2HUwG8o41ZSZ4rwg8xnZS+ELHlE1GUyPxDSCpQjAyrFCVCQSOIGBE1UEhXo0QOwh9ymqHQ +scT4RAjEBH/4EeZ2YgJGDgolEld8kdVJAYriGe3wazK0iro0QmtkbVDCTklrZB5DYUFlfyeF5TAs +s7Ux7E3kYpwc6Ur4y9GqrOYe8RCrQtzUYAuq5Jv8XHPieKCRAkwJF2GmBi4SyxMAUa1CbQ9RUx3q +vizok/k9Mp+HYpMoBlm6eNI4R7XGzzUioyd/u3uAjVm0nUUOTgQcdIhQ+dHj29oDg6Ax2FxP5L4h +vz/kdfm9CKsJue0ql1NptwRMRj9lYnTJrL1+syVgM/ldqLroQsQMcclC81Oqggol2VBZt+O4Uyot +T2EvdEiaE4ET/F60W5MSJtWhJ7AKl49mDCQIJWpbyd0nVE9uh5yIBGnhcFNGMtYF6eSwRRTwDGWK +NEiCRo7TwQlCN43SGgkKhX+RoDHslWSnI/9Hhlk6gdYAQSPPAlZNSamlD4+0OsK4iL8R6H4t0MjZ +LILFiBTIEFR34CLCU1UgeieJBOQECr8HOn8I7kYILwogJsP3kMv7tk700fsa7YHj7wEBjQO4yMsY +RzQ6jgLj8ffnN/HMQdBo3/mpPAgd0el1mlFAMeCyKFxmpckIV2LA0ufhVP2QC+qjD9okpg60K9Kf +SDkiIllOzA+puH4Tp1UI1GE4kQp10k4coJAQybYJ5KGsDFJihI2U0Y3sp1xuNGyNDYfVsFYnUDJs +2GSoEz9NX+ZEDvY1wuVGHkforAy1BKzk9OQUSWqHC5YKeJNCYSnlkfVXoTVKhT0jWiProxFoFC/C +S2Zg6KNwkS+Ra4Qee2aIrxzjOA6tUbgYweBHxmmo4QBFmMTxDIxkcQW+RqUP8o6bKoeB2CEMjaTr +fxMn7ug1j/bAV9cDLBUPwkIh5EYfI0YMjbiuv7rrH235y/dANDT+P61JmcVZosWwAAAAAElFTkSu +QmCC + +------=_NextPart_000_013A_01DB5155.2500C8C0-- + diff --git a/provisioning/integration_test/provisioning.sh b/provisioning/integration_test/provisioning.sh index 20b2dac809..553449dd21 100755 --- a/provisioning/integration_test/provisioning.sh +++ b/provisioning/integration_test/provisioning.sh @@ -2,7 +2,7 @@ # Define users and folders users=("alice" "bob" "brian" "charlotte" "david" "emma") -bobFolders=("Search Emails" "Forward Emails") +bobFolders=("Search Emails" "Forward Emails" "Disposition") # Add users for user in "${users[@]}"; do @@ -25,4 +25,9 @@ done # For test forward email # Import emails into 'Forward Emails' folder for user Bob echo "Importing 0.eml into 'Forward Emails' folder for user bob" -james-cli ImportEml \#private "bob@example.com" "Forward Emails" "/root/conf/integration_test/eml/forward_email/0.eml" \ No newline at end of file +james-cli ImportEml \#private "bob@example.com" "Forward Emails" "/root/conf/integration_test/eml/forward_email/0.eml" + +# For test email with no-disposition inline image +# Import email into 'Disposition' folder for user Bob +echo "Importing no_disposition_inline.eml into 'Disposition' folder for user bob" +james-cli ImportEml \#private "bob@example.com" "Disposition" "/root/conf/integration_test/eml/no_disposition_inline/no_disposition_inline.eml" \ No newline at end of file From cc50992e7e5fd30a29169224542f67b16ce394b4 Mon Sep 17 00:00:00 2001 From: Dat PHAM HOANG Date: Thu, 26 Dec 2024 15:00:55 +0700 Subject: [PATCH 32/72] TF-3334 Synchonize cache all the time have new changes even in search or not --- .../thread/presentation/thread_controller.dart | 12 +++++++++--- .../controller/thread_controller_test.dart | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index c279b89826..9b87815818 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -557,6 +557,8 @@ class ThreadController extends BaseController with EmailActionController { } Future _refreshChangeSearchEmail() async { + await _refreshChangeListEmailCache(); + log('ThreadController::_refreshChangeSearchEmail:'); canSearchMore = false; searchController.updateFilterEmail( @@ -596,9 +598,8 @@ class ThreadController extends BaseController with EmailActionController { } } - Future _refreshChangeListEmail() async { - log('ThreadController::_refreshChangeListEmail:'); - final refreshViewState = await _refreshChangesEmailsInMailboxInteractor.execute( + Future> _refreshChangeListEmailCache() async { + return _refreshChangesEmailsInMailboxInteractor.execute( _session!, _accountId!, mailboxDashBoardController.currentEmailState!, @@ -614,6 +615,11 @@ class ThreadController extends BaseController with EmailActionController { mailboxId: selectedMailboxId, ), ).last; + } + + Future _refreshChangeListEmail() async { + log('ThreadController::_refreshChangeListEmail:'); + final refreshViewState = await _refreshChangeListEmailCache(); final refreshState = refreshViewState .foldSuccessWithResult(); diff --git a/test/features/thread/presentation/controller/thread_controller_test.dart b/test/features/thread/presentation/controller/thread_controller_test.dart index 571219a5fd..acc30367d7 100644 --- a/test/features/thread/presentation/controller/thread_controller_test.dart +++ b/test/features/thread/presentation/controller/thread_controller_test.dart @@ -35,6 +35,7 @@ import 'package:tmail_ui_user/features/network_connection/presentation/network_c import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/refresh_changes_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_email_by_id_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart'; @@ -302,6 +303,19 @@ void main() { properties: anyNamed('properties'), needRefreshSearchState: anyNamed('needRefreshSearchState'), )).thenAnswer((_) => Stream.value(Right(SearchEmailSuccess(emailList)))); + + when(mockRefreshChangesEmailsInMailboxInteractor.execute( + any, + any, + any, + sort: anyNamed('sort'), + propertiesCreated: anyNamed('propertiesCreated'), + propertiesUpdated: anyNamed('propertiesUpdated'), + emailFilter: anyNamed('emailFilter'), + )).thenAnswer((_) => Stream.value(Right(RefreshChangesAllEmailSuccess( + emailList: emailList, + currentEmailState: State('old-state')))) + ); // Act threadController.onInit(); From c3d7014af2c007b1200e2ed402e4f1539d5d5733 Mon Sep 17 00:00:00 2001 From: Dat PHAM HOANG Date: Thu, 26 Dec 2024 10:57:03 +0700 Subject: [PATCH 33/72] Bump version to v0.14.5 --- CHANGELOG.md | 16 +++++++++++++++- pubspec.yaml | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c91aeb111..a1496ced94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,18 @@ -## [0.14.3] - 2024-12-18 +## [0.14.5] - 2024-12-26 +### Fixed +- #3336 Make Echo ping of web socket optional +- #3347 Format Calendar event description +- #3341 Enable Web socket for mobile in foreground +- #3334 Remove all own resynch after any actions in mailbox, email +- #3333 Remove Mailbox/query for spam banner when reload app +- #3332 Remove resynch when switching mailbox +- #3372 Remove Mailbox resynch when Emptying trash +- #3369 Display CID without disposition as an attachment + +### Added +- #3181 Contact support + +## [0.14.4] - 2024-12-18 ### Fixed - #3349 Sanitize HTML when forward/reply/replyAll an email - #3349 Sanitize HTML when print email diff --git a/pubspec.yaml b/pubspec.yaml index 5c38a352cb..c79f835f38 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.14.4 +version: 0.14.5 environment: sdk: ">=3.0.0 <4.0.0" From dd19f3aabe90f5fa90f5fda42960a2a1b4b3aa8e Mon Sep 17 00:00:00 2001 From: Dat PHAM HOANG Date: Mon, 30 Dec 2024 12:11:45 +0700 Subject: [PATCH 34/72] [CI] Fix Ruby conflict version --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 06b1f3cf2a..613926c057 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -38,7 +38,7 @@ jobs: - name: Setup Fastlane uses: ruby/setup-ruby@v1 with: - ruby-version: "ruby" + ruby-version: "3.3" bundler-cache: true working-directory: ${{ matrix.os }} From 352131746af3eb0ac00e4e7ebbce5d48c23c86e8 Mon Sep 17 00:00:00 2001 From: Dat PHAM HOANG Date: Mon, 30 Dec 2024 15:38:01 +0700 Subject: [PATCH 35/72] TF-3372 Fix Empty Trash/Spam base on receivedAt --- .../thread/data/network/thread_isolate_worker.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/features/thread/data/network/thread_isolate_worker.dart b/lib/features/thread/data/network/thread_isolate_worker.dart index 73b69e6666..665db16fb4 100644 --- a/lib/features/thread/data/network/thread_isolate_worker.dart +++ b/lib/features/thread/data/network/thread_isolate_worker.dart @@ -84,7 +84,11 @@ class ThreadIsolateWorker { EmailComparator(EmailComparatorProperty.receivedAt) ..setIsAscending(false)), filter: EmailFilterCondition(inMailbox: args.mailboxId, before: lastEmail?.receivedAt), - properties: Properties({EmailProperty.id})); + properties: Properties({ + EmailProperty.id, + EmailProperty.receivedAt + }), + ); var newEmailList = emailsResponse.emailList ?? []; if (lastEmail != null) { @@ -131,7 +135,11 @@ class ThreadIsolateWorker { EmailComparator(EmailComparatorProperty.receivedAt) ..setIsAscending(false)), filter: EmailFilterCondition(inMailbox: mailboxId, before: lastEmail?.receivedAt), - properties: Properties({EmailProperty.id})); + properties: Properties({ + EmailProperty.id, + EmailProperty.receivedAt + }), + ); var newEmailList = emailsResponse.emailList ?? []; if (lastEmail != null) { From 32a7f1ff3c32cd49b7c4d82412d5cce9f860b15b Mon Sep 17 00:00:00 2001 From: Dat PHAM HOANG Date: Mon, 30 Dec 2024 22:03:42 +0700 Subject: [PATCH 36/72] TF-3372 Add onProgressController to get the progress of emptying task --- .../data/datasource/thread_datasource.dart | 5 ++++ .../local_thread_datasource_impl.dart | 5 ++++ .../thread_datasource_impl.dart | 11 +++++-- .../repository/thread_repository_impl.dart | 29 ++++++++++++++++--- .../domain/repository/thread_repository.dart | 7 +++++ 5 files changed, 51 insertions(+), 6 deletions(-) diff --git a/lib/features/thread/data/datasource/thread_datasource.dart b/lib/features/thread/data/datasource/thread_datasource.dart index ccd7f46d89..697ead4206 100644 --- a/lib/features/thread/data/datasource/thread_datasource.dart +++ b/lib/features/thread/data/datasource/thread_datasource.dart @@ -1,5 +1,8 @@ import 'dart:async'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart' as dartz; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; @@ -68,6 +71,8 @@ abstract class ThreadDataSource { Session session, AccountId accountId, MailboxId mailboxId, + int totalEmails, + StreamController> onProgressController ); Future getEmailById(Session session, AccountId accountId, EmailId emailId, {Properties? properties}); diff --git a/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart b/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart index 769df93776..c563fc0e7a 100644 --- a/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart +++ b/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart @@ -1,5 +1,8 @@ import 'dart:async'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart' as dartz; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; @@ -112,6 +115,8 @@ class LocalThreadDataSourceImpl extends ThreadDataSource { Session session, AccountId accountId, MailboxId mailboxId, + int totalEmails, + StreamController> onProgressController ) { throw UnimplementedError(); } diff --git a/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart b/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart index 48c317357a..490b1677ec 100644 --- a/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart +++ b/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart @@ -1,5 +1,8 @@ import 'dart:async'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart' as dartz; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; @@ -115,13 +118,17 @@ class ThreadDataSourceImpl extends ThreadDataSource { Future> emptyMailboxFolder( Session session, AccountId accountId, - MailboxId mailboxId + MailboxId mailboxId, + int totalEmails, + StreamController> onProgressController ) { return Future.sync(() async { return await _threadIsolateWorker.emptyMailboxFolder( session, accountId, - mailboxId + mailboxId, + totalEmails, + onProgressController ); }).catchError(_exceptionThrower.throwException); } diff --git a/lib/features/thread/data/repository/thread_repository_impl.dart b/lib/features/thread/data/repository/thread_repository_impl.dart index 7334f919c6..9b4c5d94d8 100644 --- a/lib/features/thread/data/repository/thread_repository_impl.dart +++ b/lib/features/thread/data/repository/thread_repository_impl.dart @@ -1,5 +1,8 @@ +import 'dart:async'; import 'package:core/data/model/source_type/data_source_type.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart' as dartz; import 'package:jmap_dart_client/jmap/account_id.dart'; @@ -313,11 +316,20 @@ class ThreadRepositoryImpl extends ThreadRepository { } @override - Future> emptyTrashFolder(Session session, AccountId accountId, MailboxId trashMailboxId) async { + Future> emptyTrashFolder( + Session session, + AccountId accountId, + MailboxId trashMailboxId, + int totalEmails, + StreamController> onProgressController + ) async { final listEmailIdDeleted = await mapDataSource[DataSourceType.network]!.emptyMailboxFolder( session, accountId, - trashMailboxId); + trashMailboxId, + totalEmails, + onProgressController + ); await _updateEmailCache( accountId, @@ -396,11 +408,20 @@ class ThreadRepositoryImpl extends ThreadRepository { } @override - Future> emptySpamFolder(Session session, AccountId accountId, MailboxId spamMailboxId) async { + Future> emptySpamFolder( + Session session, + AccountId accountId, + MailboxId spamMailboxId, + int totalEmails, + StreamController> onProgressController + ) async { final listEmailIdDeleted = await mapDataSource[DataSourceType.network]!.emptyMailboxFolder( session, accountId, - spamMailboxId); + spamMailboxId, + totalEmails, + onProgressController + ); await _updateEmailCache( accountId, diff --git a/lib/features/thread/domain/repository/thread_repository.dart b/lib/features/thread/domain/repository/thread_repository.dart index e794b893c3..ec05030612 100644 --- a/lib/features/thread/domain/repository/thread_repository.dart +++ b/lib/features/thread/domain/repository/thread_repository.dart @@ -1,5 +1,8 @@ import 'dart:async'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart' as dartz; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; @@ -59,6 +62,8 @@ abstract class ThreadRepository { Session session, AccountId accountId, MailboxId trashMailboxId, + int totalEmails, + StreamController> onProgressController ); Future getEmailById( @@ -72,5 +77,7 @@ abstract class ThreadRepository { Session session, AccountId accountId, MailboxId spamMailboxId, + int totalEmails, + StreamController> onProgressController ); } \ No newline at end of file From 3d8960ef03ff76678267b8f1904ebcc21439975b Mon Sep 17 00:00:00 2001 From: Dat PHAM HOANG Date: Mon, 30 Dec 2024 22:05:26 +0700 Subject: [PATCH 37/72] TF-3372 Worker adds progress state to onProgressController in mobile/web --- .../data/network/thread_isolate_worker.dart | 26 +++++++++++++++++-- .../domain/state/empty_spam_folder_state.dart | 12 +++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/features/thread/data/network/thread_isolate_worker.dart b/lib/features/thread/data/network/thread_isolate_worker.dart index 665db16fb4..bd9a8b37b2 100644 --- a/lib/features/thread/data/network/thread_isolate_worker.dart +++ b/lib/features/thread/data/network/thread_isolate_worker.dart @@ -1,7 +1,11 @@ import 'dart:async'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; +import 'package:dartz/dartz.dart' as dartz; +import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; @@ -19,6 +23,7 @@ import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; import 'package:tmail_ui_user/features/thread/data/model/empty_mailbox_folder_arguments.dart'; import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; import 'package:tmail_ui_user/features/thread/domain/exceptions/thread_exceptions.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; import 'package:tmail_ui_user/main/exceptions/isolate_exception.dart'; import 'package:worker_manager/worker_manager.dart'; @@ -33,9 +38,11 @@ class ThreadIsolateWorker { Session session, AccountId accountId, MailboxId mailboxId, + int totalEmails, + StreamController> onProgressController ) async { if (PlatformInfo.isWeb) { - return _emptyMailboxFolderOnWeb(session, accountId, mailboxId); + return _emptyMailboxFolderOnWeb(session, accountId, mailboxId, totalEmails, onProgressController); } else { final rootIsolateToken = RootIsolateToken.instance; if (rootIsolateToken == null) { @@ -51,7 +58,15 @@ class ThreadIsolateWorker { mailboxId, rootIsolateToken ), - fun1: _emptyMailboxFolderAction + fun1: _emptyMailboxFolderAction, + notification: (value) { + if (value is List) { + log('ThreadIsolateWorker::emptyMailboxFolder(): onUpdateProgress ${value.length / totalEmails}'); + onProgressController.add(Right(EmptyingFolderState( + mailboxId, value.length, totalEmails + ))); + } + }, ); if (result.isEmpty) { @@ -105,6 +120,7 @@ class ThreadIsolateWorker { args.accountId, newEmailList.listEmailIds); emailListCompleted.addAll(listEmailIdDeleted); + sendPort.send(listEmailIdDeleted); } else { hasEmails = false; } @@ -121,6 +137,8 @@ class ThreadIsolateWorker { Session session, AccountId accountId, MailboxId mailboxId, + int totalEmails, + StreamController> onProgressController ) async { List emailListCompleted = List.empty(growable: true); try { @@ -156,6 +174,10 @@ class ThreadIsolateWorker { accountId, newEmailList.listEmailIds); emailListCompleted.addAll(listEmailIdDeleted); + + onProgressController.add(Right(EmptyingFolderState( + mailboxId, listEmailIdDeleted.length, totalEmails + ))); } else { hasEmails = false; } diff --git a/lib/features/thread/domain/state/empty_spam_folder_state.dart b/lib/features/thread/domain/state/empty_spam_folder_state.dart index fedb07ae34..6823c8f1b8 100644 --- a/lib/features/thread/domain/state/empty_spam_folder_state.dart +++ b/lib/features/thread/domain/state/empty_spam_folder_state.dart @@ -1,6 +1,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; class EmptySpamFolderLoading extends LoadingState {} @@ -17,4 +18,15 @@ class EmptySpamFolderSuccess extends UIState { class EmptySpamFolderFailure extends FeatureFailure { EmptySpamFolderFailure(dynamic exception) : super(exception: exception); +} + +class EmptyingFolderState extends UIState { + final MailboxId mailboxId; + final int countEmailsDeleted; + final int totalEmails; + + EmptyingFolderState(this.mailboxId, this.countEmailsDeleted, this.totalEmails); + + @override + List get props => [mailboxId, countEmailsDeleted, totalEmails]; } \ No newline at end of file From 0b4ade0ad5292e9d2dde008b1920efeb319262bb Mon Sep 17 00:00:00 2001 From: Dat PHAM HOANG Date: Mon, 30 Dec 2024 22:07:22 +0700 Subject: [PATCH 38/72] TF-3372 Adding onProgressController to interactor layer --- .../empty_spam_folder_interactor.dart | 20 +++++++++++++++++-- .../empty_trash_folder_interactor.dart | 20 +++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/lib/features/thread/domain/usecases/empty_spam_folder_interactor.dart b/lib/features/thread/domain/usecases/empty_spam_folder_interactor.dart index 601998a4cc..0093f76447 100644 --- a/lib/features/thread/domain/usecases/empty_spam_folder_interactor.dart +++ b/lib/features/thread/domain/usecases/empty_spam_folder_interactor.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; @@ -12,10 +14,24 @@ class EmptySpamFolderInteractor { EmptySpamFolderInteractor(this.threadRepository); - Stream> execute(Session session, AccountId accountId, MailboxId spamMailboxId) async* { + Stream> execute( + Session session, + AccountId accountId, + MailboxId spamMailboxId, + int totalEmails, + StreamController> onProgressController + ) async* { try { yield Right(EmptySpamFolderLoading()); - final emailIdDeleted = await threadRepository.emptySpamFolder(session, accountId, spamMailboxId); + onProgressController.add(Right(EmptySpamFolderLoading())); + + final emailIdDeleted = await threadRepository.emptySpamFolder( + session, + accountId, + spamMailboxId, + totalEmails, + onProgressController + ); yield Right(EmptySpamFolderSuccess(emailIdDeleted)); } catch (e) { yield Left(EmptySpamFolderFailure(e)); diff --git a/lib/features/thread/domain/usecases/empty_trash_folder_interactor.dart b/lib/features/thread/domain/usecases/empty_trash_folder_interactor.dart index 5a540c8b91..d2474096f5 100644 --- a/lib/features/thread/domain/usecases/empty_trash_folder_interactor.dart +++ b/lib/features/thread/domain/usecases/empty_trash_folder_interactor.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; @@ -12,10 +14,24 @@ class EmptyTrashFolderInteractor { EmptyTrashFolderInteractor(this.threadRepository); - Stream> execute(Session session, AccountId accountId, MailboxId trashMailboxId) async* { + Stream> execute( + Session session, + AccountId accountId, + MailboxId trashMailboxId, + int totalEmails, + StreamController> onProgressController + ) async* { try { yield Right(EmptyTrashFolderLoading()); - final emailIdDeleted = await threadRepository.emptyTrashFolder(session, accountId, trashMailboxId); + onProgressController.add(Right(EmptyTrashFolderLoading())); + + final emailIdDeleted = await threadRepository.emptyTrashFolder( + session, + accountId, + trashMailboxId, + totalEmails, + onProgressController + ); yield Right(EmptyTrashFolderSuccess(emailIdDeleted,)); } catch (e) { yield Left(EmptyTrashFolderFailure(e)); From 32d12f0c9b67e6604528855e742b668ab4c3673b Mon Sep 17 00:00:00 2001 From: Dat PHAM HOANG Date: Mon, 30 Dec 2024 22:15:38 +0700 Subject: [PATCH 39/72] TF-3372 Use viewStateMailboxActionProgress to build progress bar for not only MarkAsRead but also Empty mailbox action --- .../mailbox_dashboard_controller.dart | 26 +++++++++++++++---- .../mailbox_dashboard_view_web.dart | 2 +- .../thread/presentation/thread_view.dart | 2 +- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 7966608a71..b8f2bb98e7 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -225,7 +225,7 @@ class MailboxDashBoardController extends ReloadableController final filterMessageOption = FilterMessageOption.all.obs; final listEmailSelected = [].obs; final composerOverlayState = ComposerOverlayState.inActive.obs; - final viewStateMarkAsReadMailbox = Rx>(Right(UIState.idle)); + final viewStateMailboxActionProgress = Rx>(Right(UIState.idle)); final vacationResponse = Rxn(); final routerParameters = Rxn>(); final _isDraggingMailbox = RxBool(false); @@ -559,7 +559,7 @@ class MailboxDashBoardController extends ReloadableController void _registerStreamListener() { progressState.listen((state) { - viewStateMarkAsReadMailbox.value = state; + viewStateMailboxActionProgress.value = state; }); _refreshActionEventController.stream @@ -1395,6 +1395,8 @@ class MailboxDashBoardController extends ReloadableController } void _emptyTrashFolderSuccess(EmptyTrashFolderSuccess success) { + viewStateMailboxActionProgress.value = Right(UIState.idle); + if (currentOverlayContext != null && currentContext != null) { appToast.showToastSuccessMessage( currentOverlayContext!, @@ -1537,7 +1539,7 @@ class MailboxDashBoardController extends ReloadableController } void _markAsReadMailboxSuccess(Success success) { - viewStateMarkAsReadMailbox.value = Right(UIState.idle); + viewStateMailboxActionProgress.value = Right(UIState.idle); if (success is MarkAsMailboxReadAllSuccess) { if (currentContext != null && currentOverlayContext != null) { @@ -1557,7 +1559,7 @@ class MailboxDashBoardController extends ReloadableController } void _markAsReadMailboxFailure(MarkAsMailboxReadFailure failure) { - viewStateMarkAsReadMailbox.value = Right(UIState.idle); + viewStateMailboxActionProgress.value = Right(UIState.idle); if (currentOverlayContext != null && currentContext != null) { appToast.showToastErrorMessage( currentOverlayContext!, @@ -1569,7 +1571,7 @@ class MailboxDashBoardController extends ReloadableController } void _markAsReadMailboxAllFailure(MarkAsMailboxReadAllFailure failure) { - viewStateMarkAsReadMailbox.value = Right(UIState.idle); + viewStateMailboxActionProgress.value = Right(UIState.idle); if (currentOverlayContext != null && currentContext != null) { appToast.showToastErrorMessage( currentOverlayContext!, @@ -2392,6 +2394,8 @@ class MailboxDashBoardController extends ReloadableController } void _emptySpamFolderSuccess(EmptySpamFolderSuccess success) { + viewStateMailboxActionProgress.value = Right(UIState.idle); + if (currentOverlayContext != null && currentContext != null) { appToast.showToastSuccessMessage( currentOverlayContext!, @@ -2776,6 +2780,18 @@ class MailboxDashBoardController extends ReloadableController ); } + void _handleEmptySpamFolderFailure(EmptySpamFolderFailure failure) { + viewStateMailboxActionProgress.value = Right(UIState.idle); + + toastManager.showMessageFailure(failure); + } + + void _handleEmptyTrashFolderFailure(EmptyTrashFolderFailure failure) { + viewStateMailboxActionProgress.value = Right(UIState.idle); + + toastManager.showMessageFailure(failure); + } + void _handleGetRestoredDeletedMessageSuccess(GetRestoredDeletedMessageSuccess success) async { if (selectedMailbox.value != null && selectedMailbox.value!.isRecovered) { dispatchEmailUIAction(RefreshChangeEmailAction(null)); diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart index bef9b16a6a..334c542fc8 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -107,7 +107,7 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { responsiveUtils: controller.responsiveUtils, )), Obx(() => MarkMailboxAsReadLoadingBanner( - viewState: controller.viewStateMarkAsReadMailbox.value, + viewState: controller.viewStateMailboxActionProgress.value, )), const SpamReportBannerWebWidget(), QuotasBannerWidget(), diff --git a/lib/features/thread/presentation/thread_view.dart b/lib/features/thread/presentation/thread_view.dart index a12a60f5aa..0e06890400 100644 --- a/lib/features/thread/presentation/thread_view.dart +++ b/lib/features/thread/presentation/thread_view.dart @@ -864,7 +864,7 @@ class ThreadView extends GetWidget Widget _buildMarkAsMailboxReadLoading(BuildContext context) { return Obx(() { - final viewState = controller.mailboxDashBoardController.viewStateMarkAsReadMailbox.value; + final viewState = controller.mailboxDashBoardController.viewStateMailboxActionProgress.value; return viewState.fold( (failure) => const SizedBox.shrink(), (success) { From 6ae5cd168af35a31ce8cdfe5198672ea547329f7 Mon Sep 17 00:00:00 2001 From: Dat PHAM HOANG Date: Mon, 30 Dec 2024 22:17:03 +0700 Subject: [PATCH 40/72] TF-3372 Handle Empty mailbox success + failure states --- .../mixin/mailbox_action_handler_mixin.dart | 10 ++++-- .../data/network/mailbox_isolate_worker.dart | 4 +-- .../presentation/mailbox_controller.dart | 10 ++++-- .../mailbox_dashboard_controller.dart | 31 +++++++++++++++---- .../mark_mailbox_as_read_loading_banner.dart | 15 +++++++-- .../thread/presentation/thread_view.dart | 28 ++++++++++++----- 6 files changed, 75 insertions(+), 23 deletions(-) diff --git a/lib/features/base/mixin/mailbox_action_handler_mixin.dart b/lib/features/base/mixin/mailbox_action_handler_mixin.dart index f124709534..bf23a00edc 100644 --- a/lib/features/base/mixin/mailbox_action_handler_mixin.dart +++ b/lib/features/base/mixin/mailbox_action_handler_mixin.dart @@ -64,7 +64,10 @@ mixin MailboxActionHandlerMixin { ..onConfirmAction(AppLocalizations.of(context).delete, () { popBack(); if (mailbox.countTotalEmails > 0) { - dashboardController.emptyTrashFolderAction(trashFolderId: mailbox.id); + dashboardController.emptyTrashFolderAction( + trashFolderId: mailbox.id, + totalEmails: mailbox.countTotalEmails + ); } else { appToast.showToastWarningMessage( context, @@ -89,7 +92,10 @@ mixin MailboxActionHandlerMixin { ..onConfirmButtonAction(AppLocalizations.of(context).delete, () { popBack(); if (mailbox.countTotalEmails > 0) { - dashboardController.emptyTrashFolderAction(trashFolderId: mailbox.id); + dashboardController.emptyTrashFolderAction( + trashFolderId: mailbox.id, + totalEmails: mailbox.countTotalEmails, + ); } else { appToast.showToastWarningMessage( context, diff --git a/lib/features/mailbox/data/network/mailbox_isolate_worker.dart b/lib/features/mailbox/data/network/mailbox_isolate_worker.dart index 33b0146ebe..09468ccd09 100644 --- a/lib/features/mailbox/data/network/mailbox_isolate_worker.dart +++ b/lib/features/mailbox/data/network/mailbox_isolate_worker.dart @@ -8,9 +8,9 @@ import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/core/utc_date.dart'; -import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_comparator.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_comparator_property.dart'; @@ -69,7 +69,7 @@ class MailboxIsolateWorker { ), fun1: _handleMarkAsMailboxReadAction, notification: (value) { - if (value is List) { + if (value is List) { log('MailboxIsolateWorker::markAsMailboxRead(): onUpdateProgress: PERCENT ${value.length / totalEmailUnread}'); onProgressController.add(Right(UpdatingMarkAsMailboxReadState( mailboxId: mailboxId, diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 524f7d1a04..904068006a 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -1359,9 +1359,15 @@ class MailboxController extends BaseMailboxController void emptyMailboxAction(BuildContext context, PresentationMailbox presentationMailbox) { log('MailboxController::emptyMailboxAction:presentationMailbox: ${presentationMailbox.name}'); if (presentationMailbox.isTrash) { - mailboxDashBoardController.emptyTrashFolderAction(trashFolderId: presentationMailbox.id); + mailboxDashBoardController.emptyTrashFolderAction( + trashFolderId: presentationMailbox.id, + totalEmails: presentationMailbox.countTotalEmails + ); } else if (presentationMailbox.isSpam) { - mailboxDashBoardController.emptySpamFolderAction(spamFolderId: presentationMailbox.id); + mailboxDashBoardController.emptySpamFolderAction( + spamFolderId: presentationMailbox.id, + totalEmails: presentationMailbox.countTotalEmails + ); } } diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index b8f2bb98e7..3e6b323840 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -439,8 +439,11 @@ class MailboxDashBoardController extends ReloadableController _handleRestoreDeletedMessageFailed(); } else if (failure is GetRestoredDeletedMessageFailure) { _handleRestoreDeletedMessageFailed(); - } else if (failure is EmptySpamFolderFailure - || failure is MoveMultipleEmailToMailboxFailure) { + } else if (failure is EmptySpamFolderFailure) { + _handleEmptySpamFolderFailure(failure); + } else if (failure is EmptyTrashFolderFailure) { + _handleEmptyTrashFolderFailure(failure); + } else if (failure is MoveMultipleEmailToMailboxFailure) { toastManager.showMessageFailure(failure); } else if (failure is GetComposerCacheFailure) { _handleIdentityCache(); @@ -1385,12 +1388,22 @@ class MailboxDashBoardController extends ReloadableController } } - void emptyTrashFolderAction({Function? onCancelSelectionEmail, MailboxId? trashFolderId}) { + void emptyTrashFolderAction({ + Function? onCancelSelectionEmail, + MailboxId? trashFolderId, + int totalEmails = 0, + }) { onCancelSelectionEmail?.call(); final trashMailboxId = trashFolderId ?? mapDefaultMailboxIdByRole[PresentationMailbox.roleTrash]; if (sessionCurrent != null && accountId.value != null && trashMailboxId != null) { - consumeState(_emptyTrashFolderInteractor.execute(sessionCurrent!, accountId.value!, trashMailboxId)); + consumeState(_emptyTrashFolderInteractor.execute( + sessionCurrent!, + accountId.value!, + trashMailboxId, + totalEmails, + _progressStateController + )); } } @@ -2371,7 +2384,11 @@ class MailboxDashBoardController extends ReloadableController consumeState(_storeSessionInteractor.execute(session)); } - void emptySpamFolderAction({Function? onCancelSelectionEmail, MailboxId? spamFolderId}) { + void emptySpamFolderAction({ + Function? onCancelSelectionEmail, + MailboxId? spamFolderId, + int totalEmails = 0 + }) { onCancelSelectionEmail?.call(); spamFolderId ??= spamMailboxId; @@ -2389,7 +2406,9 @@ class MailboxDashBoardController extends ReloadableController consumeState(_emptySpamFolderInteractor.execute( sessionCurrent!, accountId.value!, - spamFolderId + spamFolderId, + totalEmails, + _progressStateController )); } diff --git a/lib/features/mailbox_dashboard/presentation/widgets/mark_mailbox_as_read_loading_banner.dart b/lib/features/mailbox_dashboard/presentation/widgets/mark_mailbox_as_read_loading_banner.dart index 52f65afc53..806a063cf3 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/mark_mailbox_as_read_loading_banner.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/mark_mailbox_as_read_loading_banner.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/mark_mailbox_as_read_loading_banner_style.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; class MarkMailboxAsReadLoadingBanner extends StatelessWidget with AppLoaderMixin { final Either viewState; @@ -25,13 +26,21 @@ class MarkMailboxAsReadLoadingBanner extends StatelessWidget with AppLoaderMixin child: horizontalLoadingWidget); } else if (success is UpdatingMarkAsMailboxReadState) { final percent = success.countRead / success.totalUnread; - return Padding( - padding: MarkMailboxAsReadLoadingBannerStyle.bannerMargin, - child: horizontalPercentLoadingWidget(percent)); + return _buildProgressBanner(percent); + } else if (success is EmptyingFolderState) { + final percent = success.countEmailsDeleted / success.totalEmails; + return _buildProgressBanner(percent); } else { return const SizedBox.shrink(); } } ); } + + Padding _buildProgressBanner(double percent) { + return Padding( + padding: MarkMailboxAsReadLoadingBannerStyle.bannerMargin, + child: horizontalPercentLoadingWidget(percent) + ); + } } \ No newline at end of file diff --git a/lib/features/thread/presentation/thread_view.dart b/lib/features/thread/presentation/thread_view.dart index 0e06890400..21eb085388 100644 --- a/lib/features/thread/presentation/thread_view.dart +++ b/lib/features/thread/presentation/thread_view.dart @@ -18,6 +18,8 @@ import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widg import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_banner_widget.dart'; import 'package:tmail_ui_user/features/quotas/presentation/widget/quotas_banner_widget.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart'; @@ -868,7 +870,10 @@ class ThreadView extends GetWidget return viewState.fold( (failure) => const SizedBox.shrink(), (success) { - if (success is MarkAsMailboxReadLoading) { + if (success is MarkAsMailboxReadLoading + || success is EmptySpamFolderLoading + || success is EmptyTrashFolderLoading + ) { return Padding( padding: EdgeInsets.only( top: controller.responsiveUtils.isDesktop(context) ? 16 : 0, @@ -878,16 +883,23 @@ class ThreadView extends GetWidget child: horizontalLoadingWidget); } else if (success is UpdatingMarkAsMailboxReadState) { final percent = success.countRead / success.totalUnread; - return Padding( - padding: EdgeInsets.only( - top: controller.responsiveUtils.isDesktop(context) ? 16 : 0, - left: 16, - right: 16, - bottom: controller.responsiveUtils.isDesktop(context) ? 0 : 16), - child: horizontalPercentLoadingWidget(percent)); + return _buildProgressBanner(context, percent); + } else if (success is EmptyingFolderState) { + final percent = success.countEmailsDeleted / success.totalEmails; + return _buildProgressBanner(context, percent); } return const SizedBox.shrink(); }); }); } + + Padding _buildProgressBanner(BuildContext context, double percent) { + return Padding( + padding: EdgeInsets.only( + top: controller.responsiveUtils.isDesktop(context) ? 16 : 0, + left: 16, + right: 16, + bottom: controller.responsiveUtils.isDesktop(context) ? 0 : 16), + child: horizontalPercentLoadingWidget(percent)); + } } \ No newline at end of file From 97483013e0075b434953012e658cb5089ade56bf Mon Sep 17 00:00:00 2001 From: Dat PHAM HOANG Date: Tue, 31 Dec 2024 13:02:46 +0700 Subject: [PATCH 41/72] TF-3372 Handling error for empty trash failure --- .../mixin/mailbox_action_handler_mixin.dart | 4 +- .../mailbox_dashboard_controller.dart | 8 +- .../mark_mailbox_as_read_loading_banner.dart | 9 +- .../data/network/thread_isolate_worker.dart | 6 +- .../thread/presentation/thread_view.dart | 85 ++++++++++++------- lib/l10n/intl_messages.arb | 8 +- lib/main/localizations/app_localizations.dart | 6 ++ lib/main/utils/toast_manager.dart | 3 + 8 files changed, 86 insertions(+), 43 deletions(-) diff --git a/lib/features/base/mixin/mailbox_action_handler_mixin.dart b/lib/features/base/mixin/mailbox_action_handler_mixin.dart index bf23a00edc..ca34bba977 100644 --- a/lib/features/base/mixin/mailbox_action_handler_mixin.dart +++ b/lib/features/base/mixin/mailbox_action_handler_mixin.dart @@ -131,7 +131,7 @@ mixin MailboxActionHandlerMixin { ..onConfirmAction(AppLocalizations.of(context).delete_all, () { popBack(); if (mailbox.countTotalEmails > 0) { - dashboardController.emptySpamFolderAction(spamFolderId: mailbox.id); + dashboardController.emptySpamFolderAction(spamFolderId: mailbox.id, totalEmails: mailbox.countTotalEmails); } else { appToast.showToastWarningMessage( context, @@ -156,7 +156,7 @@ mixin MailboxActionHandlerMixin { ..onConfirmButtonAction(AppLocalizations.of(context).delete_all, () { popBack(); if (mailbox.countTotalEmails > 0) { - dashboardController.emptySpamFolderAction(spamFolderId: mailbox.id); + dashboardController.emptySpamFolderAction(spamFolderId: mailbox.id, totalEmails: mailbox.countTotalEmails); } else { appToast.showToastWarningMessage( context, diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 3e6b323840..8fafc74f2d 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -1396,12 +1396,14 @@ class MailboxDashBoardController extends ReloadableController onCancelSelectionEmail?.call(); final trashMailboxId = trashFolderId ?? mapDefaultMailboxIdByRole[PresentationMailbox.roleTrash]; + final trashMailbox = mapMailboxById[trashMailboxId]; + final totalEmailsInTrash = totalEmails == 0 ? trashMailbox?.countTotalEmails : totalEmails; if (sessionCurrent != null && accountId.value != null && trashMailboxId != null) { consumeState(_emptyTrashFolderInteractor.execute( sessionCurrent!, accountId.value!, trashMailboxId, - totalEmails, + totalEmailsInTrash ?? 0, _progressStateController )); } @@ -2458,7 +2460,7 @@ class MailboxDashBoardController extends ReloadableController ..onConfirmAction(AppLocalizations.of(context).delete_all, () { popBack(); if (spamMailbox.countTotalEmails > 0) { - emptySpamFolderAction(spamFolderId: spamMailbox.id); + emptySpamFolderAction(spamFolderId: spamMailbox.id, totalEmails: spamMailbox.countTotalEmails); } else { appToast.showToastWarningMessage( context, @@ -2483,7 +2485,7 @@ class MailboxDashBoardController extends ReloadableController ..onConfirmButtonAction(AppLocalizations.of(context).delete_all, () { popBack(); if (spamMailbox.countTotalEmails > 0) { - emptySpamFolderAction(spamFolderId: spamMailbox.id); + emptySpamFolderAction(spamFolderId: spamMailbox.id, totalEmails: spamMailbox.countTotalEmails); } else { appToast.showToastWarningMessage( context, diff --git a/lib/features/mailbox_dashboard/presentation/widgets/mark_mailbox_as_read_loading_banner.dart b/lib/features/mailbox_dashboard/presentation/widgets/mark_mailbox_as_read_loading_banner.dart index 806a063cf3..083f353b9c 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/mark_mailbox_as_read_loading_banner.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/mark_mailbox_as_read_loading_banner.dart @@ -25,11 +25,9 @@ class MarkMailboxAsReadLoadingBanner extends StatelessWidget with AppLoaderMixin padding: MarkMailboxAsReadLoadingBannerStyle.bannerMargin, child: horizontalLoadingWidget); } else if (success is UpdatingMarkAsMailboxReadState) { - final percent = success.countRead / success.totalUnread; - return _buildProgressBanner(percent); + return _buildProgressBanner(success.countRead, success.totalUnread); } else if (success is EmptyingFolderState) { - final percent = success.countEmailsDeleted / success.totalEmails; - return _buildProgressBanner(percent); + return _buildProgressBanner(success.countEmailsDeleted, success.totalEmails); } else { return const SizedBox.shrink(); } @@ -37,7 +35,8 @@ class MarkMailboxAsReadLoadingBanner extends StatelessWidget with AppLoaderMixin ); } - Padding _buildProgressBanner(double percent) { + Padding _buildProgressBanner(int progress, int total) { + final percent = total > 0 ? progress / total : 0.68; return Padding( padding: MarkMailboxAsReadLoadingBannerStyle.bannerMargin, child: horizontalPercentLoadingWidget(percent) diff --git a/lib/features/thread/data/network/thread_isolate_worker.dart b/lib/features/thread/data/network/thread_isolate_worker.dart index bd9a8b37b2..03ffe5112d 100644 --- a/lib/features/thread/data/network/thread_isolate_worker.dart +++ b/lib/features/thread/data/network/thread_isolate_worker.dart @@ -61,7 +61,7 @@ class ThreadIsolateWorker { fun1: _emptyMailboxFolderAction, notification: (value) { if (value is List) { - log('ThreadIsolateWorker::emptyMailboxFolder(): onUpdateProgress ${value.length / totalEmails}'); + log('ThreadIsolateWorker::emptyMailboxFolder(): processed ${value.length} - totalEmails $totalEmails'); onProgressController.add(Right(EmptyingFolderState( mailboxId, value.length, totalEmails ))); @@ -120,7 +120,7 @@ class ThreadIsolateWorker { args.accountId, newEmailList.listEmailIds); emailListCompleted.addAll(listEmailIdDeleted); - sendPort.send(listEmailIdDeleted); + sendPort.send(emailListCompleted); } else { hasEmails = false; } @@ -176,7 +176,7 @@ class ThreadIsolateWorker { emailListCompleted.addAll(listEmailIdDeleted); onProgressController.add(Right(EmptyingFolderState( - mailboxId, listEmailIdDeleted.length, totalEmails + mailboxId, emailListCompleted.length, totalEmails ))); } else { hasEmails = false; diff --git a/lib/features/thread/presentation/thread_view.dart b/lib/features/thread/presentation/thread_view.dart index 21eb085388..5d8924ac2f 100644 --- a/lib/features/thread/presentation/thread_view.dart +++ b/lib/features/thread/presentation/thread_view.dart @@ -1,4 +1,5 @@ import 'package:core/core.dart'; +import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -164,7 +165,7 @@ class ThreadView extends GetWidget } }), if (!controller.responsiveUtils.isDesktop(context)) - _buildMarkAsMailboxReadLoading(context), + _buildMailboxActionProgressBanner(context), Obx(() => ThreadViewLoadingBarWidget(viewState: controller.viewState.value)), Expanded( child: Container( @@ -864,42 +865,68 @@ class ThreadView extends GetWidget ); } - Widget _buildMarkAsMailboxReadLoading(BuildContext context) { + Widget _buildMailboxActionProgressBanner(BuildContext context) { return Obx(() { - final viewState = controller.mailboxDashBoardController.viewStateMailboxActionProgress.value; - return viewState.fold( - (failure) => const SizedBox.shrink(), - (success) { - if (success is MarkAsMailboxReadLoading - || success is EmptySpamFolderLoading - || success is EmptyTrashFolderLoading - ) { - return Padding( - padding: EdgeInsets.only( - top: controller.responsiveUtils.isDesktop(context) ? 16 : 0, - left: 16, - right: 16, - bottom: controller.responsiveUtils.isDesktop(context) ? 0 : 16), - child: horizontalLoadingWidget); - } else if (success is UpdatingMarkAsMailboxReadState) { - final percent = success.countRead / success.totalUnread; - return _buildProgressBanner(context, percent); - } else if (success is EmptyingFolderState) { - final percent = success.countEmailsDeleted / success.totalEmails; - return _buildProgressBanner(context, percent); - } - return const SizedBox.shrink(); - }); + return _MailboxActionProgressBanner( + viewState: controller.mailboxDashBoardController.viewStateMailboxActionProgress.value, + responsiveUtils: controller.responsiveUtils, + ); }); } +} + +class _MailboxActionProgressBanner extends StatelessWidget with AppLoaderMixin { + final Either viewState; + final ResponsiveUtils responsiveUtils; + + const _MailboxActionProgressBanner({ + required this.viewState, + required this.responsiveUtils, + }); + + @override + Widget build(BuildContext context) { + return viewState.fold( + (failure) => const SizedBox.shrink(), + (success) { + if (success is MarkAsMailboxReadLoading || + success is EmptySpamFolderLoading || + success is EmptyTrashFolderLoading) { + return Padding( + padding: EdgeInsets.only( + top: responsiveUtils.isDesktop(context) ? 16 : 0, + left: 16, + right: 16, + bottom: responsiveUtils.isDesktop(context) ? 0 : 16, + ), + child: horizontalLoadingWidget, + ); + } else if (success is UpdatingMarkAsMailboxReadState) { + return _buildProgressBanner( + context, + success.countRead, + success.totalUnread, + ); + } else if (success is EmptyingFolderState) { + return _buildProgressBanner( + context, + success.countEmailsDeleted, + success.totalEmails, + ); + } + return const SizedBox.shrink(); + }, + ); + } - Padding _buildProgressBanner(BuildContext context, double percent) { + Padding _buildProgressBanner(BuildContext context, int progress, int total) { + final percent = total > 0 ? progress / total : 0.68; return Padding( padding: EdgeInsets.only( - top: controller.responsiveUtils.isDesktop(context) ? 16 : 0, + top: responsiveUtils.isDesktop(context) ? 16 : 0, left: 16, right: 16, - bottom: controller.responsiveUtils.isDesktop(context) ? 0 : 16), + bottom: responsiveUtils.isDesktop(context) ? 0 : 16), child: horizontalPercentLoadingWidget(percent)); } } \ No newline at end of file diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index ababfd56d1..c86038fb03 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-10-31T13:18:32.336494", + "@@last_modified": "2024-12-31T12:11:05.777668", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3970,6 +3970,12 @@ "placeholders_order": [], "placeholders": {} }, + "emptyTrashFolderFailed": "Empty trash folder failed", + "@emptyTrashFolderFailed": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, "markAsSpamFailed": "Mark as spam failed", "@markAsSpamFailed": { "type": "text", diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 01ff076bd1..0af16a8f84 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4162,6 +4162,12 @@ class AppLocalizations { name: 'emptySpamFolderFailed'); } + String get emptyTrashFolderFailed { + return Intl.message( + 'Empty trash folder failed', + name: 'emptyTrashFolderFailed'); + } + String get markAsSpamFailed { return Intl.message( 'Mark as spam failed', diff --git a/lib/main/utils/toast_manager.dart b/lib/main/utils/toast_manager.dart index 595b20f6a4..fd0d076bbf 100644 --- a/lib/main/utils/toast_manager.dart +++ b/lib/main/utils/toast_manager.dart @@ -12,6 +12,7 @@ import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_ex import 'package:tmail_ui_user/features/starting_page/domain/state/sign_in_twake_workplace_state.dart'; import 'package:tmail_ui_user/features/starting_page/domain/state/sign_up_twake_workplace_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -63,6 +64,8 @@ class ToastManager { ?? AppLocalizations.of(currentContext!).unknownError; } else if (failure is EmptySpamFolderFailure) { message = AppLocalizations.of(currentContext!).emptySpamFolderFailed; + } else if (failure is EmptyTrashFolderFailure) { + message = AppLocalizations.of(currentContext!).emptyTrashFolderFailed; } else if (failure is MoveMultipleEmailToMailboxFailure && failure.emailActionType == EmailActionType.moveToSpam && failure.moveAction == MoveAction.moving) { From f2ec6f2330ff70c45eccd4dc08caceab5027e8d7 Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 23 Dec 2024 19:15:34 +0700 Subject: [PATCH 42/72] TF-3337 Set maximum objects in `Email/Set` method when mark as read/star and move emails --- .../email/data/network/email_api.dart | 261 +++++++++++------- .../domain/model/move_to_mailbox_request.dart | 5 +- lib/main/error/capability_validator.dart | 1 + 3 files changed, 170 insertions(+), 97 deletions(-) diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index 4f764d31cb..44e7b87ba2 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'dart:typed_data'; import 'package:core/core.dart'; @@ -43,7 +44,7 @@ import 'package:model/account/account_request.dart'; import 'package:model/account/authentication_type.dart'; import 'package:model/download/download_task_id.dart'; import 'package:model/email/attachment.dart'; -import 'package:model/email/email_action_type.dart'; +import 'package:model/email/email_property.dart'; import 'package:model/email/mark_star_action.dart'; import 'package:model/email/read_actions.dart'; import 'package:model/extensions/email_extension.dart'; @@ -60,7 +61,6 @@ import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart' import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; import 'package:tmail_ui_user/features/email/domain/extensions/email_id_extensions.dart'; import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; -import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart'; import 'package:tmail_ui_user/features/email/domain/model/restore_deleted_message_request.dart'; import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart'; @@ -243,37 +243,52 @@ class EmailAPI with HandleSetErrorMixin { List emailIds, ReadActions readActions, ) async { - final setEmailMethod = SetEmailMethod(accountId) - ..addUpdates(emailIds.generateMapUpdateObjectMarkAsRead(readActions)); + final maxBatches = _getMaxObjectsInSetMethod(session, accountId); + final totalEmails = emails.length; - final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); + final List updatedEmails = List.empty(growable: true); - final setEmailInvocation = requestBuilder.invocation(setEmailMethod); + for (int start = 0; start < totalEmails; start += maxBatches) { + int end = (start + maxBatches < totalEmails) + ? start + maxBatches + : totalEmails; + log('EmailAPI::markAsRead:emails from ${start + 1} to $end'); - final capabilities = setEmailMethod.requiredCapabilities - .toCapabilitiesSupportTeamMailboxes(session, accountId); + final currentListEmails = emails.sublist(start, end); - final response = await (requestBuilder - ..usings(capabilities)) - .build() - .execute(); + final setEmailMethod = SetEmailMethod(accountId) + ..addUpdates(currentListEmails.listEmailIds.generateMapUpdateObjectMarkAsRead(readActions)); - final setEmailResponse = response.parse( - setEmailInvocation.methodCallId, - SetEmailResponse.deserialize, - ); + final getEmailMethod = GetEmailMethod(accountId) + ..addIds(emails.listEmailIds.toIds().toSet()) + ..addProperties(Properties({EmailProperty.keywords})); - final emailIdUpdated = setEmailResponse?.updated - ?.keys - .map((id) => EmailId(id)) - .toList() ?? []; - final mapErrors = handleSetResponse([setEmailResponse]); + final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); - if (emailIdUpdated.isNotEmpty) { - return emailIdUpdated; - } else { - throw SetMethodException(mapErrors); + requestBuilder.invocation(setEmailMethod); + + final getEmailInvocation = requestBuilder.invocation(getEmailMethod); + + final capabilities = setEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + + final response = await (requestBuilder + ..usings(capabilities)) + .build() + .execute(); + + final getEmailResponse = response.parse( + getEmailInvocation.methodCallId, + GetEmailResponse.deserialize, + ); + + final listEmails = getEmailResponse?.list ?? []; + if (listEmails.isNotEmpty) { + updatedEmails.addAll(listEmails); + } } + + return updatedEmails; } Future> downloadAttachments( @@ -387,65 +402,104 @@ class EmailAPI with HandleSetErrorMixin { AccountId accountId, MoveToMailboxRequest moveRequest ) async { - final coreCapability = session.getCapabilityProperties( - accountId, - CapabilityIdentifier.jmapCore - ); - int maxMethodCount = coreCapability?.maxCallsInRequest?.value.toInt() ?? CapabilityIdentifierExtension.defaultMaxCallsInRequest; - log('EmailAPI::moveToMailbox:maxMethodCount: $maxMethodCount'); - int start = 0; - int end = 0; + final maxBatches = _getMaxObjectsInSetMethod(session, accountId); final List listEmailIdResult = List.empty(growable: true); - final listCurrentMailboxesEntries = moveRequest.currentMailboxes.entries.toList(); - - while (end < moveRequest.currentMailboxes.length) { - start = end; - if (moveRequest.currentMailboxes.length - start >= maxMethodCount) { - end = maxMethodCount; - } else { - end = moveRequest.currentMailboxes.length; - } - log('EmailAPI::moveToMailbox(): move from $start to $end / ${listCurrentMailboxesEntries.length}'); - final currentExecuteList = listCurrentMailboxesEntries.sublist(start, end); - final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); - final currentSetEmailInvocations = currentExecuteList.map((currentItem) { + final listMailboxIds = moveRequest.currentMailboxes.keys.toList(); + for (int i = 0; i < listMailboxIds.length; i++) { + final currentMailboxId = listMailboxIds[i]; + final listEmailIds = moveRequest.currentMailboxes[currentMailboxId]!; + log('EmailAPI::moveToMailbox:from mailbox ${currentMailboxId.asString} with ${listEmailIds.length} emails to mailbox ${moveRequest.destinationMailboxId.asString}'); + final movedEmailIds = await _moveEmailsBetweenMailboxes( + session: session, + accountId: accountId, + listEmailIds: listEmailIds, + currentMailboxId: currentMailboxId, + maxBatches: maxBatches, + destinationMailboxId: moveRequest.destinationMailboxId, + isMovingToSpam: moveRequest.isMovingToSpam, + ); + listEmailIdResult.addAll(movedEmailIds); + } + return listEmailIdResult; + } + + Future> _moveEmailsBetweenMailboxes({ + required Session session, + required AccountId accountId, + required List listEmailIds, + required MailboxId currentMailboxId, + required MailboxId destinationMailboxId, + required int maxBatches, + bool isMovingToSpam = false, + }) async { + final maxBatches = _getMaxObjectsInSetMethod(session, accountId); + final totalEmails = listEmailIds.length; + + final List updatedEmailIds = List.empty(growable: true); + + for (int start = 0; start < totalEmails; start += maxBatches) { + int end = (start + maxBatches < totalEmails) + ? start + maxBatches + : totalEmails; + log('EmailAPI::_moveEmailsBetweenMailboxes:emails from ${start + 1} to $end'); + + final currentEmailIds = listEmailIds.sublist(start, end); + + final moveProperties = isMovingToSpam + ? currentEmailIds.generateMapUpdateObjectMoveToSpam( + currentMailboxId, + destinationMailboxId, + ) + : currentEmailIds.generateMapUpdateObjectMoveToMailbox( + currentMailboxId, + destinationMailboxId, + ); + + final setEmailMethod = SetEmailMethod(accountId) + ..addUpdates(moveProperties); - final moveProperties = (moveRequest.moveAction == MoveAction.moving && moveRequest.emailActionType == EmailActionType.moveToSpam) - ? currentItem.value.generateMapUpdateObjectMoveToSpam(currentItem.key, moveRequest.destinationMailboxId) - : currentItem.value.generateMapUpdateObjectMoveToMailbox(currentItem.key, moveRequest.destinationMailboxId); + final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); - return SetEmailMethod(accountId) - ..addUpdates(moveProperties); - }).map(requestBuilder.invocation).toList(); + final setEmailInvocation = requestBuilder.invocation(setEmailMethod); - final capabilities = {CapabilityIdentifier.jmapCore, CapabilityIdentifier.jmapMail} - .toCapabilitiesSupportTeamMailboxes(session, accountId); + final capabilities = setEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); - final response = await (requestBuilder..usings(capabilities)) + final response = await (requestBuilder + ..usings(capabilities)) .build() .execute(); - Future.sync(() async { - final listSetEmailResponse = currentSetEmailInvocations - .map((currentInvocation) => response.parse(currentInvocation.methodCallId, SetEmailResponse.deserialize)) - .toList(); - - listEmailIdResult.addAll(_getListEmailIdUpdatedFormSetEmailResponse(listSetEmailResponse, moveRequest)); + final setEmailResponse = response.parse( + setEmailInvocation.methodCallId, + SetEmailResponse.deserialize, + ); - }).catchError((error) { - throw error; - }); + final listIdsUpdated = setEmailResponse?.updated?.keys ?? []; + if (listIdsUpdated.isNotEmpty == true) { + final listEmailIdsUpdated = listIdsUpdated.map((e) => EmailId(e)).toList(); + updatedEmailIds.addAll(listEmailIdsUpdated); + } } - - return listEmailIdResult; + return updatedEmailIds; } - List _getListEmailIdUpdatedFormSetEmailResponse(List listSetEmailResponse, MoveToMailboxRequest moveRequest) { - final listUpdated = listSetEmailResponse.map((e) => e!.updated!.keys).toList(); - List listEmailIdRequest = moveRequest.currentMailboxes.values.expand((e) => e).toList(); - return listEmailIdRequest.where((emailId) => listUpdated.expand((e) => e).toList().contains(emailId.id)).toList(); + int _getMaxObjectsInSetMethod(Session session, AccountId accountId) { + final coreCapability = session.getCapabilityProperties( + accountId, + CapabilityIdentifier.jmapCore, + ); + final maxObjectsInSetMethod = coreCapability?.maxObjectsInSet?.value.toInt() + ?? CapabilityIdentifierExtension.defaultMaxObjectsInSet; + + final minOfMaxObjectsInSetMethod = min( + maxObjectsInSetMethod, + CapabilityIdentifierExtension.defaultMaxObjectsInSet, + ); + log('EmailAPI::_getMaxObjectsInSetMethod:minOfMaxObjectsInSetMethod = $minOfMaxObjectsInSetMethod'); + return minOfMaxObjectsInSetMethod; } Future> markAsStar( @@ -454,37 +508,52 @@ class EmailAPI with HandleSetErrorMixin { List emailIds, MarkStarAction markStarAction ) async { - final setEmailMethod = SetEmailMethod(accountId) - ..addUpdates(emailIds.generateMapUpdateObjectMarkAsStar(markStarAction)); + final maxBatches = _getMaxObjectsInSetMethod(session, accountId); + final totalEmails = emails.length; - final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); + final List updatedEmails = List.empty(growable: true); - final setEmailInvocation = requestBuilder.invocation(setEmailMethod); + for (int start = 0; start < totalEmails; start += maxBatches) { + int end = (start + maxBatches < totalEmails) + ? start + maxBatches + : totalEmails; + log('EmailAPI::markAsStar:emails from ${start + 1} to $end'); - final capabilities = setEmailMethod.requiredCapabilities - .toCapabilitiesSupportTeamMailboxes(session, accountId); + final currentListEmails = emails.sublist(start, end); - final response = await (requestBuilder - ..usings(capabilities)) - .build() - .execute(); + final setEmailMethod = SetEmailMethod(accountId) + ..addUpdates(currentListEmails.listEmailIds.generateMapUpdateObjectMarkAsStar(markStarAction)); - final setEmailResponse = response.parse( - setEmailInvocation.methodCallId, - SetEmailResponse.deserialize, - ); + final getEmailMethod = GetEmailMethod(accountId) + ..addIds(emails.listEmailIds.toIds().toSet()) + ..addProperties(Properties({EmailProperty.keywords})); - final emailIdUpdated = setEmailResponse?.updated - ?.keys - .map((id) => EmailId(id)) - .toList() ?? []; - final mapErrors = handleSetResponse([setEmailResponse]); + final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); - if (emailIdUpdated.isNotEmpty) { - return emailIdUpdated; - } else { - throw SetMethodException(mapErrors); + requestBuilder.invocation(setEmailMethod); + + final getEmailInvocation = requestBuilder.invocation(getEmailMethod); + + final capabilities = setEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + + final response = await (requestBuilder + ..usings(capabilities)) + .build() + .execute(); + + final getEmailResponse = response.parse( + getEmailInvocation.methodCallId, + GetEmailResponse.deserialize, + ); + + final listEmails = getEmailResponse?.list ?? []; + if (listEmails.isNotEmpty) { + updatedEmails.addAll(listEmails); + } } + + return updatedEmails; } Future saveEmailAsDrafts( @@ -723,7 +792,7 @@ class EmailAPI with HandleSetErrorMixin { ..usings(emailRecoveryActionSetMethod.requiredCapabilities)) .build() .execute(); - + final emailRecoveryActionSetResponse = response.parse( emailRecoveryActionSetInvocation.methodCallId, SetEmailRecoveryActionResponse.deserialize @@ -739,12 +808,12 @@ class EmailAPI with HandleSetErrorMixin { final getEmailRecoveryActionMethod = GetEmailRecoveryActionMethod() ..addIds({emailRecoveryActionId.id}); final getEmailRecoveryActionInvocation = requestBuilder.invocation(getEmailRecoveryActionMethod); - + final response = await (requestBuilder ..usings(getEmailRecoveryActionMethod.requiredCapabilities)) .build() .execute(); - + final getEmailRecoveryActionResponse = response.parse( getEmailRecoveryActionInvocation.methodCallId, GetEmailRecoveryActionResponse.deserialize diff --git a/lib/features/email/domain/model/move_to_mailbox_request.dart b/lib/features/email/domain/model/move_to_mailbox_request.dart index aa958d274f..9f047b71cb 100644 --- a/lib/features/email/domain/model/move_to_mailbox_request.dart +++ b/lib/features/email/domain/model/move_to_mailbox_request.dart @@ -7,7 +7,7 @@ import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; class MoveToMailboxRequest with EquatableMixin { - final Map> currentMailboxes; + final Map> currentMailboxes; final MailboxId destinationMailboxId; final MoveAction moveAction; final EmailActionType emailActionType; @@ -25,6 +25,9 @@ class MoveToMailboxRequest with EquatableMixin { .values .fold(0, (sum, element) => sum + element.length); + bool get isMovingToSpam => moveAction == MoveAction.moving && + emailActionType == EmailActionType.moveToSpam; + @override List get props => [ currentMailboxes, diff --git a/lib/main/error/capability_validator.dart b/lib/main/error/capability_validator.dart index 5741f74be1..90cd99aa0a 100644 --- a/lib/main/error/capability_validator.dart +++ b/lib/main/error/capability_validator.dart @@ -42,6 +42,7 @@ extension ListCapabilityIdentifierExtension on List { extension CapabilityIdentifierExtension on CapabilityIdentifier { static const int defaultMaxCallsInRequest = 1; + static const int defaultMaxObjectsInSet = 50; bool isSupported(Session session, AccountId accountId) { try { From 8bd4fbdbc31edb77923d30e88bee22cfe825b8e0 Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 23 Dec 2024 19:17:30 +0700 Subject: [PATCH 43/72] TF-3337 Write unit test to verify number of times call request when performing mark as read/star and move emails --- .../email/data/network/email_api_test.dart | 689 ++++++++++++++++++ test/fixtures/session_fixtures.dart | 101 +++ 2 files changed, 790 insertions(+) create mode 100644 test/features/email/data/network/email_api_test.dart diff --git a/test/features/email/data/network/email_api_test.dart b/test/features/email/data/network/email_api_test.dart new file mode 100644 index 0000000000..d442bf9dad --- /dev/null +++ b/test/features/email/data/network/email_api_test.dart @@ -0,0 +1,689 @@ +import 'dart:math'; + +import 'package:core/data/network/dio_client.dart'; +import 'package:core/data/network/download/download_manager.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/http/http_client.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:model/email/mark_star_action.dart'; +import 'package:model/email/read_actions.dart'; +import 'package:model/extensions/account_id_extensions.dart'; +import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; +import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; +import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart'; +import 'package:uuid/uuid.dart'; + +import '../../../../fixtures/account_fixtures.dart'; +import '../../../../fixtures/session_fixtures.dart'; +import 'email_api_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), +]) +void main() { + group('EmailAPI::test', () { + late HttpClient httpClient; + late DownloadManager downloadManager; + late DioClient dioClient; + late Uuid uuid; + late EmailAPI emailApi; + + setUp(() { + httpClient = MockHttpClient(); + downloadManager = MockDownloadManager(); + dioClient = MockDioClient(); + uuid = MockUuid(); + + emailApi = EmailAPI( + httpClient, + downloadManager, + dioClient, + uuid, + ); + }); + + group('markAsRead::test', () { + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN maxObjectsInSet equal defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 50; + const totalEmails = 100; + final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "updated": { + for (var item in List.generate(maxBatches, (index) => {"email_$index": null})) + item.keys.first: item.values.first + } + }, + "c0" + ], + [ + "Email/get", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "state": "b965db40-c11c-11ef-9cfb-ef2eae0e64b1", + "list": List.generate(maxBatches, (index) => { + "id": "email_$index", + "keywords": { + "\$seen": true + } + }), + "notFound": [] + }, + "c1" + ] + ] + }); + + final emails = List.generate( + totalEmails, + (index) => Email(id: EmailId(Id('email_$index'))), + ); + + // Act + final result = await emailApi.markAsRead( + aliceSession, + AccountFixtures.aliceAccountId, + emails, + ReadActions.markAsRead, + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.length, totalEmails); + }); + + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN maxObjectsInSet is greater than defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 200; + const totalEmails = 100; + final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "updated": { + for (var item in List.generate(maxBatches, (index) => {"email_$index": null})) + item.keys.first: item.values.first + } + }, + "c0" + ], + [ + "Email/get", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "state": "b965db40-c11c-11ef-9cfb-ef2eae0e64b1", + "list": List.generate(maxBatches, (index) => { + "id": "email_$index", + "keywords": { + "\$seen": true + } + }), + "notFound": [] + }, + "c1" + ] + ] + }); + + final emails = List.generate( + totalEmails, + (index) => Email(id: EmailId(Id('email_$index'))), + ); + + // Act + final result = await emailApi.markAsRead( + aliceSession, + AccountFixtures.aliceAccountId, + emails, + ReadActions.markAsRead, + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.length, totalEmails); + }); + + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN maxObjectsInSet is less than defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 20; + const totalEmails = 100; + final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "updated": { + for (var item in List.generate(maxBatches, (index) => {"email_$index": null})) + item.keys.first: item.values.first + } + }, + "c0" + ], + [ + "Email/get", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "state": "b965db40-c11c-11ef-9cfb-ef2eae0e64b1", + "list": List.generate(maxBatches, (index) => { + "id": "email_$index", + "keywords": { + "\$seen": true + } + }), + "notFound": [] + }, + "c1" + ] + ] + }); + + final emails = List.generate( + totalEmails, + (index) => Email(id: EmailId(Id('email_$index'))), + ); + + // Act + final result = await emailApi.markAsRead( + aliceSession, + AccountFixtures.aliceAccountId, + emails, + ReadActions.markAsRead, + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.length, totalEmails); + }); + }); + + group('markAsStar::test', () { + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN maxObjectsInSet equal defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 50; + const totalEmails = 100; + final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "updated": { + for (var item in List.generate(maxBatches, (index) => {"email_$index": null})) + item.keys.first: item.values.first + } + }, + "c0" + ], + [ + "Email/get", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "state": "b965db40-c11c-11ef-9cfb-ef2eae0e64b1", + "list": List.generate(maxBatches, (index) => { + "id": "email_$index", + "keywords": { + "\$flagged": true + } + }), + "notFound": [] + }, + "c1" + ] + ] + }); + + final emails = List.generate( + totalEmails, + (index) => Email(id: EmailId(Id('email_$index'))), + ); + + // Act + final result = await emailApi.markAsStar( + aliceSession, + AccountFixtures.aliceAccountId, + emails, + MarkStarAction.markStar, + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.length, totalEmails); + }); + + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN maxObjectsInSet is greater than defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 200; + const totalEmails = 100; + final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "updated": { + for (var item in List.generate(maxBatches, (index) => {"email_$index": null})) + item.keys.first: item.values.first + } + }, + "c0" + ], + [ + "Email/get", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "state": "b965db40-c11c-11ef-9cfb-ef2eae0e64b1", + "list": List.generate(maxBatches, (index) => { + "id": "email_$index", + "keywords": { + "\$flagged": true + } + }), + "notFound": [] + }, + "c1" + ] + ] + }); + + final emails = List.generate( + totalEmails, + (index) => Email(id: EmailId(Id('email_$index'))), + ); + + // Act + final result = await emailApi.markAsStar( + aliceSession, + AccountFixtures.aliceAccountId, + emails, + MarkStarAction.markStar, + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.length, totalEmails); + }); + + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN maxObjectsInSet is less than defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 20; + const totalEmails = 100; + final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "updated": { + for (var item in List.generate(maxBatches, (index) => {"email_$index": null})) + item.keys.first: item.values.first + } + }, + "c0" + ], + [ + "Email/get", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "state": "b965db40-c11c-11ef-9cfb-ef2eae0e64b1", + "list": List.generate(maxBatches, (index) => { + "id": "email_$index", + "keywords": { + "\$flagged": true + } + }), + "notFound": [] + }, + "c1" + ] + ] + }); + + final emails = List.generate( + totalEmails, + (index) => Email(id: EmailId(Id('email_$index'))), + ); + + // Act + final result = await emailApi.markAsStar( + aliceSession, + AccountFixtures.aliceAccountId, + emails, + MarkStarAction.markStar, + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.length, totalEmails); + }); + }); + + group('moveToMailbox::test', () { + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN maxObjectsInSet equal defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 50; + const totalEmails = 100; + final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "updated": { + for (var item in List.generate(maxBatches, (index) => {"email_$index": null})) + item.keys.first: item.values.first + } + }, + "c0" + ] + ] + }); + + final emailIds = List.generate( + totalEmails, + (index) => EmailId(Id('email_$index')), + ); + + // Act + final result = await emailApi.moveToMailbox( + aliceSession, + AccountFixtures.aliceAccountId, + MoveToMailboxRequest( + { + MailboxId(Id('mailboxA')): emailIds, + }, + MailboxId(Id('mailboxB')), + MoveAction.moving, + EmailActionType.moveToMailbox, + ), + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.length, totalEmails); + }); + + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN maxObjectsInSet is greater than defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 200; + const totalEmails = 100; + final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "updated": { + for (var item in List.generate(maxBatches, (index) => {"email_$index": null})) + item.keys.first: item.values.first + } + }, + "c0" + ] + ] + }); + + final emailIds = List.generate( + totalEmails, + (index) => EmailId(Id('email_$index')), + ); + + // Act + final result = await emailApi.moveToMailbox( + aliceSession, + AccountFixtures.aliceAccountId, + MoveToMailboxRequest( + { + MailboxId(Id('mailboxA')): emailIds, + }, + MailboxId(Id('mailboxB')), + MoveAction.moving, + EmailActionType.moveToMailbox, + ), + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.length, totalEmails); + }); + + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN maxObjectsInSet is less than defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 20; + const totalEmails = 100; + final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "updated": { + for (var item in List.generate(maxBatches, (index) => {"email_$index": null})) + item.keys.first: item.values.first + } + }, + "c0" + ] + ] + }); + + final emailIds = List.generate( + totalEmails, + (index) => EmailId(Id('email_$index')), + ); + + // Act + final result = await emailApi.moveToMailbox( + aliceSession, + AccountFixtures.aliceAccountId, + MoveToMailboxRequest( + { + MailboxId(Id('mailboxA')): emailIds, + }, + MailboxId(Id('mailboxB')), + MoveAction.moving, + EmailActionType.moveToMailbox, + ), + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.length, totalEmails); + }); + }); + }); +} diff --git a/test/fixtures/session_fixtures.dart b/test/fixtures/session_fixtures.dart index b416492f7b..7364bf02a3 100644 --- a/test/fixtures/session_fixtures.dart +++ b/test/fixtures/session_fixtures.dart @@ -101,4 +101,105 @@ class SessionFixtures { Uri.parse('http://domain.com/eventSource?types={types}&closeAfter={closeafter}&ping={ping}'), State('2c9f1b12-b35a-43e6-9af2-0106fb53a943') ); + + static getAliceSessionWithMaxObjectsInSet(int maxObjectsInSet) => Session( + { + CapabilityIdentifier.jmapSubmission: SubmissionCapability( + maxDelayedSend: UnsignedInt(0), submissionExtensions: {}), + CapabilityIdentifier.jmapCore: CoreCapability( + maxSizeUpload: UnsignedInt(20971520), + maxConcurrentUpload: UnsignedInt(4), + maxSizeRequest: UnsignedInt(10000000), + maxConcurrentRequests: UnsignedInt(4), + maxCallsInRequest: UnsignedInt(16), + maxObjectsInGet: UnsignedInt(500), + maxObjectsInSet: UnsignedInt(maxObjectsInSet), + collationAlgorithms: {CollationIdentifier("i;unicode-casemap")}), + CapabilityIdentifier.jmapMail: MailCapability( + maxMailboxesPerEmail: UnsignedInt(10000000), + maxSizeAttachmentsPerEmail: UnsignedInt(20000000), + emailQuerySortOptions: { + "receivedAt", + "sentAt", + "size", + "from", + "to", + "subject" + }, + mayCreateTopLevelMailbox: true), + CapabilityIdentifier.jmapWebSocket: WebSocketCapability( + supportsPush: true, url: Uri.parse('ws://domain.com/jmap/ws')), + CapabilityIdentifier( + Uri.parse('urn:apache:james:params:jmap:mail:quota')): + DefaultCapability({}), + CapabilityIdentifier( + Uri.parse('urn:apache:james:params:jmap:mail:shares')): + DefaultCapability({}), + CapabilityIdentifier.jmapVacationResponse: VacationCapability(), + CapabilityIdentifier.jmapMdn: MdnCapability(), + }, + { + AccountFixtures.aliceAccountId: + Account(AccountName('alice@domain.tld'), true, false, { + CapabilityIdentifier.jmapSubmission: SubmissionCapability( + maxDelayedSend: UnsignedInt(0), submissionExtensions: {}), + CapabilityIdentifier.jmapWebSocket: WebSocketCapability( + supportsPush: true, url: Uri.parse('ws://domain.com/jmap/ws')), + CapabilityIdentifier.jmapCore: CoreCapability( + maxSizeUpload: UnsignedInt(20971520), + maxConcurrentUpload: UnsignedInt(4), + maxSizeRequest: UnsignedInt(10000000), + maxConcurrentRequests: UnsignedInt(4), + maxCallsInRequest: UnsignedInt(16), + maxObjectsInGet: UnsignedInt(500), + maxObjectsInSet: UnsignedInt(maxObjectsInSet), + collationAlgorithms: { + CollationIdentifier("i;unicode-casemap") + }), + CapabilityIdentifier.jmapMail: MailCapability( + maxMailboxesPerEmail: UnsignedInt(10000000), + maxSizeAttachmentsPerEmail: UnsignedInt(20000000), + emailQuerySortOptions: { + "receivedAt", + "sentAt", + "size", + "from", + "to", + "subject" + }, + mayCreateTopLevelMailbox: true), + CapabilityIdentifier( + Uri.parse('urn:apache:james:params:jmap:mail:quota')): + DefaultCapability({}), + CapabilityIdentifier( + Uri.parse('urn:apache:james:params:jmap:mail:shares')): + DefaultCapability({}), + CapabilityIdentifier.jmapVacationResponse: VacationCapability(), + CapabilityIdentifier.jmapMdn: MdnCapability() + }) + }, + { + CapabilityIdentifier.jmapSubmission: AccountFixtures.aliceAccountId, + CapabilityIdentifier.jmapWebSocket: AccountFixtures.aliceAccountId, + CapabilityIdentifier.jmapCore: AccountFixtures.aliceAccountId, + CapabilityIdentifier.jmapMail: AccountFixtures.aliceAccountId, + CapabilityIdentifier( + Uri.parse('urn:apache:james:params:jmap:mail:quota')): + AccountFixtures.aliceAccountId, + CapabilityIdentifier( + Uri.parse('urn:apache:james:params:jmap:mail:shares')): + AccountFixtures.aliceAccountId, + CapabilityIdentifier.jmapVacationResponse: + AccountFixtures.aliceAccountId, + CapabilityIdentifier.jmapMdn: AccountFixtures.aliceAccountId, + }, + UserName('alice@domain.tld'), + Uri.parse('http://domain.com/jmap'), + Uri.parse( + 'http://domain.com/download/{accountId}/{blobId}/?type={type}&name={name}'), + Uri.parse('http://domain.com/upload/{accountId}'), + Uri.parse( + 'http://domain.com/eventSource?types={types}&closeAfter={closeafter}&ping={ping}'), + State('2c9f1b12-b35a-43e6-9af2-0106fb53a943'), + ); } \ No newline at end of file From e523ef1762d54190c46e698db37c2d9668cc2681 Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 2 Jan 2025 13:58:30 +0700 Subject: [PATCH 44/72] TF-3370 Handle SetError when make email action (Mark as read/star/move/delete) --- .../data/datasource/email_datasource.dart | 31 +- .../email_datasource_impl.dart | 36 +- .../email_hive_cache_datasource_impl.dart | 35 +- .../email/data/network/email_api.dart | 213 ++++--- .../repository/email_repository_impl.dart | 42 +- .../domain/repository/email_repository.dart | 30 +- ...ultiple_emails_permanently_interactor.dart | 8 +- .../mark_as_email_read_interactor.dart | 14 +- .../usecases/move_to_mailbox_interactor.dart | 4 +- .../data/network/mailbox_isolate_worker.dart | 8 +- .../data/network/thread_isolate_worker.dart | 8 +- ...ark_as_multiple_email_read_interactor.dart | 8 +- ...ark_as_star_multiple_email_interactor.dart | 6 +- ..._multiple_email_to_mailbox_interactor.dart | 8 +- model/lib/extensions/list_id_extension.dart | 6 + model/lib/model.dart | 1 + .../email/data/network/email_api_test.dart | 523 ++++++++++++++++-- 17 files changed, 800 insertions(+), 181 deletions(-) create mode 100644 model/lib/extensions/list_id_extension.dart diff --git a/lib/features/email/data/datasource/email_datasource.dart b/lib/features/email/data/datasource/email_datasource.dart index bdfd90afe7..9d9e55b372 100644 --- a/lib/features/email/data/datasource/email_datasource.dart +++ b/lib/features/email/data/datasource/email_datasource.dart @@ -9,6 +9,8 @@ import 'package:dio/dio.dart'; import 'package:email_recovery/email_recovery/email_recovery_action.dart'; import 'package:email_recovery/email_recovery/email_recovery_action_id.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/error/set_error.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; @@ -43,7 +45,10 @@ abstract class EmailDataSource { } ); - Future> markAsRead( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsRead( Session session, AccountId accountId, List emailIds, @@ -75,9 +80,19 @@ abstract class EmailDataSource { {CancelToken? cancelToken} ); - Future> moveToMailbox(Session session, AccountId accountId, MoveToMailboxRequest moveRequest); + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> moveToMailbox( + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest, + ); - Future> markAsStar( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsStar( Session session, AccountId accountId, List emailIds, @@ -106,8 +121,14 @@ abstract class EmailDataSource { {CancelToken? cancelToken} ); - Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds); - + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> deleteMultipleEmailsPermanently( + Session session, + AccountId accountId, + List emailIds, + ); Future deleteEmailPermanently( Session session, AccountId accountId, diff --git a/lib/features/email/data/datasource_impl/email_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_datasource_impl.dart index 2354d2c415..dcbf0d0814 100644 --- a/lib/features/email/data/datasource_impl/email_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_datasource_impl.dart @@ -10,6 +10,8 @@ import 'package:email_recovery/email_recovery/email_recovery_action.dart'; import 'package:email_recovery/email_recovery/email_recovery_action_id.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/error/set_error.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; @@ -75,7 +77,10 @@ class EmailDataSourceImpl extends EmailDataSource { } @override - Future> markAsRead( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsRead( Session session, AccountId accountId, List emailIds, @@ -112,14 +117,24 @@ class EmailDataSourceImpl extends EmailDataSource { } @override - Future> moveToMailbox(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> moveToMailbox( + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest, + ) { return Future.sync(() async { return await emailAPI.moveToMailbox(session, accountId, moveRequest); }).catchError(_exceptionThrower.throwException); } @override - Future> markAsStar( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsStar( Session session, AccountId accountId, List emailIds, @@ -211,9 +226,20 @@ class EmailDataSourceImpl extends EmailDataSource { } @override - Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds) { + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> deleteMultipleEmailsPermanently( + Session session, + AccountId accountId, + List emailIds, + ) { return Future.sync(() async { - return await emailAPI.deleteMultipleEmailsPermanently(session, accountId, emailIds); + return await emailAPI.deleteMultipleEmailsPermanently( + session, + accountId, + emailIds, + ); }).catchError(_exceptionThrower.throwException); } diff --git a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart index 85df8381d5..9e96e02936 100644 --- a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart @@ -12,6 +12,8 @@ import 'package:dio/dio.dart'; import 'package:email_recovery/email_recovery/email_recovery_action.dart'; import 'package:email_recovery/email_recovery/email_recovery_action_id.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/error/set_error.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; @@ -82,7 +84,14 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { } @override - Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds) { + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> deleteMultipleEmailsPermanently( + Session session, + AccountId accountId, + List emailIds, + ) { throw UnimplementedError(); } @@ -120,7 +129,10 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { } @override - Future> markAsRead( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsRead( Session session, AccountId accountId, List emailIds, @@ -130,12 +142,27 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { } @override - Future> markAsStar(Session session, AccountId accountId, List emailIds, MarkStarAction markStarAction) { + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsStar( + Session session, + AccountId accountId, + List emailIds, + MarkStarAction markStarAction, + ) { throw UnimplementedError(); } @override - Future> moveToMailbox(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> moveToMailbox( + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest, + ) { throw UnimplementedError(); } diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index 44e7b87ba2..0fac20749f 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -18,6 +18,7 @@ import 'package:jmap_dart_client/http/http_client.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/core_capability.dart'; +import 'package:jmap_dart_client/jmap/core/error/set_error.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/patch_object.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; @@ -44,13 +45,13 @@ import 'package:model/account/account_request.dart'; import 'package:model/account/authentication_type.dart'; import 'package:model/download/download_task_id.dart'; import 'package:model/email/attachment.dart'; -import 'package:model/email/email_property.dart'; import 'package:model/email/mark_star_action.dart'; import 'package:model/email/read_actions.dart'; import 'package:model/extensions/email_extension.dart'; import 'package:model/extensions/email_id_extensions.dart'; import 'package:model/extensions/keyword_identifier_extension.dart'; import 'package:model/extensions/list_email_id_extension.dart'; +import 'package:model/extensions/list_id_extension.dart'; import 'package:model/extensions/mailbox_id_extension.dart'; import 'package:model/extensions/session_extension.dart'; import 'package:model/oidc/token_oidc.dart'; @@ -237,16 +238,21 @@ class EmailAPI with HandleSetErrorMixin { } } - Future> markAsRead( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsRead( Session session, AccountId accountId, List emailIds, ReadActions readActions, ) async { - final maxBatches = _getMaxObjectsInSetMethod(session, accountId); - final totalEmails = emails.length; + final maxObjects = _getMaxObjectsInSetMethod(session, accountId); + final totalEmails = emailIds.length; + final maxBatches = min(totalEmails, maxObjects); - final List updatedEmails = List.empty(growable: true); + final List updatedEmailIds = List.empty(growable: true); + final Map mapErrors = {}; for (int start = 0; start < totalEmails; start += maxBatches) { int end = (start + maxBatches < totalEmails) @@ -254,20 +260,16 @@ class EmailAPI with HandleSetErrorMixin { : totalEmails; log('EmailAPI::markAsRead:emails from ${start + 1} to $end'); - final currentListEmails = emails.sublist(start, end); + final currentListEmailIds = emailIds.sublist(start, end); final setEmailMethod = SetEmailMethod(accountId) - ..addUpdates(currentListEmails.listEmailIds.generateMapUpdateObjectMarkAsRead(readActions)); - - final getEmailMethod = GetEmailMethod(accountId) - ..addIds(emails.listEmailIds.toIds().toSet()) - ..addProperties(Properties({EmailProperty.keywords})); + ..addUpdates( + currentListEmailIds.generateMapUpdateObjectMarkAsRead(readActions) + ); final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); - requestBuilder.invocation(setEmailMethod); - - final getEmailInvocation = requestBuilder.invocation(getEmailMethod); + final setEmailInvocation = requestBuilder.invocation(setEmailMethod); final capabilities = setEmailMethod.requiredCapabilities .toCapabilitiesSupportTeamMailboxes(session, accountId); @@ -277,18 +279,19 @@ class EmailAPI with HandleSetErrorMixin { .build() .execute(); - final getEmailResponse = response.parse( - getEmailInvocation.methodCallId, - GetEmailResponse.deserialize, + final setEmailResponse = response.parse( + setEmailInvocation.methodCallId, + SetEmailResponse.deserialize, ); - final listEmails = getEmailResponse?.list ?? []; - if (listEmails.isNotEmpty) { - updatedEmails.addAll(listEmails); - } + final listEmailIds = setEmailResponse?.updated?.keys.toEmailIds() ?? []; + final mapErrors = handleSetResponse([setEmailResponse]); + + updatedEmailIds.addAll(listEmailIds); + mapErrors.addAll(mapErrors); } - return updatedEmails; + return (emailIdsSuccess: updatedEmailIds, mapErrors: mapErrors); } Future> downloadAttachments( @@ -397,47 +400,55 @@ class EmailAPI with HandleSetErrorMixin { return bytesDownloaded; } - Future> moveToMailbox( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> moveToMailbox( Session session, AccountId accountId, MoveToMailboxRequest moveRequest ) async { - final maxBatches = _getMaxObjectsInSetMethod(session, accountId); - final List listEmailIdResult = List.empty(growable: true); + final Map mapErrors = {}; final listMailboxIds = moveRequest.currentMailboxes.keys.toList(); for (int i = 0; i < listMailboxIds.length; i++) { final currentMailboxId = listMailboxIds[i]; final listEmailIds = moveRequest.currentMailboxes[currentMailboxId]!; log('EmailAPI::moveToMailbox:from mailbox ${currentMailboxId.asString} with ${listEmailIds.length} emails to mailbox ${moveRequest.destinationMailboxId.asString}'); - final movedEmailIds = await _moveEmailsBetweenMailboxes( + final resultRecords = await _moveEmailsBetweenMailboxes( session: session, accountId: accountId, - listEmailIds: listEmailIds, + emailIds: listEmailIds, currentMailboxId: currentMailboxId, - maxBatches: maxBatches, destinationMailboxId: moveRequest.destinationMailboxId, isMovingToSpam: moveRequest.isMovingToSpam, ); - listEmailIdResult.addAll(movedEmailIds); + + listEmailIdResult.addAll(resultRecords.emailIdsSuccess); + mapErrors.addAll(resultRecords.mapErrors); } - return listEmailIdResult; + + return (emailIdsSuccess: listEmailIdResult, mapErrors: mapErrors); } - Future> _moveEmailsBetweenMailboxes({ + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> _moveEmailsBetweenMailboxes({ required Session session, required AccountId accountId, - required List listEmailIds, + required List emailIds, required MailboxId currentMailboxId, required MailboxId destinationMailboxId, - required int maxBatches, bool isMovingToSpam = false, }) async { - final maxBatches = _getMaxObjectsInSetMethod(session, accountId); - final totalEmails = listEmailIds.length; + final maxObjects = _getMaxObjectsInSetMethod(session, accountId); + final totalEmails = emailIds.length; + final maxBatches = min(totalEmails, maxObjects); final List updatedEmailIds = List.empty(growable: true); + final Map mapErrors = {}; for (int start = 0; start < totalEmails; start += maxBatches) { int end = (start + maxBatches < totalEmails) @@ -445,7 +456,7 @@ class EmailAPI with HandleSetErrorMixin { : totalEmails; log('EmailAPI::_moveEmailsBetweenMailboxes:emails from ${start + 1} to $end'); - final currentEmailIds = listEmailIds.sublist(start, end); + final currentEmailIds = emailIds.sublist(start, end); final moveProperties = isMovingToSpam ? currentEmailIds.generateMapUpdateObjectMoveToSpam( @@ -477,13 +488,14 @@ class EmailAPI with HandleSetErrorMixin { SetEmailResponse.deserialize, ); - final listIdsUpdated = setEmailResponse?.updated?.keys ?? []; - if (listIdsUpdated.isNotEmpty == true) { - final listEmailIdsUpdated = listIdsUpdated.map((e) => EmailId(e)).toList(); - updatedEmailIds.addAll(listEmailIdsUpdated); - } + final listEmailIds = setEmailResponse?.updated?.keys.toEmailIds() ?? []; + final mapErrors = handleSetResponse([setEmailResponse]); + + updatedEmailIds.addAll(listEmailIds); + mapErrors.addAll(mapErrors); } - return updatedEmailIds; + + return (emailIdsSuccess: updatedEmailIds, mapErrors: mapErrors); } int _getMaxObjectsInSetMethod(Session session, AccountId accountId) { @@ -502,16 +514,21 @@ class EmailAPI with HandleSetErrorMixin { return minOfMaxObjectsInSetMethod; } - Future> markAsStar( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsStar( Session session, AccountId accountId, List emailIds, MarkStarAction markStarAction ) async { - final maxBatches = _getMaxObjectsInSetMethod(session, accountId); - final totalEmails = emails.length; + final maxObjects = _getMaxObjectsInSetMethod(session, accountId); + final totalEmails = emailIds.length; + final maxBatches = min(totalEmails, maxObjects); - final List updatedEmails = List.empty(growable: true); + final List updatedEmailIds = List.empty(growable: true); + final Map mapErrors = {}; for (int start = 0; start < totalEmails; start += maxBatches) { int end = (start + maxBatches < totalEmails) @@ -519,20 +536,16 @@ class EmailAPI with HandleSetErrorMixin { : totalEmails; log('EmailAPI::markAsStar:emails from ${start + 1} to $end'); - final currentListEmails = emails.sublist(start, end); + final currentListEmailIds = emailIds.sublist(start, end); final setEmailMethod = SetEmailMethod(accountId) - ..addUpdates(currentListEmails.listEmailIds.generateMapUpdateObjectMarkAsStar(markStarAction)); - - final getEmailMethod = GetEmailMethod(accountId) - ..addIds(emails.listEmailIds.toIds().toSet()) - ..addProperties(Properties({EmailProperty.keywords})); + ..addUpdates( + currentListEmailIds.generateMapUpdateObjectMarkAsStar(markStarAction), + ); final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); - requestBuilder.invocation(setEmailMethod); - - final getEmailInvocation = requestBuilder.invocation(getEmailMethod); + final setEmailInvocation = requestBuilder.invocation(setEmailMethod); final capabilities = setEmailMethod.requiredCapabilities .toCapabilitiesSupportTeamMailboxes(session, accountId); @@ -542,18 +555,19 @@ class EmailAPI with HandleSetErrorMixin { .build() .execute(); - final getEmailResponse = response.parse( - getEmailInvocation.methodCallId, - GetEmailResponse.deserialize, + final setEmailResponse = response.parse( + setEmailInvocation.methodCallId, + SetEmailResponse.deserialize, ); - final listEmails = getEmailResponse?.list ?? []; - if (listEmails.isNotEmpty) { - updatedEmails.addAll(listEmails); - } + final listEmailIds = setEmailResponse?.updated?.keys.toEmailIds() ?? []; + final mapErrors = handleSetResponse([setEmailResponse]); + + updatedEmailIds.addAll(listEmailIds); + mapErrors.addAll(mapErrors); } - return updatedEmails; + return (emailIdsSuccess: updatedEmailIds, mapErrors: mapErrors); } Future saveEmailAsDrafts( @@ -656,36 +670,56 @@ class EmailAPI with HandleSetErrorMixin { return emailCreated; } - Future> deleteMultipleEmailsPermanently( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> deleteMultipleEmailsPermanently( Session session, AccountId accountId, List emailIds ) async { - final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); - final setEmailMethod = SetEmailMethod(accountId) - ..addDestroy(emailIds.map((emailId) => emailId.id).toSet()); + final maxObjects = _getMaxObjectsInSetMethod(session, accountId); + final totalEmails = emailIds.length; + final maxBatches = min(totalEmails, maxObjects); - final setEmailInvocation = requestBuilder.invocation(setEmailMethod); + final List destroyedEmailIds = List.empty(growable: true); + final Map mapErrors = {}; - final capabilities = setEmailMethod.requiredCapabilities - .toCapabilitiesSupportTeamMailboxes(session, accountId); + for (int start = 0; start < totalEmails; start += maxBatches) { + int end = (start + maxBatches < totalEmails) + ? start + maxBatches + : totalEmails; + log('EmailAPI::deleteMultipleEmailsPermanently:emails from ${start + 1} to $end'); - final response = await (requestBuilder - ..usings(capabilities)) - .build() - .execute(); + final currentListEmailIds = emailIds.sublist(start, end); - final setEmailResponse = response.parse( + final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); + final setEmailMethod = SetEmailMethod(accountId) + ..addDestroy(currentListEmailIds.toIds().toSet()); + + final setEmailInvocation = requestBuilder.invocation(setEmailMethod); + + final capabilities = setEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + + final response = await (requestBuilder + ..usings(capabilities)) + .build() + .execute(); + + final setEmailResponse = response.parse( setEmailInvocation.methodCallId, - SetEmailResponse.deserialize); + SetEmailResponse.deserialize, + ); - final listIdResult = setEmailResponse?.destroyed; + final listEmailIds = setEmailResponse?.destroyed?.toEmailIds() ?? []; + final mapErrors = handleSetResponse([setEmailResponse]); - if (listIdResult != null) { - return listIdResult.map((id) => EmailId(id)).toList(); + destroyedEmailIds.addAll(listEmailIds); + mapErrors.addAll(mapErrors); } - return List.empty(); + return (emailIdsSuccess: destroyedEmailIds, mapErrors: mapErrors); } Future deleteEmailPermanently( @@ -698,19 +732,20 @@ class EmailAPI with HandleSetErrorMixin { final setEmailMethod = SetEmailMethod(accountId) ..addDestroy({emailId.id}); - final setEmailInvocation = requestBuilder.invocation(setEmailMethod); + final setEmailInvocation = requestBuilder.invocation(setEmailMethod); - final capabilities = setEmailMethod.requiredCapabilities - .toCapabilitiesSupportTeamMailboxes(session, accountId); + final capabilities = setEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); final response = await (requestBuilder ..usings(capabilities)) .build() .execute(cancelToken: cancelToken); - final setEmailResponse = response.parse( + final setEmailResponse = response.parse( setEmailInvocation.methodCallId, - SetEmailResponse.deserialize); + SetEmailResponse.deserialize, + ); return setEmailResponse?.destroyed?.contains(emailId.id) == true; } @@ -757,10 +792,10 @@ class EmailAPI with HandleSetErrorMixin { final capabilities = setEmailMethod.requiredCapabilities.toCapabilitiesSupportTeamMailboxes(session, accountId); - final response = await (requestBuilder - ..usings(capabilities)) - .build() - .execute(); + final response = await (requestBuilder + ..usings(capabilities)) + .build() + .execute(); final setEmailResponse = response.parse( setEmailInvocation.methodCallId, diff --git a/lib/features/email/data/repository/email_repository_impl.dart b/lib/features/email/data/repository/email_repository_impl.dart index c48b4c3dfe..c849e3b22c 100644 --- a/lib/features/email/data/repository/email_repository_impl.dart +++ b/lib/features/email/data/repository/email_repository_impl.dart @@ -11,6 +11,8 @@ import 'package:dio/dio.dart'; import 'package:email_recovery/email_recovery/email_recovery_action.dart'; import 'package:email_recovery/email_recovery/email_recovery_action_id.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/error/set_error.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; @@ -83,7 +85,10 @@ class EmailRepositoryImpl extends EmailRepository { } @override - Future> markAsRead( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsRead( Session session, AccountId accountId, List emailIds, @@ -124,12 +129,26 @@ class EmailRepositoryImpl extends EmailRepository { } @override - Future> moveToMailbox(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { - return emailDataSource[DataSourceType.network]!.moveToMailbox(session, accountId, moveRequest); + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> moveToMailbox( + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest, + ) { + return emailDataSource[DataSourceType.network]!.moveToMailbox( + session, + accountId, + moveRequest, + ); } @override - Future> markAsStar( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsStar( Session session, AccountId accountId, List emailIds, @@ -228,8 +247,19 @@ class EmailRepositoryImpl extends EmailRepository { } @override - Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds) { - return emailDataSource[DataSourceType.network]!.deleteMultipleEmailsPermanently(session, accountId, emailIds); + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> deleteMultipleEmailsPermanently( + Session session, + AccountId accountId, + List emailIds, + ) { + return emailDataSource[DataSourceType.network]!.deleteMultipleEmailsPermanently( + session, + accountId, + emailIds, + ); } @override diff --git a/lib/features/email/domain/repository/email_repository.dart b/lib/features/email/domain/repository/email_repository.dart index 7aaf27b9d7..d1211c6b91 100644 --- a/lib/features/email/domain/repository/email_repository.dart +++ b/lib/features/email/domain/repository/email_repository.dart @@ -10,6 +10,8 @@ import 'package:dio/dio.dart'; import 'package:email_recovery/email_recovery/email_recovery_action.dart'; import 'package:email_recovery/email_recovery/email_recovery_action_id.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/error/set_error.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; @@ -45,7 +47,10 @@ abstract class EmailRepository { } ); - Future> markAsRead( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsRead( Session session, AccountId accountId, List emailIds, @@ -77,9 +82,19 @@ abstract class EmailRepository { {CancelToken? cancelToken} ); - Future> moveToMailbox(Session session, AccountId accountId, MoveToMailboxRequest moveRequest); + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> moveToMailbox( + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest, + ); - Future> markAsStar( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsStar( Session session, AccountId accountId, List emailIds, @@ -114,7 +129,14 @@ abstract class EmailRepository { {CancelToken? cancelToken} ); - Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds); + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> deleteMultipleEmailsPermanently( + Session session, + AccountId accountId, + List emailIds, + ); Future deleteEmailPermanently( Session session, diff --git a/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart b/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart index 685933fd57..b8195c8e60 100644 --- a/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart +++ b/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart @@ -16,10 +16,10 @@ class DeleteMultipleEmailsPermanentlyInteractor { try { yield Right(LoadingDeleteMultipleEmailsPermanentlyAll()); final listResult = await _emailRepository.deleteMultipleEmailsPermanently(session, accountId, emailIds); - if (listResult.length == emailIds.length) { - yield Right(DeleteMultipleEmailsPermanentlyAllSuccess(listResult)); - } else if (listResult.isNotEmpty) { - yield Right(DeleteMultipleEmailsPermanentlyHasSomeEmailFailure(listResult)); + if (listResult.emailIdsSuccess.length == emailIds.length) { + yield Right(DeleteMultipleEmailsPermanentlyAllSuccess(listResult.emailIdsSuccess)); + } else if (listResult.emailIdsSuccess.isNotEmpty) { + yield Right(DeleteMultipleEmailsPermanentlyHasSomeEmailFailure(listResult.emailIdsSuccess)); } else { yield Left(DeleteMultipleEmailsPermanentlyAllFailure()); } diff --git a/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart b/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart index 7db473e033..5f5659bc66 100644 --- a/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart +++ b/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart @@ -29,11 +29,15 @@ class MarkAsEmailReadInteractor { readAction, ); - yield Right(MarkAsEmailReadSuccess( - result.first, - readAction, - markReadAction, - )); + if (result.emailIdsSuccess.isEmpty) { + yield Left(MarkAsEmailReadFailure(readAction)); + } else { + yield Right(MarkAsEmailReadSuccess( + result.emailIdsSuccess.first, + readAction, + markReadAction, + )); + } } catch (e) { yield Left(MarkAsEmailReadFailure(readAction, exception: e)); } diff --git a/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart b/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart index eebdebd2e6..0303c36483 100644 --- a/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart +++ b/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart @@ -16,9 +16,9 @@ class MoveToMailboxInteractor { try { yield Right(LoadingMoveToMailbox()); final result = await _emailRepository.moveToMailbox(session, accountId, moveRequest); - if (result.isNotEmpty) { + if (result.emailIdsSuccess.isNotEmpty) { yield Right(MoveToMailboxSuccess( - result.first, + result.emailIdsSuccess.first, moveRequest.currentMailboxes.keys.first, moveRequest.destinationMailboxId, moveRequest.moveAction, diff --git a/lib/features/mailbox/data/network/mailbox_isolate_worker.dart b/lib/features/mailbox/data/network/mailbox_isolate_worker.dart index 09468ccd09..376757f7ac 100644 --- a/lib/features/mailbox/data/network/mailbox_isolate_worker.dart +++ b/lib/features/mailbox/data/network/mailbox_isolate_worker.dart @@ -138,8 +138,8 @@ class MailboxIsolateWorker { listEmailUnread.listEmailIds, ReadActions.markAsRead); - log('MailboxIsolateWorker::_handleMarkAsMailboxRead(): MARK_READ: ${result.length}'); - emailIdsCompleted.addAll(result); + log('MailboxIsolateWorker::_handleMarkAsMailboxRead(): MARK_READ: ${result.emailIdsSuccess.length}'); + emailIdsCompleted.addAll(result.emailIdsSuccess); sendPort.send(emailIdsCompleted); } } @@ -206,8 +206,8 @@ class MailboxIsolateWorker { listEmailUnread.listEmailIds, ReadActions.markAsRead, ); - log('MailboxIsolateWorker::_handleMarkAsMailboxReadActionOnWeb(): MARK_READ: ${result.length}'); - emailIdsCompleted.addAll(result); + log('MailboxIsolateWorker::_handleMarkAsMailboxReadActionOnWeb(): MARK_READ: ${result.emailIdsSuccess.length}'); + emailIdsCompleted.addAll(result.emailIdsSuccess); onProgressController.add(Right(UpdatingMarkAsMailboxReadState( mailboxId: mailboxId, diff --git a/lib/features/thread/data/network/thread_isolate_worker.dart b/lib/features/thread/data/network/thread_isolate_worker.dart index 03ffe5112d..f95dba31e6 100644 --- a/lib/features/thread/data/network/thread_isolate_worker.dart +++ b/lib/features/thread/data/network/thread_isolate_worker.dart @@ -100,7 +100,7 @@ class ThreadIsolateWorker { ..setIsAscending(false)), filter: EmailFilterCondition(inMailbox: args.mailboxId, before: lastEmail?.receivedAt), properties: Properties({ - EmailProperty.id, + EmailProperty.id, EmailProperty.receivedAt }), ); @@ -119,7 +119,7 @@ class ThreadIsolateWorker { args.session, args.accountId, newEmailList.listEmailIds); - emailListCompleted.addAll(listEmailIdDeleted); + emailListCompleted.addAll(listEmailIdDeleted.emailIdsSuccess); sendPort.send(emailListCompleted); } else { hasEmails = false; @@ -154,7 +154,7 @@ class ThreadIsolateWorker { ..setIsAscending(false)), filter: EmailFilterCondition(inMailbox: mailboxId, before: lastEmail?.receivedAt), properties: Properties({ - EmailProperty.id, + EmailProperty.id, EmailProperty.receivedAt }), ); @@ -173,7 +173,7 @@ class ThreadIsolateWorker { session, accountId, newEmailList.listEmailIds); - emailListCompleted.addAll(listEmailIdDeleted); + emailListCompleted.addAll(listEmailIdDeleted.emailIdsSuccess); onProgressController.add(Right(EmptyingFolderState( mailboxId, emailListCompleted.length, totalEmails diff --git a/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart b/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart index 08779f9641..67299d960e 100644 --- a/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart +++ b/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart @@ -29,16 +29,16 @@ class MarkAsMultipleEmailReadInteractor { readAction, ); - if (emailIds.length == result.length) { + if (emailIds.length == result.emailIdsSuccess.length) { yield Right(MarkAsMultipleEmailReadAllSuccess( - result.length, + result.emailIdsSuccess.length, readAction, )); - } else if (result.isEmpty) { + } else if (result.emailIdsSuccess.isEmpty) { yield Left(MarkAsMultipleEmailReadAllFailure(readAction)); } else { yield Right(MarkAsMultipleEmailReadHasSomeEmailFailure( - result.length, + result.emailIdsSuccess.length, readAction, )); } diff --git a/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart b/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart index 6f828426b6..8ac2e00b4b 100644 --- a/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart +++ b/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart @@ -24,16 +24,16 @@ class MarkAsStarMultipleEmailInteractor { final result = await _emailRepository.markAsStar(session, accountId, emailIds, markStarAction); - if (emailIds.length == result.length) { + if (emailIds.length == result.emailIdsSuccess.length) { yield Right(MarkAsStarMultipleEmailAllSuccess( emailIds.length, markStarAction, )); - } else if (result.isEmpty) { + } else if (result.emailIdsSuccess.isEmpty) { yield Left(MarkAsStarMultipleEmailAllFailure(markStarAction)); } else { yield Right(MarkAsStarMultipleEmailHasSomeEmailFailure( - result.length, + result.emailIdsSuccess.length, markStarAction, )); } diff --git a/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart b/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart index 19bf07647e..f7481c5b6c 100644 --- a/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart +++ b/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart @@ -22,20 +22,20 @@ class MoveMultipleEmailToMailboxInteractor { try { yield Right(LoadingMoveMultipleEmailToMailboxAll()); final result = await _emailRepository.moveToMailbox(session, accountId, moveRequest); - if (moveRequest.totalEmails == result.length) { + if (moveRequest.totalEmails == result.emailIdsSuccess.length) { yield Right(MoveMultipleEmailToMailboxAllSuccess( - result, + result.emailIdsSuccess, moveRequest.currentMailboxes.keys.first, moveRequest.destinationMailboxId, moveRequest.moveAction, moveRequest.emailActionType, destinationPath: moveRequest.destinationPath, )); - } else if (result.isEmpty) { + } else if (result.emailIdsSuccess.isEmpty) { yield Left(MoveMultipleEmailToMailboxAllFailure(moveRequest.moveAction, moveRequest.emailActionType)); } else { yield Right(MoveMultipleEmailToMailboxHasSomeEmailFailure( - result, + result.emailIdsSuccess, moveRequest.currentMailboxes.keys.first, moveRequest.destinationMailboxId, moveRequest.moveAction, diff --git a/model/lib/extensions/list_id_extension.dart b/model/lib/extensions/list_id_extension.dart new file mode 100644 index 0000000000..cbdd0fc18d --- /dev/null +++ b/model/lib/extensions/list_id_extension.dart @@ -0,0 +1,6 @@ +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; + +extension ListIdExtension on Iterable { + Iterable toEmailIds() => map((id) => EmailId(id)); +} \ No newline at end of file diff --git a/model/lib/model.dart b/model/lib/model.dart index 29e3c97330..00f905bb32 100644 --- a/model/lib/model.dart +++ b/model/lib/model.dart @@ -60,6 +60,7 @@ export 'extensions/session_extension.dart'; export 'extensions/username_extension.dart'; export 'extensions/utc_date_extension.dart'; export 'extensions/contact_support_capability_extension.dart'; +export 'extensions/list_id_extension.dart'; // Identity export 'identity/identity_request_dto.dart'; export 'mailbox/expand_mode.dart'; diff --git a/test/features/email/data/network/email_api_test.dart b/test/features/email/data/network/email_api_test.dart index d442bf9dad..6aee8493fc 100644 --- a/test/features/email/data/network/email_api_test.dart +++ b/test/features/email/data/network/email_api_test.dart @@ -1,5 +1,4 @@ -import 'dart:math'; - +import 'package:collection/collection.dart'; import 'package:core/data/network/dio_client.dart'; import 'package:core/data/network/download/download_manager.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -60,7 +59,7 @@ void main() { const defaultMaxObjectsInSet = 50; const maxObjectsInSet = 50; const totalEmails = 100; - final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; final countIterations = (totalEmails / maxBatches).ceil(); final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); @@ -102,16 +101,16 @@ void main() { ] }); - final emails = List.generate( + final emailIds = List.generate( totalEmails, - (index) => Email(id: EmailId(Id('email_$index'))), + (index) => EmailId(Id('email_$index')), ); // Act final result = await emailApi.markAsRead( aliceSession, AccountFixtures.aliceAccountId, - emails, + emailIds, ReadActions.markAsRead, ); @@ -121,7 +120,8 @@ void main() { data: anyNamed('data'), cancelToken: anyNamed('cancelToken'), )).called(countIterations); - expect(result.length, totalEmails); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); }); test( @@ -133,7 +133,7 @@ void main() { const defaultMaxObjectsInSet = 50; const maxObjectsInSet = 200; const totalEmails = 100; - final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; final countIterations = (totalEmails / maxBatches).ceil(); final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); @@ -175,16 +175,16 @@ void main() { ] }); - final emails = List.generate( + final emailIds = List.generate( totalEmails, - (index) => Email(id: EmailId(Id('email_$index'))), + (index) => EmailId(Id('email_$index')), ); // Act final result = await emailApi.markAsRead( aliceSession, AccountFixtures.aliceAccountId, - emails, + emailIds, ReadActions.markAsRead, ); @@ -194,7 +194,8 @@ void main() { data: anyNamed('data'), cancelToken: anyNamed('cancelToken'), )).called(countIterations); - expect(result.length, totalEmails); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); }); test( @@ -206,7 +207,82 @@ void main() { const defaultMaxObjectsInSet = 50; const maxObjectsInSet = 20; const totalEmails = 100; - final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "updated": { + for (var item in List.generate(maxBatches, (index) => {"email_$index": null})) + item.keys.first: item.values.first + } + }, + "c0" + ], + [ + "Email/get", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "state": "b965db40-c11c-11ef-9cfb-ef2eae0e64b1", + "list": List.generate(maxBatches, (index) => { + "id": "email_$index", + "keywords": { + "\$seen": true + } + }), + "notFound": [] + }, + "c1" + ] + ] + }); + + final emailIds = List.generate( + totalEmails, + (index) => EmailId(Id('email_$index')), + ); + + // Act + final result = await emailApi.markAsRead( + aliceSession, + AccountFixtures.aliceAccountId, + emailIds, + ReadActions.markAsRead, + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); + }); + + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN totalEmails is less than defaultMaxObjectsInSet\n' + 'WHEN maxObjectsInSet equal defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 50; + const totalEmails = 30; + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; final countIterations = (totalEmails / maxBatches).ceil(); final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); @@ -248,16 +324,16 @@ void main() { ] }); - final emails = List.generate( + final emailIds = List.generate( totalEmails, - (index) => Email(id: EmailId(Id('email_$index'))), + (index) => EmailId(Id('email_$index')), ); // Act final result = await emailApi.markAsRead( aliceSession, AccountFixtures.aliceAccountId, - emails, + emailIds, ReadActions.markAsRead, ); @@ -267,7 +343,8 @@ void main() { data: anyNamed('data'), cancelToken: anyNamed('cancelToken'), )).called(countIterations); - expect(result.length, totalEmails); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); }); }); @@ -281,7 +358,7 @@ void main() { const defaultMaxObjectsInSet = 50; const maxObjectsInSet = 50; const totalEmails = 100; - final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; final countIterations = (totalEmails / maxBatches).ceil(); final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); @@ -323,16 +400,16 @@ void main() { ] }); - final emails = List.generate( + final emailIds = List.generate( totalEmails, - (index) => Email(id: EmailId(Id('email_$index'))), + (index) => EmailId(Id('email_$index')), ); // Act final result = await emailApi.markAsStar( aliceSession, AccountFixtures.aliceAccountId, - emails, + emailIds, MarkStarAction.markStar, ); @@ -342,7 +419,8 @@ void main() { data: anyNamed('data'), cancelToken: anyNamed('cancelToken'), )).called(countIterations); - expect(result.length, totalEmails); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); }); test( @@ -354,7 +432,7 @@ void main() { const defaultMaxObjectsInSet = 50; const maxObjectsInSet = 200; const totalEmails = 100; - final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; final countIterations = (totalEmails / maxBatches).ceil(); final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); @@ -396,16 +474,16 @@ void main() { ] }); - final emails = List.generate( + final emailIds = List.generate( totalEmails, - (index) => Email(id: EmailId(Id('email_$index'))), + (index) => EmailId(Id('email_$index')), ); // Act final result = await emailApi.markAsStar( aliceSession, AccountFixtures.aliceAccountId, - emails, + emailIds, MarkStarAction.markStar, ); @@ -415,7 +493,8 @@ void main() { data: anyNamed('data'), cancelToken: anyNamed('cancelToken'), )).called(countIterations); - expect(result.length, totalEmails); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); }); test( @@ -427,7 +506,82 @@ void main() { const defaultMaxObjectsInSet = 50; const maxObjectsInSet = 20; const totalEmails = 100; - final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "updated": { + for (var item in List.generate(maxBatches, (index) => {"email_$index": null})) + item.keys.first: item.values.first + } + }, + "c0" + ], + [ + "Email/get", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "state": "b965db40-c11c-11ef-9cfb-ef2eae0e64b1", + "list": List.generate(maxBatches, (index) => { + "id": "email_$index", + "keywords": { + "\$flagged": true + } + }), + "notFound": [] + }, + "c1" + ] + ] + }); + + final emailIds = List.generate( + totalEmails, + (index) => EmailId(Id('email_$index')), + ); + + // Act + final result = await emailApi.markAsStar( + aliceSession, + AccountFixtures.aliceAccountId, + emailIds, + MarkStarAction.markStar, + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); + }); + + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN totalEmails is less than defaultMaxObjectsInSet\n' + 'WHEN maxObjectsInSet equal defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 50; + const totalEmails = 30; + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; final countIterations = (totalEmails / maxBatches).ceil(); final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); @@ -469,16 +623,16 @@ void main() { ] }); - final emails = List.generate( + final emailIds = List.generate( totalEmails, - (index) => Email(id: EmailId(Id('email_$index'))), + (index) => EmailId(Id('email_$index')), ); // Act final result = await emailApi.markAsStar( aliceSession, AccountFixtures.aliceAccountId, - emails, + emailIds, MarkStarAction.markStar, ); @@ -488,7 +642,231 @@ void main() { data: anyNamed('data'), cancelToken: anyNamed('cancelToken'), )).called(countIterations); - expect(result.length, totalEmails); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); + }); + }); + + group('deleteMultipleEmailsPermanently::test', () { + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN maxObjectsInSet equal defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 50; + const totalEmails = 100; + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "destroyed": List.generate(maxBatches, (index) => "email_$index") + }, + "c0" + ] + ] + }); + + final emailIds = List.generate( + totalEmails, + (index) => EmailId(Id('email_$index')), + ); + + // Act + final result = await emailApi.deleteMultipleEmailsPermanently( + aliceSession, + AccountFixtures.aliceAccountId, + emailIds, + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); + }); + + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN maxObjectsInSet is greater than defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 200; + const totalEmails = 100; + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "destroyed": List.generate(maxBatches, (index) => "email_$index") + }, + "c0" + ] + ] + }); + + final emailIds = List.generate( + totalEmails, + (index) => EmailId(Id('email_$index')), + ); + + // Act + final result = await emailApi.deleteMultipleEmailsPermanently( + aliceSession, + AccountFixtures.aliceAccountId, + emailIds, + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); + }); + + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN maxObjectsInSet is less than defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 20; + const totalEmails = 100; + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "destroyed": List.generate(maxBatches, (index) => "email_$index") + }, + "c0" + ] + ] + }); + + final emailIds = List.generate( + totalEmails, + (index) => EmailId(Id('email_$index')), + ); + + // Act + final result = await emailApi.deleteMultipleEmailsPermanently( + aliceSession, + AccountFixtures.aliceAccountId, + emailIds, + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); + }); + + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN totalEmails is less than defaultMaxObjectsInSet\n' + 'WHEN maxObjectsInSet equal defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 50; + const totalEmails = 30; + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "destroyed": List.generate(maxBatches, (index) => "email_$index") + }, + "c0" + ] + ] + }); + + final emailIds = List.generate( + totalEmails, + (index) => EmailId(Id('email_$index')), + ); + + // Act + final result = await emailApi.deleteMultipleEmailsPermanently( + aliceSession, + AccountFixtures.aliceAccountId, + emailIds, + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); }); }); @@ -502,7 +880,7 @@ void main() { const defaultMaxObjectsInSet = 50; const maxObjectsInSet = 50; const totalEmails = 100; - final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; final countIterations = (totalEmails / maxBatches).ceil(); final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); @@ -554,7 +932,8 @@ void main() { data: anyNamed('data'), cancelToken: anyNamed('cancelToken'), )).called(countIterations); - expect(result.length, totalEmails); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); }); test( @@ -566,7 +945,7 @@ void main() { const defaultMaxObjectsInSet = 50; const maxObjectsInSet = 200; const totalEmails = 100; - final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; final countIterations = (totalEmails / maxBatches).ceil(); final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); @@ -618,7 +997,8 @@ void main() { data: anyNamed('data'), cancelToken: anyNamed('cancelToken'), )).called(countIterations); - expect(result.length, totalEmails); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); }); test( @@ -630,7 +1010,73 @@ void main() { const defaultMaxObjectsInSet = 50; const maxObjectsInSet = 20; const totalEmails = 100; - final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "updated": { + for (var item in List.generate(maxBatches, (index) => {"email_$index": null})) + item.keys.first: item.values.first + } + }, + "c0" + ] + ] + }); + + final emailIds = List.generate( + totalEmails, + (index) => EmailId(Id('email_$index')), + ); + + // Act + final result = await emailApi.moveToMailbox( + aliceSession, + AccountFixtures.aliceAccountId, + MoveToMailboxRequest( + { + MailboxId(Id('mailboxA')): emailIds, + }, + MailboxId(Id('mailboxB')), + MoveAction.moving, + EmailActionType.moveToMailbox, + ), + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); + }); + + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN totalEmails is less than defaultMaxObjectsInSet\n' + 'WHEN maxObjectsInSet equal defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 50; + const totalEmails = 30; + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; final countIterations = (totalEmails / maxBatches).ceil(); final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); @@ -682,7 +1128,8 @@ void main() { data: anyNamed('data'), cancelToken: anyNamed('cancelToken'), )).called(countIterations); - expect(result.length, totalEmails); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); }); }); }); From 14088531691b4472c6bbaf002da7e7bc1a93bde2 Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 2 Jan 2025 15:52:30 +0700 Subject: [PATCH 45/72] TF-3379 Show `replyAll` button when sender not me --- .../controller/single_email_controller.dart | 3 +++ .../presentation/widgets/email_receiver_widget.dart | 4 ++-- .../widgets/email_view_bottom_bar_widget.dart | 7 +++++-- .../information_sender_and_receiver_builder.dart | 4 ++-- .../widgets/sending_email_tile_widget.dart | 2 +- .../lib/extensions/list_email_address_extension.dart | 4 ++++ .../lib/extensions/presentation_email_extension.dart | 11 ++++++++++- 7 files changed, 27 insertions(+), 8 deletions(-) diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 6918ee5256..b5a703bd3b 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -17,6 +17,7 @@ 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/id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; import 'package:jmap_dart_client/jmap/mail/calendar/properties/attendee/calendar_attendee.dart'; @@ -174,6 +175,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { Session? get session => mailboxDashBoardController.sessionCurrent; + UserName? get userName => session?.username; + SingleEmailController( this._getEmailContentInteractor, this._markAsEmailReadInteractor, diff --git a/lib/features/email/presentation/widgets/email_receiver_widget.dart b/lib/features/email/presentation/widgets/email_receiver_widget.dart index 636caaf1c5..b548471bb2 100644 --- a/lib/features/email/presentation/widgets/email_receiver_widget.dart +++ b/lib/features/email/presentation/widgets/email_receiver_widget.dart @@ -132,7 +132,7 @@ class _EmailReceiverWidgetState extends State { ] ), ), - if (widget.emailSelected.numberOfAllEmailAddress() > 1) + if (widget.emailSelected.countRecipients > 1) TMailButtonWidget.fromIcon( icon: _imagePaths.icChevronDown, backgroundColor: Colors.transparent, @@ -217,7 +217,7 @@ class _EmailReceiverWidgetState extends State { ] ), ), - if (widget.emailSelected.numberOfAllEmailAddress() > 1) + if (widget.emailSelected.countRecipients > 1) TMailButtonWidget.fromIcon( icon: _imagePaths.icChevronDown, backgroundColor: Colors.transparent, diff --git a/lib/features/email/presentation/widgets/email_view_bottom_bar_widget.dart b/lib/features/email/presentation/widgets/email_view_bottom_bar_widget.dart index c0d55c4702..381ab3f984 100644 --- a/lib/features/email/presentation/widgets/email_view_bottom_bar_widget.dart +++ b/lib/features/email/presentation/widgets/email_view_bottom_bar_widget.dart @@ -48,8 +48,11 @@ class EmailViewBottomBarWidget extends StatelessWidget { child: Row( children: [ Obx(() { - if (_singleEmailController.currentEmailLoaded.value != null - && presentationEmail.numberOfAllEmailAddress() > 1) { + final emailLoader = _singleEmailController.currentEmailLoaded.value; + final countMailAddress = presentationEmail.getCountMailAddressWithoutMe( + _singleEmailController.userName?.value ?? '', + ); + if (emailLoader != null && countMailAddress > 1) { return Expanded( child: TMailButtonWidget( key: const Key('reply_all_emails_button'), diff --git a/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart b/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart index 65961b3eeb..50777bea8e 100644 --- a/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart +++ b/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart @@ -44,7 +44,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { return Padding( padding: const EdgeInsetsDirectional.only(start: 16, end: 16, top: 16), child: Row( - crossAxisAlignment: emailSelected.numberOfAllEmailAddress() > 0 + crossAxisAlignment: emailSelected.countRecipients > 0 ? CrossAxisAlignment.start : CrossAxisAlignment.center, children: [ @@ -96,7 +96,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { )), ReceivedTimeBuilder(emailSelected: emailSelected), ]), - if (emailSelected.numberOfAllEmailAddress() > 0) + if (emailSelected.countRecipients > 0) EmailReceiverWidget( emailSelected: emailSelected, maxWidth: constraints.maxWidth, diff --git a/lib/features/sending_queue/presentation/widgets/sending_email_tile_widget.dart b/lib/features/sending_queue/presentation/widgets/sending_email_tile_widget.dart index 8b5d65837e..51705e2dfa 100644 --- a/lib/features/sending_queue/presentation/widgets/sending_email_tile_widget.dart +++ b/lib/features/sending_queue/presentation/widgets/sending_email_tile_widget.dart @@ -76,7 +76,7 @@ class SendingEmailTileWidget extends StatelessWidget { ) else SvgPicture.asset( - sendingEmail.presentationEmail.numberOfAllEmailAddress() == 1 + sendingEmail.presentationEmail.countRecipients == 1 ? sendingEmail.sendingState.getAvatarPersonal(_imagePaths) : sendingEmail.sendingState.getAvatarGroup(_imagePaths), fit: BoxFit.fill, diff --git a/model/lib/extensions/list_email_address_extension.dart b/model/lib/extensions/list_email_address_extension.dart index 41b4628e8a..ebece29a4f 100644 --- a/model/lib/extensions/list_email_address_extension.dart +++ b/model/lib/extensions/list_email_address_extension.dart @@ -47,6 +47,10 @@ extension SetEmailAddressExtension on Set? { ? this!.where((emailAddress) => emailAddress.email != emailAddressNotExist).toList() : List.empty(); } + + Set withoutMe(String userName) { + return filterEmailAddress(userName).toSet(); + } } extension ListEmailAddressExtension on List { diff --git a/model/lib/extensions/presentation_email_extension.dart b/model/lib/extensions/presentation_email_extension.dart index 7b1c5c46e9..f329e164ab 100644 --- a/model/lib/extensions/presentation_email_extension.dart +++ b/model/lib/extensions/presentation_email_extension.dart @@ -29,7 +29,16 @@ extension PresentationEmailExtension on PresentationEmail { } } - int numberOfAllEmailAddress() => to.numberEmailAddress() + cc.numberEmailAddress() + bcc.numberEmailAddress(); + int get countRecipients => + to.numberEmailAddress() + + cc.numberEmailAddress() + + bcc.numberEmailAddress(); + + int getCountMailAddressWithoutMe(String userName) => + to.withoutMe(userName).numberEmailAddress() + + cc.withoutMe(userName).numberEmailAddress() + + bcc.withoutMe(userName).numberEmailAddress() + + from.withoutMe(userName).numberEmailAddress(); String getReceivedAt(String newLocale, {String? pattern}) { final emailTime = receivedAt; From 44f2464baaca10fb0e147cd8d8212cd736a45b5a Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 2 Jan 2025 16:40:57 +0700 Subject: [PATCH 46/72] TF-3379 Add unit test for `getCountMailAddressWithoutMe` method --- .../presentattion_email_extension_test.dart | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 model/test/extensions/presentattion_email_extension_test.dart diff --git a/model/test/extensions/presentattion_email_extension_test.dart b/model/test/extensions/presentattion_email_extension_test.dart new file mode 100644 index 0000000000..1eebaa3136 --- /dev/null +++ b/model/test/extensions/presentattion_email_extension_test.dart @@ -0,0 +1,65 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/extensions/presentation_email_extension.dart'; +import 'package:model/email/presentation_email.dart'; + +void main() { + group('PresentationEmailExtension::getCountMailAddressWithoutMe::', () { + test('Should return the correct count of email addresses excluding the user', () { + // Arrange + final email = PresentationEmail( + to: {EmailAddress('Alice', 'alice@example.com')}, + cc: {EmailAddress('Bob', 'bob@example.com')}, + bcc: {EmailAddress('Charlie', 'charlie@example.com')}, + from: {EmailAddress('User', 'user@example.com')}, + ); + + // Act + final count = email.getCountMailAddressWithoutMe('user@example.com'); + + // Assert + expect(count, 3); + }); + + test('Should return 0 when all email addresses are the user', () { + // Arrange + final email = PresentationEmail( + to: {EmailAddress('User', 'user@example.com')}, + cc: {EmailAddress('User', 'user@example.com')}, + bcc: {EmailAddress('User', 'user@example.com')}, + from: {EmailAddress('User', 'user@example.com')}, + ); + + // Act + final count = email.getCountMailAddressWithoutMe('user@example.com'); + + // Assert + expect(count, 0); + }); + + test('Should return the correct count excluding multiple instances of the user', () { + // Arrange + final email = PresentationEmail( + to: { + EmailAddress('Alice', 'alice@example.com'), + EmailAddress('User', 'user@example.com') + }, + cc: { + EmailAddress('User', 'user@example.com') + }, + bcc: { + EmailAddress('Charlie', 'charlie@example.com') + }, + from: { + EmailAddress('User', 'user@example.com') + }, + ); + + // Act + final count = email.getCountMailAddressWithoutMe('user@example.com'); + + // Assert + expect(count, 2); + }); + }); +} From 6627239fc63ec50d8e71a712557a1946009920de Mon Sep 17 00:00:00 2001 From: DatDang Date: Tue, 31 Dec 2024 10:12:19 +0700 Subject: [PATCH 47/72] TF-3385 Update mailbox name --- .../base/base_mailbox_controller.dart | 15 ++++++++ .../domain/state/rename_mailbox_state.dart | 10 ++++- .../usecases/rename_mailbox_interactor.dart | 2 +- .../presentation/mailbox_controller.dart | 2 + .../presentation/model/mailbox_tree.dart | 22 +++++++++++ .../search_mailbox_controller.dart | 2 + model/lib/mailbox/presentation_mailbox.dart | 38 +++++++++++++++++++ 7 files changed, 89 insertions(+), 2 deletions(-) diff --git a/lib/features/base/base_mailbox_controller.dart b/lib/features/base/base_mailbox_controller.dart index aa62a5145a..e72f03f6d7 100644 --- a/lib/features/base/base_mailbox_controller.dart +++ b/lib/features/base/base_mailbox_controller.dart @@ -553,4 +553,19 @@ abstract class BaseMailboxController extends BaseController { mailboxNode = personalMailboxTree.value.findNodeOnFirstLevel((node) => node.item.name?.name.toLowerCase() == name); return mailboxNode; } + + void updateMailboxNameById(MailboxId mailboxId, MailboxName mailboxName) { + final mailboxTrees = [ + defaultMailboxTree, + personalMailboxTree, + teamMailboxesTree, + ]; + + for (var mailboxTree in mailboxTrees) { + if (mailboxTree.value.updateMailboxNameById(mailboxId, mailboxName)) { + mailboxTree.refresh(); + break; + } + } + } } \ No newline at end of file diff --git a/lib/features/mailbox/domain/state/rename_mailbox_state.dart b/lib/features/mailbox/domain/state/rename_mailbox_state.dart index 1861185bc9..f3300267a9 100644 --- a/lib/features/mailbox/domain/state/rename_mailbox_state.dart +++ b/lib/features/mailbox/domain/state/rename_mailbox_state.dart @@ -1,9 +1,17 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/rename_mailbox_request.dart'; class LoadingRenameMailbox extends UIState {} -class RenameMailboxSuccess extends UIState {} +class RenameMailboxSuccess extends UIState { + RenameMailboxSuccess({required this.request}); + + final RenameMailboxRequest request; + + @override + List get props => [request]; +} class RenameMailboxFailure extends FeatureFailure { diff --git a/lib/features/mailbox/domain/usecases/rename_mailbox_interactor.dart b/lib/features/mailbox/domain/usecases/rename_mailbox_interactor.dart index 8e2ef950da..267be45198 100644 --- a/lib/features/mailbox/domain/usecases/rename_mailbox_interactor.dart +++ b/lib/features/mailbox/domain/usecases/rename_mailbox_interactor.dart @@ -18,7 +18,7 @@ class RenameMailboxInteractor { yield Right(LoadingRenameMailbox()); final result = await _mailboxRepository.renameMailbox(session, accountId, request); if (result) { - yield Right(RenameMailboxSuccess()); + yield Right(RenameMailboxSuccess(request: request)); } else { yield Left(RenameMailboxFailure(null)); } diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 904068006a..837bf16121 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -167,6 +167,8 @@ class MailboxController extends BaseMailboxController _deleteMultipleMailboxSuccess(success.listMailboxIdDeleted, success.currentMailboxState); } else if (success is DeleteMultipleMailboxHasSomeSuccess) { _deleteMultipleMailboxSuccess(success.listMailboxIdDeleted, success.currentMailboxState); + } else if (success is RenameMailboxSuccess) { + _renameMailboxSuccess(success); } else if (success is MoveMailboxSuccess) { _moveMailboxSuccess(success); } else if (success is SubscribeMailboxSuccess) { diff --git a/lib/features/mailbox/presentation/model/mailbox_tree.dart b/lib/features/mailbox/presentation/model/mailbox_tree.dart index 125e6e3c63..a682cecd1a 100644 --- a/lib/features/mailbox/presentation/model/mailbox_tree.dart +++ b/lib/features/mailbox/presentation/model/mailbox_tree.dart @@ -3,6 +3,7 @@ import 'dart:collection'; import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/mailbox/expand_mode.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; @@ -83,6 +84,27 @@ class MailboxTree with EquatableMixin { } } + bool updateMailboxNameById(MailboxId mailboxId, MailboxName mailboxName) { + final matchedNode = findNode((node) => node.item.id == mailboxId); + if (matchedNode != null) { + matchedNode.item = matchedNode.item.copyWith(name: mailboxName); + return true; + } + return false; + } + + void updateMailboxUnreadCountById(MailboxId mailboxId, int unreadCount) { + final matchedNode = findNode((node) => node.item.id == mailboxId); + if (matchedNode != null) { + final currentUnreadCount = matchedNode.item.unreadEmails?.value.value ?? 0; + final updatedUnreadCount = currentUnreadCount + unreadCount; + if (updatedUnreadCount < 0) return; + matchedNode.item = matchedNode.item.copyWith( + unreadEmails: UnreadEmails(UnsignedInt(updatedUnreadCount)), + ); + } + } + String? getNodePath(MailboxId mailboxId) { final matchedNode = findNode((node) => node.item.id == mailboxId); if (matchedNode == null) { diff --git a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart index 2ae4bc4455..e28d98aeb6 100644 --- a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart +++ b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart @@ -147,6 +147,8 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa searchMailboxAction(); } else if (success is SearchMailboxSuccess) { _handleSearchMailboxSuccess(success); + } else if (success is RenameMailboxSuccess) { + updateMailboxNameById(success.request.mailboxId, success.request.newName); } else if (success is MoveMailboxSuccess) { _moveMailboxSuccess(success); } else if (success is DeleteMultipleMailboxAllSuccess) { diff --git a/model/lib/mailbox/presentation_mailbox.dart b/model/lib/mailbox/presentation_mailbox.dart index a810e1037c..5765975131 100644 --- a/model/lib/mailbox/presentation_mailbox.dart +++ b/model/lib/mailbox/presentation_mailbox.dart @@ -89,4 +89,42 @@ class PresentationMailbox with EquatableMixin { namespace, displayName, ]; + + PresentationMailbox copyWith({ + MailboxId? id, + MailboxName? name, + MailboxId? parentId, + Role? role, + SortOrder? sortOrder, + TotalEmails? totalEmails, + UnreadEmails? unreadEmails, + TotalThreads? totalThreads, + UnreadThreads? unreadThreads, + MailboxRights? myRights, + IsSubscribed? isSubscribed, + SelectMode? selectMode, + String? mailboxPath, + MailboxState? state, + Namespace? namespace, + String? displayName, + }) { + return PresentationMailbox( + id ?? this.id, + name: name ?? this.name, + parentId: parentId ?? this.parentId, + role: role ?? this.role, + sortOrder: sortOrder ?? this.sortOrder, + totalEmails: totalEmails ?? this.totalEmails, + unreadEmails: unreadEmails ?? this.unreadEmails, + totalThreads: totalThreads ?? this.totalThreads, + unreadThreads: unreadThreads ?? this.unreadThreads, + myRights: myRights ?? this.myRights, + isSubscribed: isSubscribed ?? this.isSubscribed, + selectMode: selectMode ?? this.selectMode, + mailboxPath: mailboxPath ?? this.mailboxPath, + state: state ?? this.state, + namespace: namespace ?? this.namespace, + displayName: displayName ?? this.displayName, + ); + } } \ No newline at end of file From 8d1af535f34c8973690a58a7cc2362ae0bc51c83 Mon Sep 17 00:00:00 2001 From: DatDang Date: Tue, 31 Dec 2024 10:12:43 +0700 Subject: [PATCH 48/72] TF-3385 Update mark as read and star --- .../base/base_mailbox_controller.dart | 37 +++++ .../state/mark_as_email_read_state.dart | 5 +- .../state/mark_as_email_star_state.dart | 6 +- .../mark_as_email_read_interactor.dart | 5 +- .../mark_as_star_email_interactor.dart | 2 +- .../controller/single_email_controller.dart | 10 +- .../state/mark_as_mailbox_read_state.dart | 10 ++ .../mark_as_mailbox_read_interactor.dart | 4 +- .../presentation/mailbox_controller.dart | 72 +++++++++ .../presentation/model/mailbox_tree.dart | 6 +- .../mailbox_dashboard_controller.dart | 5 +- ...update_current_emails_flags_extension.dart | 56 +++++++ .../presentation/search_email_controller.dart | 40 +++++ .../search_mailbox_controller.dart | 13 ++ .../repository/thread_repository_impl.dart | 3 +- .../domain/repository/thread_repository.dart | 1 + .../mark_as_multiple_email_read_state.dart | 20 ++- .../mark_as_star_multiple_email_state.dart | 9 +- .../get_emails_in_mailbox_interactor.dart | 4 +- ...ark_as_multiple_email_read_interactor.dart | 14 +- ...ark_as_star_multiple_email_interactor.dart | 10 +- .../mixin/email_action_controller.dart | 1 + .../presentation/thread_controller.dart | 70 +++++++- ...e_current_emails_flags_extension_test.dart | 150 ++++++++++++++++++ 24 files changed, 521 insertions(+), 32 deletions(-) create mode 100644 lib/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart create mode 100644 test/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension_test.dart diff --git a/lib/features/base/base_mailbox_controller.dart b/lib/features/base/base_mailbox_controller.dart index e72f03f6d7..62ba44dc2d 100644 --- a/lib/features/base/base_mailbox_controller.dart +++ b/lib/features/base/base_mailbox_controller.dart @@ -568,4 +568,41 @@ abstract class BaseMailboxController extends BaseController { } } } + + void updateUnreadCountOfMailboxById( + MailboxId mailboxId, { + required int unreadChanges, + }) { + final mailboxTrees = [ + defaultMailboxTree, + personalMailboxTree, + teamMailboxesTree, + ]; + + for (var mailboxTree in mailboxTrees) { + if (mailboxTree.value.updateMailboxUnreadCountById(mailboxId, unreadChanges)) { + mailboxTree.refresh(); + break; + } + } + } + + void clearUnreadCount(MailboxId mailboxId) { + final mailboxTrees = [ + defaultMailboxTree, + personalMailboxTree, + teamMailboxesTree, + ]; + + for (var mailboxTree in mailboxTrees) { + final selectedNode = mailboxTree.value.findNode((node) => node.item.id == mailboxId); + if (selectedNode == null) continue; + final currentUnreadCount = selectedNode.item.unreadEmails?.value.value.toInt(); + teamMailboxesTree.value.updateMailboxUnreadCountById( + mailboxId, + -(currentUnreadCount ?? 0)); + teamMailboxesTree.refresh(); + break; + } + } } \ No newline at end of file diff --git a/lib/features/email/domain/state/mark_as_email_read_state.dart b/lib/features/email/domain/state/mark_as_email_read_state.dart index 1c327f1da0..1bfb2e2052 100644 --- a/lib/features/email/domain/state/mark_as_email_read_state.dart +++ b/lib/features/email/domain/state/mark_as_email_read_state.dart @@ -1,6 +1,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/read_actions.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; @@ -8,15 +9,17 @@ class MarkAsEmailReadSuccess extends UIState { final EmailId emailId; final ReadActions readActions; final MarkReadAction markReadAction; + final MailboxId? mailboxId; MarkAsEmailReadSuccess( this.emailId, this.readActions, this.markReadAction, + this.mailboxId, ); @override - List get props => [emailId, readActions, markReadAction]; + List get props => [emailId, readActions, markReadAction, mailboxId]; } class MarkAsEmailReadFailure extends FeatureFailure { diff --git a/lib/features/email/domain/state/mark_as_email_star_state.dart b/lib/features/email/domain/state/mark_as_email_star_state.dart index 3313c95253..91e87c19f0 100644 --- a/lib/features/email/domain/state/mark_as_email_star_state.dart +++ b/lib/features/email/domain/state/mark_as_email_star_state.dart @@ -1,14 +1,16 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/email/mark_star_action.dart'; class MarkAsStarEmailSuccess extends UIState { final MarkStarAction markStarAction; + final EmailId emailId; - MarkAsStarEmailSuccess(this.markStarAction); + MarkAsStarEmailSuccess(this.markStarAction, this.emailId); @override - List get props => [markStarAction]; + List get props => [markStarAction, emailId]; } class MarkAsStarEmailFailure extends FeatureFailure { diff --git a/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart b/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart index 5f5659bc66..684289d7b7 100644 --- a/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart +++ b/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart @@ -4,6 +4,7 @@ import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/read_actions.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; @@ -20,6 +21,7 @@ class MarkAsEmailReadInteractor { EmailId emailId, ReadActions readAction, MarkReadAction markReadAction, + MailboxId? mailboxId, ) async* { try { final result = await _emailRepository.markAsRead( @@ -36,7 +38,8 @@ class MarkAsEmailReadInteractor { result.emailIdsSuccess.first, readAction, markReadAction, - )); + mailboxId, + )); } } catch (e) { yield Left(MarkAsEmailReadFailure(readAction, exception: e)); diff --git a/lib/features/email/domain/usecases/mark_as_star_email_interactor.dart b/lib/features/email/domain/usecases/mark_as_star_email_interactor.dart index aa8cc68c7e..db2b9704b8 100644 --- a/lib/features/email/domain/usecases/mark_as_star_email_interactor.dart +++ b/lib/features/email/domain/usecases/mark_as_star_email_interactor.dart @@ -26,7 +26,7 @@ class MarkAsStarEmailInteractor { [emailId], markStarAction, ); - yield Right(MarkAsStarEmailSuccess(markStarAction)); + yield Right(MarkAsStarEmailSuccess(markStarAction, emailId)); } catch (e) { yield Left(MarkAsStarEmailFailure(markStarAction, exception: e)); } diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index b5a703bd3b..4b06b88019 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -96,6 +96,7 @@ import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.d import 'package:tmail_ui_user/features/mailbox/presentation/action/mailbox_ui_action.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/download/download_task_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_email_rule_filter_request.dart'; @@ -663,11 +664,18 @@ class SingleEmailController extends BaseController with AppLoaderMixin { presentationEmail.id!, readActions, markReadAction, + presentationEmail.mailboxContain?.mailboxId, )); } } void _handleMarkAsEmailReadCompleted(ReadActions readActions) { + if (_currentEmailId != null) { + mailboxDashBoardController.updateEmailFlagByEmailIds( + [_currentEmailId!], + readAction: ReadActions.markAsRead, + ); + } if (readActions == ReadActions.markAsUnread) { closeEmailView(context: currentContext); } @@ -1083,7 +1091,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void _markAsEmailStarSuccess(MarkAsStarEmailSuccess success) { final newEmail = currentEmail?.updateKeywords({ - KeyWordIdentifier.emailFlagged: true, + KeyWordIdentifier.emailFlagged: success.markStarAction == MarkStarAction.markStar, }); mailboxDashBoardController.setSelectedEmail(newEmail); } diff --git a/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart b/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart index 60a2df0c15..6be792a793 100644 --- a/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart +++ b/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart @@ -1,6 +1,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; @@ -24,8 +25,10 @@ class UpdatingMarkAsMailboxReadState extends UIState { class MarkAsMailboxReadAllSuccess extends UIActionState { final String mailboxDisplayName; + final MailboxId mailboxId; MarkAsMailboxReadAllSuccess(this.mailboxDisplayName, + this.mailboxId, { jmap.State? currentEmailState, jmap.State? currentMailboxState, @@ -35,6 +38,7 @@ class MarkAsMailboxReadAllSuccess extends UIActionState { @override List get props => [ mailboxDisplayName, + mailboxId, ...super.props ]; } @@ -43,16 +47,22 @@ class MarkAsMailboxReadHasSomeEmailFailure extends UIState { final String mailboxDisplayName; final int countEmailsRead; + final MailboxId mailboxId; + final List successEmailIds; MarkAsMailboxReadHasSomeEmailFailure( this.mailboxDisplayName, this.countEmailsRead, + this.mailboxId, + this.successEmailIds, ); @override List get props => [ mailboxDisplayName, countEmailsRead, + mailboxId, + successEmailIds, ]; } diff --git a/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart b/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart index 074efdd514..547ec1e57e 100644 --- a/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart +++ b/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart @@ -34,11 +34,13 @@ class MarkAsMailboxReadInteractor { onProgressController); if (totalEmailUnread == listEmails.length) { - yield Right(MarkAsMailboxReadAllSuccess(mailboxDisplayName)); + yield Right(MarkAsMailboxReadAllSuccess(mailboxDisplayName, mailboxId)); } else if (listEmails.isNotEmpty) { yield Right(MarkAsMailboxReadHasSomeEmailFailure( mailboxDisplayName, listEmails.length, + mailboxId, + listEmails, )); } else { yield Left(MarkAsMailboxReadAllFailure(mailboxDisplayName: mailboxDisplayName)); diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 837bf16121..79e462fe0c 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -20,6 +20,7 @@ import 'package:tmail_ui_user/features/base/base_mailbox_controller.dart'; import 'package:tmail_ui_user/features/base/mixin/contact_support_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/mailbox_action_handler_mixin.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; +import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart'; @@ -36,6 +37,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/model/subscribe_request.da import 'package:tmail_ui_user/features/mailbox/domain/state/create_new_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/delete_multiple_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/get_all_mailboxes_state.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/move_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/refresh_all_mailboxes_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/refresh_changes_all_mailboxes_state.dart'; @@ -70,6 +72,7 @@ import 'package:tmail_ui_user/features/push_notification/presentation/websocket/ import 'package:tmail_ui_user/features/push_notification/presentation/websocket/web_socket_queue_handler.dart'; import 'package:tmail_ui_user/features/search/mailbox/presentation/search_mailbox_bindings.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/dialog_router.dart'; @@ -255,6 +258,71 @@ class MailboxController extends BaseMailboxController mailboxDashBoardController.clearMailboxUIAction(); } }); + + ever(mailboxDashBoardController.viewState, (viewState) { + final reactionState = viewState.getOrElse(() => UIState.idle); + if (reactionState is MarkAsEmailReadSuccess) { + _handleMarkEmailsAsReadOrUnread( + affectedMailboxId: reactionState.mailboxId, + readCount: reactionState.readActions == ReadActions.markAsRead + ? 1 + : null, + unreadCount: reactionState.readActions == ReadActions.markAsUnread + ? 1 + : null, + ); + } else if (reactionState is MarkAsMultipleEmailReadAllSuccess) { + _handleMarkEmailsAsReadOrUnread( + affectedMailboxId: reactionState.mailboxId, + readCount: reactionState.readActions == ReadActions.markAsRead + ? reactionState.emailIds.length + : null, + unreadCount: reactionState.readActions == ReadActions.markAsUnread + ? reactionState.emailIds.length + : null, + ); + } else if (reactionState is MarkAsMultipleEmailReadHasSomeEmailFailure) { + _handleMarkEmailsAsReadOrUnread( + affectedMailboxId: reactionState.mailboxId, + readCount: reactionState.readActions == ReadActions.markAsRead + ? reactionState.successEmailIds.length + : null, + unreadCount: reactionState.readActions == ReadActions.markAsUnread + ? reactionState.successEmailIds.length + : null, + ); + } else if (reactionState is MarkAsMailboxReadAllSuccess) { + _handleMarkMailboxAsRead( + affectedMailboxId: reactionState.mailboxId, + ); + } else if (reactionState is MarkAsMailboxReadHasSomeEmailFailure) { + _handleMarkEmailsAsReadOrUnread( + affectedMailboxId: reactionState.mailboxId, + readCount: reactionState.successEmailIds.length, + ); + } + }); + } + + void _handleMarkEmailsAsReadOrUnread({ + required MailboxId? affectedMailboxId, + int? readCount, + int? unreadCount, + }) { + if (affectedMailboxId == null) return; + + updateUnreadCountOfMailboxById( + affectedMailboxId, + unreadChanges: (unreadCount ?? 0) - (readCount ?? 0), + ); + } + + void _handleMarkMailboxAsRead({ + required MailboxId? affectedMailboxId, + }) { + if (affectedMailboxId == null) return; + + clearUnreadCount(affectedMailboxId); } void _initWebSocketQueueHandler() { @@ -638,6 +706,10 @@ class MailboxController extends BaseMailboxController } } + void _renameMailboxSuccess(RenameMailboxSuccess success) { + updateMailboxNameById(success.request.mailboxId, success.request.newName); + } + void _renameMailboxFailure(RenameMailboxFailure failure) { if (currentOverlayContext != null && currentContext != null) { final exception = failure.exception; diff --git a/lib/features/mailbox/presentation/model/mailbox_tree.dart b/lib/features/mailbox/presentation/model/mailbox_tree.dart index a682cecd1a..97bff1f3f8 100644 --- a/lib/features/mailbox/presentation/model/mailbox_tree.dart +++ b/lib/features/mailbox/presentation/model/mailbox_tree.dart @@ -93,16 +93,18 @@ class MailboxTree with EquatableMixin { return false; } - void updateMailboxUnreadCountById(MailboxId mailboxId, int unreadCount) { + bool updateMailboxUnreadCountById(MailboxId mailboxId, int unreadCount) { final matchedNode = findNode((node) => node.item.id == mailboxId); if (matchedNode != null) { final currentUnreadCount = matchedNode.item.unreadEmails?.value.value ?? 0; final updatedUnreadCount = currentUnreadCount + unreadCount; - if (updatedUnreadCount < 0) return; + if (updatedUnreadCount < 0) return true; matchedNode.item = matchedNode.item.copyWith( unreadEmails: UnreadEmails(UnsignedInt(updatedUnreadCount)), ); + return true; } + return false; } String? getNodePath(MailboxId mailboxId) { diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 8fafc74f2d..b2791f0644 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -884,6 +884,7 @@ class MailboxDashBoardController extends ReloadableController EmailId emailId, ReadActions readActions, MarkReadAction markReadAction, + MailboxId? mailboxId, ) { if (accountId.value != null && sessionCurrent != null) { consumeState(_markAsEmailReadInteractor.execute( @@ -892,6 +893,7 @@ class MailboxDashBoardController extends ReloadableController emailId, readActions, markReadAction, + mailboxId, )); } } @@ -923,6 +925,7 @@ class MailboxDashBoardController extends ReloadableController accountId.value!, listEmailNeedMarkAsRead.listEmailIds, readActions, + listPresentationEmail.firstOrNull?.mailboxContain?.mailboxId, )); } } @@ -960,7 +963,7 @@ class MailboxDashBoardController extends ReloadableController message, actionName: AppLocalizations.of(currentContext!).undo, onActionClick: () { - markAsEmailRead(success.emailId, undoAction, MarkReadAction.undo); + markAsEmailRead(success.emailId, undoAction, MarkReadAction.undo, success.mailboxId); }, leadingSVGIcon: imagePaths.icToastSuccessMessage, backgroundColor: AppColor.toastSuccessBackgroundColor, diff --git a/lib/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart b/lib/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart new file mode 100644 index 0000000000..4572d768a7 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart @@ -0,0 +1,56 @@ +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; +import 'package:model/email/mark_star_action.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:model/email/read_actions.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; + +extension UpdateCurrentEmailsFlagsExtension on MailboxDashBoardController { + void updateEmailFlagByEmailIds( + List emailIds, { + ReadActions? readAction, + MarkStarAction? markStarAction, + }) { + if (readAction == null && markStarAction == null) return; + + for (var email in emailsInCurrentMailbox) { + if (!emailIds.contains(email.id)) continue; + + switch (readAction) { + case ReadActions.markAsRead: + _updateKeyword(email, KeyWordIdentifier.emailSeen, true); + break; + case ReadActions.markAsUnread: + _updateKeyword(email, KeyWordIdentifier.emailSeen, false); + break; + default: + break; + } + + switch (markStarAction) { + case MarkStarAction.markStar: + _updateKeyword(email, KeyWordIdentifier.emailFlagged, true); + break; + case MarkStarAction.unMarkStar: + _updateKeyword(email, KeyWordIdentifier.emailFlagged, false); + break; + default: + break; + } + } + + emailsInCurrentMailbox.refresh(); + } + + void _updateKeyword( + PresentationEmail presentationEmail, + KeyWordIdentifier keyword, + bool value, + ) { + if (value) { + presentationEmail.keywords?[keyword] = true; + } else { + presentationEmail.keywords?.remove(keyword); + } + } +} \ No newline at end of file diff --git a/lib/features/search/email/presentation/search_email_controller.dart b/lib/features/search/email/presentation/search_email_controller.dart index ccd7b79f7b..4d088d6740 100644 --- a/lib/features/search/email/presentation/search_email_controller.dart +++ b/lib/features/search/email/presentation/search_email_controller.dart @@ -33,6 +33,8 @@ import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_e import 'package:tmail_ui_user/features/contact/presentation/model/contact_arguments.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; +import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; @@ -44,6 +46,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_all import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/quick_search_email_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_recent_search_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart'; @@ -60,6 +63,8 @@ import 'package:tmail_ui_user/features/search/email/presentation/model/search_mo import 'package:tmail_ui_user/features/search/email/presentation/search_email_bindings.dart'; import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_as_star_multiple_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_more_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/search_email_interactor.dart'; @@ -260,6 +265,41 @@ class SearchEmailController extends BaseController } }, ); + + ever(mailboxDashBoardController.viewState, (viewState) { + final reactionState = viewState.getOrElse(() => UIState.idle); + if (reactionState is MarkAsEmailReadSuccess) { + mailboxDashBoardController.updateEmailFlagByEmailIds( + [reactionState.emailId], + readAction: reactionState.readActions, + ); + } else if (reactionState is MarkAsMultipleEmailReadAllSuccess) { + mailboxDashBoardController.updateEmailFlagByEmailIds( + reactionState.emailIds, + readAction: reactionState.readActions, + ); + } else if (reactionState is MarkAsMultipleEmailReadHasSomeEmailFailure) { + mailboxDashBoardController.updateEmailFlagByEmailIds( + reactionState.successEmailIds, + readAction: reactionState.readActions, + ); + } else if (reactionState is MarkAsStarEmailSuccess) { + mailboxDashBoardController.updateEmailFlagByEmailIds( + [reactionState.emailId], + markStarAction: reactionState.markStarAction, + ); + } else if (reactionState is MarkAsStarMultipleEmailAllSuccess) { + mailboxDashBoardController.updateEmailFlagByEmailIds( + reactionState.emailIds, + markStarAction: reactionState.markStarAction, + ); + } else if (reactionState is MarkAsStarMultipleEmailHasSomeEmailFailure) { + mailboxDashBoardController.updateEmailFlagByEmailIds( + reactionState.successEmailIds, + markStarAction: reactionState.markStarAction, + ); + } + }); } void _refreshEmailChanges({jmap.State? newState}) { diff --git a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart index e28d98aeb6..6cc65fe53d 100644 --- a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart +++ b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart @@ -33,6 +33,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/model/subscribe_request.da import 'package:tmail_ui_user/features/mailbox/domain/state/create_new_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/delete_multiple_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/get_all_mailboxes_state.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/move_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/refresh_changes_all_mailboxes_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/rename_mailbox_state.dart'; @@ -185,6 +186,18 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa _refreshMailboxChanges(newState: action.newState); } }); + + ever(dashboardController.viewState, (viewState) { + final reactionState = viewState.getOrElse(() => UIState.idle); + if (reactionState is MarkAsMailboxReadAllSuccess) { + clearUnreadCount(reactionState.mailboxId); + } else if (reactionState is MarkAsMailboxReadHasSomeEmailFailure) { + updateUnreadCountOfMailboxById( + reactionState.mailboxId, + unreadChanges: -reactionState.countEmailsRead, + ); + } + }); } void _getAllMailboxAction() { diff --git a/lib/features/thread/data/repository/thread_repository_impl.dart b/lib/features/thread/data/repository/thread_repository_impl.dart index 9b4c5d94d8..6df6a4a030 100644 --- a/lib/features/thread/data/repository/thread_repository_impl.dart +++ b/lib/features/thread/data/repository/thread_repository_impl.dart @@ -49,6 +49,7 @@ class ThreadRepositoryImpl extends ThreadRepository { Properties? propertiesCreated, Properties? propertiesUpdated, bool getLatestChanges = true, + bool skipCache = false, } ) async* { log('ThreadRepositoryImpl::getAllEmail(): filter = ${emailFilter?.mailboxId}'); @@ -88,7 +89,7 @@ class ThreadRepositoryImpl extends ThreadRepository { ); } yield networkEmailResponse; - } else { + } else if (!skipCache) { yield localEmailResponse; } diff --git a/lib/features/thread/domain/repository/thread_repository.dart b/lib/features/thread/domain/repository/thread_repository.dart index ec05030612..8eb198ec6e 100644 --- a/lib/features/thread/domain/repository/thread_repository.dart +++ b/lib/features/thread/domain/repository/thread_repository.dart @@ -29,6 +29,7 @@ abstract class ThreadRepository { Properties? propertiesCreated, Properties? propertiesUpdated, bool getLatestChanges = true, + bool skipCache = false, } ); diff --git a/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart b/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart index 9134f49b2b..0eb6ab7fc8 100644 --- a/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart +++ b/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart @@ -1,20 +1,24 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/read_actions.dart'; class LoadingMarkAsMultipleEmailReadAll extends UIState {} class MarkAsMultipleEmailReadAllSuccess extends UIState { - final int countMarkAsReadSuccess; + final List emailIds; final ReadActions readActions; + final MailboxId? mailboxId; MarkAsMultipleEmailReadAllSuccess( - this.countMarkAsReadSuccess, - this.readActions, + this.emailIds, + this.readActions, + this.mailboxId, ); @override - List get props => [countMarkAsReadSuccess, readActions]; + List get props => [emailIds, readActions, mailboxId]; } class MarkAsMultipleEmailReadAllFailure extends FeatureFailure { @@ -27,16 +31,18 @@ class MarkAsMultipleEmailReadAllFailure extends FeatureFailure { } class MarkAsMultipleEmailReadHasSomeEmailFailure extends UIState { - final int countMarkAsReadSuccess; + final List successEmailIds; final ReadActions readActions; + final MailboxId? mailboxId; MarkAsMultipleEmailReadHasSomeEmailFailure( - this.countMarkAsReadSuccess, + this.successEmailIds, this.readActions, + this.mailboxId, ); @override - List get props => [countMarkAsReadSuccess, readActions]; + List get props => [successEmailIds, readActions, mailboxId]; } class MarkAsMultipleEmailReadFailure extends FeatureFailure { diff --git a/lib/features/thread/domain/state/mark_as_star_multiple_email_state.dart b/lib/features/thread/domain/state/mark_as_star_multiple_email_state.dart index b41a891546..c7c5cecabf 100644 --- a/lib/features/thread/domain/state/mark_as_star_multiple_email_state.dart +++ b/lib/features/thread/domain/state/mark_as_star_multiple_email_state.dart @@ -1,5 +1,6 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/email/mark_star_action.dart'; class LoadingMarkAsStarMultipleEmailAll extends UIState {} @@ -7,14 +8,16 @@ class LoadingMarkAsStarMultipleEmailAll extends UIState {} class MarkAsStarMultipleEmailAllSuccess extends UIState { final int countMarkStarSuccess; final MarkStarAction markStarAction; + final List emailIds; MarkAsStarMultipleEmailAllSuccess( this.countMarkStarSuccess, this.markStarAction, + this.emailIds, ); @override - List get props => [countMarkStarSuccess, markStarAction]; + List get props => [countMarkStarSuccess, markStarAction, emailIds]; } class MarkAsStarMultipleEmailAllFailure extends FeatureFailure { @@ -29,14 +32,16 @@ class MarkAsStarMultipleEmailAllFailure extends FeatureFailure { class MarkAsStarMultipleEmailHasSomeEmailFailure extends UIState { final int countMarkStarSuccess; final MarkStarAction markStarAction; + final List successEmailIds; MarkAsStarMultipleEmailHasSomeEmailFailure( this.countMarkStarSuccess, this.markStarAction, + this.successEmailIds, ); @override - List get props => [countMarkStarSuccess, markStarAction]; + List get props => [countMarkStarSuccess, markStarAction, successEmailIds]; } class MarkAsStarMultipleEmailFailure extends FeatureFailure { diff --git a/lib/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart b/lib/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart index 0eccaae504..8e8a98bf55 100644 --- a/lib/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart +++ b/lib/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart @@ -27,6 +27,7 @@ class GetEmailsInMailboxInteractor { Properties? propertiesCreated, Properties? propertiesUpdated, bool getLatestChanges = true, + bool skipCache = false, } ) async* { try { @@ -41,7 +42,8 @@ class GetEmailsInMailboxInteractor { emailFilter: emailFilter, propertiesCreated: propertiesCreated, propertiesUpdated: propertiesUpdated, - getLatestChanges: getLatestChanges) + getLatestChanges: getLatestChanges, + skipCache: skipCache) .map((emailResponse) => _toGetEmailState( emailResponse: emailResponse, currentMailboxId: emailFilter?.mailboxId diff --git a/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart b/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart index 67299d960e..5bd0763619 100644 --- a/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart +++ b/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart @@ -4,6 +4,7 @@ import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; @@ -17,7 +18,8 @@ class MarkAsMultipleEmailReadInteractor { Session session, AccountId accountId, List emailIds, - ReadActions readAction + ReadActions readAction, + MailboxId? mailboxId, ) async* { try { yield Right(LoadingMarkAsMultipleEmailReadAll()); @@ -31,15 +33,17 @@ class MarkAsMultipleEmailReadInteractor { if (emailIds.length == result.emailIdsSuccess.length) { yield Right(MarkAsMultipleEmailReadAllSuccess( - result.emailIdsSuccess.length, - readAction, + result.emailIdsSuccess, + readAction, + mailboxId, )); } else if (result.emailIdsSuccess.isEmpty) { yield Left(MarkAsMultipleEmailReadAllFailure(readAction)); } else { yield Right(MarkAsMultipleEmailReadHasSomeEmailFailure( - result.emailIdsSuccess.length, - readAction, + result.emailIdsSuccess, + readAction, + mailboxId, )); } } catch (e) { diff --git a/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart b/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart index 8ac2e00b4b..980a1ff967 100644 --- a/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart +++ b/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart @@ -26,15 +26,17 @@ class MarkAsStarMultipleEmailInteractor { if (emailIds.length == result.emailIdsSuccess.length) { yield Right(MarkAsStarMultipleEmailAllSuccess( - emailIds.length, - markStarAction, + emailIds.length, + markStarAction, + result.emailIdsSuccess, )); } else if (result.emailIdsSuccess.isEmpty) { yield Left(MarkAsStarMultipleEmailAllFailure(markStarAction)); } else { yield Right(MarkAsStarMultipleEmailHasSomeEmailFailure( - result.emailIdsSuccess.length, - markStarAction, + result.emailIdsSuccess.length, + markStarAction, + result.emailIdsSuccess, )); } } catch (e) { diff --git a/lib/features/thread/presentation/mixin/email_action_controller.dart b/lib/features/thread/presentation/mixin/email_action_controller.dart index baefee994b..04e2f12dd4 100644 --- a/lib/features/thread/presentation/mixin/email_action_controller.dart +++ b/lib/features/thread/presentation/mixin/email_action_controller.dart @@ -233,6 +233,7 @@ mixin EmailActionController { presentationEmail.id!, readActions, markReadAction, + presentationEmail.mailboxContain?.mailboxId, ); } diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 9b87815818..1a07186387 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -20,11 +20,15 @@ import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; +import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart' as search; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart'; @@ -45,6 +49,8 @@ import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_email_by_id_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/load_more_emails_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/mark_as_star_multiple_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/refresh_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/refresh_changes_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; @@ -331,6 +337,59 @@ class ThreadController extends BaseController with EmailActionController { mailboxDashBoardController.clearEmailUIAction(); } }); + + ever(mailboxDashBoardController.viewState, (viewState) { + final reactionState = viewState.getOrElse(() => UIState.idle); + if (reactionState is MarkAsEmailReadSuccess) { + mailboxDashBoardController.updateEmailFlagByEmailIds( + [reactionState.emailId], + readAction: reactionState.readActions, + ); + } else if (reactionState is MarkAsMultipleEmailReadAllSuccess) { + mailboxDashBoardController.updateEmailFlagByEmailIds( + reactionState.emailIds, + readAction: reactionState.readActions, + ); + } else if (reactionState is MarkAsMultipleEmailReadHasSomeEmailFailure) { + mailboxDashBoardController.updateEmailFlagByEmailIds( + reactionState.successEmailIds, + readAction: reactionState.readActions, + ); + } else if (reactionState is MarkAsMailboxReadAllSuccess) { + _handleMarkEmailsAsReadByMailboxId(reactionState.mailboxId); + } else if (reactionState is MarkAsMailboxReadHasSomeEmailFailure) { + mailboxDashBoardController.updateEmailFlagByEmailIds( + reactionState.successEmailIds, + readAction: ReadActions.markAsRead, + ); + } else if (reactionState is MarkAsStarEmailSuccess) { + mailboxDashBoardController.updateEmailFlagByEmailIds( + [reactionState.emailId], + markStarAction: reactionState.markStarAction, + ); + } else if (reactionState is MarkAsStarMultipleEmailAllSuccess) { + mailboxDashBoardController.updateEmailFlagByEmailIds( + reactionState.emailIds, + markStarAction: reactionState.markStarAction, + ); + } else if (reactionState is MarkAsStarMultipleEmailHasSomeEmailFailure) { + mailboxDashBoardController.updateEmailFlagByEmailIds( + reactionState.successEmailIds, + markStarAction: reactionState.markStarAction, + ); + } + }); + } + + void _handleMarkEmailsAsReadByMailboxId(MailboxId mailboxId) { + if (mailboxDashBoardController.selectedMailbox.value?.id != mailboxId) return; + + for (var presentationEmail in mailboxDashBoardController.emailsInCurrentMailbox) { + if (presentationEmail.mailboxContain?.id != mailboxId) continue; + + presentationEmail.keywords?[KeyWordIdentifier.emailSeen] = true; + } + mailboxDashBoardController.emailsInCurrentMailbox.refresh(); } void _registerBrowserResizeListener() { @@ -444,7 +503,10 @@ class ThreadController extends BaseController with EmailActionController { } } - void _getAllEmailAction({bool getLatestChanges = true}) { + void _getAllEmailAction({ + bool getLatestChanges = true, + bool skipCache = false, + }) { log('ThreadController::_getAllEmailAction:'); if (_session != null &&_accountId != null) { consumeState(_getEmailsInMailboxInteractor.execute( @@ -460,6 +522,7 @@ class ThreadController extends BaseController with EmailActionController { propertiesCreated: EmailUtils.getPropertiesForEmailGetMethod(_session!, _accountId!), propertiesUpdated: ThreadConstants.propertiesUpdatedDefault, getLatestChanges: getLatestChanges, + skipCache: skipCache, )); } else { consumeState(Stream.value(Left(GetAllEmailFailure(NotFoundSessionException())))); @@ -508,7 +571,7 @@ class ThreadController extends BaseController with EmailActionController { if (searchController.isSearchEmailRunning) { _searchEmail(limit: limitEmailFetched); } else { - _getAllEmailAction(); + _getAllEmailAction(skipCache: true); } } @@ -792,6 +855,9 @@ class ThreadController extends BaseController with EmailActionController { } void cancelSelectEmail() { + if (mailboxDashBoardController.currentSelectMode.value == SelectMode.INACTIVE) { + return; + } final newEmailList = mailboxDashBoardController.emailsInCurrentMailbox .map((email) => email.toSelectedEmail(selectMode: SelectMode.INACTIVE)) .toList(); diff --git a/test/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension_test.dart b/test/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension_test.dart new file mode 100644 index 0000000000..39fcfe770c --- /dev/null +++ b/test/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension_test.dart @@ -0,0 +1,150 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:model/email/mark_star_action.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:model/email/read_actions.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart'; + +import 'update_current_emails_flags_extension_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + const numberOfEmails = 3; + late List emailIds; + final mailboxDashBoardController = MockMailboxDashBoardController(); + + setUp(() { + emailIds = List.generate( + numberOfEmails, + (index) => EmailId(Id('email-id-$index')), + ); + }); + + group('updateEmailFlagByEmailIds test:', () { + test( + 'should mark emails as read', + () { + // arrange + final readEmailIds = emailIds.sublist(1); + when(mailboxDashBoardController.emailsInCurrentMailbox).thenReturn( + emailIds.map((emailId) => PresentationEmail( + id: emailId, + keywords: {}, + )).toList().obs, + ); + expect( + mailboxDashBoardController.emailsInCurrentMailbox.every( + (presentationEmail) => !presentationEmail.hasRead, + ), + true, + ); + + // act + mailboxDashBoardController.updateEmailFlagByEmailIds( + readEmailIds, + readAction: ReadActions.markAsRead, + ); + + // assert + expect(mailboxDashBoardController.emailsInCurrentMailbox[0].hasRead, false); + expect(mailboxDashBoardController.emailsInCurrentMailbox[1].hasRead, true); + expect(mailboxDashBoardController.emailsInCurrentMailbox[2].hasRead, true); + }); + + test( + 'should mark emails as unread', + () { + // arrange + final unreadEmailIds = emailIds.sublist(1); + when(mailboxDashBoardController.emailsInCurrentMailbox).thenReturn( + emailIds.map((emailId) => PresentationEmail( + id: emailId, + keywords: {KeyWordIdentifier.emailSeen: true}, + )).toList().obs, + ); + expect( + mailboxDashBoardController.emailsInCurrentMailbox.every( + (presentationEmail) => presentationEmail.hasRead, + ), + true, + ); + + // act + mailboxDashBoardController.updateEmailFlagByEmailIds( + unreadEmailIds, + readAction: ReadActions.markAsUnread, + ); + + // assert + expect(mailboxDashBoardController.emailsInCurrentMailbox[0].hasRead, true); + expect(mailboxDashBoardController.emailsInCurrentMailbox[1].hasRead, false); + expect(mailboxDashBoardController.emailsInCurrentMailbox[2].hasRead, false); + }); + + test( + 'should mark emails as starred', + () { + // arrange + final starredEmailIds = emailIds.sublist(1); + when(mailboxDashBoardController.emailsInCurrentMailbox).thenReturn( + emailIds.map((emailId) => PresentationEmail( + id: emailId, + keywords: {}, + )).toList().obs, + ); + expect( + mailboxDashBoardController.emailsInCurrentMailbox.every( + (presentationEmail) => !presentationEmail.hasStarred, + ), + true, + ); + + // act + mailboxDashBoardController.updateEmailFlagByEmailIds( + starredEmailIds, + markStarAction: MarkStarAction.markStar, + ); + + // assert + expect(mailboxDashBoardController.emailsInCurrentMailbox[0].hasStarred, false); + expect(mailboxDashBoardController.emailsInCurrentMailbox[1].hasStarred, true); + expect(mailboxDashBoardController.emailsInCurrentMailbox[2].hasStarred, true); + }); + + test( + 'should mark emails as unstarred', + () { + // arrange + final unstarredEmailIds = emailIds.sublist(1); + when(mailboxDashBoardController.emailsInCurrentMailbox).thenReturn( + emailIds.map((emailId) => PresentationEmail( + id: emailId, + keywords: {KeyWordIdentifier.emailFlagged: true}, + )).toList().obs, + ); + expect( + mailboxDashBoardController.emailsInCurrentMailbox.every( + (presentationEmail) => presentationEmail.hasStarred, + ), + true, + ); + + // act + mailboxDashBoardController.updateEmailFlagByEmailIds( + unstarredEmailIds, + markStarAction: MarkStarAction.unMarkStar, + ); + + // assert + expect(mailboxDashBoardController.emailsInCurrentMailbox[0].hasStarred, true); + expect(mailboxDashBoardController.emailsInCurrentMailbox[1].hasStarred, false); + expect(mailboxDashBoardController.emailsInCurrentMailbox[2].hasStarred, false); + }); + }); +} \ No newline at end of file From 7fa1a2d4932a72340f12ffa1530e5b7a1bbd5e33 Mon Sep 17 00:00:00 2001 From: DatDang Date: Tue, 31 Dec 2024 16:16:36 +0700 Subject: [PATCH 49/72] TF-3385 Update recover deleted emails --- .../mailbox/presentation/mailbox_controller.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 79e462fe0c..3e4aff9e45 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -20,6 +20,7 @@ import 'package:tmail_ui_user/features/base/base_mailbox_controller.dart'; import 'package:tmail_ui_user/features/base/mixin/contact_support_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/mailbox_action_handler_mixin.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; +import 'package:tmail_ui_user/features/email/domain/state/get_restored_deleted_message_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; @@ -300,6 +301,15 @@ class MailboxController extends BaseMailboxController affectedMailboxId: reactionState.mailboxId, readCount: reactionState.successEmailIds.length, ); + } else if (reactionState is GetRestoredDeletedMessageCompleted) { + _handleMarkEmailsAsReadOrUnread( + affectedMailboxId: reactionState.recoveredMailbox?.id, + unreadCount: reactionState + .emailRecoveryAction + .successfulRestoreCount + ?.value + .toInt() ?? 0, + ); } }); } From 7e71eb11bec1b280b656712918d4e35b04b3c182 Mon Sep 17 00:00:00 2001 From: DatDang Date: Thu, 2 Jan 2025 08:55:08 +0700 Subject: [PATCH 50/72] TF-3385 Update save and remove draft email --- .../base/base_mailbox_controller.dart | 15 ++++++++++++ .../state/save_email_as_drafts_state.dart | 6 +++-- ...w_and_save_email_to_drafts_interactor.dart | 5 +++- .../presentation/mailbox_controller.dart | 24 +++++++++++++++++++ .../presentation/model/mailbox_tree.dart | 14 +++++++++++ .../state/remove_email_drafts_state.dart | 10 +++++++- .../remove_email_drafts_interactor.dart | 10 ++++++-- .../mailbox_dashboard_controller.dart | 11 ++++++--- .../composer_controller_test.dart | 4 ++-- 9 files changed, 88 insertions(+), 11 deletions(-) diff --git a/lib/features/base/base_mailbox_controller.dart b/lib/features/base/base_mailbox_controller.dart index 62ba44dc2d..73e6194155 100644 --- a/lib/features/base/base_mailbox_controller.dart +++ b/lib/features/base/base_mailbox_controller.dart @@ -605,4 +605,19 @@ abstract class BaseMailboxController extends BaseController { break; } } + + void updateMailboxTotalEmailsCountById(MailboxId mailboxId, int totalEmails) { + final mailboxTrees = [ + defaultMailboxTree, + personalMailboxTree, + teamMailboxesTree, + ]; + + for (var mailboxTree in mailboxTrees) { + if (mailboxTree.value.updateMailboxTotalEmailsCountById(mailboxId, totalEmails)) { + mailboxTree.refresh(); + break; + } + } + } } \ No newline at end of file diff --git a/lib/features/composer/domain/state/save_email_as_drafts_state.dart b/lib/features/composer/domain/state/save_email_as_drafts_state.dart index 1eddfdeeae..e9e9f7682b 100644 --- a/lib/features/composer/domain/state/save_email_as_drafts_state.dart +++ b/lib/features/composer/domain/state/save_email_as_drafts_state.dart @@ -1,17 +1,19 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; class SaveEmailAsDraftsLoading extends LoadingState {} class SaveEmailAsDraftsSuccess extends UIState { final EmailId emailId; + final MailboxId? draftMailboxId; - SaveEmailAsDraftsSuccess(this.emailId); + SaveEmailAsDraftsSuccess(this.emailId, this.draftMailboxId); @override - List get props => [emailId, ...super.props]; + List get props => [emailId, draftMailboxId, ...super.props]; } class SaveEmailAsDraftsFailure extends FeatureFailure { diff --git a/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart b/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart index ac651fc187..65a3bd226f 100644 --- a/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart +++ b/lib/features/composer/domain/usecases/create_new_and_save_email_to_drafts_interactor.dart @@ -44,7 +44,10 @@ class CreateNewAndSaveEmailToDraftsInteractor { ); yield dartz.Right( - SaveEmailAsDraftsSuccess(emailDraftSaved.id!) + SaveEmailAsDraftsSuccess( + emailDraftSaved.id!, + createEmailRequest.draftsMailboxId, + ) ); } else { yield dartz.Right(UpdatingEmailDrafts()); diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 3e4aff9e45..80f9146255 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -19,6 +19,7 @@ import 'package:rxdart/transformers.dart'; import 'package:tmail_ui_user/features/base/base_mailbox_controller.dart'; import 'package:tmail_ui_user/features/base/mixin/contact_support_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/mailbox_action_handler_mixin.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_restored_deleted_message_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; @@ -66,6 +67,7 @@ import 'package:tmail_ui_user/features/mailbox/presentation/utils/mailbox_utils. import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_creator/presentation/model/mailbox_creator_arguments.dart'; import 'package:tmail_ui_user/features/mailbox_creator/presentation/model/new_mailbox_arguments.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.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'; @@ -310,6 +312,16 @@ class MailboxController extends BaseMailboxController ?.value .toInt() ?? 0, ); + } else if (reactionState is SaveEmailAsDraftsSuccess) { + _handleDraftSaved( + affectedMailboxId: reactionState.draftMailboxId, + totalEmailsChanged: 1, + ); + } else if (reactionState is RemoveEmailDraftsSuccess) { + _handleDraftSaved( + affectedMailboxId: reactionState.draftMailboxId, + totalEmailsChanged: -1, + ); } }); } @@ -335,6 +347,18 @@ class MailboxController extends BaseMailboxController clearUnreadCount(affectedMailboxId); } + void _handleDraftSaved({ + required MailboxId? affectedMailboxId, + required int totalEmailsChanged, + }) { + if (affectedMailboxId == null) return; + + updateMailboxTotalEmailsCountById( + affectedMailboxId, + totalEmailsChanged, + ); + } + void _initWebSocketQueueHandler() { _webSocketQueueHandler = WebSocketQueueHandler( processMessageCallback: _handleWebSocketMessage, diff --git a/lib/features/mailbox/presentation/model/mailbox_tree.dart b/lib/features/mailbox/presentation/model/mailbox_tree.dart index 97bff1f3f8..2b1b54ecc7 100644 --- a/lib/features/mailbox/presentation/model/mailbox_tree.dart +++ b/lib/features/mailbox/presentation/model/mailbox_tree.dart @@ -107,6 +107,20 @@ class MailboxTree with EquatableMixin { return false; } + bool updateMailboxTotalEmailsCountById(MailboxId mailboxId, int totalEmailsCount) { + final matchedNode = findNode((node) => node.item.id == mailboxId); + if (matchedNode != null) { + final currentTotalEmailsCount = matchedNode.item.totalEmails?.value.value ?? 0; + final updatedTotalEmailsCount = currentTotalEmailsCount + totalEmailsCount; + if (updatedTotalEmailsCount < 0) return true; + matchedNode.item = matchedNode.item.copyWith( + totalEmails: TotalEmails(UnsignedInt(updatedTotalEmailsCount)), + ); + return true; + } + return false; + } + String? getNodePath(MailboxId mailboxId) { final matchedNode = findNode((node) => node.item.id == mailboxId); if (matchedNode == null) { diff --git a/lib/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart b/lib/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart index 5c79d586bd..f783378518 100644 --- a/lib/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart +++ b/lib/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart @@ -1,7 +1,15 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -class RemoveEmailDraftsSuccess extends UIState {} +class RemoveEmailDraftsSuccess extends UIState { + final MailboxId? draftMailboxId; + + RemoveEmailDraftsSuccess(this.draftMailboxId); + + @override + List get props => [draftMailboxId]; +} class RemoveEmailDraftsFailure extends FeatureFailure { diff --git a/lib/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart b/lib/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart index a832a29a1b..e0894477c7 100644 --- a/lib/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart +++ b/lib/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart @@ -4,6 +4,7 @@ import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart'; @@ -12,11 +13,16 @@ class RemoveEmailDraftsInteractor { RemoveEmailDraftsInteractor(this._emailRepository); - Stream> execute(Session session, AccountId accountId, EmailId emailId) async* { + Stream> execute( + Session session, + AccountId accountId, + EmailId emailId, + MailboxId? draftMailboxId, + ) async* { try { final result = await _emailRepository.removeEmailDrafts(session, accountId, emailId); if (result) { - yield Right(RemoveEmailDraftsSuccess()); + yield Right(RemoveEmailDraftsSuccess(draftMailboxId)); } else { yield Left(RemoveEmailDraftsFailure(result)); } diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index b2791f0644..c0862c726e 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -811,7 +811,7 @@ class MailboxDashBoardController extends ReloadableController currentOverlayContext!, AppLocalizations.of(currentContext!).drafts_saved, actionName: AppLocalizations.of(currentContext!).discard, - onActionClick: () => _discardEmail(success.emailId), + onActionClick: () => _discardEmail(success.emailId, success.draftMailboxId), leadingSVGIcon: imagePaths.icMailboxDrafts, leadingSVGIconColor: Colors.white, backgroundColor: AppColor.toastSuccessBackgroundColor, @@ -854,11 +854,16 @@ class MailboxDashBoardController extends ReloadableController } } - void _discardEmail(EmailId emailId) { + void _discardEmail(EmailId emailId, MailboxId? draftMailboxId) { final currentAccountId = accountId.value; final session = sessionCurrent; if (currentAccountId != null && session != null) { - consumeState(_removeEmailDraftsInteractor.execute(session, currentAccountId, emailId)); + consumeState(_removeEmailDraftsInteractor.execute( + session, + currentAccountId, + emailId, + draftMailboxId, + )); } } diff --git a/test/features/composer/presentation/composer_controller_test.dart b/test/features/composer/presentation/composer_controller_test.dart index fa4e11ed4d..6315f219e6 100644 --- a/test/features/composer/presentation/composer_controller_test.dart +++ b/test/features/composer/presentation/composer_controller_test.dart @@ -591,7 +591,7 @@ void main() { createEmailRequest: anyNamed('createEmailRequest'), cancelToken: anyNamed('cancelToken'))) .thenAnswer((_) => Stream.value( - Right(SaveEmailAsDraftsSuccess(EmailId(Id('123')))))); + Right(SaveEmailAsDraftsSuccess(EmailId(Id('123')), null)))); final savedEmailDraft = SavedEmailDraft( content: emailContent, @@ -1039,7 +1039,7 @@ void main() { createEmailRequest: anyNamed('createEmailRequest'), cancelToken: anyNamed('cancelToken'))) .thenAnswer((_) => Stream.value( - Right(SaveEmailAsDraftsSuccess(EmailId(Id('123')))))); + Right(SaveEmailAsDraftsSuccess(EmailId(Id('123')), null)))); final savedEmailDraft = SavedEmailDraft( content: emailContent, From cb50c00e60dcfd36a293803bcae3d11296deec9d Mon Sep 17 00:00:00 2001 From: DatDang Date: Thu, 2 Jan 2025 09:50:20 +0700 Subject: [PATCH 51/72] TF-3385 Update permanently delete emails --- .../state/delete_email_permanently_state.dart | 12 +++++++- ...ete_multiple_emails_permanently_state.dart | 11 ++++--- .../delete_email_permanently_interactor.dart | 10 +++++-- ...ultiple_emails_permanently_interactor.dart | 12 ++++++-- .../presentation/mailbox_controller.dart | 29 +++++++++++++++++++ .../mailbox_dashboard_controller.dart | 10 +++++-- .../delete_emails_in_mailbox_extension.dart | 16 ++++++++++ .../presentation/search_email_controller.dart | 18 ++++++++++++ .../presentation/thread_controller.dart | 18 ++++++++++++ 9 files changed, 124 insertions(+), 12 deletions(-) create mode 100644 lib/features/mailbox_dashboard/presentation/extensions/delete_emails_in_mailbox_extension.dart diff --git a/lib/features/email/domain/state/delete_email_permanently_state.dart b/lib/features/email/domain/state/delete_email_permanently_state.dart index 22f2a0a6c4..21f596ef30 100644 --- a/lib/features/email/domain/state/delete_email_permanently_state.dart +++ b/lib/features/email/domain/state/delete_email_permanently_state.dart @@ -1,9 +1,19 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; class StartDeleteEmailPermanently extends UIState {} -class DeleteEmailPermanentlySuccess extends UIState {} +class DeleteEmailPermanentlySuccess extends UIState { + final EmailId emailId; + final MailboxId? mailboxId; + + DeleteEmailPermanentlySuccess(this.emailId, this.mailboxId); + + @override + List get props => [emailId, mailboxId]; +} class DeleteEmailPermanentlyFailure extends FeatureFailure { diff --git a/lib/features/email/domain/state/delete_multiple_emails_permanently_state.dart b/lib/features/email/domain/state/delete_multiple_emails_permanently_state.dart index 07ac090d6c..b091224989 100644 --- a/lib/features/email/domain/state/delete_multiple_emails_permanently_state.dart +++ b/lib/features/email/domain/state/delete_multiple_emails_permanently_state.dart @@ -1,27 +1,30 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; class LoadingDeleteMultipleEmailsPermanentlyAll extends UIState {} class DeleteMultipleEmailsPermanentlyAllSuccess extends UIState { List emailIds; + final MailboxId? mailboxId; - DeleteMultipleEmailsPermanentlyAllSuccess(this.emailIds); + DeleteMultipleEmailsPermanentlyAllSuccess(this.emailIds, this.mailboxId); @override - List get props => [emailIds]; + List get props => [emailIds, mailboxId]; } class DeleteMultipleEmailsPermanentlyHasSomeEmailFailure extends UIState { List emailIds; + final MailboxId? mailboxId; - DeleteMultipleEmailsPermanentlyHasSomeEmailFailure(this.emailIds); + DeleteMultipleEmailsPermanentlyHasSomeEmailFailure(this.emailIds, this.mailboxId); @override - List get props => [emailIds]; + List get props => [emailIds, mailboxId]; } class DeleteMultipleEmailsPermanentlyAllFailure extends FeatureFailure {} diff --git a/lib/features/email/domain/usecases/delete_email_permanently_interactor.dart b/lib/features/email/domain/usecases/delete_email_permanently_interactor.dart index 20260256ee..8e874d33ab 100644 --- a/lib/features/email/domain/usecases/delete_email_permanently_interactor.dart +++ b/lib/features/email/domain/usecases/delete_email_permanently_interactor.dart @@ -4,6 +4,7 @@ import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanently_state.dart'; @@ -12,12 +13,17 @@ class DeleteEmailPermanentlyInteractor { DeleteEmailPermanentlyInteractor(this._emailRepository); - Stream> execute(Session session, AccountId accountId, EmailId emailId) async* { + Stream> execute( + Session session, + AccountId accountId, + EmailId emailId, + MailboxId? mailboxId, + ) async* { try { yield Right(StartDeleteEmailPermanently()); final result = await _emailRepository.deleteEmailPermanently(session, accountId, emailId); if (result) { - yield Right(DeleteEmailPermanentlySuccess()); + yield Right(DeleteEmailPermanentlySuccess(emailId, mailboxId)); } else { yield Left(DeleteEmailPermanentlyFailure(null)); } diff --git a/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart b/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart index b8195c8e60..3510c2bec2 100644 --- a/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart +++ b/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart @@ -4,6 +4,7 @@ import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails_permanently_state.dart'; @@ -12,14 +13,19 @@ class DeleteMultipleEmailsPermanentlyInteractor { DeleteMultipleEmailsPermanentlyInteractor(this._emailRepository); - Stream> execute(Session session, AccountId accountId, List emailIds) async* { + Stream> execute( + Session session, + AccountId accountId, + List emailIds, + MailboxId? mailboxId, + ) async* { try { yield Right(LoadingDeleteMultipleEmailsPermanentlyAll()); final listResult = await _emailRepository.deleteMultipleEmailsPermanently(session, accountId, emailIds); if (listResult.emailIdsSuccess.length == emailIds.length) { - yield Right(DeleteMultipleEmailsPermanentlyAllSuccess(listResult.emailIdsSuccess)); + yield Right(DeleteMultipleEmailsPermanentlyAllSuccess(listResult.emailIdsSuccess, mailboxId)); } else if (listResult.emailIdsSuccess.isNotEmpty) { - yield Right(DeleteMultipleEmailsPermanentlyHasSomeEmailFailure(listResult.emailIdsSuccess)); + yield Right(DeleteMultipleEmailsPermanentlyHasSomeEmailFailure(listResult.emailIdsSuccess, mailboxId)); } else { yield Left(DeleteMultipleEmailsPermanentlyAllFailure()); } diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 80f9146255..f50120708a 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -21,6 +21,8 @@ import 'package:tmail_ui_user/features/base/mixin/contact_support_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/mailbox_action_handler_mixin.dart'; import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; +import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanently_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails_permanently_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_restored_deleted_message_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; @@ -322,6 +324,21 @@ class MailboxController extends BaseMailboxController affectedMailboxId: reactionState.draftMailboxId, totalEmailsChanged: -1, ); + } else if (reactionState is DeleteEmailPermanentlySuccess) { + _handleDeleteEmailsFromMailbox( + affectedMailboxId: reactionState.mailboxId, + totalEmailsChanged: -1, + ); + } else if (reactionState is DeleteMultipleEmailsPermanentlyAllSuccess) { + _handleDeleteEmailsFromMailbox( + affectedMailboxId: reactionState.mailboxId, + totalEmailsChanged: -reactionState.emailIds.length, + ); + } else if (reactionState is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure) { + _handleDeleteEmailsFromMailbox( + affectedMailboxId: reactionState.mailboxId, + totalEmailsChanged: -reactionState.emailIds.length, + ); } }); } @@ -359,6 +376,18 @@ class MailboxController extends BaseMailboxController ); } + void _handleDeleteEmailsFromMailbox({ + required MailboxId? affectedMailboxId, + required int totalEmailsChanged, + }) { + if (affectedMailboxId == null) return; + + updateMailboxTotalEmailsCountById( + affectedMailboxId, + totalEmailsChanged, + ); + } + void _initWebSocketQueueHandler() { _webSocketQueueHandler = WebSocketQueueHandler( processMessageCallback: _handleWebSocketMessage, diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index c0862c726e..b9a89e6552 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -871,7 +871,12 @@ class MailboxDashBoardController extends ReloadableController final currentAccountId = accountId.value; final session = sessionCurrent; if (currentAccountId != null && session != null && email.id != null) { - consumeState(_deleteEmailPermanentlyInteractor.execute(session, currentAccountId, email.id!)); + consumeState(_deleteEmailPermanentlyInteractor.execute( + session, + currentAccountId, + email.id!, + email.mailboxContain?.mailboxId, + )); } } @@ -1434,7 +1439,8 @@ class MailboxDashBoardController extends ReloadableController consumeState(_deleteMultipleEmailsPermanentlyInteractor.execute( sessionCurrent!, accountId.value!, - listEmails.listEmailIds)); + listEmails.listEmailIds, + listEmails.firstOrNull?.mailboxContain?.mailboxId)); } } diff --git a/lib/features/mailbox_dashboard/presentation/extensions/delete_emails_in_mailbox_extension.dart b/lib/features/mailbox_dashboard/presentation/extensions/delete_emails_in_mailbox_extension.dart new file mode 100644 index 0000000000..6a542df58b --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/extensions/delete_emails_in_mailbox_extension.dart @@ -0,0 +1,16 @@ +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; + +extension DeleteEmailsInMailboxExtension on MailboxDashBoardController { + void handleDeleteEmailsInMailbox({ + required List emailIds, + required MailboxId? affectedMailboxId, + }) { + if (selectedMailbox.value?.id != affectedMailboxId) { + return; + } + + emailsInCurrentMailbox.removeWhere((email) => emailIds.contains(email.id)); + } +} \ No newline at end of file diff --git a/lib/features/search/email/presentation/search_email_controller.dart b/lib/features/search/email/presentation/search_email_controller.dart index 4d088d6740..769b8d4232 100644 --- a/lib/features/search/email/presentation/search_email_controller.dart +++ b/lib/features/search/email/presentation/search_email_controller.dart @@ -33,6 +33,8 @@ import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_e import 'package:tmail_ui_user/features/contact/presentation/model/contact_arguments.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; +import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanently_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails_permanently_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; @@ -46,6 +48,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_all import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/quick_search_email_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_recent_search_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/delete_emails_in_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; @@ -298,6 +301,21 @@ class SearchEmailController extends BaseController reactionState.successEmailIds, markStarAction: reactionState.markStarAction, ); + } else if (reactionState is DeleteEmailPermanentlySuccess) { + mailboxDashBoardController.handleDeleteEmailsInMailbox( + emailIds: [reactionState.emailId], + affectedMailboxId: reactionState.mailboxId, + ); + } else if (reactionState is DeleteMultipleEmailsPermanentlyAllSuccess) { + mailboxDashBoardController.handleDeleteEmailsInMailbox( + emailIds: reactionState.emailIds, + affectedMailboxId: reactionState.mailboxId, + ); + } else if (reactionState is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure) { + mailboxDashBoardController.handleDeleteEmailsInMailbox( + emailIds: reactionState.emailIds, + affectedMailboxId: reactionState.mailboxId, + ); } }); } diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 1a07186387..5b7a94d8db 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -20,6 +20,8 @@ import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; +import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanently_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails_permanently_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; @@ -28,6 +30,7 @@ import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.d import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart' as search; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/delete_emails_in_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart'; @@ -377,6 +380,21 @@ class ThreadController extends BaseController with EmailActionController { reactionState.successEmailIds, markStarAction: reactionState.markStarAction, ); + } else if (reactionState is DeleteEmailPermanentlySuccess) { + mailboxDashBoardController.handleDeleteEmailsInMailbox( + emailIds: [reactionState.emailId], + affectedMailboxId: reactionState.mailboxId, + ); + } else if (reactionState is DeleteMultipleEmailsPermanentlyAllSuccess) { + mailboxDashBoardController.handleDeleteEmailsInMailbox( + emailIds: reactionState.emailIds, + affectedMailboxId: reactionState.mailboxId, + ); + } else if (reactionState is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure) { + mailboxDashBoardController.handleDeleteEmailsInMailbox( + emailIds: reactionState.emailIds, + affectedMailboxId: reactionState.mailboxId, + ); } }); } From 76388ab7a690c9a94c604309640dc5e877b1252c Mon Sep 17 00:00:00 2001 From: DatDang Date: Thu, 2 Jan 2025 09:57:56 +0700 Subject: [PATCH 52/72] TF-3385 Update empty trash and spam emails --- .../mailbox/presentation/mailbox_controller.dart | 12 ++++++++++++ .../email/presentation/search_email_controller.dart | 12 ++++++++++++ .../thread/domain/state/empty_spam_folder_state.dart | 5 +++-- .../domain/state/empty_trash_folder_state.dart | 6 ++++-- .../usecases/empty_spam_folder_interactor.dart | 2 +- .../usecases/empty_trash_folder_interactor.dart | 2 +- .../thread/presentation/thread_controller.dart | 12 ++++++++++++ 7 files changed, 45 insertions(+), 6 deletions(-) diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index f50120708a..4bc677cbcf 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -77,6 +77,8 @@ import 'package:tmail_ui_user/features/push_notification/presentation/websocket/ import 'package:tmail_ui_user/features/push_notification/presentation/websocket/web_socket_queue_handler.dart'; import 'package:tmail_ui_user/features/search/mailbox/presentation/search_mailbox_bindings.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; @@ -339,6 +341,16 @@ class MailboxController extends BaseMailboxController affectedMailboxId: reactionState.mailboxId, totalEmailsChanged: -reactionState.emailIds.length, ); + } else if (reactionState is EmptyTrashFolderSuccess) { + _handleDeleteEmailsFromMailbox( + affectedMailboxId: reactionState.mailboxId, + totalEmailsChanged: -reactionState.emailIds.length, + ); + } else if (reactionState is EmptySpamFolderSuccess) { + _handleDeleteEmailsFromMailbox( + affectedMailboxId: reactionState.mailboxId, + totalEmailsChanged: -reactionState.emailIds.length, + ); } }); } diff --git a/lib/features/search/email/presentation/search_email_controller.dart b/lib/features/search/email/presentation/search_email_controller.dart index 769b8d4232..edf34f0a0b 100644 --- a/lib/features/search/email/presentation/search_email_controller.dart +++ b/lib/features/search/email/presentation/search_email_controller.dart @@ -66,6 +66,8 @@ import 'package:tmail_ui_user/features/search/email/presentation/model/search_mo import 'package:tmail_ui_user/features/search/email/presentation/search_email_bindings.dart'; import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_as_star_multiple_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; @@ -316,6 +318,16 @@ class SearchEmailController extends BaseController emailIds: reactionState.emailIds, affectedMailboxId: reactionState.mailboxId, ); + } else if (reactionState is EmptyTrashFolderSuccess) { + mailboxDashBoardController.handleDeleteEmailsInMailbox( + emailIds: reactionState.emailIds, + affectedMailboxId: reactionState.mailboxId, + ); + } else if (reactionState is EmptySpamFolderSuccess) { + mailboxDashBoardController.handleDeleteEmailsInMailbox( + emailIds: reactionState.emailIds, + affectedMailboxId: reactionState.mailboxId, + ); } }); } diff --git a/lib/features/thread/domain/state/empty_spam_folder_state.dart b/lib/features/thread/domain/state/empty_spam_folder_state.dart index 6823c8f1b8..3a53ff3f1d 100644 --- a/lib/features/thread/domain/state/empty_spam_folder_state.dart +++ b/lib/features/thread/domain/state/empty_spam_folder_state.dart @@ -8,11 +8,12 @@ class EmptySpamFolderLoading extends LoadingState {} class EmptySpamFolderSuccess extends UIState { final List emailIds; + final MailboxId? mailboxId; - EmptySpamFolderSuccess(this.emailIds); + EmptySpamFolderSuccess(this.emailIds, this.mailboxId); @override - List get props => [emailIds]; + List get props => [emailIds, mailboxId]; } class EmptySpamFolderFailure extends FeatureFailure { diff --git a/lib/features/thread/domain/state/empty_trash_folder_state.dart b/lib/features/thread/domain/state/empty_trash_folder_state.dart index 577eb6d7cc..0d04ce4a27 100644 --- a/lib/features/thread/domain/state/empty_trash_folder_state.dart +++ b/lib/features/thread/domain/state/empty_trash_folder_state.dart @@ -1,17 +1,19 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; class EmptyTrashFolderLoading extends LoadingState {} class EmptyTrashFolderSuccess extends UIState { final List emailIds; + final MailboxId? mailboxId; - EmptyTrashFolderSuccess(this.emailIds); + EmptyTrashFolderSuccess(this.emailIds, this.mailboxId); @override - List get props => [emailIds]; + List get props => [emailIds, mailboxId]; } class EmptyTrashFolderFailure extends FeatureFailure { diff --git a/lib/features/thread/domain/usecases/empty_spam_folder_interactor.dart b/lib/features/thread/domain/usecases/empty_spam_folder_interactor.dart index 0093f76447..0eb7d07d8e 100644 --- a/lib/features/thread/domain/usecases/empty_spam_folder_interactor.dart +++ b/lib/features/thread/domain/usecases/empty_spam_folder_interactor.dart @@ -32,7 +32,7 @@ class EmptySpamFolderInteractor { totalEmails, onProgressController ); - yield Right(EmptySpamFolderSuccess(emailIdDeleted)); + yield Right(EmptySpamFolderSuccess(emailIdDeleted, spamMailboxId)); } catch (e) { yield Left(EmptySpamFolderFailure(e)); } diff --git a/lib/features/thread/domain/usecases/empty_trash_folder_interactor.dart b/lib/features/thread/domain/usecases/empty_trash_folder_interactor.dart index d2474096f5..c3764860b3 100644 --- a/lib/features/thread/domain/usecases/empty_trash_folder_interactor.dart +++ b/lib/features/thread/domain/usecases/empty_trash_folder_interactor.dart @@ -32,7 +32,7 @@ class EmptyTrashFolderInteractor { totalEmails, onProgressController ); - yield Right(EmptyTrashFolderSuccess(emailIdDeleted,)); + yield Right(EmptyTrashFolderSuccess(emailIdDeleted, trashMailboxId)); } catch (e) { yield Left(EmptyTrashFolderFailure(e)); } diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 5b7a94d8db..8008e179d9 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -49,6 +49,8 @@ import 'package:tmail_ui_user/features/thread/domain/model/email_filter.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/model/get_email_request.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_email_by_id_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/load_more_emails_state.dart'; @@ -395,6 +397,16 @@ class ThreadController extends BaseController with EmailActionController { emailIds: reactionState.emailIds, affectedMailboxId: reactionState.mailboxId, ); + } else if (reactionState is EmptyTrashFolderSuccess) { + mailboxDashBoardController.handleDeleteEmailsInMailbox( + emailIds: reactionState.emailIds, + affectedMailboxId: reactionState.mailboxId, + ); + } else if (reactionState is EmptySpamFolderSuccess) { + mailboxDashBoardController.handleDeleteEmailsInMailbox( + emailIds: reactionState.emailIds, + affectedMailboxId: reactionState.mailboxId, + ); } }); } From 1e1452ea8e60b0195f3c4d00cc34cd0d5d4bd701 Mon Sep 17 00:00:00 2001 From: DatDang Date: Thu, 2 Jan 2025 14:27:26 +0700 Subject: [PATCH 53/72] TF-3385 Update move emails --- .../domain/state/move_to_mailbox_state.dart | 6 + .../usecases/move_to_mailbox_interactor.dart | 10 +- .../controller/single_email_controller.dart | 76 ++++++++--- .../presentation/mailbox_controller.dart | 61 +++++++++ .../mailbox_dashboard_controller.dart | 118 ++++++++++++++---- .../move_emails_to_mailbox_extension.dart | 26 ++++ ..._emails_with_new_mailbox_id_extension.dart | 30 +++++ .../presentation/search_email_controller.dart | 18 +++ .../move_multiple_email_to_mailbox_state.dart | 12 ++ ..._multiple_email_to_mailbox_interactor.dart | 20 ++- .../mixin/email_action_controller.dart | 61 +++++++-- .../presentation/thread_controller.dart | 18 +++ model/lib/email/presentation_email.dart | 50 ++++++++ 13 files changed, 450 insertions(+), 56 deletions(-) create mode 100644 lib/features/mailbox_dashboard/presentation/extensions/move_emails_to_mailbox_extension.dart create mode 100644 lib/features/mailbox_dashboard/presentation/extensions/update_emails_with_new_mailbox_id_extension.dart diff --git a/lib/features/email/domain/state/move_to_mailbox_state.dart b/lib/features/email/domain/state/move_to_mailbox_state.dart index 56809a70dd..f91ff1e39a 100644 --- a/lib/features/email/domain/state/move_to_mailbox_state.dart +++ b/lib/features/email/domain/state/move_to_mailbox_state.dart @@ -14,6 +14,8 @@ class MoveToMailboxSuccess extends UIState { final MoveAction moveAction; final EmailActionType emailActionType; final String? destinationPath; + final Map> originalMailboxIdsWithEmailIds; + final Map emailIdsWithReadStatus; MoveToMailboxSuccess( this.emailId, @@ -23,6 +25,8 @@ class MoveToMailboxSuccess extends UIState { this.emailActionType, { this.destinationPath, + required this.originalMailboxIdsWithEmailIds, + required this.emailIdsWithReadStatus, } ); @@ -34,6 +38,8 @@ class MoveToMailboxSuccess extends UIState { moveAction, emailActionType, destinationPath, + originalMailboxIdsWithEmailIds, + emailIdsWithReadStatus, ]; } diff --git a/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart b/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart index 0303c36483..0a92e06f92 100644 --- a/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart +++ b/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart @@ -3,6 +3,7 @@ import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; @@ -12,7 +13,12 @@ class MoveToMailboxInteractor { MoveToMailboxInteractor(this._emailRepository); - Stream> execute(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) async* { + Stream> execute( + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus, + ) async* { try { yield Right(LoadingMoveToMailbox()); final result = await _emailRepository.moveToMailbox(session, accountId, moveRequest); @@ -24,6 +30,8 @@ class MoveToMailboxInteractor { moveRequest.moveAction, moveRequest.emailActionType, destinationPath: moveRequest.destinationPath, + originalMailboxIdsWithEmailIds: moveRequest.currentMailboxes, + emailIdsWithReadStatus: emailIdsWithReadStatus, )); } else { yield Left(MoveToMailboxFailure(moveRequest.emailActionType)); diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 4b06b88019..2c9868b085 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -942,7 +942,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { {currentMailbox.id: [emailSelected.id!]}, destinationMailbox.id, MoveAction.moving, - EmailActionType.moveToTrash)); + EmailActionType.moveToTrash), + {emailSelected.id!: emailSelected.hasRead}); } else if (destinationMailbox.isSpam) { _moveToSpamAction( context, @@ -952,7 +953,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { {currentMailbox.id: [emailSelected.id!]}, destinationMailbox.id, MoveAction.moving, - EmailActionType.moveToSpam)); + EmailActionType.moveToSpam), + {emailSelected.id!: emailSelected.hasRead}); } else { _moveToMailbox( context, @@ -963,13 +965,25 @@ class SingleEmailController extends BaseController with AppLoaderMixin { destinationMailbox.id, MoveAction.moving, EmailActionType.moveToMailbox, - destinationPath: destinationMailbox.mailboxPath)); + destinationPath: destinationMailbox.mailboxPath), + {emailSelected.id!: emailSelected.hasRead}); } } - void _moveToMailbox(BuildContext context, Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { + void _moveToMailbox( + BuildContext context, + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus, + ) { closeEmailView(context: context); - consumeState(_moveToMailboxInteractor.execute(session, accountId, moveRequest)); + consumeState(_moveToMailboxInteractor.execute( + session, + accountId, + moveRequest, + emailIdsWithReadStatus, + )); } void _moveToMailboxSuccess(MoveToMailboxSuccess success) { @@ -981,10 +995,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin { actionName: AppLocalizations.of(currentContext!).undo, onActionClick: () { _revertedToOriginalMailbox(MoveToMailboxRequest( - {success.destinationMailboxId: [success.emailId]}, - success.currentMailboxId, - MoveAction.undo, - success.emailActionType)); + {success.destinationMailboxId: [success.emailId]}, + success.currentMailboxId, + MoveAction.undo, + success.emailActionType), + success.emailIdsWithReadStatus, + ); }, leadingSVGIcon: imagePaths.icFolderMailbox, leadingSVGIconColor: Colors.white, @@ -995,9 +1011,18 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } - void _revertedToOriginalMailbox(MoveToMailboxRequest newMoveRequest) { + void _revertedToOriginalMailbox( + MoveToMailboxRequest newMoveRequest, + Map emailIdsWithReadStatus, + ) { if (accountId != null && session != null) { - _moveToMailbox(currentContext!, session!, accountId!, newMoveRequest); + _moveToMailbox( + currentContext!, + session!, + accountId!, + newMoveRequest, + emailIdsWithReadStatus, + ); } } @@ -1014,7 +1039,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { {currentMailbox.id: [email.id!]}, trashMailboxId, MoveAction.moving, - EmailActionType.moveToTrash) + EmailActionType.moveToTrash), + {email.id!: email.hasRead}, ); } } @@ -1023,10 +1049,16 @@ class SingleEmailController extends BaseController with AppLoaderMixin { BuildContext context, Session session, AccountId accountId, - MoveToMailboxRequest moveRequest + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus, ) { closeEmailView(context: context); - mailboxDashBoardController.moveToMailbox(session, accountId, moveRequest); + mailboxDashBoardController.moveToMailbox( + session, + accountId, + moveRequest, + emailIdsWithReadStatus, + ); } void moveToSpam(BuildContext context, PresentationEmail email) { @@ -1042,7 +1074,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { {currentMailbox.id: [email.id!]}, spamMailboxId, MoveAction.moving, - EmailActionType.moveToSpam) + EmailActionType.moveToSpam), + {email.id!: email.hasRead}, ); } } @@ -1060,7 +1093,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { {spamMailboxId: [email.id!]}, inboxMailboxId, MoveAction.moving, - EmailActionType.unSpam) + EmailActionType.unSpam), + {email.id!: email.hasRead}, ); } } @@ -1069,10 +1103,16 @@ class SingleEmailController extends BaseController with AppLoaderMixin { BuildContext context, Session session, AccountId accountId, - MoveToMailboxRequest moveRequest + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus, ) { closeEmailView(context: context); - mailboxDashBoardController.moveToMailbox(session, accountId, moveRequest); + mailboxDashBoardController.moveToMailbox( + session, + accountId, + moveRequest, + emailIdsWithReadStatus, + ); } void markAsStarEmail( diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 4bc677cbcf..ad8ff75775 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -25,6 +25,7 @@ import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanent import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails_permanently_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_restored_deleted_message_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart'; @@ -80,6 +81,7 @@ import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/dialog_router.dart'; @@ -351,6 +353,24 @@ class MailboxController extends BaseMailboxController affectedMailboxId: reactionState.mailboxId, totalEmailsChanged: -reactionState.emailIds.length, ); + } else if (reactionState is MoveToMailboxSuccess) { + _handleMoveEmailsToMailbox( + originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithEmailIds, + destinationMailboxId: reactionState.destinationMailboxId, + emailIdsWithReadStatus: reactionState.emailIdsWithReadStatus, + ); + } else if (reactionState is MoveMultipleEmailToMailboxAllSuccess) { + _handleMoveEmailsToMailbox( + originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithEmailIds, + destinationMailboxId: reactionState.destinationMailboxId, + emailIdsWithReadStatus: reactionState.emailIdsWithReadStatus, + ); + } else if (reactionState is MoveMultipleEmailToMailboxHasSomeEmailFailure) { + _handleMoveEmailsToMailbox( + originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithMoveSucceededEmailIds, + destinationMailboxId: reactionState.destinationMailboxId, + emailIdsWithReadStatus: reactionState.moveSucceededEmailIdsWithReadStatus, + ); } }); } @@ -400,6 +420,47 @@ class MailboxController extends BaseMailboxController ); } + void _handleMoveEmailsToMailbox({ + required Map> originalMailboxIdsWithEmailIds, + required MailboxId destinationMailboxId, + required Map emailIdsWithReadStatus, + }) { + // Update changes in original mailboxes + for (var originalMailboxIdWithEmailIds in originalMailboxIdsWithEmailIds.entries) { + final originalMailboxId = originalMailboxIdWithEmailIds.key; + final emailsMovedCount = originalMailboxIdWithEmailIds.value.length; + final unreadEmailMovedCount = originalMailboxIdWithEmailIds.value + .where((emailId) => emailIdsWithReadStatus[emailId] == false) + .length; + updateMailboxTotalEmailsCountById( + originalMailboxId, + -emailsMovedCount, + ); + updateUnreadCountOfMailboxById( + originalMailboxId, + unreadChanges: -unreadEmailMovedCount, + ); + } + + // Update changes in destination mailbox + updateMailboxTotalEmailsCountById( + destinationMailboxId, + originalMailboxIdsWithEmailIds.entries.fold( + 0, + (sum, entry) => sum + entry.value.length, + ), + ); + updateUnreadCountOfMailboxById( + destinationMailboxId, + unreadChanges: originalMailboxIdsWithEmailIds + .values + .fold( + 0, + (sum, emails) => sum + emails.where((emailId) => emailIdsWithReadStatus[emailId] == false).length + ), + ); + } + void _initWebSocketQueueHandler() { _webSocketQueueHandler = WebSocketQueueHandler( processMessageCallback: _handleWebSocketMessage, diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index b9a89e6552..25db5e08d2 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -820,8 +820,18 @@ class MailboxDashBoardController extends ReloadableController } } - void moveToMailbox(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { - consumeState(_moveToMailboxInteractor.execute(session, accountId, moveRequest)); + void moveToMailbox( + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus, + ) { + consumeState(_moveToMailboxInteractor.execute( + session, + accountId, + moveRequest, + emailIdsWithReadStatus, + )); } void _moveToMailboxSuccess(MoveToMailboxSuccess success) { @@ -836,7 +846,7 @@ class MailboxDashBoardController extends ReloadableController success.currentMailboxId, MoveAction.undo, success.emailActionType - )); + ), success.emailIdsWithReadStatus); }, leadingSVGIcon: imagePaths.icFolderMailbox, leadingSVGIconColor: Colors.white, @@ -846,11 +856,19 @@ class MailboxDashBoardController extends ReloadableController } } - void _revertedToOriginalMailbox(MoveToMailboxRequest newMoveRequest) { + void _revertedToOriginalMailbox( + MoveToMailboxRequest newMoveRequest, + Map emailIdsWithReadStatus, + ) { final currentAccountId = accountId.value; final session = sessionCurrent; if (currentAccountId != null && session != null) { - consumeState(_moveToMailboxInteractor.execute(session, currentAccountId, newMoveRequest)); + consumeState(_moveToMailboxInteractor.execute( + session, + currentAccountId, + newMoveRequest, + emailIdsWithReadStatus, + )); } } @@ -1048,7 +1066,12 @@ class MailboxDashBoardController extends ReloadableController sessionCurrent!, listEmails.listEmailIds, currentMailbox, - destinationMailbox + destinationMailbox, + Map.fromEntries( + listEmails + .where((email) => email.id != null) + .map((email) => MapEntry(email.id!, email.hasRead)), + ), ); } } @@ -1059,7 +1082,8 @@ class MailboxDashBoardController extends ReloadableController Session session, List listEmailIds, PresentationMailbox currentMailbox, - PresentationMailbox destinationMailbox + PresentationMailbox destinationMailbox, + Map emailIdsWithReadStatus, ) { if (destinationMailbox.isTrash) { _moveSelectedEmailMultipleToMailboxAction( @@ -1069,7 +1093,8 @@ class MailboxDashBoardController extends ReloadableController {currentMailbox.id: listEmailIds}, destinationMailbox.id, MoveAction.moving, - EmailActionType.moveToTrash)); + EmailActionType.moveToTrash), + emailIdsWithReadStatus); } else if (destinationMailbox.isSpam) { _moveSelectedEmailMultipleToMailboxAction( session, @@ -1078,7 +1103,8 @@ class MailboxDashBoardController extends ReloadableController {currentMailbox.id: listEmailIds}, destinationMailbox.id, MoveAction.moving, - EmailActionType.moveToSpam)); + EmailActionType.moveToSpam), + emailIdsWithReadStatus); } else { _moveSelectedEmailMultipleToMailboxAction( session, @@ -1088,7 +1114,8 @@ class MailboxDashBoardController extends ReloadableController destinationMailbox.id, MoveAction.moving, EmailActionType.moveToMailbox, - destinationPath: destinationMailbox.mailboxPath)); + destinationPath: destinationMailbox.mailboxPath), + emailIdsWithReadStatus); } } @@ -1096,6 +1123,10 @@ class MailboxDashBoardController extends ReloadableController List listEmails, PresentationMailbox destinationMailbox, ) { + final emailIdsWithReadStatus = Map.fromEntries(listEmails + .where((email) => email.id != null) + .map((e) => MapEntry(e.id!, e.hasRead)) + ); if (searchController.isSearchEmailRunning){ final Map> mapListEmailSelectedByMailBoxId = {}; for (var element in listEmails) { @@ -1108,11 +1139,17 @@ class MailboxDashBoardController extends ReloadableController } } } - _handleDragSelectedMultipleEmailToMailboxAction(mapListEmailSelectedByMailBoxId, destinationMailbox); - } else { - if (selectedMailbox.value != null) { - _handleDragSelectedMultipleEmailToMailboxAction({selectedMailbox.value!.id: listEmails.listEmailIds}, destinationMailbox); - } + _handleDragSelectedMultipleEmailToMailboxAction( + mapListEmailSelectedByMailBoxId, + destinationMailbox, + emailIdsWithReadStatus, + ); + } else if (selectedMailbox.value != null) { + _handleDragSelectedMultipleEmailToMailboxAction( + {selectedMailbox.value!.id: listEmails.listEmailIds}, + destinationMailbox, + emailIdsWithReadStatus, + ); } } @@ -1120,6 +1157,7 @@ class MailboxDashBoardController extends ReloadableController void _handleDragSelectedMultipleEmailToMailboxAction( Map> mapListEmails, PresentationMailbox destinationMailbox, + Map emailIdsWithReadStatus, ) async { if (accountId.value != null && sessionCurrent != null) { if (destinationMailbox.isTrash) { @@ -1132,6 +1170,7 @@ class MailboxDashBoardController extends ReloadableController MoveAction.moving, EmailActionType.moveToTrash, ), + emailIdsWithReadStatus, ); } else if (destinationMailbox.isSpam) { moveToMailbox( @@ -1143,6 +1182,7 @@ class MailboxDashBoardController extends ReloadableController MoveAction.moving, EmailActionType.moveToSpam, ), + emailIdsWithReadStatus, ); } else { moveToMailbox( @@ -1155,6 +1195,7 @@ class MailboxDashBoardController extends ReloadableController EmailActionType.moveToMailbox, destinationPath: destinationMailbox.mailboxPath, ), + emailIdsWithReadStatus, ); } } @@ -1164,9 +1205,15 @@ class MailboxDashBoardController extends ReloadableController void _moveSelectedEmailMultipleToMailboxAction( Session session, AccountId accountId, - MoveToMailboxRequest moveRequest + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus, ) { - consumeState(_moveMultipleEmailToMailboxInteractor.execute(session, accountId, moveRequest)); + consumeState(_moveMultipleEmailToMailboxInteractor.execute( + session, + accountId, + moveRequest, + emailIdsWithReadStatus, + )); } void _moveSelectedMultipleEmailToMailboxSuccess(Success success) { @@ -1176,6 +1223,7 @@ class MailboxDashBoardController extends ReloadableController MailboxId? destinationMailboxId; MoveAction? moveAction; EmailActionType? emailActionType; + Map? emailIdsWithReadStatus; if (success is MoveMultipleEmailToMailboxAllSuccess) { destinationPath = success.destinationPath; @@ -1184,6 +1232,7 @@ class MailboxDashBoardController extends ReloadableController destinationMailboxId = success.destinationMailboxId; moveAction = success.moveAction; emailActionType = success.emailActionType; + emailIdsWithReadStatus = success.emailIdsWithReadStatus; } else if (success is MoveMultipleEmailToMailboxHasSomeEmailFailure) { destinationPath = success.destinationPath; movedEmailIds = success.movedListEmailId; @@ -1191,6 +1240,7 @@ class MailboxDashBoardController extends ReloadableController destinationMailboxId = success.destinationMailboxId; moveAction = success.moveAction; emailActionType = success.emailActionType; + emailIdsWithReadStatus = success.moveSucceededEmailIdsWithReadStatus; } if (currentContext != null && @@ -1213,7 +1263,7 @@ class MailboxDashBoardController extends ReloadableController MoveAction.undo, emailActionType!, destinationPath: destinationPath - )); + ), emailIdsWithReadStatus ?? {}); } }, leadingSVGIconColor: Colors.white, @@ -1225,12 +1275,16 @@ class MailboxDashBoardController extends ReloadableController } } - void _revertedSelectionEmailToOriginalMailbox(MoveToMailboxRequest newMoveRequest) { + void _revertedSelectionEmailToOriginalMailbox( + MoveToMailboxRequest newMoveRequest, + Map emailIdsWithReadStatus, + ) { if (accountId.value != null && sessionCurrent != null) { consumeState(_moveMultipleEmailToMailboxInteractor.execute( sessionCurrent!, accountId.value!, - newMoveRequest)); + newMoveRequest, + emailIdsWithReadStatus)); } } @@ -1244,7 +1298,12 @@ class MailboxDashBoardController extends ReloadableController {mailboxCurrent.id: listEmails.listEmailIds}, trashMailboxId, MoveAction.moving, - EmailActionType.moveToTrash) + EmailActionType.moveToTrash), + Map.fromIterable( + listEmails + .where((email) => email.id != null) + .map((email) => MapEntry(email.id!, email.hasRead)), + ), ); } } @@ -1279,7 +1338,12 @@ class MailboxDashBoardController extends ReloadableController {mailboxCurrent.id: listEmail.listEmailIds}, spamMailboxId!, MoveAction.moving, - EmailActionType.moveToSpam) + EmailActionType.moveToSpam), + Map.fromIterable( + listEmail + .where((email) => email.id != null) + .map((email) => MapEntry(email.id!, email.hasRead)), + ), ); } @@ -1325,7 +1389,12 @@ class MailboxDashBoardController extends ReloadableController {spamMailboxId!: listEmail.listEmailIds}, inboxMailboxId, MoveAction.moving, - EmailActionType.unSpam) + EmailActionType.unSpam), + Map.fromIterable( + listEmail + .where((email) => email.id != null) + .map((email) => MapEntry(email.id!, email.hasRead)), + ), ); } @@ -2793,7 +2862,8 @@ class MailboxDashBoardController extends ReloadableController moveToMailbox( sessionCurrent!, accountId.value!, - moveToArchiveMailboxRequest + moveToArchiveMailboxRequest, + {email.id!: email.hasRead} ); } } diff --git a/lib/features/mailbox_dashboard/presentation/extensions/move_emails_to_mailbox_extension.dart b/lib/features/mailbox_dashboard/presentation/extensions/move_emails_to_mailbox_extension.dart new file mode 100644 index 0000000000..e0e4e6d9ce --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/extensions/move_emails_to_mailbox_extension.dart @@ -0,0 +1,26 @@ +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; + +extension MoveEmailsToMailboxExtension on MailboxDashBoardController { + void handleMoveEmailsToMailbox({ + required Map> originalMailboxIdsWithEmailIds, + required MailboxId destinationMailboxId, + }) { + if (destinationMailboxId == selectedMailbox.value?.id) return; + + final currentEmails = List.from( + emailsInCurrentMailbox, + ); + final movedEmailIds = originalMailboxIdsWithEmailIds.entries.fold( + {}, + (emailIds, entry) { + emailIds.addAll(entry.value); + return emailIds; + }, + ).toList(); + currentEmails.removeWhere((email) => movedEmailIds.contains(email.id)); + updateEmailList(currentEmails); + } +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/extensions/update_emails_with_new_mailbox_id_extension.dart b/lib/features/mailbox_dashboard/presentation/extensions/update_emails_with_new_mailbox_id_extension.dart new file mode 100644 index 0000000000..a94af546d2 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/extensions/update_emails_with_new_mailbox_id_extension.dart @@ -0,0 +1,30 @@ +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; + +extension UpdateEmailsWithNewMailboxIdExtension on MailboxDashBoardController { + handleUpdateEmailsWithNewMailboxId({ + required Map> originalMailboxIdsWithEmailIds, + required MailboxId destinationMailboxId, + }) { + final currentEmails = List.from( + emailsInCurrentMailbox, + ); + final movedEmailIds = originalMailboxIdsWithEmailIds.entries.fold( + {}, + (emailIds, entry) { + emailIds.addAll(entry.value); + return emailIds; + }, + ).toList(); + for (var email in currentEmails) { + if (!movedEmailIds.contains(email.id)) continue; + + email = email.copyWith( + mailboxIds: {destinationMailboxId: true}, + mailboxContain: mapMailboxById[destinationMailboxId], + ); + } + } +} \ No newline at end of file diff --git a/lib/features/search/email/presentation/search_email_controller.dart b/lib/features/search/email/presentation/search_email_controller.dart index edf34f0a0b..6748769676 100644 --- a/lib/features/search/email/presentation/search_email_controller.dart +++ b/lib/features/search/email/presentation/search_email_controller.dart @@ -37,6 +37,7 @@ import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanent import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails_permanently_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; @@ -50,6 +51,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_re import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/delete_emails_in_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_emails_with_new_mailbox_id_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart'; @@ -70,6 +72,7 @@ import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_sta import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_as_star_multiple_email_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_more_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/search_email_interactor.dart'; @@ -328,6 +331,21 @@ class SearchEmailController extends BaseController emailIds: reactionState.emailIds, affectedMailboxId: reactionState.mailboxId, ); + } else if (reactionState is MoveToMailboxSuccess) { + mailboxDashBoardController.handleUpdateEmailsWithNewMailboxId( + originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithEmailIds, + destinationMailboxId: reactionState.destinationMailboxId, + ); + } else if (reactionState is MoveMultipleEmailToMailboxAllSuccess) { + mailboxDashBoardController.handleUpdateEmailsWithNewMailboxId( + originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithEmailIds, + destinationMailboxId: reactionState.destinationMailboxId, + ); + } else if (reactionState is MoveMultipleEmailToMailboxHasSomeEmailFailure) { + mailboxDashBoardController.handleUpdateEmailsWithNewMailboxId( + originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithMoveSucceededEmailIds, + destinationMailboxId: reactionState.destinationMailboxId, + ); } }); } diff --git a/lib/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart b/lib/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart index b4f179a334..8d2ce732ad 100644 --- a/lib/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart +++ b/lib/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart @@ -14,6 +14,8 @@ class MoveMultipleEmailToMailboxAllSuccess extends UIState { final MoveAction moveAction; final EmailActionType emailActionType; final String? destinationPath; + final Map> originalMailboxIdsWithEmailIds; + final Map emailIdsWithReadStatus; MoveMultipleEmailToMailboxAllSuccess( this.movedListEmailId, @@ -23,6 +25,8 @@ class MoveMultipleEmailToMailboxAllSuccess extends UIState { this.emailActionType, { this.destinationPath, + required this.originalMailboxIdsWithEmailIds, + required this.emailIdsWithReadStatus, } ); @@ -34,6 +38,8 @@ class MoveMultipleEmailToMailboxAllSuccess extends UIState { moveAction, emailActionType, destinationPath, + originalMailboxIdsWithEmailIds, + emailIdsWithReadStatus, ]; } @@ -54,6 +60,8 @@ class MoveMultipleEmailToMailboxHasSomeEmailFailure extends UIState { final MoveAction moveAction; final EmailActionType emailActionType; final String? destinationPath; + final Map> originalMailboxIdsWithMoveSucceededEmailIds; + final Map moveSucceededEmailIdsWithReadStatus; MoveMultipleEmailToMailboxHasSomeEmailFailure( this.movedListEmailId, @@ -63,6 +71,8 @@ class MoveMultipleEmailToMailboxHasSomeEmailFailure extends UIState { this.emailActionType, { this.destinationPath, + required this.originalMailboxIdsWithMoveSucceededEmailIds, + required this.moveSucceededEmailIdsWithReadStatus, } ); @@ -74,6 +84,8 @@ class MoveMultipleEmailToMailboxHasSomeEmailFailure extends UIState { moveAction, emailActionType, destinationPath, + originalMailboxIdsWithMoveSucceededEmailIds, + moveSucceededEmailIdsWithReadStatus, ]; } diff --git a/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart b/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart index f7481c5b6c..4ba72049f4 100644 --- a/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart +++ b/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart @@ -1,10 +1,13 @@ import 'dart:async'; +import 'package:core/presentation/extensions/map_extensions.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; @@ -17,7 +20,8 @@ class MoveMultipleEmailToMailboxInteractor { Stream> execute( Session session, AccountId accountId, - MoveToMailboxRequest moveRequest + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus, ) async* { try { yield Right(LoadingMoveMultipleEmailToMailboxAll()); @@ -30,10 +34,22 @@ class MoveMultipleEmailToMailboxInteractor { moveRequest.moveAction, moveRequest.emailActionType, destinationPath: moveRequest.destinationPath, + originalMailboxIdsWithEmailIds: moveRequest.currentMailboxes, + emailIdsWithReadStatus: emailIdsWithReadStatus, )); } else if (result.emailIdsSuccess.isEmpty) { yield Left(MoveMultipleEmailToMailboxAllFailure(moveRequest.moveAction, moveRequest.emailActionType)); } else { + final originalMailboxIdsWithEmailIds = Map>.from( + moveRequest.currentMailboxes, + ); + final originalMailboxIdsWithMoveSucceededEmailIds = originalMailboxIdsWithEmailIds + .map((key, value) => MapEntry( + key, + value.where(result.emailIdsSuccess.contains).toList() + )); + final moveSucceededEmailIdsWithReadStatus = emailIdsWithReadStatus + .where((emailId, _) => result.emailIdsSuccess.contains(emailId)); yield Right(MoveMultipleEmailToMailboxHasSomeEmailFailure( result.emailIdsSuccess, moveRequest.currentMailboxes.keys.first, @@ -41,6 +57,8 @@ class MoveMultipleEmailToMailboxInteractor { moveRequest.moveAction, moveRequest.emailActionType, destinationPath: moveRequest.destinationPath, + originalMailboxIdsWithMoveSucceededEmailIds: originalMailboxIdsWithMoveSucceededEmailIds, + moveSucceededEmailIdsWithReadStatus: moveSucceededEmailIdsWithReadStatus, )); } } catch (e) { diff --git a/lib/features/thread/presentation/mixin/email_action_controller.dart b/lib/features/thread/presentation/mixin/email_action_controller.dart index 04e2f12dd4..64e4b1e724 100644 --- a/lib/features/thread/presentation/mixin/email_action_controller.dart +++ b/lib/features/thread/presentation/mixin/email_action_controller.dart @@ -11,6 +11,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/email/email_action_type.dart'; import 'package:model/email/mark_star_action.dart'; import 'package:model/email/presentation_email.dart'; @@ -60,13 +61,24 @@ mixin EmailActionController { {mailboxContain.id: email.id != null ? [email.id!] : []}, trashMailboxId, MoveAction.moving, - EmailActionType.moveToTrash) + EmailActionType.moveToTrash), + email.id != null ? {email.id! : email.hasRead} : {}, ); } } - void _moveToTrashAction(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { - mailboxDashBoardController.moveToMailbox(session, accountId, moveRequest); + void _moveToTrashAction( + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus, + ) { + mailboxDashBoardController.moveToMailbox( + session, + accountId, + moveRequest, + emailIdsWithReadStatus, + ); } void moveToSpam(PresentationEmail email, {PresentationMailbox? mailboxContain}) async { @@ -82,7 +94,8 @@ mixin EmailActionController { {mailboxContain.id: email.id != null ? [email.id!] : []}, spamMailboxId, MoveAction.moving, - EmailActionType.moveToSpam) + EmailActionType.moveToSpam), + email.id != null ? {email.id! : email.hasRead} : {}, ); } } @@ -101,13 +114,24 @@ mixin EmailActionController { {spamMailboxId: email.id != null ? [email.id!] : []}, inboxMailboxId, MoveAction.moving, - EmailActionType.unSpam) + EmailActionType.unSpam), + email.id != null ? {email.id! : email.hasRead} : {}, ); } } - void moveToSpamAction(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { - mailboxDashBoardController.moveToMailbox(session, accountId, moveRequest); + void moveToSpamAction( + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus, + ) { + mailboxDashBoardController.moveToMailbox( + session, + accountId, + moveRequest, + emailIdsWithReadStatus, + ); } void moveToMailbox( @@ -157,7 +181,8 @@ mixin EmailActionController { {currentMailbox.id: emailSelected.id != null ? [emailSelected.id!] : []}, destinationMailbox.id, MoveAction.moving, - EmailActionType.moveToTrash)); + EmailActionType.moveToTrash), + emailSelected.id != null ? {emailSelected.id! : emailSelected.hasRead} : {}); } else if (destinationMailbox.isSpam) { moveToSpamAction( session, @@ -166,7 +191,8 @@ mixin EmailActionController { {currentMailbox.id: emailSelected.id != null ? [emailSelected.id!] : []}, destinationMailbox.id, MoveAction.moving, - EmailActionType.moveToSpam)); + EmailActionType.moveToSpam), + emailSelected.id != null ? {emailSelected.id! : emailSelected.hasRead} : {}); } else { _moveToMailboxAction( session, @@ -176,12 +202,23 @@ mixin EmailActionController { destinationMailbox.id, MoveAction.moving, EmailActionType.moveToMailbox, - destinationPath: destinationMailbox.mailboxPath)); + destinationPath: destinationMailbox.mailboxPath), + emailSelected.id != null ? {emailSelected.id! : emailSelected.hasRead} : {}); } } - void _moveToMailboxAction(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { - mailboxDashBoardController.moveToMailbox(session, accountId, moveRequest); + void _moveToMailboxAction( + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest, + Map emailIdsWithReadStatus, + ) { + mailboxDashBoardController.moveToMailbox( + session, + accountId, + moveRequest, + emailIdsWithReadStatus, + ); } void deleteEmailPermanently(BuildContext context, PresentationEmail email) { diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 8008e179d9..549d2d79a9 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -24,6 +24,7 @@ import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanent import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails_permanently_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; @@ -31,6 +32,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart' as search; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/delete_emails_in_mailbox_extension.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/move_emails_to_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart'; @@ -56,6 +58,7 @@ import 'package:tmail_ui_user/features/thread/domain/state/get_email_by_id_state import 'package:tmail_ui_user/features/thread/domain/state/load_more_emails_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_as_star_multiple_email_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/refresh_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/refresh_changes_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; @@ -407,6 +410,21 @@ class ThreadController extends BaseController with EmailActionController { emailIds: reactionState.emailIds, affectedMailboxId: reactionState.mailboxId, ); + } else if (reactionState is MoveToMailboxSuccess) { + mailboxDashBoardController.handleMoveEmailsToMailbox( + originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithEmailIds, + destinationMailboxId: reactionState.destinationMailboxId, + ); + } else if (reactionState is MoveMultipleEmailToMailboxAllSuccess) { + mailboxDashBoardController.handleMoveEmailsToMailbox( + originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithEmailIds, + destinationMailboxId: reactionState.destinationMailboxId, + ); + } else if (reactionState is MoveMultipleEmailToMailboxHasSomeEmailFailure) { + mailboxDashBoardController.handleMoveEmailsToMailbox( + originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithMoveSucceededEmailIds, + destinationMailboxId: reactionState.destinationMailboxId, + ); } }); } diff --git a/model/lib/email/presentation_email.dart b/model/lib/email/presentation_email.dart index 1989247b83..d3ec44432e 100644 --- a/model/lib/email/presentation_email.dart +++ b/model/lib/email/presentation_email.dart @@ -176,4 +176,54 @@ class PresentationEmail with EquatableMixin, SearchSnippetMixin { searchSnippetSubject, searchSnippetPreview, ]; + + PresentationEmail copyWith({ + EmailId? id, + Id? blobId, + Map? keywords, + UnsignedInt? size, + UTCDate? receivedAt, + bool? hasAttachment, + String? preview, + String? subject, + UTCDate? sentAt, + Set? from, + Set? to, + Set? cc, + Set? bcc, + Set? replyTo, + Map? mailboxIds, + SelectMode? selectMode, + Uri? routeWeb, + PresentationMailbox? mailboxContain, + List? emailHeader, + Set? htmlBody, + Map? bodyValues, + Map? headerCalendarEvent, + }) { + return PresentationEmail( + id: id ?? this.id, + blobId: blobId ?? this.blobId, + keywords: keywords ?? this.keywords, + size: size ?? this.size, + receivedAt: receivedAt ?? this.receivedAt, + hasAttachment: hasAttachment ?? this.hasAttachment, + preview: preview ?? this.preview, + subject: subject ?? this.subject, + sentAt: sentAt ?? this.sentAt, + from: from ?? this.from, + to: to ?? this.to, + cc: cc ?? this.cc, + bcc: bcc ?? this.bcc, + replyTo: replyTo ?? this.replyTo, + mailboxIds: mailboxIds ?? this.mailboxIds, + selectMode: selectMode ?? this.selectMode, + routeWeb: routeWeb ?? this.routeWeb, + mailboxContain: mailboxContain ?? this.mailboxContain, + emailHeader: emailHeader ?? this.emailHeader, + htmlBody: htmlBody ?? this.htmlBody, + bodyValues: bodyValues ?? this.bodyValues, + headerCalendarEvent: headerCalendarEvent ?? this.headerCalendarEvent, + ); + } } \ No newline at end of file From 8bba046d57770d58cf78aa64ee2537f10957a199 Mon Sep 17 00:00:00 2001 From: DatDang Date: Thu, 2 Jan 2025 15:11:36 +0700 Subject: [PATCH 54/72] TF-3385 Clean up ThreadController and SearchEmailController ever listener --- .../mailbox_dashboard_controller.dart | 43 +++++++++++- .../presentation/search_email_controller.dart | 67 +------------------ .../presentation/thread_controller.dart | 66 +----------------- 3 files changed, 42 insertions(+), 134 deletions(-) diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 25db5e08d2..63914b9fb6 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -55,6 +55,7 @@ import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails import 'package:tmail_ui_user/features/email/domain/state/delete_sending_email_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_restored_deleted_message_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/restore_deleted_message_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/store_sending_email_state.dart'; @@ -97,7 +98,9 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/download/download_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart' as search; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/delete_emails_in_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/set_error_extension.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mixin/user_setting_popup_menu_mixin.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/composer_overlay_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; @@ -366,18 +369,20 @@ class MailboxDashBoardController extends ReloadableController } else if (success is UpdateVacationSuccess) { _handleUpdateVacationSuccess(success); } else if (success is MarkAsMultipleEmailReadAllSuccess) { - _markAsReadSelectedMultipleEmailSuccess(success.readActions); + _markAsReadSelectedMultipleEmailSuccess(success.readActions, success.emailIds); } else if (success is MarkAsMultipleEmailReadHasSomeEmailFailure) { - _markAsReadSelectedMultipleEmailSuccess(success.readActions); + _markAsReadSelectedMultipleEmailSuccess(success.readActions, success.successEmailIds); } else if (success is MarkAsStarMultipleEmailAllSuccess) { _markAsStarMultipleEmailSuccess( success.markStarAction, success.countMarkStarSuccess, + success.emailIds, ); } else if (success is MarkAsStarMultipleEmailHasSomeEmailFailure) { _markAsStarMultipleEmailSuccess( success.markStarAction, success.countMarkStarSuccess, + success.successEmailIds, ); } else if (success is MoveMultipleEmailToMailboxAllSuccess || success is MoveMultipleEmailToMailboxHasSomeEmailFailure) { @@ -415,6 +420,11 @@ class MailboxDashBoardController extends ReloadableController goToComposer(ComposerArguments.fromSessionStorageBrowser(success.composerCache)); } else if (success is GetIdentityCacheOnWebSuccess) { goToSettings(); + } else if (success is MarkAsStarEmailSuccess) { + updateEmailFlagByEmailIds( + [success.emailId], + markStarAction: success.markStarAction, + ); } } @@ -899,6 +909,10 @@ class MailboxDashBoardController extends ReloadableController } void _deleteEmailPermanentlySuccess(DeleteEmailPermanentlySuccess success) { + handleDeleteEmailsInMailbox( + emailIds: [success.emailId], + affectedMailboxId: success.mailboxId, + ); if (currentOverlayContext != null && currentContext != null) { appToast.showToastSuccessMessage( currentOverlayContext!, @@ -958,7 +972,8 @@ class MailboxDashBoardController extends ReloadableController } } - void _markAsReadSelectedMultipleEmailSuccess(ReadActions readActions) { + void _markAsReadSelectedMultipleEmailSuccess(ReadActions readActions, List emailIds) { + updateEmailFlagByEmailIds(emailIds, readAction: readActions); if (currentContext != null && currentOverlayContext != null) { final message = readActions == ReadActions.markAsUnread ? AppLocalizations.of(currentContext!).marked_message_toast(AppLocalizations.of(currentContext!).unread) @@ -975,6 +990,10 @@ class MailboxDashBoardController extends ReloadableController } void _markAsReadEmailSuccess(MarkAsEmailReadSuccess success) { + updateEmailFlagByEmailIds( + [success.emailId], + readAction: success.readActions, + ); if (currentContext != null && currentOverlayContext != null && success.markReadAction == MarkReadAction.swipeOnThread) { @@ -1025,7 +1044,9 @@ class MailboxDashBoardController extends ReloadableController void _markAsStarMultipleEmailSuccess( MarkStarAction markStarAction, int countMarkStarSuccess, + List emailIds, ) { + updateEmailFlagByEmailIds(emailIds, markStarAction: markStarAction); if (currentOverlayContext != null && currentContext != null) { final message = markStarAction == MarkStarAction.unMarkStar ? AppLocalizations.of(currentContext!).marked_unstar_multiple_item(countMarkStarSuccess) @@ -1494,6 +1515,10 @@ class MailboxDashBoardController extends ReloadableController void _emptyTrashFolderSuccess(EmptyTrashFolderSuccess success) { viewStateMailboxActionProgress.value = Right(UIState.idle); + handleDeleteEmailsInMailbox( + emailIds: success.emailIds, + affectedMailboxId: success.mailboxId, + ); if (currentOverlayContext != null && currentContext != null) { appToast.showToastSuccessMessage( currentOverlayContext!, @@ -1516,8 +1541,16 @@ class MailboxDashBoardController extends ReloadableController void _deleteMultipleEmailsPermanentlySuccess(Success success) { List listEmailIdResult = []; if (success is DeleteMultipleEmailsPermanentlyAllSuccess) { + handleDeleteEmailsInMailbox( + emailIds: success.emailIds, + affectedMailboxId: success.mailboxId, + ); listEmailIdResult = success.emailIds; } else if (success is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure) { + handleDeleteEmailsInMailbox( + emailIds: success.emailIds, + affectedMailboxId: success.mailboxId, + ); listEmailIdResult = success.emailIds; } @@ -2500,6 +2533,10 @@ class MailboxDashBoardController extends ReloadableController void _emptySpamFolderSuccess(EmptySpamFolderSuccess success) { viewStateMailboxActionProgress.value = Right(UIState.idle); + handleDeleteEmailsInMailbox( + emailIds: success.emailIds, + affectedMailboxId: success.mailboxId, + ); if (currentOverlayContext != null && currentContext != null) { appToast.showToastSuccessMessage( currentOverlayContext!, diff --git a/lib/features/search/email/presentation/search_email_controller.dart b/lib/features/search/email/presentation/search_email_controller.dart index 6748769676..82d679f34f 100644 --- a/lib/features/search/email/presentation/search_email_controller.dart +++ b/lib/features/search/email/presentation/search_email_controller.dart @@ -33,10 +33,6 @@ import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_e import 'package:tmail_ui_user/features/contact/presentation/model/contact_arguments.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; -import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanently_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails_permanently_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; @@ -49,8 +45,6 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_all import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/quick_search_email_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_recent_search_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/delete_emails_in_mailbox_extension.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_emails_with_new_mailbox_id_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; @@ -68,10 +62,6 @@ import 'package:tmail_ui_user/features/search/email/presentation/model/search_mo import 'package:tmail_ui_user/features/search/email/presentation/search_email_bindings.dart'; import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/mark_as_star_multiple_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_more_email_state.dart'; @@ -276,62 +266,7 @@ class SearchEmailController extends BaseController ever(mailboxDashBoardController.viewState, (viewState) { final reactionState = viewState.getOrElse(() => UIState.idle); - if (reactionState is MarkAsEmailReadSuccess) { - mailboxDashBoardController.updateEmailFlagByEmailIds( - [reactionState.emailId], - readAction: reactionState.readActions, - ); - } else if (reactionState is MarkAsMultipleEmailReadAllSuccess) { - mailboxDashBoardController.updateEmailFlagByEmailIds( - reactionState.emailIds, - readAction: reactionState.readActions, - ); - } else if (reactionState is MarkAsMultipleEmailReadHasSomeEmailFailure) { - mailboxDashBoardController.updateEmailFlagByEmailIds( - reactionState.successEmailIds, - readAction: reactionState.readActions, - ); - } else if (reactionState is MarkAsStarEmailSuccess) { - mailboxDashBoardController.updateEmailFlagByEmailIds( - [reactionState.emailId], - markStarAction: reactionState.markStarAction, - ); - } else if (reactionState is MarkAsStarMultipleEmailAllSuccess) { - mailboxDashBoardController.updateEmailFlagByEmailIds( - reactionState.emailIds, - markStarAction: reactionState.markStarAction, - ); - } else if (reactionState is MarkAsStarMultipleEmailHasSomeEmailFailure) { - mailboxDashBoardController.updateEmailFlagByEmailIds( - reactionState.successEmailIds, - markStarAction: reactionState.markStarAction, - ); - } else if (reactionState is DeleteEmailPermanentlySuccess) { - mailboxDashBoardController.handleDeleteEmailsInMailbox( - emailIds: [reactionState.emailId], - affectedMailboxId: reactionState.mailboxId, - ); - } else if (reactionState is DeleteMultipleEmailsPermanentlyAllSuccess) { - mailboxDashBoardController.handleDeleteEmailsInMailbox( - emailIds: reactionState.emailIds, - affectedMailboxId: reactionState.mailboxId, - ); - } else if (reactionState is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure) { - mailboxDashBoardController.handleDeleteEmailsInMailbox( - emailIds: reactionState.emailIds, - affectedMailboxId: reactionState.mailboxId, - ); - } else if (reactionState is EmptyTrashFolderSuccess) { - mailboxDashBoardController.handleDeleteEmailsInMailbox( - emailIds: reactionState.emailIds, - affectedMailboxId: reactionState.mailboxId, - ); - } else if (reactionState is EmptySpamFolderSuccess) { - mailboxDashBoardController.handleDeleteEmailsInMailbox( - emailIds: reactionState.emailIds, - affectedMailboxId: reactionState.mailboxId, - ); - } else if (reactionState is MoveToMailboxSuccess) { + if (reactionState is MoveToMailboxSuccess) { mailboxDashBoardController.handleUpdateEmailsWithNewMailboxId( originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithEmailIds, destinationMailboxId: reactionState.destinationMailboxId, diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 549d2d79a9..c91e9c4a80 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -20,10 +20,6 @@ import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; -import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanently_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails_permanently_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; @@ -31,7 +27,6 @@ import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.d import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart' as search; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/delete_emails_in_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/move_emails_to_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; @@ -51,13 +46,9 @@ import 'package:tmail_ui_user/features/thread/domain/model/email_filter.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/model/get_email_request.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_email_by_id_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/load_more_emails_state.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/mark_as_star_multiple_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/refresh_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/refresh_changes_all_email_state.dart'; @@ -348,68 +339,13 @@ class ThreadController extends BaseController with EmailActionController { ever(mailboxDashBoardController.viewState, (viewState) { final reactionState = viewState.getOrElse(() => UIState.idle); - if (reactionState is MarkAsEmailReadSuccess) { - mailboxDashBoardController.updateEmailFlagByEmailIds( - [reactionState.emailId], - readAction: reactionState.readActions, - ); - } else if (reactionState is MarkAsMultipleEmailReadAllSuccess) { - mailboxDashBoardController.updateEmailFlagByEmailIds( - reactionState.emailIds, - readAction: reactionState.readActions, - ); - } else if (reactionState is MarkAsMultipleEmailReadHasSomeEmailFailure) { - mailboxDashBoardController.updateEmailFlagByEmailIds( - reactionState.successEmailIds, - readAction: reactionState.readActions, - ); - } else if (reactionState is MarkAsMailboxReadAllSuccess) { + if (reactionState is MarkAsMailboxReadAllSuccess) { _handleMarkEmailsAsReadByMailboxId(reactionState.mailboxId); } else if (reactionState is MarkAsMailboxReadHasSomeEmailFailure) { mailboxDashBoardController.updateEmailFlagByEmailIds( reactionState.successEmailIds, readAction: ReadActions.markAsRead, ); - } else if (reactionState is MarkAsStarEmailSuccess) { - mailboxDashBoardController.updateEmailFlagByEmailIds( - [reactionState.emailId], - markStarAction: reactionState.markStarAction, - ); - } else if (reactionState is MarkAsStarMultipleEmailAllSuccess) { - mailboxDashBoardController.updateEmailFlagByEmailIds( - reactionState.emailIds, - markStarAction: reactionState.markStarAction, - ); - } else if (reactionState is MarkAsStarMultipleEmailHasSomeEmailFailure) { - mailboxDashBoardController.updateEmailFlagByEmailIds( - reactionState.successEmailIds, - markStarAction: reactionState.markStarAction, - ); - } else if (reactionState is DeleteEmailPermanentlySuccess) { - mailboxDashBoardController.handleDeleteEmailsInMailbox( - emailIds: [reactionState.emailId], - affectedMailboxId: reactionState.mailboxId, - ); - } else if (reactionState is DeleteMultipleEmailsPermanentlyAllSuccess) { - mailboxDashBoardController.handleDeleteEmailsInMailbox( - emailIds: reactionState.emailIds, - affectedMailboxId: reactionState.mailboxId, - ); - } else if (reactionState is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure) { - mailboxDashBoardController.handleDeleteEmailsInMailbox( - emailIds: reactionState.emailIds, - affectedMailboxId: reactionState.mailboxId, - ); - } else if (reactionState is EmptyTrashFolderSuccess) { - mailboxDashBoardController.handleDeleteEmailsInMailbox( - emailIds: reactionState.emailIds, - affectedMailboxId: reactionState.mailboxId, - ); - } else if (reactionState is EmptySpamFolderSuccess) { - mailboxDashBoardController.handleDeleteEmailsInMailbox( - emailIds: reactionState.emailIds, - affectedMailboxId: reactionState.mailboxId, - ); } else if (reactionState is MoveToMailboxSuccess) { mailboxDashBoardController.handleMoveEmailsToMailbox( originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithEmailIds, From 5c5233b5cee8c738c30a6241412a566b4b7e5856 Mon Sep 17 00:00:00 2001 From: DatDang Date: Thu, 2 Jan 2025 17:23:18 +0700 Subject: [PATCH 55/72] TF-3385 Update cache on set method --- .../email_hive_cache_datasource_impl.dart | 111 +++++++++++++++--- .../repository/email_repository_impl.dart | 103 +++++++++++++--- .../mailbox_cache_datasource_impl.dart | 53 +++++++-- .../repository/mailbox_repository_impl.dart | 41 +++++-- .../presentation/mailbox_bindings.dart | 2 + model/lib/extensions/mailbox_extension.dart | 34 ++++++ 6 files changed, 296 insertions(+), 48 deletions(-) diff --git a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart index 9e96e02936..b2caa73b35 100644 --- a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart @@ -18,11 +18,13 @@ import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:model/account/account_request.dart'; import 'package:model/download/download_task_id.dart'; import 'package:model/email/attachment.dart'; import 'package:model/email/mark_star_action.dart'; import 'package:model/email/read_actions.dart'; +import 'package:model/extensions/email_extension.dart'; import 'package:model/extensions/email_id_extensions.dart'; import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; @@ -79,8 +81,13 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { AccountId accountId, EmailId emailId, {CancelToken? cancelToken} - ) { - throw UnimplementedError(); + ) async { + await _emailCacheManager.update( + accountId, + session.username, + destroyed: [emailId], + ); + return true; } @override @@ -91,8 +98,16 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { Session session, AccountId accountId, List emailIds, - ) { - throw UnimplementedError(); + ) async { + await _emailCacheManager.update( + accountId, + session.username, + destroyed: emailIds, + ); + return ( + emailIdsSuccess: emailIds, + mapErrors: {} + ); } @override @@ -137,8 +152,24 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { AccountId accountId, List emailIds, ReadActions readActions, - ) { - throw UnimplementedError(); + ) async { + final storedEmails = await Future.wait(emailIds.map( + (emailId) => getStoredEmail(session, accountId, emailId), + )); + for (var email in storedEmails) { + if (readActions == ReadActions.markAsUnread) { + email.keywords?.remove(KeyWordIdentifier.emailSeen); + } else { + email.keywords?[KeyWordIdentifier.emailSeen] = true; + } + } + await Future.wait(storedEmails.map( + (email) => storeEmail(session, accountId, email), + )); + return ( + emailIdsSuccess: emailIds, + mapErrors: {} + ); } @override @@ -150,8 +181,24 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { AccountId accountId, List emailIds, MarkStarAction markStarAction, - ) { - throw UnimplementedError(); + ) async { + final storedEmails = await Future.wait(emailIds.map( + (emailId) => getStoredEmail(session, accountId, emailId), + )); + for (var email in storedEmails) { + if (markStarAction == MarkStarAction.unMarkStar) { + email.keywords?.remove(KeyWordIdentifier.emailFlagged); + } else { + email.keywords?[KeyWordIdentifier.emailFlagged] = true; + } + } + await Future.wait(storedEmails.map( + (email) => storeEmail(session, accountId, email), + )); + return ( + emailIdsSuccess: emailIds, + mapErrors: {} + ); } @override @@ -162,8 +209,29 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { Session session, AccountId accountId, MoveToMailboxRequest moveRequest, - ) { - throw UnimplementedError(); + ) async { + final emailIds = moveRequest.currentMailboxes.entries.fold( + {}, + (emailIds, entry) { + emailIds.addAll(entry.value); + return emailIds; + }, + ).toList(); + final storedEmails = await Future.wait(emailIds.map( + (emailId) => getStoredEmail(session, accountId, emailId), + )); + for (int i = 0; i < storedEmails.length; i++) { + storedEmails[i] = storedEmails[i].updatedEmail( + newMailboxIds: {moveRequest.destinationMailboxId: true}, + ); + } + await Future.wait(storedEmails.map( + (email) => storeEmail(session, accountId, email), + )); + return ( + emailIdsSuccess: emailIds, + mapErrors: {} + ); } @override @@ -172,8 +240,13 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { AccountId accountId, EmailId emailId, {CancelToken? cancelToken} - ) { - throw UnimplementedError(); + ) async { + await _emailCacheManager.update( + accountId, + session.username, + destroyed: [emailId], + ); + return true; } @override @@ -182,8 +255,9 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { AccountId accountId, Email email, {CancelToken? cancelToken} - ) { - throw UnimplementedError(); + ) async { + await _emailCacheManager.update(accountId, session.username, created: [email]); + return email; } @override @@ -232,8 +306,13 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { Email newEmail, EmailId oldEmailId, {CancelToken? cancelToken} - ) { - throw UnimplementedError(); + ) async { + await _emailCacheManager.update( + accountId, + session.username, + updated: [newEmail], + ); + return newEmail; } @override diff --git a/lib/features/email/data/repository/email_repository_impl.dart b/lib/features/email/data/repository/email_repository_impl.dart index c849e3b22c..89ba6ae113 100644 --- a/lib/features/email/data/repository/email_repository_impl.dart +++ b/lib/features/email/data/repository/email_repository_impl.dart @@ -93,13 +93,22 @@ class EmailRepositoryImpl extends EmailRepository { AccountId accountId, List emailIds, ReadActions readActions, - ) { - return emailDataSource[DataSourceType.network]!.markAsRead( + ) async { + final result = await emailDataSource[DataSourceType.network]!.markAsRead( session, accountId, emailIds, readActions, ); + + await emailDataSource[DataSourceType.hiveCache]!.markAsRead( + session, + accountId, + result.emailIdsSuccess, + readActions, + ); + + return result; } @override @@ -136,12 +145,34 @@ class EmailRepositoryImpl extends EmailRepository { Session session, AccountId accountId, MoveToMailboxRequest moveRequest, - ) { - return emailDataSource[DataSourceType.network]!.moveToMailbox( + ) async { + final result = await emailDataSource[DataSourceType.network] + !.moveToMailbox( session, accountId, moveRequest, ); + final updatedCurrentMailboxes = moveRequest.currentMailboxes.map( + (key, value) => MapEntry( + key, + value.where(result.emailIdsSuccess.contains).toList(), + ), + ); + + await emailDataSource[DataSourceType.hiveCache] + !.moveToMailbox( + session, + accountId, + MoveToMailboxRequest( + updatedCurrentMailboxes, + moveRequest.destinationMailboxId, + moveRequest.moveAction, + moveRequest.emailActionType, + destinationPath: moveRequest.destinationPath, + ), + ); + + return result; } @override @@ -153,13 +184,20 @@ class EmailRepositoryImpl extends EmailRepository { AccountId accountId, List emailIds, MarkStarAction markStarAction - ) { - return emailDataSource[DataSourceType.network]!.markAsStar( + ) async { + final result = await emailDataSource[DataSourceType.network]!.markAsStar( session, accountId, emailIds, markStarAction, ); + await emailDataSource[DataSourceType.hiveCache]!.markAsStar( + session, + accountId, + result.emailIdsSuccess, + markStarAction + ); + return result; } @override @@ -185,13 +223,20 @@ class EmailRepositoryImpl extends EmailRepository { AccountId accountId, Email email, {CancelToken? cancelToken} - ) { - return emailDataSource[DataSourceType.network]!.saveEmailAsDrafts( + ) async { + final result = await emailDataSource[DataSourceType.network]!.saveEmailAsDrafts( session, accountId, email, cancelToken: cancelToken ); + await emailDataSource[DataSourceType.hiveCache]!.saveEmailAsDrafts( + session, + accountId, + result, + cancelToken: cancelToken + ); + return result; } @override @@ -200,13 +245,20 @@ class EmailRepositoryImpl extends EmailRepository { AccountId accountId, EmailId emailId, {CancelToken? cancelToken} - ) { - return emailDataSource[DataSourceType.network]!.removeEmailDrafts( + ) async { + final result = await emailDataSource[DataSourceType.network]!.removeEmailDrafts( + session, + accountId, + emailId, + cancelToken: cancelToken + ); + await emailDataSource[DataSourceType.hiveCache]!.removeEmailDrafts( session, accountId, emailId, cancelToken: cancelToken ); + return result; } @override @@ -216,14 +268,21 @@ class EmailRepositoryImpl extends EmailRepository { Email newEmail, EmailId oldEmailId, {CancelToken? cancelToken} - ) { - return emailDataSource[DataSourceType.network]!.updateEmailDrafts( + ) async { + final result = await emailDataSource[DataSourceType.network]!.updateEmailDrafts( session, accountId, newEmail, oldEmailId, cancelToken: cancelToken ); + await emailDataSource[DataSourceType.hiveCache]!.updateEmailDrafts( + session, + accountId, + result, + oldEmailId, + ); + return result; } @override @@ -254,12 +313,17 @@ class EmailRepositoryImpl extends EmailRepository { Session session, AccountId accountId, List emailIds, - ) { - return emailDataSource[DataSourceType.network]!.deleteMultipleEmailsPermanently( + ) async { + final result = await emailDataSource[DataSourceType.network] + !.deleteMultipleEmailsPermanently( session, accountId, emailIds, ); + await emailDataSource[DataSourceType.hiveCache] + !.deleteMultipleEmailsPermanently(session, accountId, result.emailIdsSuccess); + + return result; } @override @@ -268,13 +332,20 @@ class EmailRepositoryImpl extends EmailRepository { AccountId accountId, EmailId emailId, {CancelToken? cancelToken} - ) { - return emailDataSource[DataSourceType.network]!.deleteEmailPermanently( + ) async { + final result = await emailDataSource[DataSourceType.network]!.deleteEmailPermanently( session, accountId, emailId, cancelToken: cancelToken ); + await emailDataSource[DataSourceType.hiveCache]!.deleteEmailPermanently( + session, + accountId, + emailId, + ); + + return result; } @override diff --git a/lib/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart b/lib/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart index e0c604a39a..3c5496f133 100644 --- a/lib/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart +++ b/lib/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart @@ -13,6 +13,8 @@ import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/extensions/list_mailbox_extension.dart'; +import 'package:model/extensions/mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/mailbox_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/local/mailbox_cache_manager.dart'; import 'package:tmail_ui_user/features/mailbox/data/model/mailbox_change_response.dart'; @@ -68,8 +70,26 @@ class MailboxCacheDataSourceImpl extends MailboxDataSource { } @override - Future renameMailbox(Session session, AccountId accountId, RenameMailboxRequest request) { - throw UnimplementedError(); + Future renameMailbox( + Session session, + AccountId accountId, + RenameMailboxRequest request, + ) async { + final cachedMailboxes = await getAllMailboxCache(accountId, session.username); + final updatedMailbox = cachedMailboxes.findMailbox(request.mailboxId); + if (updatedMailbox == null) return false; + + final updatedMailboxIndex = cachedMailboxes.indexOf(updatedMailbox); + if (updatedMailboxIndex == -1) return false; + + cachedMailboxes[updatedMailboxIndex] = updatedMailbox.copyWith(name: request.newName); + await update( + accountId, + session.username, + updated: cachedMailboxes, + ); + + return true; } @override @@ -79,12 +99,29 @@ class MailboxCacheDataSourceImpl extends MailboxDataSource { @override Future> markAsMailboxRead( - Session session, - AccountId accountId, - MailboxId mailboxId, - int totalEmailUnread, - StreamController> onProgressController) { - throw UnimplementedError(); + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmailUnread, + StreamController> onProgressController, + ) async { + final mailboxes = await getAllMailboxCache(accountId, session.username); + final updatedMailbox = mailboxes.findMailbox(mailboxId); + if (updatedMailbox == null) return []; + + final updatedMailboxIndex = mailboxes.indexOf(updatedMailbox); + if (updatedMailboxIndex == -1) return []; + + mailboxes[updatedMailboxIndex] = updatedMailbox.copyWith( + unreadEmails: UnreadEmails(UnsignedInt(totalEmailUnread)), + ); + await _mailboxCacheManager.update( + accountId, + session.username, + updated: mailboxes, + ); + + return []; } @override diff --git a/lib/features/mailbox/data/repository/mailbox_repository_impl.dart b/lib/features/mailbox/data/repository/mailbox_repository_impl.dart index bfc6fcc068..cf4a9c1206 100644 --- a/lib/features/mailbox/data/repository/mailbox_repository_impl.dart +++ b/lib/features/mailbox/data/repository/mailbox_repository_impl.dart @@ -15,8 +15,10 @@ import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/email/read_actions.dart'; import 'package:model/extensions/list_mailbox_extension.dart'; import 'package:model/extensions/mailbox_extension.dart'; +import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/mailbox_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/extensions/state_extension.dart'; @@ -36,10 +38,12 @@ class MailboxRepositoryImpl extends MailboxRepository { final Map mapDataSource; final StateDataSource stateDataSource; + final EmailDataSource? emailDataSource; MailboxRepositoryImpl( this.mapDataSource, this.stateDataSource, + [this.emailDataSource,] ); @override @@ -211,23 +215,44 @@ class MailboxRepositoryImpl extends MailboxRepository { } @override - Future renameMailbox(Session session, AccountId accountId, RenameMailboxRequest request) { - return mapDataSource[DataSourceType.network]!.renameMailbox(session, accountId, request); + Future renameMailbox(Session session, AccountId accountId, RenameMailboxRequest request) async { + final result = await mapDataSource[DataSourceType.network] + !.renameMailbox(session, accountId, request); + + await mapDataSource[DataSourceType.local] + !.renameMailbox(session, accountId, request); + + return result; } @override Future> markAsMailboxRead( - Session session, - AccountId accountId, - MailboxId mailboxId, - int totalEmailUnread, - StreamController> onProgressController) async { - return mapDataSource[DataSourceType.network]!.markAsMailboxRead( + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmailUnread, + StreamController> onProgressController, + ) async { + final result = await mapDataSource[DataSourceType.network]!.markAsMailboxRead( session, accountId, mailboxId, totalEmailUnread, onProgressController); + await mapDataSource[DataSourceType.local]!.markAsMailboxRead( + session, + accountId, + mailboxId, + totalEmailUnread - result.length, + onProgressController, + ); + await emailDataSource?.markAsRead( + session, + accountId, + result, + ReadActions.markAsRead, + ); + return result; } @override diff --git a/lib/features/mailbox/presentation/mailbox_bindings.dart b/lib/features/mailbox/presentation/mailbox_bindings.dart index 2458018923..9fd2fd2a11 100644 --- a/lib/features/mailbox/presentation/mailbox_bindings.dart +++ b/lib/features/mailbox/presentation/mailbox_bindings.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource_impl/email_datasource_impl.dart'; +import 'package:tmail_ui_user/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/mailbox_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart'; @@ -122,6 +123,7 @@ class MailboxBindings extends BaseBindings { DataSourceType.local: Get.find() }, Get.find(), + Get.find(), )); } } \ No newline at end of file diff --git a/model/lib/extensions/mailbox_extension.dart b/model/lib/extensions/mailbox_extension.dart index 44dcefa265..6219188be3 100644 --- a/model/lib/extensions/mailbox_extension.dart +++ b/model/lib/extensions/mailbox_extension.dart @@ -1,5 +1,7 @@ import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox_rights.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/namespace.dart'; import 'package:model/model.dart'; extension MailboxExtension on Mailbox { @@ -68,4 +70,36 @@ extension MailboxExtension on Mailbox { namespace: namespace, ); } + + Mailbox copyWith({ + MailboxId? id, + MailboxName? name, + MailboxId? parentId, + Role? role, + SortOrder? sortOrder, + TotalEmails? totalEmails, + UnreadEmails? unreadEmails, + TotalThreads? totalThreads, + UnreadThreads? unreadThreads, + MailboxRights? myRights, + IsSubscribed? isSubscribed, + Namespace? namespace, + Map?>? rights, + }) { + return Mailbox( + id: id ?? this.id, + name: name ?? this.name, + parentId: parentId ?? this.parentId, + role: role ?? this.role, + sortOrder: sortOrder ?? this.sortOrder, + totalEmails: totalEmails ?? this.totalEmails, + unreadEmails: unreadEmails ?? this.unreadEmails, + totalThreads: totalThreads ?? this.totalThreads, + unreadThreads: unreadThreads ?? this.unreadThreads, + myRights: myRights ?? this.myRights, + isSubscribed: isSubscribed ?? this.isSubscribed, + namespace: namespace ?? this.namespace, + rights: rights ?? this.rights, + ); + } } \ No newline at end of file From aec0cd7653888502f521b4d75ad091da2e9dc7d4 Mon Sep 17 00:00:00 2001 From: DatDang Date: Fri, 3 Jan 2025 15:06:37 +0700 Subject: [PATCH 56/72] TF-3385 Create abstract group for update mailbox actions --- .../update_mailbox_name_action.dart | 18 ++++++ .../update_mailbox_properties_action.dart | 24 ++++++++ ...ate_mailbox_total_emails_count_action.dart | 20 +++++++ .../update_mailbox_unread_count_action.dart | 17 ++++++ .../base/base_mailbox_controller.dart | 58 +++++++------------ 5 files changed, 99 insertions(+), 38 deletions(-) create mode 100644 lib/features/base/action/update_mailbox_properties_action/update_mailbox_name_action.dart create mode 100644 lib/features/base/action/update_mailbox_properties_action/update_mailbox_properties_action.dart create mode 100644 lib/features/base/action/update_mailbox_properties_action/update_mailbox_total_emails_count_action.dart create mode 100644 lib/features/base/action/update_mailbox_properties_action/update_mailbox_unread_count_action.dart diff --git a/lib/features/base/action/update_mailbox_properties_action/update_mailbox_name_action.dart b/lib/features/base/action/update_mailbox_properties_action/update_mailbox_name_action.dart new file mode 100644 index 0000000000..5327133a20 --- /dev/null +++ b/lib/features/base/action/update_mailbox_properties_action/update_mailbox_name_action.dart @@ -0,0 +1,18 @@ +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:tmail_ui_user/features/base/action/update_mailbox_properties_action/update_mailbox_properties_action.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree.dart'; + +class UpdateMailboxNameAction extends UpdateMailboxPropertiesAction { + const UpdateMailboxNameAction({ + required super.mailboxTrees, + required super.mailboxId, + required this.mailboxName, + }); + + final MailboxName mailboxName; + + @override + bool updateProperty(MailboxTree mailboxTree) { + return mailboxTree.updateMailboxNameById(mailboxId, mailboxName); + } +} \ No newline at end of file diff --git a/lib/features/base/action/update_mailbox_properties_action/update_mailbox_properties_action.dart b/lib/features/base/action/update_mailbox_properties_action/update_mailbox_properties_action.dart new file mode 100644 index 0000000000..ed37f10c20 --- /dev/null +++ b/lib/features/base/action/update_mailbox_properties_action/update_mailbox_properties_action.dart @@ -0,0 +1,24 @@ +import 'package:get/get_rx/get_rx.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree.dart'; + +abstract class UpdateMailboxPropertiesAction { + const UpdateMailboxPropertiesAction({ + required this.mailboxTrees, + required this.mailboxId, + }); + + final List> mailboxTrees; + final MailboxId mailboxId; + + bool updateProperty(MailboxTree mailboxTree); + + void execute() { + for (var mailboxTree in mailboxTrees) { + if (updateProperty(mailboxTree.value)) { + mailboxTree.refresh(); + break; + } + } + } +} \ No newline at end of file diff --git a/lib/features/base/action/update_mailbox_properties_action/update_mailbox_total_emails_count_action.dart b/lib/features/base/action/update_mailbox_properties_action/update_mailbox_total_emails_count_action.dart new file mode 100644 index 0000000000..35c3e69c00 --- /dev/null +++ b/lib/features/base/action/update_mailbox_properties_action/update_mailbox_total_emails_count_action.dart @@ -0,0 +1,20 @@ +import 'package:tmail_ui_user/features/base/action/update_mailbox_properties_action/update_mailbox_properties_action.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree.dart'; + +class UpdateMailboxTotalEmailsCountAction extends UpdateMailboxPropertiesAction { + const UpdateMailboxTotalEmailsCountAction({ + required super.mailboxTrees, + required super.mailboxId, + required this.totalEmailsCountChanged, + }); + + final int totalEmailsCountChanged; + + @override + bool updateProperty(MailboxTree mailboxTree) { + return mailboxTree.updateMailboxTotalEmailsCountById( + mailboxId, + totalEmailsCountChanged, + ); + } +} \ No newline at end of file diff --git a/lib/features/base/action/update_mailbox_properties_action/update_mailbox_unread_count_action.dart b/lib/features/base/action/update_mailbox_properties_action/update_mailbox_unread_count_action.dart new file mode 100644 index 0000000000..5651d24b4f --- /dev/null +++ b/lib/features/base/action/update_mailbox_properties_action/update_mailbox_unread_count_action.dart @@ -0,0 +1,17 @@ +import 'package:tmail_ui_user/features/base/action/update_mailbox_properties_action/update_mailbox_properties_action.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree.dart'; + +class UpdateMailboxUnreadCountAction extends UpdateMailboxPropertiesAction { + const UpdateMailboxUnreadCountAction({ + required super.mailboxTrees, + required super.mailboxId, + required this.unreadChanges, + }); + + final int unreadChanges; + + @override + bool updateProperty(MailboxTree mailboxTree) { + return mailboxTree.updateMailboxUnreadCountById(mailboxId, unreadChanges); + } +} \ No newline at end of file diff --git a/lib/features/base/base_mailbox_controller.dart b/lib/features/base/base_mailbox_controller.dart index 73e6194155..ee92dc783d 100644 --- a/lib/features/base/base_mailbox_controller.dart +++ b/lib/features/base/base_mailbox_controller.dart @@ -20,6 +20,9 @@ import 'package:model/mailbox/expand_mode.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:model/mailbox/select_mode.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:tmail_ui_user/features/base/action/update_mailbox_properties_action/update_mailbox_name_action.dart'; +import 'package:tmail_ui_user/features/base/action/update_mailbox_properties_action/update_mailbox_total_emails_count_action.dart'; +import 'package:tmail_ui_user/features/base/action/update_mailbox_properties_action/update_mailbox_unread_count_action.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/mailbox_subscribe_action_state.dart'; @@ -555,36 +558,22 @@ abstract class BaseMailboxController extends BaseController { } void updateMailboxNameById(MailboxId mailboxId, MailboxName mailboxName) { - final mailboxTrees = [ - defaultMailboxTree, - personalMailboxTree, - teamMailboxesTree, - ]; - - for (var mailboxTree in mailboxTrees) { - if (mailboxTree.value.updateMailboxNameById(mailboxId, mailboxName)) { - mailboxTree.refresh(); - break; - } - } + UpdateMailboxNameAction( + mailboxTrees: [defaultMailboxTree, personalMailboxTree, teamMailboxesTree], + mailboxId: mailboxId, + mailboxName: mailboxName, + ).execute(); } void updateUnreadCountOfMailboxById( MailboxId mailboxId, { required int unreadChanges, }) { - final mailboxTrees = [ - defaultMailboxTree, - personalMailboxTree, - teamMailboxesTree, - ]; - - for (var mailboxTree in mailboxTrees) { - if (mailboxTree.value.updateMailboxUnreadCountById(mailboxId, unreadChanges)) { - mailboxTree.refresh(); - break; - } - } + UpdateMailboxUnreadCountAction( + mailboxTrees: [defaultMailboxTree, personalMailboxTree, teamMailboxesTree], + mailboxId: mailboxId, + unreadChanges: unreadChanges, + ).execute(); } void clearUnreadCount(MailboxId mailboxId) { @@ -598,26 +587,19 @@ abstract class BaseMailboxController extends BaseController { final selectedNode = mailboxTree.value.findNode((node) => node.item.id == mailboxId); if (selectedNode == null) continue; final currentUnreadCount = selectedNode.item.unreadEmails?.value.value.toInt(); - teamMailboxesTree.value.updateMailboxUnreadCountById( + mailboxTree.value.updateMailboxUnreadCountById( mailboxId, -(currentUnreadCount ?? 0)); - teamMailboxesTree.refresh(); + mailboxTree.refresh(); break; } } void updateMailboxTotalEmailsCountById(MailboxId mailboxId, int totalEmails) { - final mailboxTrees = [ - defaultMailboxTree, - personalMailboxTree, - teamMailboxesTree, - ]; - - for (var mailboxTree in mailboxTrees) { - if (mailboxTree.value.updateMailboxTotalEmailsCountById(mailboxId, totalEmails)) { - mailboxTree.refresh(); - break; - } - } + UpdateMailboxTotalEmailsCountAction( + mailboxTrees: [defaultMailboxTree, personalMailboxTree, teamMailboxesTree], + mailboxId: mailboxId, + totalEmailsCountChanged: totalEmails, + ).execute(); } } \ No newline at end of file From a20e32557195916d52545dd700474116f2fac886 Mon Sep 17 00:00:00 2001 From: DatDang Date: Fri, 3 Jan 2025 15:08:54 +0700 Subject: [PATCH 57/72] TF-3385 Get & set multiple changes in email cache --- .../email_hive_cache_datasource_impl.dart | 63 +++++++++++-------- .../data/local/email_cache_manager.dart | 20 ++++++ 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart index b2caa73b35..c607b59bb0 100644 --- a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart @@ -153,9 +153,12 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { List emailIds, ReadActions readActions, ) async { - final storedEmails = await Future.wait(emailIds.map( - (emailId) => getStoredEmail(session, accountId, emailId), - )); + final cacheEmails = await _emailCacheManager.getMultipleStoredEmails( + accountId, + session.username, + emailIds, + ); + final storedEmails = cacheEmails.map((emailCache) => emailCache.toEmail()).toList(); for (var email in storedEmails) { if (readActions == ReadActions.markAsUnread) { email.keywords?.remove(KeyWordIdentifier.emailSeen); @@ -163,9 +166,11 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { email.keywords?[KeyWordIdentifier.emailSeen] = true; } } - await Future.wait(storedEmails.map( - (email) => storeEmail(session, accountId, email), - )); + await _emailCacheManager.storeMultipleEmails( + accountId, + session.username, + storedEmails.map((email) => email.toEmailCache()).toList(), + ); return ( emailIdsSuccess: emailIds, mapErrors: {} @@ -182,9 +187,12 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { List emailIds, MarkStarAction markStarAction, ) async { - final storedEmails = await Future.wait(emailIds.map( - (emailId) => getStoredEmail(session, accountId, emailId), - )); + final cacheEmails = await _emailCacheManager.getMultipleStoredEmails( + accountId, + session.username, + emailIds, + ); + final storedEmails = cacheEmails.map((emailCache) => emailCache.toEmail()).toList(); for (var email in storedEmails) { if (markStarAction == MarkStarAction.unMarkStar) { email.keywords?.remove(KeyWordIdentifier.emailFlagged); @@ -192,9 +200,11 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { email.keywords?[KeyWordIdentifier.emailFlagged] = true; } } - await Future.wait(storedEmails.map( - (email) => storeEmail(session, accountId, email), - )); + await _emailCacheManager.storeMultipleEmails( + accountId, + session.username, + storedEmails.map((email) => email.toEmailCache()).toList(), + ); return ( emailIdsSuccess: emailIds, mapErrors: {} @@ -210,24 +220,27 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { AccountId accountId, MoveToMailboxRequest moveRequest, ) async { - final emailIds = moveRequest.currentMailboxes.entries.fold( - {}, - (emailIds, entry) { - emailIds.addAll(entry.value); - return emailIds; - }, - ).toList(); - final storedEmails = await Future.wait(emailIds.map( - (emailId) => getStoredEmail(session, accountId, emailId), - )); + final emailIds = moveRequest + .currentMailboxes + .values + .expand((emails) => emails) + .toList(); + final cacheEmails = await _emailCacheManager.getMultipleStoredEmails( + accountId, + session.username, + emailIds, + ); + final storedEmails = cacheEmails.map((emailCache) => emailCache.toEmail()).toList(); for (int i = 0; i < storedEmails.length; i++) { storedEmails[i] = storedEmails[i].updatedEmail( newMailboxIds: {moveRequest.destinationMailboxId: true}, ); } - await Future.wait(storedEmails.map( - (email) => storeEmail(session, accountId, email), - )); + await _emailCacheManager.storeMultipleEmails( + accountId, + session.username, + storedEmails.map((email) => email.toEmailCache()).toList(), + ); return ( emailIdsSuccess: emailIds, mapErrors: {} diff --git a/lib/features/thread/data/local/email_cache_manager.dart b/lib/features/thread/data/local/email_cache_manager.dart index 9fa8d0383b..d7ea24c971 100644 --- a/lib/features/thread/data/local/email_cache_manager.dart +++ b/lib/features/thread/data/local/email_cache_manager.dart @@ -95,6 +95,14 @@ class EmailCacheManager { return _emailCacheClient.insertItem(keyCache, emailCache); } + Future storeMultipleEmails(AccountId accountId, UserName userName, List emailsCache) { + return Future.wait(emailsCache.map((emailCache) => storeEmail( + accountId, + userName, + emailCache, + ))); + } + Future getStoredEmail(AccountId accountId, UserName userName, EmailId emailId) async { final keyCache = TupleKey(emailId.asString, accountId.asString, userName.value).encodeKey; final emailCache = await _emailCacheClient.getItem(keyCache, needToReopen: true); @@ -104,4 +112,16 @@ class EmailCacheManager { throw NotFoundStoredEmailException(); } } + + Future> getMultipleStoredEmails( + AccountId accountId, + UserName userName, + List emailIds, + ) async { + final keys = emailIds + .map((emailId) => TupleKey(emailId.asString, accountId.asString, userName.value).encodeKey) + .toList(); + final emails = await _emailCacheClient.getValuesByListKey(keys); + return emails; + } } \ No newline at end of file From f4bcd6b4045e2723c5b48adcfcb747129838b3c8 Mon Sep 17 00:00:00 2001 From: DatDang Date: Fri, 3 Jan 2025 15:09:27 +0700 Subject: [PATCH 58/72] TF-3385 Add try-catch to cache method calls on user actions --- .../repository/email_repository_impl.dart | 131 +++++++++++------- .../repository/mailbox_repository_impl.dart | 38 +++-- 2 files changed, 105 insertions(+), 64 deletions(-) diff --git a/lib/features/email/data/repository/email_repository_impl.dart b/lib/features/email/data/repository/email_repository_impl.dart index 89ba6ae113..5b19266ebd 100644 --- a/lib/features/email/data/repository/email_repository_impl.dart +++ b/lib/features/email/data/repository/email_repository_impl.dart @@ -6,6 +6,7 @@ import 'package:core/data/network/download/downloaded_response.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; import 'package:email_recovery/email_recovery/email_recovery_action.dart'; @@ -101,12 +102,16 @@ class EmailRepositoryImpl extends EmailRepository { readActions, ); - await emailDataSource[DataSourceType.hiveCache]!.markAsRead( - session, - accountId, - result.emailIdsSuccess, - readActions, - ); + try { + await emailDataSource[DataSourceType.hiveCache]!.markAsRead( + session, + accountId, + result.emailIdsSuccess, + readActions, + ); + } catch (e) { + logError('EmailRepositoryImpl::markAsRead:exception $e'); + } return result; } @@ -159,18 +164,22 @@ class EmailRepositoryImpl extends EmailRepository { ), ); - await emailDataSource[DataSourceType.hiveCache] - !.moveToMailbox( - session, - accountId, - MoveToMailboxRequest( - updatedCurrentMailboxes, - moveRequest.destinationMailboxId, - moveRequest.moveAction, - moveRequest.emailActionType, - destinationPath: moveRequest.destinationPath, - ), - ); + try { + await emailDataSource[DataSourceType.hiveCache] + !.moveToMailbox( + session, + accountId, + MoveToMailboxRequest( + updatedCurrentMailboxes, + moveRequest.destinationMailboxId, + moveRequest.moveAction, + moveRequest.emailActionType, + destinationPath: moveRequest.destinationPath, + ), + ); + } catch (e) { + logError('EmailRepositoryImpl::moveToMailbox:exception $e'); + } return result; } @@ -191,12 +200,16 @@ class EmailRepositoryImpl extends EmailRepository { emailIds, markStarAction, ); - await emailDataSource[DataSourceType.hiveCache]!.markAsStar( - session, - accountId, - result.emailIdsSuccess, - markStarAction - ); + try { + await emailDataSource[DataSourceType.hiveCache]!.markAsStar( + session, + accountId, + result.emailIdsSuccess, + markStarAction + ); + } catch (e) { + logError('EmailRepositoryImpl::markAsStar:exception $e'); + } return result; } @@ -230,12 +243,16 @@ class EmailRepositoryImpl extends EmailRepository { email, cancelToken: cancelToken ); - await emailDataSource[DataSourceType.hiveCache]!.saveEmailAsDrafts( - session, - accountId, - result, - cancelToken: cancelToken - ); + try { + await emailDataSource[DataSourceType.hiveCache]!.saveEmailAsDrafts( + session, + accountId, + result, + cancelToken: cancelToken + ); + } catch (e) { + logError('EmailRepositoryImpl::saveEmailAsDrafts:exception $e'); + } return result; } @@ -252,12 +269,16 @@ class EmailRepositoryImpl extends EmailRepository { emailId, cancelToken: cancelToken ); - await emailDataSource[DataSourceType.hiveCache]!.removeEmailDrafts( - session, - accountId, - emailId, - cancelToken: cancelToken - ); + try { + await emailDataSource[DataSourceType.hiveCache]!.removeEmailDrafts( + session, + accountId, + emailId, + cancelToken: cancelToken + ); + } catch (e) { + logError('EmailRepositoryImpl::removeEmailDrafts:exception $e'); + } return result; } @@ -276,12 +297,16 @@ class EmailRepositoryImpl extends EmailRepository { oldEmailId, cancelToken: cancelToken ); - await emailDataSource[DataSourceType.hiveCache]!.updateEmailDrafts( - session, - accountId, - result, - oldEmailId, - ); + try { + await emailDataSource[DataSourceType.hiveCache]!.updateEmailDrafts( + session, + accountId, + result, + oldEmailId, + ); + } catch (e) { + logError('EmailRepositoryImpl::updateEmailDrafts:exception $e'); + } return result; } @@ -320,8 +345,12 @@ class EmailRepositoryImpl extends EmailRepository { accountId, emailIds, ); - await emailDataSource[DataSourceType.hiveCache] - !.deleteMultipleEmailsPermanently(session, accountId, result.emailIdsSuccess); + try { + await emailDataSource[DataSourceType.hiveCache] + !.deleteMultipleEmailsPermanently(session, accountId, result.emailIdsSuccess); + } catch (e) { + logError('EmailRepositoryImpl::deleteMultipleEmailsPermanently:exception $e'); + } return result; } @@ -339,11 +368,15 @@ class EmailRepositoryImpl extends EmailRepository { emailId, cancelToken: cancelToken ); - await emailDataSource[DataSourceType.hiveCache]!.deleteEmailPermanently( - session, - accountId, - emailId, - ); + try { + await emailDataSource[DataSourceType.hiveCache]!.deleteEmailPermanently( + session, + accountId, + emailId, + ); + } catch (e) { + logError('EmailRepositoryImpl::deleteEmailPermanently:exception $e'); + } return result; } diff --git a/lib/features/mailbox/data/repository/mailbox_repository_impl.dart b/lib/features/mailbox/data/repository/mailbox_repository_impl.dart index cf4a9c1206..5fbf9394e9 100644 --- a/lib/features/mailbox/data/repository/mailbox_repository_impl.dart +++ b/lib/features/mailbox/data/repository/mailbox_repository_impl.dart @@ -219,8 +219,12 @@ class MailboxRepositoryImpl extends MailboxRepository { final result = await mapDataSource[DataSourceType.network] !.renameMailbox(session, accountId, request); - await mapDataSource[DataSourceType.local] - !.renameMailbox(session, accountId, request); + try { + await mapDataSource[DataSourceType.local] + !.renameMailbox(session, accountId, request); + } catch (e) { + logError('MailboxRepositoryImpl::renameMailbox: Exception: $e'); + } return result; } @@ -239,19 +243,23 @@ class MailboxRepositoryImpl extends MailboxRepository { mailboxId, totalEmailUnread, onProgressController); - await mapDataSource[DataSourceType.local]!.markAsMailboxRead( - session, - accountId, - mailboxId, - totalEmailUnread - result.length, - onProgressController, - ); - await emailDataSource?.markAsRead( - session, - accountId, - result, - ReadActions.markAsRead, - ); + try { + await mapDataSource[DataSourceType.local]!.markAsMailboxRead( + session, + accountId, + mailboxId, + totalEmailUnread - result.length, + onProgressController, + ); + await emailDataSource?.markAsRead( + session, + accountId, + result, + ReadActions.markAsRead, + ); + } catch (e) { + logError('MailboxRepositoryImpl::markAsMailboxRead: Exception: $e'); + } return result; } From cd447aa80d7bebad1a21a1fe0f65c6f01381d5aa Mon Sep 17 00:00:00 2001 From: DatDang Date: Fri, 3 Jan 2025 15:11:03 +0700 Subject: [PATCH 59/72] TF-3385 Remove skipCache mechanism in get all emails --- .../thread/data/repository/thread_repository_impl.dart | 3 +-- lib/features/thread/domain/repository/thread_repository.dart | 1 - .../domain/usecases/get_emails_in_mailbox_interactor.dart | 4 +--- lib/features/thread/presentation/thread_controller.dart | 4 +--- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/lib/features/thread/data/repository/thread_repository_impl.dart b/lib/features/thread/data/repository/thread_repository_impl.dart index 6df6a4a030..9b4c5d94d8 100644 --- a/lib/features/thread/data/repository/thread_repository_impl.dart +++ b/lib/features/thread/data/repository/thread_repository_impl.dart @@ -49,7 +49,6 @@ class ThreadRepositoryImpl extends ThreadRepository { Properties? propertiesCreated, Properties? propertiesUpdated, bool getLatestChanges = true, - bool skipCache = false, } ) async* { log('ThreadRepositoryImpl::getAllEmail(): filter = ${emailFilter?.mailboxId}'); @@ -89,7 +88,7 @@ class ThreadRepositoryImpl extends ThreadRepository { ); } yield networkEmailResponse; - } else if (!skipCache) { + } else { yield localEmailResponse; } diff --git a/lib/features/thread/domain/repository/thread_repository.dart b/lib/features/thread/domain/repository/thread_repository.dart index 8eb198ec6e..ec05030612 100644 --- a/lib/features/thread/domain/repository/thread_repository.dart +++ b/lib/features/thread/domain/repository/thread_repository.dart @@ -29,7 +29,6 @@ abstract class ThreadRepository { Properties? propertiesCreated, Properties? propertiesUpdated, bool getLatestChanges = true, - bool skipCache = false, } ); diff --git a/lib/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart b/lib/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart index 8e8a98bf55..0eccaae504 100644 --- a/lib/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart +++ b/lib/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart @@ -27,7 +27,6 @@ class GetEmailsInMailboxInteractor { Properties? propertiesCreated, Properties? propertiesUpdated, bool getLatestChanges = true, - bool skipCache = false, } ) async* { try { @@ -42,8 +41,7 @@ class GetEmailsInMailboxInteractor { emailFilter: emailFilter, propertiesCreated: propertiesCreated, propertiesUpdated: propertiesUpdated, - getLatestChanges: getLatestChanges, - skipCache: skipCache) + getLatestChanges: getLatestChanges) .map((emailResponse) => _toGetEmailState( emailResponse: emailResponse, currentMailboxId: emailFilter?.mailboxId diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index c91e9c4a80..5bc655f8d5 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -489,7 +489,6 @@ class ThreadController extends BaseController with EmailActionController { void _getAllEmailAction({ bool getLatestChanges = true, - bool skipCache = false, }) { log('ThreadController::_getAllEmailAction:'); if (_session != null &&_accountId != null) { @@ -506,7 +505,6 @@ class ThreadController extends BaseController with EmailActionController { propertiesCreated: EmailUtils.getPropertiesForEmailGetMethod(_session!, _accountId!), propertiesUpdated: ThreadConstants.propertiesUpdatedDefault, getLatestChanges: getLatestChanges, - skipCache: skipCache, )); } else { consumeState(Stream.value(Left(GetAllEmailFailure(NotFoundSessionException())))); @@ -555,7 +553,7 @@ class ThreadController extends BaseController with EmailActionController { if (searchController.isSearchEmailRunning) { _searchEmail(limit: limitEmailFetched); } else { - _getAllEmailAction(skipCache: true); + _getAllEmailAction(); } } From 23852dadd022f149d406fa3959e7e5d41b7f5c5e Mon Sep 17 00:00:00 2001 From: DatDang Date: Fri, 3 Jan 2025 15:12:53 +0700 Subject: [PATCH 60/72] TF-3385 Fix mark as read not work properly --- .../controller/single_email_controller.dart | 2 +- .../presentation/mailbox_controller.dart | 40 ++++++++++--------- .../mailbox_dashboard_controller.dart | 2 +- .../mark_as_multiple_email_read_state.dart | 12 +++--- ...ark_as_multiple_email_read_interactor.dart | 12 ++++-- .../list_presentation_email_extension.dart | 11 +++++ 6 files changed, 50 insertions(+), 29 deletions(-) diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 2c9868b085..901d64fe54 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -673,7 +673,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { if (_currentEmailId != null) { mailboxDashBoardController.updateEmailFlagByEmailIds( [_currentEmailId!], - readAction: ReadActions.markAsRead, + readAction: readActions, ); } if (readActions == ReadActions.markAsUnread) { diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index ad8ff75775..43f6d6d17a 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -281,25 +281,29 @@ class MailboxController extends BaseMailboxController : null, ); } else if (reactionState is MarkAsMultipleEmailReadAllSuccess) { - _handleMarkEmailsAsReadOrUnread( - affectedMailboxId: reactionState.mailboxId, - readCount: reactionState.readActions == ReadActions.markAsRead - ? reactionState.emailIds.length - : null, - unreadCount: reactionState.readActions == ReadActions.markAsUnread - ? reactionState.emailIds.length - : null, - ); + for (var emailIdsByMailboxId in reactionState.markSuccessEmailIdsByMailboxId.entries) { + _handleMarkEmailsAsReadOrUnread( + affectedMailboxId: emailIdsByMailboxId.key, + readCount: reactionState.readActions == ReadActions.markAsRead + ? emailIdsByMailboxId.value.length + : null, + unreadCount: reactionState.readActions == ReadActions.markAsUnread + ? emailIdsByMailboxId.value.length + : null, + ); + } } else if (reactionState is MarkAsMultipleEmailReadHasSomeEmailFailure) { - _handleMarkEmailsAsReadOrUnread( - affectedMailboxId: reactionState.mailboxId, - readCount: reactionState.readActions == ReadActions.markAsRead - ? reactionState.successEmailIds.length - : null, - unreadCount: reactionState.readActions == ReadActions.markAsUnread - ? reactionState.successEmailIds.length - : null, - ); + for (var emailIdsByMailboxId in reactionState.markSuccessEmailIdsByMailboxId.entries) { + _handleMarkEmailsAsReadOrUnread( + affectedMailboxId: emailIdsByMailboxId.key, + readCount: reactionState.readActions == ReadActions.markAsRead + ? emailIdsByMailboxId.value.length + : null, + unreadCount: reactionState.readActions == ReadActions.markAsUnread + ? emailIdsByMailboxId.value.length + : null, + ); + } } else if (reactionState is MarkAsMailboxReadAllSuccess) { _handleMarkMailboxAsRead( affectedMailboxId: reactionState.mailboxId, diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 63914b9fb6..1cf4e630b8 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -967,7 +967,7 @@ class MailboxDashBoardController extends ReloadableController accountId.value!, listEmailNeedMarkAsRead.listEmailIds, readActions, - listPresentationEmail.firstOrNull?.mailboxContain?.mailboxId, + listEmailNeedMarkAsRead.emailIdsByMailboxId, )); } } diff --git a/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart b/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart index 0eb6ab7fc8..c3c322f5b5 100644 --- a/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart +++ b/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart @@ -9,16 +9,16 @@ class LoadingMarkAsMultipleEmailReadAll extends UIState {} class MarkAsMultipleEmailReadAllSuccess extends UIState { final List emailIds; final ReadActions readActions; - final MailboxId? mailboxId; + final Map> markSuccessEmailIdsByMailboxId; MarkAsMultipleEmailReadAllSuccess( this.emailIds, this.readActions, - this.mailboxId, + this.markSuccessEmailIdsByMailboxId, ); @override - List get props => [emailIds, readActions, mailboxId]; + List get props => [emailIds, readActions, markSuccessEmailIdsByMailboxId]; } class MarkAsMultipleEmailReadAllFailure extends FeatureFailure { @@ -33,16 +33,16 @@ class MarkAsMultipleEmailReadAllFailure extends FeatureFailure { class MarkAsMultipleEmailReadHasSomeEmailFailure extends UIState { final List successEmailIds; final ReadActions readActions; - final MailboxId? mailboxId; + final Map> markSuccessEmailIdsByMailboxId; MarkAsMultipleEmailReadHasSomeEmailFailure( this.successEmailIds, this.readActions, - this.mailboxId, + this.markSuccessEmailIdsByMailboxId, ); @override - List get props => [successEmailIds, readActions, mailboxId]; + List get props => [successEmailIds, readActions, markSuccessEmailIdsByMailboxId]; } class MarkAsMultipleEmailReadFailure extends FeatureFailure { diff --git a/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart b/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart index 5bd0763619..505a15642b 100644 --- a/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart +++ b/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart @@ -19,7 +19,7 @@ class MarkAsMultipleEmailReadInteractor { AccountId accountId, List emailIds, ReadActions readAction, - MailboxId? mailboxId, + Map> emailIdsByMailboxId, ) async* { try { yield Right(LoadingMarkAsMultipleEmailReadAll()); @@ -30,12 +30,18 @@ class MarkAsMultipleEmailReadInteractor { emailIds, readAction, ); + final markSuccessEmailIdsByMailboxId = emailIdsByMailboxId.map( + (key, value) => MapEntry( + key, + value.where(result.emailIdsSuccess.contains).toList(), + ), + ); if (emailIds.length == result.emailIdsSuccess.length) { yield Right(MarkAsMultipleEmailReadAllSuccess( result.emailIdsSuccess, readAction, - mailboxId, + markSuccessEmailIdsByMailboxId, )); } else if (result.emailIdsSuccess.isEmpty) { yield Left(MarkAsMultipleEmailReadAllFailure(readAction)); @@ -43,7 +49,7 @@ class MarkAsMultipleEmailReadInteractor { yield Right(MarkAsMultipleEmailReadHasSomeEmailFailure( result.emailIdsSuccess, readAction, - mailboxId, + markSuccessEmailIdsByMailboxId, )); } } catch (e) { diff --git a/model/lib/extensions/list_presentation_email_extension.dart b/model/lib/extensions/list_presentation_email_extension.dart index 2535b39a84..b4260a9b87 100644 --- a/model/lib/extensions/list_presentation_email_extension.dart +++ b/model/lib/extensions/list_presentation_email_extension.dart @@ -22,6 +22,17 @@ extension ListPresentationEmailExtension on List { List get listEmailIds => map((email) => email.id).whereNotNull().toList(); + Map> get emailIdsByMailboxId => Map.from( + where((email) => email.mailboxContain?.mailboxId != null && email.id != null) + .fold(>{}, (combine, email) { + final mailboxId = email.mailboxContain!.mailboxId!; + combine[mailboxId] ??= []; + combine[mailboxId]!.add(email.id!); + return combine; + } + ), + ); + bool isAllCanDeletePermanently(Map mapMailbox) { final listMailboxContain = map((email) => email.findMailboxContain(mapMailbox)) .whereType() From 20450370bb858390e370987a7606038235db4c1e Mon Sep 17 00:00:00 2001 From: DatDang Date: Fri, 3 Jan 2025 15:13:53 +0700 Subject: [PATCH 61/72] TF-3385 Fix move and delete emails not work properly --- ...ete_multiple_emails_permanently_state.dart | 4 +-- .../mailbox_dashboard_controller.dart | 27 ++++++++++++------ .../move_emails_to_mailbox_extension.dart | 18 +++++++++--- ..._emails_with_new_mailbox_id_extension.dart | 7 +++-- .../presentation/search_email_controller.dart | 1 + .../presentation/thread_controller.dart | 28 +++++++++++++++++++ 6 files changed, 68 insertions(+), 17 deletions(-) diff --git a/lib/features/email/domain/state/delete_multiple_emails_permanently_state.dart b/lib/features/email/domain/state/delete_multiple_emails_permanently_state.dart index b091224989..bdcb3cd22d 100644 --- a/lib/features/email/domain/state/delete_multiple_emails_permanently_state.dart +++ b/lib/features/email/domain/state/delete_multiple_emails_permanently_state.dart @@ -7,7 +7,7 @@ class LoadingDeleteMultipleEmailsPermanentlyAll extends UIState {} class DeleteMultipleEmailsPermanentlyAllSuccess extends UIState { - List emailIds; + final List emailIds; final MailboxId? mailboxId; DeleteMultipleEmailsPermanentlyAllSuccess(this.emailIds, this.mailboxId); @@ -18,7 +18,7 @@ class DeleteMultipleEmailsPermanentlyAllSuccess extends UIState { class DeleteMultipleEmailsPermanentlyHasSomeEmailFailure extends UIState { - List emailIds; + final List emailIds; final MailboxId? mailboxId; DeleteMultipleEmailsPermanentlyHasSomeEmailFailure(this.emailIds, this.mailboxId); diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 1cf4e630b8..231a16e637 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -253,6 +253,7 @@ class MailboxDashBoardController extends ReloadableController StreamSubscription? _pendingSharedFileInfoSubscription; StreamSubscription? _receivingFileSharingStreamSubscription; StreamSubscription? _currentEmailIdInNotificationIOSStreamSubscription; + List emailsToBeUndo = []; final StreamController> _progressStateController = StreamController>.broadcast(); @@ -836,12 +837,22 @@ class MailboxDashBoardController extends ReloadableController MoveToMailboxRequest moveRequest, Map emailIdsWithReadStatus, ) { - consumeState(_moveToMailboxInteractor.execute( - session, - accountId, - moveRequest, - emailIdsWithReadStatus, - )); + final currentMailboxes = moveRequest.currentMailboxes; + if (currentMailboxes.length == 1 && currentMailboxes.values.first.length == 1) { + consumeState(_moveToMailboxInteractor.execute( + session, + accountId, + moveRequest, + emailIdsWithReadStatus, + )); + } else { + consumeState(_moveMultipleEmailToMailboxInteractor.execute( + session, + accountId, + moveRequest, + emailIdsWithReadStatus, + )); + } } void _moveToMailboxSuccess(MoveToMailboxSuccess success) { @@ -873,12 +884,12 @@ class MailboxDashBoardController extends ReloadableController final currentAccountId = accountId.value; final session = sessionCurrent; if (currentAccountId != null && session != null) { - consumeState(_moveToMailboxInteractor.execute( + moveToMailbox( session, currentAccountId, newMoveRequest, emailIdsWithReadStatus, - )); + ); } } diff --git a/lib/features/mailbox_dashboard/presentation/extensions/move_emails_to_mailbox_extension.dart b/lib/features/mailbox_dashboard/presentation/extensions/move_emails_to_mailbox_extension.dart index e0e4e6d9ce..eb0053794b 100644 --- a/lib/features/mailbox_dashboard/presentation/extensions/move_emails_to_mailbox_extension.dart +++ b/lib/features/mailbox_dashboard/presentation/extensions/move_emails_to_mailbox_extension.dart @@ -1,15 +1,15 @@ import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/presentation_email.dart'; +import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; extension MoveEmailsToMailboxExtension on MailboxDashBoardController { void handleMoveEmailsToMailbox({ required Map> originalMailboxIdsWithEmailIds, required MailboxId destinationMailboxId, + required MoveAction moveAction, }) { - if (destinationMailboxId == selectedMailbox.value?.id) return; - final currentEmails = List.from( emailsInCurrentMailbox, ); @@ -20,7 +20,17 @@ extension MoveEmailsToMailboxExtension on MailboxDashBoardController { return emailIds; }, ).toList(); - currentEmails.removeWhere((email) => movedEmailIds.contains(email.id)); - updateEmailList(currentEmails); + final currentEmailsToBeMoved = currentEmails + .where((email) => movedEmailIds.contains(email.id)) + .toList(); + if (currentEmailsToBeMoved.isNotEmpty && destinationMailboxId != selectedMailbox.value?.id) { + emailsToBeUndo = currentEmailsToBeMoved; + currentEmails.removeWhere(currentEmailsToBeMoved.contains); + updateEmailList(currentEmails); + } else if (moveAction == MoveAction.undo && destinationMailboxId == selectedMailbox.value?.id) { + currentEmails.addAll(emailsToBeUndo); + currentEmails.sort((a, b) => b.receivedAt?.value.compareTo(a.receivedAt?.value ?? DateTime.now()) ?? -1); + updateEmailList(currentEmails); + } } } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/extensions/update_emails_with_new_mailbox_id_extension.dart b/lib/features/mailbox_dashboard/presentation/extensions/update_emails_with_new_mailbox_id_extension.dart index a94af546d2..729a8dab68 100644 --- a/lib/features/mailbox_dashboard/presentation/extensions/update_emails_with_new_mailbox_id_extension.dart +++ b/lib/features/mailbox_dashboard/presentation/extensions/update_emails_with_new_mailbox_id_extension.dart @@ -18,13 +18,14 @@ extension UpdateEmailsWithNewMailboxIdExtension on MailboxDashBoardController { return emailIds; }, ).toList(); - for (var email in currentEmails) { - if (!movedEmailIds.contains(email.id)) continue; + for (int i = 0; i < currentEmails.length; i++) { + if (!movedEmailIds.contains(currentEmails[i].id)) continue; - email = email.copyWith( + currentEmails[i] = currentEmails[i].copyWith( mailboxIds: {destinationMailboxId: true}, mailboxContain: mapMailboxById[destinationMailboxId], ); } + updateEmailList(currentEmails); } } \ No newline at end of file diff --git a/lib/features/search/email/presentation/search_email_controller.dart b/lib/features/search/email/presentation/search_email_controller.dart index 82d679f34f..e92905595f 100644 --- a/lib/features/search/email/presentation/search_email_controller.dart +++ b/lib/features/search/email/presentation/search_email_controller.dart @@ -265,6 +265,7 @@ class SearchEmailController extends BaseController ); ever(mailboxDashBoardController.viewState, (viewState) { + if (!mailboxDashBoardController.searchController.isSearchEmailRunning) return; final reactionState = viewState.getOrElse(() => UIState.idle); if (reactionState is MoveToMailboxSuccess) { mailboxDashBoardController.handleUpdateEmailsWithNewMailboxId( diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 5bc655f8d5..1f3b927f26 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -20,6 +20,8 @@ import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; +import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanently_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails_permanently_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; @@ -338,6 +340,7 @@ class ThreadController extends BaseController with EmailActionController { }); ever(mailboxDashBoardController.viewState, (viewState) { + if (mailboxDashBoardController.searchController.isSearchEmailRunning) return; final reactionState = viewState.getOrElse(() => UIState.idle); if (reactionState is MarkAsMailboxReadAllSuccess) { _handleMarkEmailsAsReadByMailboxId(reactionState.mailboxId); @@ -350,17 +353,30 @@ class ThreadController extends BaseController with EmailActionController { mailboxDashBoardController.handleMoveEmailsToMailbox( originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithEmailIds, destinationMailboxId: reactionState.destinationMailboxId, + moveAction: reactionState.moveAction, ); + _checkIfCurrentMailboxCanLoadMore(); } else if (reactionState is MoveMultipleEmailToMailboxAllSuccess) { mailboxDashBoardController.handleMoveEmailsToMailbox( originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithEmailIds, destinationMailboxId: reactionState.destinationMailboxId, + moveAction: reactionState.moveAction, ); + _checkIfCurrentMailboxCanLoadMore(); } else if (reactionState is MoveMultipleEmailToMailboxHasSomeEmailFailure) { mailboxDashBoardController.handleMoveEmailsToMailbox( originalMailboxIdsWithEmailIds: reactionState.originalMailboxIdsWithMoveSucceededEmailIds, destinationMailboxId: reactionState.destinationMailboxId, + moveAction: reactionState.moveAction, ); + _checkIfCurrentMailboxCanLoadMore(); + } else if (reactionState is DeleteEmailPermanentlySuccess + || reactionState is DeleteMultipleEmailsPermanentlyAllSuccess + || reactionState is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure + ) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkIfCurrentMailboxCanLoadMore(); + }); } }); } @@ -376,6 +392,18 @@ class ThreadController extends BaseController with EmailActionController { mailboxDashBoardController.emailsInCurrentMailbox.refresh(); } + void _checkIfCurrentMailboxCanLoadMore() { + final currentMailbox = mailboxDashBoardController.selectedMailbox.value; + if (currentMailbox == null) return; + + final totalEmailsCount = currentMailbox.totalEmails?.value.value ?? 0; + if (totalEmailsCount == 0 + || mailboxDashBoardController.emailsInCurrentMailbox.isNotEmpty + ) return; + + dispatchState(Right(GetAllEmailLoading())); + } + void _registerBrowserResizeListener() { _resizeBrowserStreamSubscription = html.window.onResize.listen((_) { _validateBrowserHeight(); From ec2a43ebb9e62aa13397ff99d47a4d78712105bf Mon Sep 17 00:00:00 2001 From: DatDang Date: Fri, 3 Jan 2025 17:00:16 +0700 Subject: [PATCH 62/72] TF-3385 Optimize misc --- .../repository/mailbox_repository_impl.dart | 28 ++++++++++--------- .../data/local/email_cache_manager.dart | 14 ++++++---- .../presentation/thread_controller.dart | 6 ++-- .../list_presentation_email_extension.dart | 19 +++++++------ 4 files changed, 37 insertions(+), 30 deletions(-) diff --git a/lib/features/mailbox/data/repository/mailbox_repository_impl.dart b/lib/features/mailbox/data/repository/mailbox_repository_impl.dart index 5fbf9394e9..0aa44a6607 100644 --- a/lib/features/mailbox/data/repository/mailbox_repository_impl.dart +++ b/lib/features/mailbox/data/repository/mailbox_repository_impl.dart @@ -244,19 +244,21 @@ class MailboxRepositoryImpl extends MailboxRepository { totalEmailUnread, onProgressController); try { - await mapDataSource[DataSourceType.local]!.markAsMailboxRead( - session, - accountId, - mailboxId, - totalEmailUnread - result.length, - onProgressController, - ); - await emailDataSource?.markAsRead( - session, - accountId, - result, - ReadActions.markAsRead, - ); + await Future.wait([ + mapDataSource[DataSourceType.local]!.markAsMailboxRead( + session, + accountId, + mailboxId, + totalEmailUnread - result.length, + onProgressController, + ), + emailDataSource?.markAsRead( + session, + accountId, + result, + ReadActions.markAsRead, + ) ?? Future.value(), + ]); } catch (e) { logError('MailboxRepositoryImpl::markAsMailboxRead: Exception: $e'); } diff --git a/lib/features/thread/data/local/email_cache_manager.dart b/lib/features/thread/data/local/email_cache_manager.dart index d7ea24c971..47bfe07eb6 100644 --- a/lib/features/thread/data/local/email_cache_manager.dart +++ b/lib/features/thread/data/local/email_cache_manager.dart @@ -95,12 +95,14 @@ class EmailCacheManager { return _emailCacheClient.insertItem(keyCache, emailCache); } - Future storeMultipleEmails(AccountId accountId, UserName userName, List emailsCache) { - return Future.wait(emailsCache.map((emailCache) => storeEmail( - accountId, - userName, - emailCache, - ))); + Future storeMultipleEmails(AccountId accountId, UserName userName, List emailsCache) async { + final emailsToCache = Map.fromEntries(emailsCache.map( + (emailCache) => MapEntry( + TupleKey(emailCache.id, accountId.asString, userName.value).encodeKey, + emailCache, + ), + )); + await _emailCacheClient.insertMultipleItem(emailsToCache); } Future getStoredEmail(AccountId accountId, UserName userName, EmailId emailId) async { diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 1f3b927f26..0fbb495016 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -369,7 +369,9 @@ class ThreadController extends BaseController with EmailActionController { destinationMailboxId: reactionState.destinationMailboxId, moveAction: reactionState.moveAction, ); - _checkIfCurrentMailboxCanLoadMore(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkIfCurrentMailboxCanLoadMore(); + }); } else if (reactionState is DeleteEmailPermanentlySuccess || reactionState is DeleteMultipleEmailsPermanentlyAllSuccess || reactionState is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure @@ -396,7 +398,7 @@ class ThreadController extends BaseController with EmailActionController { final currentMailbox = mailboxDashBoardController.selectedMailbox.value; if (currentMailbox == null) return; - final totalEmailsCount = currentMailbox.totalEmails?.value.value ?? 0; + final totalEmailsCount = currentMailbox.countTotalEmails; if (totalEmailsCount == 0 || mailboxDashBoardController.emailsInCurrentMailbox.isNotEmpty ) return; diff --git a/model/lib/extensions/list_presentation_email_extension.dart b/model/lib/extensions/list_presentation_email_extension.dart index b4260a9b87..7ab345961b 100644 --- a/model/lib/extensions/list_presentation_email_extension.dart +++ b/model/lib/extensions/list_presentation_email_extension.dart @@ -22,16 +22,17 @@ extension ListPresentationEmailExtension on List { List get listEmailIds => map((email) => email.id).whereNotNull().toList(); - Map> get emailIdsByMailboxId => Map.from( - where((email) => email.mailboxContain?.mailboxId != null && email.id != null) - .fold(>{}, (combine, email) { - final mailboxId = email.mailboxContain!.mailboxId!; - combine[mailboxId] ??= []; - combine[mailboxId]!.add(email.id!); - return combine; + Map> get emailIdsByMailboxId { + final Map> result = {}; + for (final email in this) { + final mailboxId = email.mailboxContain?.mailboxId; + final emailId = email.id; + if (mailboxId != null && emailId != null) { + (result[mailboxId] ??= []).add(emailId); } - ), - ); + } + return result; + } bool isAllCanDeletePermanently(Map mapMailbox) { final listMailboxContain = map((email) => email.findMailboxContain(mapMailbox)) From c13c7f3746f2d9869202815167291b3342e98ce1 Mon Sep 17 00:00:00 2001 From: DatDang Date: Fri, 3 Jan 2025 17:00:50 +0700 Subject: [PATCH 63/72] TF-3385 Revert undo operation handling --- .../controller/mailbox_dashboard_controller.dart | 1 - .../extensions/move_emails_to_mailbox_extension.dart | 5 ----- 2 files changed, 6 deletions(-) diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 231a16e637..6ba1c65090 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -253,7 +253,6 @@ class MailboxDashBoardController extends ReloadableController StreamSubscription? _pendingSharedFileInfoSubscription; StreamSubscription? _receivingFileSharingStreamSubscription; StreamSubscription? _currentEmailIdInNotificationIOSStreamSubscription; - List emailsToBeUndo = []; final StreamController> _progressStateController = StreamController>.broadcast(); diff --git a/lib/features/mailbox_dashboard/presentation/extensions/move_emails_to_mailbox_extension.dart b/lib/features/mailbox_dashboard/presentation/extensions/move_emails_to_mailbox_extension.dart index eb0053794b..468d3f5236 100644 --- a/lib/features/mailbox_dashboard/presentation/extensions/move_emails_to_mailbox_extension.dart +++ b/lib/features/mailbox_dashboard/presentation/extensions/move_emails_to_mailbox_extension.dart @@ -24,13 +24,8 @@ extension MoveEmailsToMailboxExtension on MailboxDashBoardController { .where((email) => movedEmailIds.contains(email.id)) .toList(); if (currentEmailsToBeMoved.isNotEmpty && destinationMailboxId != selectedMailbox.value?.id) { - emailsToBeUndo = currentEmailsToBeMoved; currentEmails.removeWhere(currentEmailsToBeMoved.contains); updateEmailList(currentEmails); - } else if (moveAction == MoveAction.undo && destinationMailboxId == selectedMailbox.value?.id) { - currentEmails.addAll(emailsToBeUndo); - currentEmails.sort((a, b) => b.receivedAt?.value.compareTo(a.receivedAt?.value ?? DateTime.now()) ?? -1); - updateEmailList(currentEmails); } } } \ No newline at end of file From a3ddf9b0c1a8866a874fe56479e677bbbc4cc524 Mon Sep 17 00:00:00 2001 From: DatDang Date: Fri, 3 Jan 2025 17:01:26 +0700 Subject: [PATCH 64/72] TF-3385 Fix change flags not working --- .../presentation/controller/single_email_controller.dart | 7 +++++++ .../update_current_emails_flags_extension.dart | 9 +++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 901d64fe54..556c4a399b 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -1134,6 +1134,13 @@ class SingleEmailController extends BaseController with AppLoaderMixin { KeyWordIdentifier.emailFlagged: success.markStarAction == MarkStarAction.markStar, }); mailboxDashBoardController.setSelectedEmail(newEmail); + + final emailId = newEmail?.id; + if (emailId == null) return; + mailboxDashBoardController.updateEmailFlagByEmailIds( + [emailId], + markStarAction: success.markStarAction, + ); } void handleEmailAction(BuildContext context, PresentationEmail presentationEmail, EmailActionType actionType) { diff --git a/lib/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart b/lib/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart index 4572d768a7..f0771be0f4 100644 --- a/lib/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart +++ b/lib/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart @@ -4,6 +4,7 @@ import 'package:model/email/mark_star_action.dart'; import 'package:model/email/presentation_email.dart'; import 'package:model/email/read_actions.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'; extension UpdateCurrentEmailsFlagsExtension on MailboxDashBoardController { void updateEmailFlagByEmailIds( @@ -13,7 +14,11 @@ extension UpdateCurrentEmailsFlagsExtension on MailboxDashBoardController { }) { if (readAction == null && markStarAction == null) return; - for (var email in emailsInCurrentMailbox) { + final currentEmails = dashboardRoute.value == DashboardRoutes.searchEmail + ? listResultSearch + : emailsInCurrentMailbox; + + for (var email in currentEmails) { if (!emailIds.contains(email.id)) continue; switch (readAction) { @@ -39,7 +44,7 @@ extension UpdateCurrentEmailsFlagsExtension on MailboxDashBoardController { } } - emailsInCurrentMailbox.refresh(); + currentEmails.refresh(); } void _updateKeyword( From 2235fccda4e5e53348e55d60270b05bed4b839ad Mon Sep 17 00:00:00 2001 From: DatDang Date: Fri, 3 Jan 2025 17:12:31 +0700 Subject: [PATCH 65/72] TF-3385 Fix Map read status from fromIterable to fromEntries --- .../presentation/controller/mailbox_dashboard_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 6ba1c65090..49c5cfd549 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -1330,7 +1330,7 @@ class MailboxDashBoardController extends ReloadableController trashMailboxId, MoveAction.moving, EmailActionType.moveToTrash), - Map.fromIterable( + Map.fromEntries( listEmails .where((email) => email.id != null) .map((email) => MapEntry(email.id!, email.hasRead)), From 5cd272222d3aeec4490a489b50e6393a292fec2f Mon Sep 17 00:00:00 2001 From: DatDang Date: Fri, 3 Jan 2025 17:19:37 +0700 Subject: [PATCH 66/72] TF-3385 Fix tests update email flags extension --- .../extensions/update_current_emails_flags_extension_test.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension_test.dart b/test/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension_test.dart index 39fcfe770c..76bf1fac1f 100644 --- a/test/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension_test.dart +++ b/test/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension_test.dart @@ -10,6 +10,7 @@ import 'package:model/email/presentation_email.dart'; import 'package:model/email/read_actions.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'update_current_emails_flags_extension_test.mocks.dart'; @@ -24,6 +25,7 @@ void main() { numberOfEmails, (index) => EmailId(Id('email-id-$index')), ); + when(mailboxDashBoardController.dashboardRoute).thenReturn(DashboardRoutes.thread.obs); }); group('updateEmailFlagByEmailIds test:', () { From eed47e285fe7d79fc93b7a4d96633d342973ba9f Mon Sep 17 00:00:00 2001 From: Dat PHAM HOANG Date: Fri, 3 Jan 2025 21:09:12 +0700 Subject: [PATCH 67/72] TF-3385 Fix Map spam/unspam from fromIterable to fromEntries --- .../presentation/controller/mailbox_dashboard_controller.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 49c5cfd549..2f5daca595 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -1370,7 +1370,7 @@ class MailboxDashBoardController extends ReloadableController spamMailboxId!, MoveAction.moving, EmailActionType.moveToSpam), - Map.fromIterable( + Map.fromEntries( listEmail .where((email) => email.id != null) .map((email) => MapEntry(email.id!, email.hasRead)), @@ -1421,7 +1421,7 @@ class MailboxDashBoardController extends ReloadableController inboxMailboxId, MoveAction.moving, EmailActionType.unSpam), - Map.fromIterable( + Map.fromEntries( listEmail .where((email) => email.id != null) .map((email) => MapEntry(email.id!, email.hasRead)), From b8bf8ac0834d7037fe2beac67cc75d1626d518bd Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 24 Dec 2024 14:14:52 +0700 Subject: [PATCH 68/72] TF-3344 Handle separator when pasting list of mail addresses --- core/lib/utils/string_convert.dart | 35 +++- core/test/utils/string_convert_test.dart | 162 ++++++++++++++++++ .../presentation/composer_controller.dart | 142 +++++++++------ 3 files changed, 284 insertions(+), 55 deletions(-) create mode 100644 core/test/utils/string_convert_test.dart diff --git a/core/lib/utils/string_convert.dart b/core/lib/utils/string_convert.dart index 3042a1c9b6..c3fefb45a1 100644 --- a/core/lib/utils/string_convert.dart +++ b/core/lib/utils/string_convert.dart @@ -1,10 +1,41 @@ +import 'dart:convert'; + +import 'app_logger.dart'; + class StringConvert { - static String? writeEmptyToNull(String text) { + static const String separatorPattern = r'[ ,;]+'; + + static String? writeEmptyToNull(String text) { if (text.isEmpty) return null; return text; } - static String writeNullToEmpty(String? text) { + static String writeNullToEmpty(String? text) { return text ?? ''; } + + static List extractStrings(String input) { + try { + // Check if the input is URL encoded + if (input.contains('%')) { + input = Uri.decodeComponent(input); // Decode URL encoding + } + + // Check if the input is Base64 encoded + if (input.length % 4 == 0 && RegExp(r'^[A-Za-z0-9+/=]+$').hasMatch(input)) { + input = utf8.decode(base64.decode(input)); // Decode Base64 encoding + } + + final RegExp separator = RegExp(separatorPattern); + final listStrings = input + .replaceAll('\n', ' ') + .split(separator) + .where((value) => value.trim().isNotEmpty) + .toList(); + log('StringConvert::extractStrings:listStrings = $listStrings'); + return listStrings; + } catch (e) { + return []; + } + } } diff --git a/core/test/utils/string_convert_test.dart b/core/test/utils/string_convert_test.dart new file mode 100644 index 0000000000..cecd1a2715 --- /dev/null +++ b/core/test/utils/string_convert_test.dart @@ -0,0 +1,162 @@ +import 'dart:convert'; + +import 'package:core/utils/string_convert.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('StringConvert::extractStrings::', () { + group('Basic Functionality', () { + test('should extract strings separated by spaces', () { + const input = 'user1@example.com user2@example.com'; + final expected = ['user1@example.com', 'user2@example.com']; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + + test('should extract strings separated by commas', () { + const input = 'user1@example.com,user2@example.com'; + final expected = ['user1@example.com', 'user2@example.com']; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + + test('should extract strings separated by semicolons', () { + const input = 'user1@example.com;user2@example.com'; + final expected = ['user1@example.com', 'user2@example.com']; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + + test('should extract strings separated by mixed separators', () { + const input = 'user1@example.com, user2@example.com; user3@example.com'; + final expected = [ + 'user1@example.com', + 'user2@example.com', + 'user3@example.com' + ]; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + + test('should handle multiple consecutive separators', () { + const input = + 'user1@example.com,,; user2@example.com;; user3@example.com'; + final expected = [ + 'user1@example.com', + 'user2@example.com', + 'user3@example.com' + ]; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + }); + + group('Edge Cases', () { + test('should handle empty input', () { + const input = ''; + final expected = []; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + + test('should handle input with only separators', () { + const input = ',, ;; '; + final expected = []; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + + test('should handle trailing and leading separators', () { + const input = ', user1@example.com; user2@example.com ,'; + final expected = ['user1@example.com', 'user2@example.com']; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + + test('should handle extra spaces between separators', () { + const input = 'user1@example.com , user2@example.com ; user3@example.com'; + final expected = [ + 'user1@example.com', + 'user2@example.com', + 'user3@example.com' + ]; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + }); + + group('Stress Tests', () { + test('should handle a large number of strings', () { + final input = List.generate( + 1000, + (index) => + 'user$index@example.com${index % 3 == 0 ? ',' : index % 3 == 1 ? ';' : ' '}', + ).join(); + final expected = + List.generate(1000, (index) => 'user$index@example.com'); + expect(StringConvert.extractStrings(input), equals(expected)); + }); + }); + + group('Encoded Input Tests', () { + test('should handle URL encoded input', () { + String input = 'user1%40example.com%20user2%40example.com%20user3%40example.com'; + final expected = [ + 'user1@example.com', + 'user2@example.com', + 'user3@example.com' + ]; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + + test('should handle Base64 encoded input', () { + String input = 'dXNlcjFAZXhhbXBsZS5jb20gdXNlcjJAZXhhbXBsZS5jb20gdXNlcjNAZXhhbXBsZS5jb20='; + final expected = [ + 'user1@example.com', + 'user2@example.com', + 'user3@example.com' + ]; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + + test('should handle input with both URL encoding and Base64 encoding', () { + String input = Uri.encodeComponent(base64.encode(utf8.encode('user1@example.com user2@example.com user3@example.com'))); + final expected = [ + 'user1@example.com', + 'user2@example.com', + 'user3@example.com' + ]; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + }); + + group('Failing Cases', () { + test('should return empty list for empty input', () { + String input = ''; + expect(StringConvert.extractStrings(input), equals([])); + }); + + test('should return empty list if input is only separators', () { + String input = ' , ; '; + expect(StringConvert.extractStrings(input), equals([])); + }); + + test('should return correct result for input with invalid separators', () { + String input = 'user1@example.com,,user2@example.com;;;user3@example.com'; + final expected = [ + 'user1@example.com', + 'user2@example.com', + 'user3@example.com' + ]; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + + test('should return empty list for input with only whitespace and separators', () { + String input = ' , ; \n'; + expect(StringConvert.extractStrings(input), equals([])); + }); + + test('should handle input with newline characters', () { + // Arrange + String input = 'user1@example.com\nuser2@example.com;user3@example.com'; + final expected = [ + 'user1@example.com', + 'user2@example.com', + 'user3@example.com' + ]; + expect(StringConvert.extractStrings(input), equals(expected)); + }); + }); + }); +} diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index e89a0a86f4..9281901953 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1545,15 +1545,30 @@ class ComposerController extends BaseController final inputToEmail = toEmailAddressController.text; final inputCcEmail = ccEmailAddressController.text; final inputBccEmail = bccEmailAddressController.text; - - if (inputToEmail.isNotEmpty) { - _autoCreateToEmailTag(inputToEmail); + log('ComposerController::_autoCreateEmailTag:inputToEmail = $inputToEmail | inputCcEmail = $inputCcEmail | inputBccEmail = $inputBccEmail'); + if (inputToEmail.trim().isNotEmpty) { + _autoCreateEmailTagForRecipientField( + prefixEmail: PrefixEmailAddress.to, + inputText: inputToEmail, + listEmailAddress: listToEmailAddress, + keyEmailTagEditor: keyToEmailTagEditor, + ); } - if (inputCcEmail.isNotEmpty) { - _autoCreateCcEmailTag(inputCcEmail); + if (inputCcEmail.trim().isNotEmpty) { + _autoCreateEmailTagForRecipientField( + prefixEmail: PrefixEmailAddress.cc, + inputText: inputCcEmail, + listEmailAddress: listCcEmailAddress, + keyEmailTagEditor: keyCcEmailTagEditor, + ); } - if (inputBccEmail.isNotEmpty) { - _autoCreateBccEmailTag(inputBccEmail); + if (inputBccEmail.trim().isNotEmpty) { + _autoCreateEmailTagForRecipientField( + prefixEmail: PrefixEmailAddress.bcc, + inputText: inputBccEmail, + listEmailAddress: listBccEmailAddress, + keyEmailTagEditor: keyBccEmailTagEditor, + ); } } @@ -1564,47 +1579,53 @@ class ComposerController extends BaseController .contains(inputEmail); } - void _autoCreateToEmailTag(String inputEmail) { - if (!_isDuplicatedRecipient(inputEmail, listToEmailAddress)) { - final emailAddress = EmailAddress(null, inputEmail); - listToEmailAddress.add(emailAddress); - isInitialRecipient.value = true; - isInitialRecipient.refresh(); - _updateStatusEmailSendButton(); - } - log('ComposerController::_autoCreateToEmailTag(): STATE: ${keyToEmailTagEditor.currentState}'); - keyToEmailTagEditor.currentState?.resetTextField(); - Future.delayed(const Duration(milliseconds: 300), () { - keyToEmailTagEditor.currentState?.closeSuggestionBox(); - }); - } - - void _autoCreateCcEmailTag(String inputEmail) { - if (!_isDuplicatedRecipient(inputEmail, listCcEmailAddress)) { - final emailAddress = EmailAddress(null, inputEmail); - listCcEmailAddress.add(emailAddress); - isInitialRecipient.value = true; - isInitialRecipient.refresh(); - _updateStatusEmailSendButton(); - } - keyCcEmailTagEditor.currentState?.resetTextField(); - Future.delayed(const Duration(milliseconds: 300), () { - keyCcEmailTagEditor.currentState?.closeSuggestionBox(); - }); - } - - void _autoCreateBccEmailTag(String inputEmail) { - if (!_isDuplicatedRecipient(inputEmail, listBccEmailAddress)) { - final emailAddress = EmailAddress(null, inputEmail); - listBccEmailAddress.add(emailAddress); - isInitialRecipient.value = true; - isInitialRecipient.refresh(); - _updateStatusEmailSendButton(); + void _autoCreateEmailTagForRecipientField({ + required PrefixEmailAddress prefixEmail, + required String inputText, + required List listEmailAddress, + required GlobalKey keyEmailTagEditor, + }) { + log('ComposerController::_autoCreateEmailTagForRecipientField:prefixEmail = $prefixEmail | inputText = $inputText | listEmailAddress = $listEmailAddress'); + switch(prefixEmail) { + case PrefixEmailAddress.to: + case PrefixEmailAddress.cc: + case PrefixEmailAddress.bcc: + final listString = StringConvert.extractStrings(inputText).toSet(); + if (listString.isEmpty && !_isDuplicatedRecipient(inputText, listEmailAddress)) { + final emailAddress = EmailAddress(null, inputText); + listEmailAddress.add(emailAddress); + isInitialRecipient.value = true; + isInitialRecipient.refresh(); + _updateStatusEmailSendButton(); + keyEmailTagEditor.currentState?.resetTextField(); + Future.delayed( + const Duration(milliseconds: 300), + keyEmailTagEditor.currentState?.closeSuggestionBox, + ); + } else if (listString.isNotEmpty) { + final listStringNotExist = listString + .where((text) => !_isDuplicatedRecipient(text, listEmailAddress)) + .toList(); + + if (listStringNotExist.isNotEmpty) { + final listAddress = listStringNotExist + .map((value) => EmailAddress(null, value)) + .toList(); + listEmailAddress.addAll(listAddress); + isInitialRecipient.value = true; + isInitialRecipient.refresh(); + _updateStatusEmailSendButton(); + keyEmailTagEditor.currentState?.resetTextField(); + Future.delayed( + const Duration(milliseconds: 300), + keyEmailTagEditor.currentState?.closeSuggestionBox, + ); + } + } + break; + default: + break; } - keyBccEmailTagEditor.currentState?.resetTextField(); - Future.delayed(const Duration(milliseconds: 300), () { - keyBccEmailTagEditor.currentState?.closeSuggestionBox(); - }); } void _closeSuggestionBox() { @@ -1664,22 +1685,37 @@ class ComposerController extends BaseController case PrefixEmailAddress.to: toAddressExpandMode.value = ExpandMode.COLLAPSE; final inputToEmail = toEmailAddressController.text; - if (inputToEmail.isNotEmpty) { - _autoCreateToEmailTag(inputToEmail); + if (inputToEmail.trim().isNotEmpty) { + _autoCreateEmailTagForRecipientField( + prefixEmail: PrefixEmailAddress.to, + inputText: inputToEmail, + listEmailAddress: listToEmailAddress, + keyEmailTagEditor: keyToEmailTagEditor, + ); } break; case PrefixEmailAddress.cc: ccAddressExpandMode.value = ExpandMode.COLLAPSE; final inputCcEmail = ccEmailAddressController.text; - if (inputCcEmail.isNotEmpty) { - _autoCreateCcEmailTag(inputCcEmail); + if (inputCcEmail.trim().isNotEmpty) { + _autoCreateEmailTagForRecipientField( + prefixEmail: PrefixEmailAddress.cc, + inputText: inputCcEmail, + listEmailAddress: listCcEmailAddress, + keyEmailTagEditor: keyCcEmailTagEditor, + ); } break; case PrefixEmailAddress.bcc: bccAddressExpandMode.value = ExpandMode.COLLAPSE; final inputBccEmail = bccEmailAddressController.text; - if (inputBccEmail.isNotEmpty) { - _autoCreateBccEmailTag(inputBccEmail); + if (inputBccEmail.trim().isNotEmpty) { + _autoCreateEmailTagForRecipientField( + prefixEmail: PrefixEmailAddress.bcc, + inputText: inputBccEmail, + listEmailAddress: listBccEmailAddress, + keyEmailTagEditor: keyBccEmailTagEditor, + ); } break; default: From b5959e6e48c893e2617d0127415be1dd8f448478 Mon Sep 17 00:00:00 2001 From: Dat PHAM HOANG Date: Fri, 3 Jan 2025 23:08:43 +0700 Subject: [PATCH 69/72] TF-3344 Handle separator when submit list of mail addresses in text --- .../widgets/recipient_composer_widget.dart | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart index c58c846632..e9fdb70736 100644 --- a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart @@ -9,6 +9,7 @@ import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; +import 'package:core/utils/string_convert.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; @@ -501,11 +502,27 @@ class _RecipientComposerWidgetState extends State { void _handleSubmitTagAction( String value, StateSetter stateSetter - ) { - final textTrim = value.trim(); - if (!_isDuplicatedRecipient(textTrim)) { - stateSetter(() => _currentListEmailAddress.add(EmailAddress(null, textTrim))); + ) => _createMailTag(value, stateSetter); + + void _createMailTag(String value, StateSetter stateSetter) { + final listString = StringConvert.extractStrings(value.trim()).toSet(); + + if (listString.isEmpty && !_isDuplicatedRecipient(value)) { + stateSetter(() => _currentListEmailAddress.add(EmailAddress(null, value))); _updateListEmailAddressAction(); + } else if (listString.isNotEmpty) { + final listStringNotExist = listString + .where((text) => !_isDuplicatedRecipient(text)) + .toList(); + + if (listStringNotExist.isNotEmpty) { + final listAddress = listStringNotExist + .map((text) => EmailAddress(null, text)) + .toList(); + + stateSetter(() => _currentListEmailAddress.addAll(listAddress)); + _updateListEmailAddressAction(); + } } } From 650cf0810db89537ce3d760f3881917cac5fc4b8 Mon Sep 17 00:00:00 2001 From: Dat PHAM HOANG Date: Fri, 3 Jan 2025 23:26:47 +0700 Subject: [PATCH 70/72] Bump version to v0.14.6 --- CHANGELOG.md | 8 ++++++++ pubspec.yaml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1496ced94..e76d20f59c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## [0.14.6] - 2025-01-03 +### Fixed +- #3372 Paging empty trash +- #3370 Limit Email/set with limit is min of (50, maxObjectsInSet) +- #3379 Show replyAll button in case recipients not include me +- #3385 Realtime update UI base on Email/set +- #3344 Paste recipients to composer + ## [0.14.5] - 2024-12-26 ### Fixed - #3336 Make Echo ping of web socket optional diff --git a/pubspec.yaml b/pubspec.yaml index c79f835f38..35881c4984 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.14.5 +version: 0.14.6 environment: sdk: ">=3.0.0 <4.0.0" From 5e1c8beca6a449fdf9d8a2dc6e81df58d92612ef Mon Sep 17 00:00:00 2001 From: Dat Dang Date: Thu, 28 Nov 2024 17:49:12 +0700 Subject: [PATCH 71/72] Delegate cache control from Flutter to browser (#3289) (cherry picked from commit 9c3954a73803a0f1921926f86b9761156430dc2f) --- .../adr/0055-remove-flutter-service-worker.md | 28 +++++++++++++++ web/index.html | 36 ++++++++++++++----- 2 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 docs/adr/0055-remove-flutter-service-worker.md diff --git a/docs/adr/0055-remove-flutter-service-worker.md b/docs/adr/0055-remove-flutter-service-worker.md new file mode 100644 index 0000000000..0a52766416 --- /dev/null +++ b/docs/adr/0055-remove-flutter-service-worker.md @@ -0,0 +1,28 @@ +# 55. Remove Flutter Service Worker + +Date: 2024-11-26 + +## Status + +Accepted + +## Context + +- Flutter Service Worker have many issues regarding updating cache resources + - https://github.com/flutter/flutter/issues/104509 + - https://github.com/flutter/flutter/issues/63500 +- Flutter is moving away from Flutter Service Worker + - https://github.com/flutter/flutter/issues/156910 + +## Decision + +- We remove the Flutter Service Worker and let the browser handle the cache by: + - Remove the Flutter Service Worker initialization + - Remove the existing Flutter Service Worker registration + +## Consequences + +- Twake Mail web now will validate cache with the server every time it is loaded + - If the status code is 200, new resources will be fetched + - If the status code is 304, old resources will be used +- All of the existing service workers will be removed, even if it is not Flutter Service Worker diff --git a/web/index.html b/web/index.html index 7fc5db2536..8b28cd82ca 100644 --- a/web/index.html +++ b/web/index.html @@ -104,15 +104,33 @@ loadLanguageResources().finally(initialTmailApp); - _flutter.loader.load({ - serviceWorkerSettings: { - serviceWorkerVersion: {{flutter_service_worker_version}}, - }, - onEntrypointLoaded: async function(engineInitializer) { - const appRunner = await engineInitializer.initializeEngine(); - await appRunner.runApp(); - } - }); + if ('serviceWorker' in navigator) { + navigator + .serviceWorker + .getRegistrations() + .then(async function(registrations) { + try { + await Promise.all(registrations.map(function(registration) { + return registration.unregister(); + })); + } catch (error) { + console.log('[Twake Mail] Error unregistering service worker: ', error); + } + _flutter.loader.load({ + onEntrypointLoaded: async function(engineInitializer) { + const appRunner = await engineInitializer.initializeEngine(); + await appRunner.runApp(); + } + }); + }); + } else { + _flutter.loader.load({ + onEntrypointLoaded: async function(engineInitializer) { + const appRunner = await engineInitializer.initializeEngine(); + await appRunner.runApp(); + } + }); + } From 671687a823b263a01446f07e942880dcd23e16ee Mon Sep 17 00:00:00 2001 From: Dat PHAM HOANG Date: Sun, 5 Jan 2025 17:46:11 +0700 Subject: [PATCH 72/72] Bump version to v0.14.7 --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e76d20f59c..a5e824ae11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [0.14.7] - 2025-01-05 +### Fixed +- Delegate cache control from Flutter to browser + ## [0.14.6] - 2025-01-03 ### Fixed - #3372 Paging empty trash diff --git a/pubspec.yaml b/pubspec.yaml index 35881c4984..d2d554a982 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.14.6 +version: 0.14.7 environment: sdk: ">=3.0.0 <4.0.0"