diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index eaefe5c0e..ed1e2c0ee 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -49,6 +49,20 @@ + + + + + + + + + + + + + + - LSApplicationQueriesSchemes - - https - http - + LSApplicationQueriesSchemes + + https + http + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -23,6 +23,17 @@ $(FLUTTER_BUILD_NAME) CFBundleSignature ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER) + + + CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements new file mode 100644 index 000000000..352d0d88f --- /dev/null +++ b/ios/Runner/Runner.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.linagora.ios.teammail + + + diff --git a/ios/TeamMailShareExtension/Base.lproj/MainInterface.storyboard b/ios/TeamMailShareExtension/Base.lproj/MainInterface.storyboard new file mode 100644 index 000000000..4c1e68d17 --- /dev/null +++ b/ios/TeamMailShareExtension/Base.lproj/MainInterface.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/TeamMailShareExtension/Info.plist b/ios/TeamMailShareExtension/Info.plist new file mode 100644 index 000000000..9a277dcf4 --- /dev/null +++ b/ios/TeamMailShareExtension/Info.plist @@ -0,0 +1,41 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + TeamMailShareExtension + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.share-services + + + \ No newline at end of file diff --git a/ios/TeamMailShareExtension/ShareViewController.swift b/ios/TeamMailShareExtension/ShareViewController.swift new file mode 100644 index 000000000..7331b153e --- /dev/null +++ b/ios/TeamMailShareExtension/ShareViewController.swift @@ -0,0 +1,134 @@ +// +// ShareViewController.swift +// TeamMailShareExtension +// +// Created by dab.dev on 06/04/2022. +// + +import UIKit +import Social +import MobileCoreServices + +class ShareViewController: SLComposeServiceViewController { + + let hostAppBundleIdentifier = "com.linagora.ios.teammail" + let sharedKey = "ShareKey" + var sharedText: [String] = [] + let textContentType = kUTTypeText as String + let urlContentType = kUTTypeURL as String + + override func isContentValid() -> Bool { + return true + } + + override func viewDidLoad() { + super.viewDidLoad(); + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments. + if let content = extensionContext!.inputItems[0] as? NSExtensionItem { + if let contents = content.attachments { + for (index, attachment) in (contents).enumerated() { + if attachment.hasItemConformingToTypeIdentifier(textContentType) { + handleText(content: content, attachment: attachment, index: index) + } else if attachment.hasItemConformingToTypeIdentifier(urlContentType) { + handleUrl(content: content, attachment: attachment, index: index) + } + } + } + } + } + + override func didSelectPost() { + print("didSelectPost"); + } + + override func configurationItems() -> [Any]! { + // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here. + return [] + } + + private func handleText (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { + attachment.loadItem(forTypeIdentifier: textContentType, options: nil) { [weak self] data, error in + + if error == nil, let item = data as? String, let this = self { + + this.sharedText.append(item) + + // If this is the last item, save imagesData in userDefaults and redirect to host app + if index == (content.attachments?.count)! - 1 { + let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") + userDefaults?.set(this.sharedText, forKey: this.sharedKey) + userDefaults?.synchronize() + this.redirectToHostApp(type: .text) + } + + } else { + self?.dismissWithError() + } + } + } + + private func handleUrl (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { + attachment.loadItem(forTypeIdentifier: urlContentType, options: nil) { [weak self] data, error in + + if error == nil, let item = data as? URL, let this = self { + + this.sharedText.append(item.absoluteString) + + // If this is the last item, save imagesData in userDefaults and redirect to host app + if index == (content.attachments?.count)! - 1 { + let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") + userDefaults?.set(this.sharedText, forKey: this.sharedKey) + userDefaults?.synchronize() + this.redirectToHostApp(type: .text) + } + + } else { + self?.dismissWithError() + } + } + } + + private func dismissWithError() { + print("[ERROR] Error loading data!") + let alert = UIAlertController(title: "Error", message: "Error loading data", preferredStyle: .alert) + + let action = UIAlertAction(title: "Error", style: .cancel) { _ in + self.dismiss(animated: true, completion: nil) + } + + alert.addAction(action) + present(alert, animated: true, completion: nil) + extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + } + + private func redirectToHostApp(type: RedirectType) { + let url = URL(string: "ShareMedia://dataUrl=\(sharedKey)#\(type)") + var responder = self as UIResponder? + let selectorOpenURL = sel_registerName("openURL:") + + while (responder != nil) { + if (responder?.responds(to: selectorOpenURL))! { + let _ = responder?.perform(selectorOpenURL, with: url) + } + responder = responder!.next + } + extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + } + + enum RedirectType { + case media + case text + case file + } +} + +extension Array { + subscript (safe index: UInt) -> Element? { + return Int(index) < count ? self[Int(index)] : nil + } +} diff --git a/ios/TeamMailShareExtension/TeamMailShareExtension.entitlements b/ios/TeamMailShareExtension/TeamMailShareExtension.entitlements new file mode 100644 index 000000000..f30d887c7 --- /dev/null +++ b/ios/TeamMailShareExtension/TeamMailShareExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.linagora.ios.teammail + + + diff --git a/lib/features/email/presentation/email_controller.dart b/lib/features/email/presentation/email_controller.dart index 009cea97c..401d90a61 100644 --- a/lib/features/email/presentation/email_controller.dart +++ b/lib/features/email/presentation/email_controller.dart @@ -82,8 +82,7 @@ class EmailController extends BaseController { ); @override - void onReady() { - super.onReady(); + void onInit() { mailboxDashBoardController.selectedEmail.listen((presentationEmail) { log('EmailController::onReady(): ${presentationEmail.toString()}'); if (_currentEmailId != presentationEmail?.id) { @@ -97,6 +96,7 @@ class EmailController extends BaseController { } } }); + super.onInit(); } @override diff --git a/lib/features/home/presentation/home_bindings.dart b/lib/features/home/presentation/home_bindings.dart index 387363a50..d656af83e 100644 --- a/lib/features/home/presentation/home_bindings.dart +++ b/lib/features/home/presentation/home_bindings.dart @@ -10,6 +10,7 @@ import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_email_cac import 'package:tmail_ui_user/features/home/presentation/home_controller.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_credential_interactor.dart'; import 'package:tmail_ui_user/features/thread/data/local/email_cache_manager.dart'; +import 'package:tmail_ui_user/main/utils/email_receive_manager.dart'; class HomeBindings extends BaseBindings { @@ -25,6 +26,7 @@ class HomeBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); } diff --git a/lib/features/home/presentation/home_controller.dart b/lib/features/home/presentation/home_controller.dart index effa7cc90..1a00d077d 100644 --- a/lib/features/home/presentation/home_controller.dart +++ b/lib/features/home/presentation/home_controller.dart @@ -2,32 +2,43 @@ import 'package:core/core.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:tmail_ui_user/features/cleanup/domain/model/cleanup_rule.dart'; import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_email_cache_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_credential_state.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_credential_interactor.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; +import 'package:tmail_ui_user/main/utils/email_receive_manager.dart'; class HomeController extends GetxController { final GetCredentialInteractor _getCredentialInteractor; final DynamicUrlInterceptors _dynamicUrlInterceptors; final AuthorizationInterceptors _authorizationInterceptors; final CleanupEmailCacheInteractor _cleanupEmailCacheInteractor; + final EmailReceiveManager _emailReceiveManager; HomeController( this._getCredentialInteractor, this._dynamicUrlInterceptors, this._authorizationInterceptors, this._cleanupEmailCacheInteractor, + this._emailReceiveManager, ); + @override + void onInit() { + log('HomeController::onInit(): '); + if (!kIsWeb) { + _initFlutterDownloader(); + _registerReceivingSharingIntent(); + } + super.onInit(); + } + @override void onReady() { super.onReady(); - if (!kIsWeb) { - _initFlutterDownloader(); - } _cleanupEmailCache(); } @@ -51,6 +62,16 @@ class HomeController extends GetxController { (success) => success is GetCredentialViewState ? _goToMailbox(success) : _goToLogin())); } + void _registerReceivingSharingIntent() { + _emailReceiveManager.receivingSharingStream.listen((uri) { + log('HomeController::onReady(): Received Email: ${uri.toString()}'); + if (uri != null) { + log('HomeController::onReady(): Address: ${uri.path}'); + _emailReceiveManager.setPendingEmailAddress(EmailAddress(null, uri.path)); + } + }); + } + void _goToLogin() { pushAndPop(AppRoutes.LOGIN); } diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 55de15634..de95b1c93 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -104,15 +104,19 @@ class MailboxController extends BaseMailboxController { ) : super(treeBuilder); @override - void onReady() { - super.onReady(); + void onInit() { mailboxDashBoardController.accountId.listen((accountId) { log('MailboxController::onReady(): accountId: $accountId'); if (accountId != null) { getAllMailboxAction(accountId); } }); + super.onInit(); + } + @override + void onReady() { + super.onReady(); mailboxDashBoardController.viewState.listen((state) { state.fold( (failure) { diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart index b2e1f5a52..f5ffff77b 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart @@ -44,6 +44,7 @@ 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/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/router_arguments.dart'; +import 'package:tmail_ui_user/main/utils/email_receive_manager.dart'; class MailboxDashBoardController extends ReloadableController { @@ -55,6 +56,7 @@ class MailboxDashBoardController extends ReloadableController { final CachingManager _cachingManager = Get.find(); final Connectivity _connectivity = Get.find(); final ResponsiveUtils _responsiveUtils = Get.find(); + final EmailReceiveManager _emailReceiveManager = Get.find(); final MoveToTrashInteractor _moveToTrashInteractor; final MoveToMailboxInteractor _moveToMailboxInteractor; @@ -79,6 +81,7 @@ class MailboxDashBoardController extends ReloadableController { FocusNode searchFocus = FocusNode(); RouterArguments? routerArguments; late StreamSubscription _connectivityStreamSubscription; + late StreamSubscription _emailReceiveManagerStreamSubscription; MailboxDashBoardController( this._moveToTrashInteractor, @@ -88,8 +91,9 @@ class MailboxDashBoardController extends ReloadableController { @override void onInit() { - super.onInit(); _registerNetworkConnectivityState(); + _registerPendingEmailAddress(); + super.onInit(); } @override @@ -178,23 +182,36 @@ class MailboxDashBoardController extends ReloadableController { }); } + void _registerPendingEmailAddress() { + _emailReceiveManagerStreamSubscription = + _emailReceiveManager.pendingEmailAddressInfo.stream.listen((emailAddress) { + log('MailboxDashBoardController::_registerPendingEmailAddress(): ${emailAddress?.email}'); + if (emailAddress != null && emailAddress.email?.isNotEmpty == true) { + _emailReceiveManager.clearPendingEmailAddress(); + final arguments = ComposerArguments( + emailActionType: EmailActionType.composeFromEmailAddress, + emailAddress: emailAddress, + mailboxRole: selectedMailbox.value?.role); + goToComposer(arguments); + } + }); + } + void _getUserProfile() async { consumeState(_getUserProfileInteractor.execute()); } void _setSessionCurrent() { - Future.delayed(const Duration(milliseconds: 100), () { - final arguments = Get.arguments; - log('MailboxDashBoardController::_setSessionCurrent(): arguments = $arguments'); - if (arguments is Session) { - sessionCurrent = Get.arguments as Session; - accountId.value = sessionCurrent?.accounts.keys.first; - } else { - if (kIsWeb) { - reload(); - } + final arguments = Get.arguments; + log('MailboxDashBoardController::_setSessionCurrent(): arguments = $arguments'); + if (arguments is Session) { + sessionCurrent = arguments; + accountId.value = sessionCurrent?.accounts.keys.first; + } else { + if (kIsWeb) { + reload(); } - }); + } } Future _initPackageInfo() async { @@ -393,6 +410,16 @@ class MailboxDashBoardController extends ReloadableController { } } + void goToComposer(ComposerArguments arguments) { + if (kIsWeb) { + if (dashBoardAction != DashBoardAction.compose) { + dispatchDashBoardAction(DashBoardAction.compose, arguments: arguments); + } + } else { + push(AppRoutes.COMPOSER, arguments: arguments); + } + } + void _deleteCredential() async { await _deleteCredentialInteractor.execute(); } @@ -409,6 +436,8 @@ class MailboxDashBoardController extends ReloadableController { @override void onClose() { + _emailReceiveManager.closeEmailReceiveManagerStream(); + _emailReceiveManagerStreamSubscription.cancel(); _connectivityStreamSubscription.cancel(); searchInputController.dispose(); searchFocus.dispose(); diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index 40d4fef3f..f9c54ae45 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -127,12 +127,6 @@ class ThreadController extends BaseController { @override void onInit() { super.onInit(); - dispatchState(Right(LoadingState())); - } - - @override - void onReady() { - super.onReady(); mailboxDashBoardController.selectedMailbox.listen((selectedMailbox) { if (_currentMailboxId != selectedMailbox?.id) { log('ThreadController::onReady(): selectMailbox: ${selectedMailbox?.name?.name}(${selectedMailbox?.id})'); @@ -141,7 +135,13 @@ class ThreadController extends BaseController { _getAllEmail(); } }); + super.onInit(); + dispatchState(Right(LoadingState())); + } + @override + void onReady() { + super.onReady(); mailboxDashBoardController.viewState.listen((state) { state.map((success) { log('ThreadController::onReady(): ${success.runtimeType}'); @@ -262,7 +262,6 @@ class ThreadController extends BaseController { canLoadMore = true; disableSearch(); cancelSelectEmail(); - listEmailController.animateTo(0, duration: Duration(milliseconds: 500), curve: Curves.fastOutSlowIn); mailboxDashBoardController.dispatchRoute(AppRoutes.THREAD); } @@ -270,6 +269,9 @@ class ThreadController extends BaseController { log('ThreadController::_getAllEmailSuccess(): ${success.emailList.length}'); _currentEmailState = success.currentEmailState; emailList.value = success.emailList; + if (listEmailController.hasClients) { + listEmailController.animateTo(0, duration: Duration(milliseconds: 500), curve: Curves.fastOutSlowIn); + } } void _refreshChangesAllEmailSuccess(RefreshChangesAllEmailSuccess success) { diff --git a/lib/main/bindings/core/core_bindings.dart b/lib/main/bindings/core/core_bindings.dart index 0712989ca..84f3d85ed 100644 --- a/lib/main/bindings/core/core_bindings.dart +++ b/lib/main/bindings/core/core_bindings.dart @@ -4,6 +4,7 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; +import 'package:tmail_ui_user/main/utils/email_receive_manager.dart'; class CoreBindings extends Bindings { @@ -16,6 +17,7 @@ class CoreBindings extends Bindings { _bindingTransformer(); _bindingToast(); _bindingDeviceManager(); + _bindingReceivingSharingStream(); } void _bindingAppImagePaths() { @@ -47,4 +49,8 @@ class CoreBindings extends Bindings { Get.put(DeviceInfoPlugin()); Get.put(DeviceManager(Get.find())); } + + void _bindingReceivingSharingStream() { + Get.put(EmailReceiveManager()); + } } \ No newline at end of file diff --git a/lib/main/utils/email_receive_manager.dart b/lib/main/utils/email_receive_manager.dart new file mode 100644 index 000000000..902118d8b --- /dev/null +++ b/lib/main/utils/email_receive_manager.dart @@ -0,0 +1,40 @@ + +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import 'package:rxdart/rxdart.dart'; + +class EmailReceiveManager { + + BehaviorSubject _pendingEmailAddressInfo = BehaviorSubject.seeded(null); + BehaviorSubject get pendingEmailAddressInfo => _pendingEmailAddressInfo; + + Stream get receivingSharingStream { + return Rx.merge([ + Stream.fromFuture(ReceiveSharingIntent.getInitialTextAsUri()), + ReceiveSharingIntent.getTextStreamAsUri() + ]); + } + Stream> get receivingSharingStream2 { + return Rx.merge([ + Stream.fromFuture(ReceiveSharingIntent.getInitialMedia()), + ReceiveSharingIntent.getMediaStream() + ]); + } + + void setPendingEmailAddress(EmailAddress emailAddress) async { + clearPendingEmailAddress(); + _pendingEmailAddressInfo.add(emailAddress); + } + + void clearPendingEmailAddress() { + if(_pendingEmailAddressInfo.isClosed) { + _pendingEmailAddressInfo = BehaviorSubject.seeded(null); + } else { + _pendingEmailAddressInfo.add(null); + } + } + + void closeEmailReceiveManagerStream() { + _pendingEmailAddressInfo.close(); + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 406fe5ab0..1c106957f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -141,6 +141,8 @@ dependencies: package_info_plus: 1.4.1 + receive_sharing_intent: 1.4.5 + dev_dependencies: flutter_test: sdk: flutter