Merge Collaboration Changes #831

Collaboration Changes
This commit is contained in:
Neeraj Gupta 2023-01-31 16:46:11 +05:30 committed by GitHub
commit 741742b23d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 273 additions and 214 deletions

View file

@ -592,12 +592,16 @@ class CollectionsService {
}
}
Future<void> createShareUrl(Collection collection) async {
Future<void> createShareUrl(
Collection collection, {
bool enableCollect = false,
}) async {
try {
final response = await _enteDio.post(
"/collections/share-url",
data: {
"collectionID": collection.id,
"enableCollect": enableCollect,
},
);
collection.publicURLs?.add(PublicURL.fromMap(response.data["result"]));

View file

@ -117,6 +117,8 @@ class UserService {
}
}
// getPublicKey returns null value if email id is not
// associated with another ente account
Future<String?> getPublicKey(String email) async {
try {
final response = await _enteDio.get(
@ -127,9 +129,11 @@ class UserService {
await PublicKeysDB.instance.setKey(PublicKey(email, publicKey));
return publicKey;
} on DioError catch (e) {
_logger.info(e);
if (e.response != null && e.response?.statusCode == 404) {
return null;
}
rethrow;
}
}
UserDetails? getCachedUserDetails() {

View file

@ -13,8 +13,10 @@ import 'package:photos/services/hidden_service.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/common/progress_dialog.dart';
import 'package:photos/ui/components/action_sheet_widget.dart';
import 'package:photos/ui/components/button_widget.dart';
import 'package:photos/ui/components/dialog_widget.dart';
import 'package:photos/ui/components/models/button_type.dart';
import 'package:photos/ui/payment/subscription.dart';
import 'package:photos/utils/date_time_util.dart';
@ -29,13 +31,37 @@ class CollectionActions {
CollectionActions(this.collectionsService);
Future<bool> publicLinkToggle(
Future<bool> enableUrl(
BuildContext context,
Collection collection,
bool enable,
) async {
// confirm if user wants to disable the url
if (!enable) {
Collection collection, {
bool enableCollect = false,
}) async {
final dialog = createProgressDialog(
context,
"Creating link...",
isDismissible: true,
);
try {
await dialog.show();
await CollectionsService.instance.createShareUrl(
collection,
enableCollect: enableCollect,
);
dialog.hide();
return true;
} catch (e) {
dialog.hide();
if (e is SharingNotPermittedForFreeAccountsError) {
_showUnSupportedAlert(context);
} else {
logger.severe("Failed to update shareUrl collection", e);
showGenericErrorDialog(context: context);
}
return false;
}
}
Future<bool> disableUrl(BuildContext context, Collection collection) async {
final ButtonAction? result = await showActionSheet(
context: context,
buttons: [
@ -71,26 +97,6 @@ class CollectionActions {
return false;
}
}
final dialog = createProgressDialog(
context,
"Creating link...",
);
try {
await dialog.show();
await CollectionsService.instance.createShareUrl(collection);
dialog.hide();
return true;
} catch (e) {
dialog.hide();
if (e is SharingNotPermittedForFreeAccountsError) {
_showUnSupportedAlert(context);
} else {
logger.severe("Failed to update shareUrl collection", e);
showGenericErrorDialog(context: context);
}
return false;
}
}
Future<Collection?> createSharedCollectionLink(
BuildContext context,
@ -137,33 +143,44 @@ class CollectionActions {
}
// removeParticipant remove the user from a share album
Future<bool?> removeParticipant(
Future<bool> removeParticipant(
BuildContext context,
Collection collection,
User user,
) async {
final result = await showChoiceDialog(
context,
title: "Remove",
body: "${user.email} will be removed",
firstButtonLabel: "Yes, remove",
firstButtonOnTap: () async {
try {
final ButtonAction? result = await showActionSheet(
context: context,
buttons: [
ButtonWidget(
buttonType: ButtonType.critical,
isInAlert: true,
shouldStickToDarkTheme: true,
buttonAction: ButtonAction.first,
shouldSurfaceExecutionStates: true,
labelText: "Yes, remove",
onTap: () async {
final newSharees = await CollectionsService.instance
.unshare(collection.id, user.email);
collection.updateSharees(newSharees);
} catch (e, s) {
Logger("EmailItemWidget").severe(e, s);
rethrow;
}
},
),
const ButtonWidget(
buttonType: ButtonType.secondary,
buttonAction: ButtonAction.cancel,
isInAlert: true,
shouldStickToDarkTheme: true,
labelText: "Cancel",
)
],
title: "Remove?",
body: '${user.email} will be removed from this shared album\n\nAny '
'photos added by them will also be removed from the album',
);
if (result != null) {
if (result == ButtonAction.error) {
await showGenericErrorDialog(context: context);
return false;
showGenericErrorDialog(context: context);
}
if (result == ButtonAction.first) {
return true;
return result == ButtonAction.first;
} else {
return false;
}
@ -174,6 +191,7 @@ class CollectionActions {
Collection collection,
String email, {
CollectionParticipantRole role = CollectionParticipantRole.viewer,
bool showProgress = false,
String? publicKey,
}) async {
if (!isValidEmail(email)) {
@ -186,76 +204,70 @@ class CollectionActions {
} else if (email == Configuration.instance.getEmail()) {
await showErrorDialog(context, "Oops", "You cannot share with yourself");
return null;
} else {
// if (collection.getSharees().any((user) => user.email == email)) {
// showErrorDialog(
// context,
// "Oops",
// "You're already sharing this with " + email,
// );
// return null;
// }
}
ProgressDialog? dialog;
if (publicKey == null) {
final dialog = createProgressDialog(context, "Searching for user...");
if (showProgress) {
dialog = createProgressDialog(context, "Searching for user...");
await dialog.show();
}
try {
publicKey = await UserService.instance.getPublicKey(email);
await dialog.hide();
await dialog?.hide();
} catch (e) {
await dialog?.hide();
logger.severe("Failed to get public key", e);
showGenericErrorDialog(context: context);
await dialog.hide();
return false;
}
}
// getPublicKey can return null
// ignore: unnecessary_null_comparison
if (publicKey == null || publicKey == '') {
final dialog = AlertDialog(
title: const Text("Invite to ente?"),
content: Text(
"Looks like " +
email +
" hasn't signed up for ente yet. would you like to invite them?",
style: const TextStyle(
height: 1.4,
),
),
actions: [
TextButton(
child: Text(
"Invite",
style: TextStyle(
color: Theme.of(context).colorScheme.greenAlternative,
),
),
onPressed: () {
// 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: "Invite to ente",
icon: Icons.info_outline,
body: "$email does not have an ente account\n\nSend them an invite to"
" add them after they sign up",
isDismissible: true,
buttons: [
ButtonWidget(
buttonType: ButtonType.neutral,
icon: Icons.adaptive.share,
labelText: "Send invite",
isInAlert: true,
onTap: () async {
shareText(
"Hey, I have some photos to share. Please install https://ente.io so that I can share them privately.",
"Download ente so we can easily share original quality photos"
" and videos\n\nhttps://ente.io/#download",
);
},
),
],
);
await showDialog(
context: context,
builder: (BuildContext context) {
return dialog;
},
);
return null;
} else {
final dialog = createProgressDialog(context, "Sharing...");
if (showProgress) {
dialog = createProgressDialog(
context,
"Sharing...",
isDismissible: true,
);
await dialog.show();
}
try {
final newSharees = await CollectionsService.instance
.share(collection.id, email, publicKey, role);
await dialog?.hide();
collection.updateSharees(newSharees);
await dialog.hide();
showShortToast(context, "Shared successfully!");
return true;
} catch (e) {
await dialog.hide();
await dialog?.hide();
if (e is SharingNotPermittedForFreeAccountsError) {
_showUnSupportedAlert(context);
} else {

View file

@ -361,7 +361,12 @@ class _CreateCollectionSheetState extends State<CreateCollectionSheet> {
}
Future<bool> _addToCollection(int collectionID) async {
final dialog = createProgressDialog(context, "Uploading files to album...");
final dialog = createProgressDialog(
context,
"Uploading files to album"
"...",
isDismissible: true,
);
await dialog.show();
try {
final List<File> files = [];
@ -434,7 +439,7 @@ class _CreateCollectionSheetState extends State<CreateCollectionSheet> {
final String message = widget.actionType == CollectionActionType.moveFiles
? "Moving files to album..."
: "Unhiding files to album";
final dialog = createProgressDialog(context, message);
final dialog = createProgressDialog(context, message, isDismissible: true);
await dialog.show();
try {
final int fromCollectionID =
@ -462,7 +467,8 @@ class _CreateCollectionSheetState extends State<CreateCollectionSheet> {
}
Future<bool> _restoreFilesToCollection(int toCollectionID) async {
final dialog = createProgressDialog(context, "Restoring files...");
final dialog = createProgressDialog(context, "Restoring files...",
isDismissible: true);
await dialog.show();
try {
await CollectionsService.instance

View file

@ -5,28 +5,29 @@ import 'package:photos/models/collection.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
import 'package:photos/ui/common/gradient_button.dart';
import 'package:photos/ui/components/button_widget.dart';
import 'package:photos/ui/components/captioned_text_widget.dart';
import 'package:photos/ui/components/divider_widget.dart';
import 'package:photos/ui/components/menu_item_widget.dart';
import 'package:photos/ui/components/menu_section_description_widget.dart';
import 'package:photos/ui/components/menu_section_title.dart';
import 'package:photos/ui/components/models/button_type.dart';
import 'package:photos/ui/sharing/user_avator_widget.dart';
class AddParticipantPage extends StatefulWidget {
final Collection collection;
final bool isAddingViewer;
const AddParticipantPage(this.collection, {super.key});
const AddParticipantPage(this.collection, this.isAddingViewer, {super.key});
@override
State<StatefulWidget> createState() => _AddParticipantPage();
}
class _AddParticipantPage extends State<AddParticipantPage> {
late bool selectAsViewer;
String selectedEmail = '';
String _email = '';
bool hideListOfEmails = false;
bool isEmailListEmpty = false;
bool _emailIsValid = false;
bool isKeypadOpen = false;
late CollectionActions collectionActions;
@ -37,7 +38,6 @@ class _AddParticipantPage extends State<AddParticipantPage> {
@override
void initState() {
selectAsViewer = true;
collectionActions = CollectionActions(CollectionsService.instance);
super.initState();
}
@ -52,13 +52,13 @@ class _AddParticipantPage extends State<AddParticipantPage> {
Widget build(BuildContext context) {
isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
final enteTextTheme = getEnteTextTheme(context);
final enteColorScheme = getEnteColorScheme(context);
final List<User> suggestedUsers = _getSuggestedUser();
hideListOfEmails = suggestedUsers.isEmpty;
debugPrint("hide list $hideListOfEmails");
isEmailListEmpty = suggestedUsers.isEmpty;
return Scaffold(
resizeToAvoidBottomInset: isKeypadOpen,
appBar: AppBar(
title: const Text("Add people"),
title: Text(widget.isAddingViewer ? "Add viewer" : "Add collaborator"),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.start,
@ -69,7 +69,8 @@ class _AddParticipantPage extends State<AddParticipantPage> {
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
"Add a new email",
style: enteTextTheme.body,
style: enteTextTheme.small
.copyWith(color: enteColorScheme.textMuted),
),
),
const SizedBox(height: 4),
@ -77,20 +78,32 @@ class _AddParticipantPage extends State<AddParticipantPage> {
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: _getEmailField(),
),
(hideListOfEmails)
? const Expanded(child: SizedBox())
(isEmailListEmpty && widget.isAddingViewer)
? const Expanded(child: SizedBox.shrink())
: Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
const SizedBox(height: 24),
const MenuSectionTitle(
title: "or pick an existing one",
),
!isEmailListEmpty
? const MenuSectionTitle(
title: "Or pick an existing one",
)
: const SizedBox.shrink(),
Expanded(
child: ListView.builder(
itemBuilder: (context, index) {
if (index >= suggestedUsers.length) {
return const Padding(
padding: EdgeInsets.symmetric(
vertical: 8.0,
),
child: MenuSectionDescriptionWidget(
content:
"Collaborators can add photos and videos to the shared album.",
),
);
}
final currentUser = suggestedUsers[index];
return Column(
children: [
@ -136,8 +149,8 @@ class _AddParticipantPage extends State<AddParticipantPage> {
],
);
},
itemCount: suggestedUsers.length,
itemCount: suggestedUsers.length +
(widget.isAddingViewer ? 0 : 1),
// physics: const ClampingScrollPhysics(),
),
),
@ -145,9 +158,6 @@ class _AddParticipantPage extends State<AddParticipantPage> {
),
),
),
const DividerWidget(
dividerType: DividerType.solid,
),
SafeArea(
child: Padding(
padding: const EdgeInsets.only(
@ -159,48 +169,14 @@ class _AddParticipantPage extends State<AddParticipantPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const MenuSectionTitle(title: "Add as"),
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Collaborator",
),
leadingIcon: Icons.edit_outlined,
menuItemColor: getEnteColorScheme(context).fillFaint,
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: !selectAsViewer ? Icons.check : null,
onTap: () async {
setState(() => {selectAsViewer = false});
},
isBottomBorderRadiusRemoved: true,
),
DividerWidget(
dividerType: DividerType.menu,
bgColor: getEnteColorScheme(context).fillFaint,
),
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Viewer",
),
leadingIcon: Icons.photo_outlined,
menuItemColor: getEnteColorScheme(context).fillFaint,
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: selectAsViewer ? Icons.check : null,
onTap: () async {
setState(() => {selectAsViewer = true});
// showShortToast(context, "yet to implement");
},
isTopBorderRadiusRemoved: true,
),
!isKeypadOpen
? const MenuSectionDescriptionWidget(
content:
"Collaborators can add photos and videos to the shared album.",
)
: const SizedBox.shrink(),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: GradientButton(
const SizedBox(height: 8),
ButtonWidget(
buttonType: ButtonType.primary,
buttonSize: ButtonSize.large,
labelText: widget.isAddingViewer
? "Add viewer"
: "Add collaborator",
isDisabled: (selectedEmail == '' && !_emailIsValid),
onTap: (selectedEmail == '' && !_emailIsValid)
? null
: () async {
@ -211,7 +187,7 @@ class _AddParticipantPage extends State<AddParticipantPage> {
context,
widget.collection,
emailToAdd,
role: selectAsViewer
role: widget.isAddingViewer
? CollectionParticipantRole.viewer
: CollectionParticipantRole.collaborator,
);
@ -219,10 +195,8 @@ class _AddParticipantPage extends State<AddParticipantPage> {
Navigator.of(context).pop(true);
}
},
text: selectAsViewer ? "Add viewer" : "Add collaborator",
),
),
const SizedBox(height: 8),
const SizedBox(height: 20),
],
),
),
@ -317,9 +291,11 @@ class _AddParticipantPage extends State<AddParticipantPage> {
}
}
if (_textController.text.trim().isNotEmpty) {
suggestedUsers.removeWhere((element) => !element.email
suggestedUsers.removeWhere(
(element) => !element.email
.toLowerCase()
.contains(_textController.text.trim().toLowerCase()));
.contains(_textController.text.trim().toLowerCase()),
);
}
suggestedUsers.sort((a, b) => a.email.compareTo(b.email));

View file

@ -51,7 +51,7 @@ class _AlbumParticipantsPageState extends State<AlbumParticipantsPage> {
Future<void> _navigateToAddUser(bool addingViewer) async {
await routeToPage(
context,
AddParticipantPage(widget.collection),
AddParticipantPage(widget.collection, addingViewer),
);
if (mounted) {
setState(() => {});
@ -72,7 +72,9 @@ class _AlbumParticipantsPageState extends State<AlbumParticipantsPage> {
final splitResult =
widget.collection.getSharees().splitMatch((x) => x.isViewer);
final List<User> viewers = splitResult.matched;
viewers.sort((a, b) => a.email.compareTo(b.email));
final List<User> collaborators = splitResult.unmatched;
collaborators.sort((a, b) => a.email.compareTo(b.email));
return Scaffold(
body: CustomScrollView(
@ -174,8 +176,9 @@ class _AlbumParticipantsPageState extends State<AlbumParticipantsPage> {
} else if (index == (1 + collaborators.length) && isOwner) {
return MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title:
collaborators.isNotEmpty ? "Add more" : "Add email",
title: collaborators.isNotEmpty
? "Add more"
: "Add collaborator",
makeTextBold: true,
),
leadingIcon: Icons.add_outlined,
@ -249,7 +252,7 @@ class _AlbumParticipantsPageState extends State<AlbumParticipantsPage> {
} else if (index == (1 + viewers.length) && isOwner) {
return MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: viewers.isNotEmpty ? "Add more" : "Add Viewer",
title: viewers.isNotEmpty ? "Add more" : "Add viewer",
makeTextBold: true,
),
leadingIcon: Icons.add_outlined,

View file

@ -80,6 +80,7 @@ class _ManageIndividualParticipantState
widget.collection,
widget.user.email,
role: CollectionParticipantRole.collaborator,
showProgress: true,
);
if ((result ?? false) && mounted) {
widget.user.role = CollectionParticipantRole
@ -112,6 +113,7 @@ class _ManageIndividualParticipantState
widget.collection,
widget.user.email,
role: CollectionParticipantRole.viewer,
showProgress: true,
);
if ((result ?? false) && mounted) {
widget.user.role =
@ -144,7 +146,7 @@ class _ManageIndividualParticipantState
widget.user,
);
if ((result ?? false) && mounted) {
if ((result) && mounted) {
Navigator.of(context).pop(true);
}
},

View file

@ -226,10 +226,9 @@ class _ManageSharedLinkWidgetState extends State<ManageSharedLinkWidget> {
menuItemColor: getEnteColorScheme(context).fillFaint,
pressedColor: getEnteColorScheme(context).fillFaint,
onTap: () async {
final bool result = await sharingActions.publicLinkToggle(
final bool result = await sharingActions.disableUrl(
context,
widget.collection!,
false,
);
if (result && mounted) {
Navigator.of(context).pop();

View file

@ -66,8 +66,38 @@ class _ShareCollectionPageState extends State<ShareCollectionPage> {
children.add(
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: _sharees.isEmpty ? "Add email" : "Add more",
captionedTextWidget: const CaptionedTextWidget(
title: "Add viewer",
makeTextBold: true,
),
leadingIcon: Icons.add,
menuItemColor: getEnteColorScheme(context).fillFaint,
pressedColor: getEnteColorScheme(context).fillFaint,
borderRadius: 4.0,
isTopBorderRadiusRemoved: _sharees.isNotEmpty,
isBottomBorderRadiusRemoved: true,
onTap: () async {
routeToPage(
context,
AddParticipantPage(widget.collection, true),
).then(
(value) => {
if (mounted) {setState(() => {})}
},
);
},
),
);
children.add(
DividerWidget(
dividerType: DividerType.menu,
bgColor: getEnteColorScheme(context).fillFaint,
),
);
children.add(
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Add collaborator",
makeTextBold: true,
),
leadingIcon: Icons.add,
@ -76,7 +106,8 @@ class _ShareCollectionPageState extends State<ShareCollectionPage> {
borderRadius: 4.0,
isTopBorderRadiusRemoved: _sharees.isNotEmpty,
onTap: () async {
routeToPage(context, AddParticipantPage(widget.collection)).then(
routeToPage(context, AddParticipantPage(widget.collection, false))
.then(
(value) => {
if (mounted) {setState(() => {})}
},
@ -198,26 +229,48 @@ class _ShareCollectionPageState extends State<ShareCollectionPage> {
],
);
} else {
children.add(
children.addAll([
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Create public link",
makeTextBold: true,
),
leadingIcon: Icons.link,
menuItemColor: getEnteColorScheme(context).fillFaint,
pressedColor: getEnteColorScheme(context).fillFaint,
isBottomBorderRadiusRemoved: true,
onTap: () async {
final bool result =
await collectionActions.enableUrl(context, widget.collection);
if (result && mounted) {
setState(() => {});
}
},
),
DividerWidget(
dividerType: DividerType.menu,
bgColor: getEnteColorScheme(context).fillFaint,
),
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: "Collect photos",
makeTextBold: true,
),
leadingIcon: Icons.link,
menuItemColor: getEnteColorScheme(context).fillFaint,
pressedColor: getEnteColorScheme(context).fillFaint,
onTap: () async {
final bool result = await collectionActions.publicLinkToggle(
final bool result = await collectionActions.enableUrl(
context,
widget.collection,
true,
enableCollect: true,
);
if (result && mounted) {
setState(() => {});
}
},
),
);
]);
if (_sharees.isEmpty && !hasUrl) {
children.add(
const MenuSectionDescriptionWidget(