From a1d9fb5969521f972019d2e3e94eb2bcbec3338e Mon Sep 17 00:00:00 2001 From: AmanRajSinghMourya Date: Wed, 27 Aug 2025 13:46:58 +0530 Subject: [PATCH] Add function to handle sharing actions --- .../collections/collections_api_client.dart | 53 ++++++- .../collections/collections_service.dart | 66 +++++++++ .../locker/lib/utils/collection_actions.dart | 135 ++++++++++++++++++ 3 files changed, 250 insertions(+), 4 deletions(-) diff --git a/mobile/apps/locker/lib/services/collections/collections_api_client.dart b/mobile/apps/locker/lib/services/collections/collections_api_client.dart index 0ebb3a1920..c4ae5fe04c 100644 --- a/mobile/apps/locker/lib/services/collections/collections_api_client.dart +++ b/mobile/apps/locker/lib/services/collections/collections_api_client.dart @@ -1,3 +1,4 @@ +import "dart:async"; import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; @@ -15,6 +16,7 @@ import 'package:locker/services/collections/models/collection_file_item.dart'; import 'package:locker/services/collections/models/collection_magic.dart'; import 'package:locker/services/collections/models/diff.dart'; import "package:locker/services/collections/models/public_url.dart"; +import "package:locker/services/collections/models/user.dart"; import 'package:locker/services/configuration.dart'; import "package:locker/services/files/sync/metadata_updater_service.dart"; import 'package:locker/services/files/sync/models/file.dart'; @@ -33,7 +35,11 @@ class CollectionApiClient { final _enteDio = Network.instance.enteDio; final _config = Configuration.instance; - Future init() async {} + late CollectionDB _db; + + Future init() async { + _db = CollectionDB.instance; + } Future> getCollections(int sinceTime) async { try { @@ -414,7 +420,7 @@ class CollectionApiClient { }, ); collection.publicURLs.add(PublicURL.fromMap(response.data["result"])); - await CollectionDB.instance.updateCollections([collection]); + await _db.updateCollections([collection]); CollectionService.instance.updateCollectionCache(collection); Bus.instance.fire(CollectionsUpdatedEvent()); } catch (e, s) { @@ -429,7 +435,7 @@ class CollectionApiClient { "/collections/share-url/" + collection.id.toString(), ); collection.publicURLs.clear(); - await CollectionDB.instance.updateCollections(List.from([collection])); + await _db.updateCollections(List.from([collection])); CollectionService.instance.updateCollectionCache(collection); Bus.instance.fire(CollectionsUpdatedEvent()); } on DioException catch (e) { @@ -451,7 +457,7 @@ class CollectionApiClient { // remove existing url information collection.publicURLs.clear(); collection.publicURLs.add(PublicURL.fromMap(response.data["result"])); - await CollectionDB.instance.updateCollections(List.from([collection])); + await _db.updateCollections(List.from([collection])); CollectionService.instance.updateCollectionCache(collection); Bus.instance.fire(CollectionsUpdatedEvent()); } on DioException catch (e) { @@ -464,6 +470,45 @@ class CollectionApiClient { rethrow; } } + + Future> share( + int collectionID, + String email, + String publicKey, + CollectionParticipantRole role, + ) async { + final collectionKey = + CollectionService.instance.getCollectionKey(collectionID); + final encryptedKey = CryptoUtil.sealSync( + collectionKey, + CryptoUtil.base642bin(publicKey), + ); + try { + final response = await _enteDio.post( + "/collections/share", + data: { + "collectionID": collectionID, + "email": email, + "encryptedKey": CryptoUtil.bin2base64(encryptedKey), + "role": role.toStringVal(), + }, + ); + final sharees = []; + for (final user in response.data["sharees"]) { + sharees.add(User.fromMap(user)); + } + final collection = CollectionService.instance.getFromCache(collectionID); + final updatedCollection = collection!.copyWith(sharees: sharees); + CollectionService.instance.updateCollectionCache(updatedCollection); + unawaited(_db.updateCollections([updatedCollection])); + return sharees; + } on DioException catch (e) { + if (e.response?.statusCode == 402) { + throw SharingNotPermittedForFreeAccountsError(); + } + rethrow; + } + } } class CreateRequest { diff --git a/mobile/apps/locker/lib/services/collections/collections_service.dart b/mobile/apps/locker/lib/services/collections/collections_service.dart index af2481faaa..c2d92a9798 100644 --- a/mobile/apps/locker/lib/services/collections/collections_service.dart +++ b/mobile/apps/locker/lib/services/collections/collections_service.dart @@ -12,6 +12,7 @@ import "package:locker/services/collections/collections_db.dart"; import 'package:locker/services/collections/models/collection.dart'; import "package:locker/services/collections/models/collection_items.dart"; import "package:locker/services/collections/models/public_url.dart"; +import "package:locker/services/collections/models/user.dart"; import 'package:locker/services/configuration.dart'; import 'package:locker/services/files/sync/models/file.dart'; import 'package:locker/services/trash/models/trash_item_request.dart'; @@ -385,6 +386,71 @@ class CollectionService { } } + // getActiveCollections returns list of collections which are not deleted yet + List getActiveCollections() { + return _collectionIDToCollections.values + .toList() + .where((element) => !element.isDeleted) + .toList(); + } + + /// Returns Contacts(Users) that are relevant to the account owner. + /// Note: "User" refers to the account owner in the points below. + /// This includes: + /// - Collaborators and viewers of collections owned by user + /// - Owners of collections shared to user. + /// - All collaborators of collections in which user is a collaborator or + /// a viewer. + /// - All family members of user. + /// - All contacts linked to a person. + List getRelevantContacts() { + final List relevantUsers = []; + final existingEmails = {}; + final int ownerID = Configuration.instance.getUserID()!; + final String ownerEmail = Configuration.instance.getEmail()!; + existingEmails.add(ownerEmail); + + for (final c in getActiveCollections()) { + // Add collaborators and viewers of collections owned by user + if (c.owner.id == ownerID) { + for (final User u in c.sharees) { + if (u.id != null && u.email.isNotEmpty) { + if (!existingEmails.contains(u.email)) { + relevantUsers.add(u); + existingEmails.add(u.email); + } + } + } + } else if (c.owner.id != null && c.owner.email.isNotEmpty) { + // Add owners of collections shared with user + if (!existingEmails.contains(c.owner.email)) { + relevantUsers.add(c.owner); + existingEmails.add(c.owner.email); + } + // Add collaborators of collections shared with user where user is a + // viewer or a collaborator + for (final User u in c.sharees) { + if (u.id != null && + u.email.isNotEmpty && + u.email == ownerEmail && + (u.isCollaborator || u.isViewer)) { + for (final User u in c.sharees) { + if (u.id != null && u.email.isNotEmpty && u.isCollaborator) { + if (!existingEmails.contains(u.email)) { + relevantUsers.add(u); + existingEmails.add(u.email); + } + } + } + break; + } + } + } + } + + return relevantUsers; + } + String getPublicUrl(Collection c) { final PublicURL url = c.publicURLs.firstOrNull!; final Uri publicUrl = Uri.parse(url.url); diff --git a/mobile/apps/locker/lib/utils/collection_actions.dart b/mobile/apps/locker/lib/utils/collection_actions.dart index e83ebeec63..5918b1d80b 100644 --- a/mobile/apps/locker/lib/utils/collection_actions.dart +++ b/mobile/apps/locker/lib/utils/collection_actions.dart @@ -1,13 +1,22 @@ +import "dart:async"; + +import "package:ente_accounts/services/user_service.dart"; import "package:ente_ui/components/action_sheet_widget.dart"; import 'package:ente_ui/components/buttons/button_widget.dart'; import 'package:ente_ui/components/buttons/models/button_type.dart'; +import "package:ente_ui/components/dialog_widget.dart"; +import "package:ente_ui/components/progress_dialog.dart"; import 'package:ente_ui/utils/dialog_util.dart'; +import "package:ente_utils/email_util.dart"; +import "package:ente_utils/share_utils.dart"; import 'package:flutter/material.dart'; import "package:locker/core/errors.dart"; import 'package:locker/l10n/l10n.dart'; import "package:locker/services/collections/collections_api_client.dart"; import 'package:locker/services/collections/collections_service.dart'; import 'package:locker/services/collections/models/collection.dart'; +import "package:locker/services/configuration.dart"; +import "package:locker/ui/components/user_dialogs.dart"; import 'package:locker/utils/snack_bar_utils.dart'; import 'package:logging/logging.dart'; @@ -274,4 +283,130 @@ class CollectionActions { barrierDismissible: true, ); } + + Future doesEmailHaveAccount( + BuildContext context, + String email, { + bool showProgress = false, + }) async { + ProgressDialog? dialog; + String? publicKey; + if (showProgress) { + dialog = createProgressDialog( + context, + context.l10n.sharing, + isDismissible: true, + ); + await dialog.show(); + } + try { + publicKey = await UserService.instance.getPublicKey(email); + } catch (e) { + await dialog?.hide(); + _logger.severe("Failed to get public key", e); + await showGenericErrorDialog(context: context, error: e); + return false; + } + // getPublicKey can return null when no user is associated with given + // email id + if (publicKey == null || publicKey == '') { + // todo: neeraj replace this as per the design where a new screen + // is used for error. Do this change along with handling of network errors + await showInviteDialog(context, email); + return false; + } else { + return true; + } + } + + // addEmailToCollection returns true if add operation was successful + Future addEmailToCollection( + BuildContext context, + Collection collection, + String email, + CollectionParticipantRole role, { + bool showProgress = false, + }) async { + if (!isValidEmail(email)) { + await showErrorDialog( + context, + context.l10n.invalidEmailAddress, + context.l10n.enterValidEmail, + ); + return false; + } else if (email.trim() == Configuration.instance.getEmail()) { + await showErrorDialog( + context, + context.l10n.oops, + context.l10n.youCannotShareWithYourself, + ); + return false; + } + + ProgressDialog? dialog; + String? publicKey; + if (showProgress) { + dialog = createProgressDialog( + context, + context.l10n.sharing, + isDismissible: true, + ); + await dialog.show(); + } + + try { + publicKey = await UserService.instance.getPublicKey(email); + } catch (e) { + await dialog?.hide(); + _logger.severe("Failed to get public key", e); + await showGenericErrorDialog(context: context, error: e); + return false; + } + // getPublicKey can return null when no user is associated with given + // email id + if (publicKey == null || publicKey == '') { + // todo: neeraj replace this as per the design where a new screen + // is used for error. Do this change along with handling of network errors + await showDialogWidget( + context: context, + title: context.l10n.inviteToEnte, + icon: Icons.info_outline, + body: context.l10n.emailNoEnteAccount(email), + isDismissible: true, + buttons: [ + ButtonWidget( + buttonType: ButtonType.neutral, + icon: Icons.adaptive.share, + labelText: context.l10n.sendInvite, + isInAlert: true, + onTap: () async { + unawaited( + shareText( + context.l10n.shareTextRecommendUsingEnte, + ), + ); + }, + ), + ], + ); + return false; + } else { + try { + final newSharees = await CollectionApiClient.instance + .share(collection.id, email, publicKey, role); + await dialog?.hide(); + collection.updateSharees(newSharees); + return true; + } catch (e) { + await dialog?.hide(); + if (e is SharingNotPermittedForFreeAccountsError) { + await _showUnSupportedAlert(context); + } else { + _logger.severe("failed to share collection", e); + await showGenericErrorDialog(context: context, error: e); + } + return false; + } + } + } }