-
Notifications
You must be signed in to change notification settings - Fork 309
compose: Prototype upload-from-library and upload-from-camera #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
} catch (e) { | ||
if (e is PlatformException && e.code == 'camera_access_denied') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could one imagine a nice language feature that lets you match on more than just the type of the thing being thrown? The following is already possible:
} on PlatformException catch (e) {
But I'm thinking like, I dunno,
} on PlatformException(code: var c) when c == 'camera_access_denied' catch (e) {
or something?
This may be relevant: dart-lang/language#112 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may be relevant: dart-lang/language#112 (comment)
Yeah. Looks like the Dart folks have indeed imagined that feature 🙂 Perhaps it'll get added at some point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! This looks good. Small comments below.
lib/widgets/compose_box.dart
Outdated
assert(f.readStream != null); // We passed `withReadStream: true` to pickFiles. | ||
return _File(content: f.readStream!, length: f.size, filename: f.name); | ||
}); | ||
_uploadFiles(context: context, contentController: contentController, contentFocusNode: contentFocusNode, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should await — it doesn't directly matter for the behavior, but if e.g. we added some more code after this call, it'd be buggy if we didn't then notice we needed to add an await. And it's a lot easier to notice the need for it now while we're factoring this out.
lib/widgets/dialog.dart
Outdated
onPressed: onActionButtonPress, | ||
child: _dialogActionText(actionButtonText ?? 'Continue')), | ||
])); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: last line should end with newline, like other lines
} catch (e) { | ||
if (e is PlatformException && e.code == 'camera_access_denied') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may be relevant: dart-lang/language#112 (comment)
Yeah. Looks like the Dart folks have indeed imagined that feature 🙂 Perhaps it'll get added at some point.
message: 'To upload an image, please grant Zulip additional permissions in Settings.', | ||
actionButtonText: 'Open settings', | ||
onActionButtonPress: () { | ||
AppSettings.openAppSettings(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this is from a third-party library: https://pub.dev/packages/app_settings
Have you tried this out on both iOS and Android and it seems to work?
If at any point we're dissatisfied with how it works, the implementation is pretty simple — especially on iOS, where all the AppSettings.openFoo
methods do the same thing:
https://github.com/spencerccf/app_settings/blob/ed65816d72933c9110dff9f4a74ee550e2af20b6/ios/Classes/SwiftAppSettingsPlugin.swift#L7-L10
but the Android version isn't complicated either:
https://github.com/spencerccf/app_settings/blob/ed65816d72933c9110dff9f4a74ee550e2af20b6/android/src/main/kotlin/com/example/appsettings/AppSettingsPlugin.kt#L45-L51
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AppSettings.openAppSettings()
worked great on my iPhone 13 Pro running iOS 16.
Android office device (Samsung Galaxy S9, Android 9) seemed OK, but I wonder: have you ever had an app link directly into the screen for managing an app's permissions? This one brought me here:
and then I scrolled down and found a link to permissions:
which I tapped and found what I needed:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one brought me here:
Same result when I try that function on my Pixel 5 running Android 13. Except that the subtitle on "Permissions" is "No permissions requested", because we're successfully using the no-privileges-needed API for this on newer Android versions. (To even call this function, I hacked up the code a bit.)
I wonder: have you ever had an app link directly into the screen for managing an app's permissions?
Not sure. It's pretty uncommon for me to encounter an app linking me to any part of its system settings.
(Which doesn't mean we shouldn't do it; for one thing, this is an uncommon case, so it's quite possible that lots of apps offer the same thing if you do get into this case.)
Looking at docs, here's the intent type that the library is using for us:
https://developer.android.com/reference/android/provider/Settings#ACTION_APPLICATION_DETAILS_SETTINGS
Browsing around that page, here's a related intent type:
https://developer.android.com/reference/android/provider/Settings#ACTION_STORAGE_VOLUME_ACCESS_SETTINGS
which was deprecated, saying that "to manage storage permissions for a specific application" we should instead use… ACTION_APPLICATION_DETAILS_SETTINGS, just as app_settings
is using for us.
There are various other intent types for particular permissions, including some storage-related:
https://developer.android.com/reference/android/provider/Settings#ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
https://developer.android.com/reference/android/provider/Settings#ACTION_REQUEST_MANAGE_MEDIA
I don't think either of those correspond to what we'd want here, though. In particular both of those were introduced only in recent API versions, and I think they correspond to the newer more-explicitly-powerful permissions that were introduced at the same time as making normal applications stop needing such permissions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That reasoning makes sense.
lib/widgets/compose_box.dart
Outdated
final String filename; | ||
} | ||
|
||
_uploadFiles({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_uploadFiles({ | |
void _uploadFiles({ |
Otherwise its return type is dynamic
.
(There's probably a lint for this, which we should enable. If we ever do want a function returning dynamic
, or a variable of type dynamic
, etc., I'd want to say so explicitly.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Future<void>
, I think, since it's async?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're going to await the result, then definitely Future<void>
instead. To explicitly say it's not the caller's business whether this function completes synchronously or does some async work, one can say void
.
The docs on this are very terse: https://dart.dev/language/built-in-types
Better description here, perhaps: https://www.reddit.com/r/dartlang/comments/oo65yd/does_void_mean_return_null_in_dart_or_simply_just/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah OK, that makes sense. Since you pointed out that we should indeed await the result, in #70 (comment), I plan to go with Future<void>
.
lib/widgets/compose_box.dart
Outdated
final ContentTextEditingController contentController; | ||
final FocusNode contentFocusNode; | ||
|
||
_handlePress(BuildContext context) async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly
_handlePress(BuildContext context) async { | |
dynamic _handlePress(BuildContext context) async { |
and I guess I missed that previously on _AttachFileButton
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I don't understand this one yet, and what are you referring to with "similarly"? Do you mean Future<void>
, or should it really be dynamic
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By "similarly" I mean to not have an implicit dynamic
.
But probably we don't actually want dynamic
, and void
is better.
lib/widgets/compose_box.dart
Outdated
class _AttachFromCameraButton extends StatelessWidget { | ||
_AttachFromCameraButton({required this.contentController, required this.contentFocusNode}); | ||
|
||
final _picker = ImagePicker(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It makes me a bit nervous to be constructing this in an initializer on a widget.
I think the right fix is that we should just construct it in _handlePress
. If there were some way that could go wrong on account of constructing it multiple times, then we'd be exposed to the same problem from constructing it here — because we'll be constructing new _AttachFromCameraButton
widgets each time StreamComposeBox
rebuilds.
In fact, from the implementation, it looks like ImagePicker
has no non-static properties at all — its methods (except for ==
and hashCode
inherited from Object
) all behave exactly the same on any instance, as if they were static methods. That means that where and when we construct it really doesn't matter.
So probably the methods should be static methods; or the class should have a const constructor, which would accomplish much the same thing (and be a smoother migration from the status quo). But because they're not and it doesn't, the API isn't promising that we have that flexibility. I'd therefore like to avoid the pattern that looks like a bug and makes me nervous 🙂
lib/widgets/compose_box.dart
Outdated
try { | ||
result = await FilePicker.platform.pickFiles(type: FileType.media, allowMultiple: true, withReadStream: true); | ||
} catch (e) { | ||
// TODO(i18n) | ||
showErrorDialog(context: context, title: 'Error', message: e.toString()); | ||
return; | ||
} | ||
if (result == null) { | ||
return; // User cancelled; do nothing | ||
} | ||
|
||
if (context.mounted) {} // https://github.com/dart-lang/linter/issues/4007 | ||
else { | ||
return; | ||
} | ||
|
||
final Iterable<_File> files = result.files.map((f) { | ||
assert(f.readStream != null); // We passed `withReadStream: true` to pickFiles. | ||
return _File(content: f.readStream!, length: f.size, filename: f.name); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kind of unsatisfying how much of this code gets duplicated between the three buttons, and especially the two that are using FilePicker.
I think it might improve things to factor much of this into something like
abstract class _AttachUploadsButton extends StatelessWidget {
// …
Icon get icon;
Future<Iterable<_File>?> getFiles(BuildContext context);
// …
}
and then _AttachMediaButton
and the others would be subclasses just implementing those two items. Those implementations would take care of error dialogs and settings requests.
The other upload buttons will extend this new base class too. Prompted by Greg's review: zulip#70 (comment)
340fb2c
to
a3a889a
Compare
Thanks for the review! Revision pushed, with a TODO about the better image-library upload experience on Android, and so marking as not-draft. In this revision I added a commit to improve the existing upload-file UI:
and applied that same improvement to image-library uploads (by reusing code) since that also uses |
And please see my reply about |
(Er, now I've done that.) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Generally looks good; one substantive question, about the attach-image behavior.
lib/widgets/compose_box.dart
Outdated
Future<Iterable<_File>?> getFiles(BuildContext context) async { | ||
// TODO: This doesn't give quite the right UI on Android. | ||
// Perhaps try `image_picker`: https://github.com/zulip/zulip-flutter/issues/56#issuecomment-1514001281 | ||
return _getFilePickerFiles(context); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this ends up identical to the attach-file button.
In the previous draft, this passed type: FileType.media
to FilePicker.platform.pickFiles
. Was it intentional to drop that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eep, thanks for the catch; I seem to have dropped that difference when refactoring to pull common logic out.
lib/widgets/compose_box.dart
Outdated
showSuggestedActionDialog(context: context, // TODO(i18n) | ||
title: 'Permissions needed', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: indentation
lib/widgets/compose_box.dart
Outdated
final FocusNode contentFocusNode; | ||
|
||
IconData get icon; | ||
Future<Iterable<_File>?> getFiles(BuildContext context); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be good to have a bit of dartdoc, particularly to say what null
means.
… Hmm, actually, perhaps better: make it non-nullable, and just return an empty list in that case. Then at the call site, skip doing anything if the list is empty (rather than if null).
That will even behave differently from this version if there's some way that pickFiles
or whatever can return an empty list: it'll do nothing in that case, whereas this version would go and focus the content input. And I think doing nothing is appropriate — if it does return an empty list, that probably means the user has effectively tried to abort the operation.
message: 'To upload an image, please grant Zulip additional permissions in Settings.', | ||
actionButtonText: 'Open settings', | ||
onActionButtonPress: () { | ||
AppSettings.openAppSettings(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one brought me here:
Same result when I try that function on my Pixel 5 running Android 13. Except that the subtitle on "Permissions" is "No permissions requested", because we're successfully using the no-privileges-needed API for this on newer Android versions. (To even call this function, I hacked up the code a bit.)
I wonder: have you ever had an app link directly into the screen for managing an app's permissions?
Not sure. It's pretty uncommon for me to encounter an app linking me to any part of its system settings.
(Which doesn't mean we shouldn't do it; for one thing, this is an uncommon case, so it's quite possible that lots of apps offer the same thing if you do get into this case.)
Looking at docs, here's the intent type that the library is using for us:
https://developer.android.com/reference/android/provider/Settings#ACTION_APPLICATION_DETAILS_SETTINGS
Browsing around that page, here's a related intent type:
https://developer.android.com/reference/android/provider/Settings#ACTION_STORAGE_VOLUME_ACCESS_SETTINGS
which was deprecated, saying that "to manage storage permissions for a specific application" we should instead use… ACTION_APPLICATION_DETAILS_SETTINGS, just as app_settings
is using for us.
There are various other intent types for particular permissions, including some storage-related:
https://developer.android.com/reference/android/provider/Settings#ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
https://developer.android.com/reference/android/provider/Settings#ACTION_REQUEST_MANAGE_MEDIA
I don't think either of those correspond to what we'd want here, though. In particular both of those were introduced only in recent API versions, and I think they correspond to the newer more-explicitly-powerful permissions that were introduced at the same time as making normal applications stop needing such permissions.
The other upload buttons will extend this new base class too. Prompted by Greg's review: zulip#70 (comment)
And pull out a helper from _AttachFileButton, since it also uses `file_picker` and it's convenient to reuse that code here. Fixes: zulip#56
Toward zulip#61, "Open camera to take and share a photo".
a3a889a
to
f99a3b3
Compare
Thanks for the review! Revision pushed. |
Thanks! Looks good; merging. |
Marking as draft while we think through how to get the right photo/video-picker UI on Android (details at #56 (comment) ) but the code is reviewable in the meantime, and people can try out the new features. 🙂
Fixes: #61
Fixes: #56