diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 5148836220..28d1c53236 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -53,6 +53,8 @@
fetch
+ NSCameraUsageDescription
+ By allowing camera access, you can take photos and send them in Zulip messages.
NSPhotoLibraryUsageDescription
Choose photos from your library and send them in Zulip messages.
diff --git a/lib/widgets/compose_box.dart b/lib/widgets/compose_box.dart
index 9b429048e2..db03721cd9 100644
--- a/lib/widgets/compose_box.dart
+++ b/lib/widgets/compose_box.dart
@@ -2,6 +2,7 @@ import 'package:app_settings/app_settings.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
+import 'package:image_picker/image_picker.dart';
import 'dialog.dart';
import '../api/route/messages.dart';
@@ -385,6 +386,51 @@ class _AttachMediaButton extends _AttachUploadsButton {
}
}
+class _AttachFromCameraButton extends _AttachUploadsButton {
+ const _AttachFromCameraButton({required super.contentController, required super.contentFocusNode});
+
+ @override
+ IconData get icon => Icons.camera_alt;
+
+ @override
+ Future> getFiles(BuildContext context) async {
+ final picker = ImagePicker();
+ final XFile? result;
+ try {
+ // Ideally we'd open a platform interface that lets you choose between
+ // taking a photo and a video. `image_picker` doesn't yet have that
+ // option: https://github.com/flutter/flutter/issues/89159
+ // so just stick with images for now. We could add another button for
+ // videos, but we don't want too many buttons.
+ result = await picker.pickImage(source: ImageSource.camera, requestFullMetadata: false);
+ } catch (e) {
+ if (e is PlatformException && e.code == 'camera_access_denied') {
+ // iOS has a quirk where it will only request the native
+ // permission-request alert once, the first time the app wants to
+ // use a protected resource. After that, the only way the user can
+ // grant it is in Settings.
+ showSuggestedActionDialog(context: context, // TODO(i18n)
+ title: 'Permissions needed',
+ message: 'To upload an image, please grant Zulip additional permissions in Settings.',
+ actionButtonText: 'Open settings',
+ onActionButtonPress: () {
+ AppSettings.openAppSettings();
+ });
+ } else {
+ // TODO(i18n)
+ showErrorDialog(context: context, title: 'Error', message: e.toString());
+ }
+ return [];
+ }
+ if (result == null) {
+ return []; // User cancelled; do nothing
+ }
+ final length = await result.length();
+
+ return [_File(content: result.openRead(), length: length, filename: result.name)];
+ }
+}
+
/// The send button for StreamComposeBox.
class _StreamSendButton extends StatefulWidget {
const _StreamSendButton({required this.topicController, required this.contentController});
@@ -584,6 +630,7 @@ class _StreamComposeBoxState extends State {
children: [
_AttachFileButton(contentController: _contentController, contentFocusNode: _contentFocusNode),
_AttachMediaButton(contentController: _contentController, contentFocusNode: _contentFocusNode),
+ _AttachFromCameraButton(contentController: _contentController, contentFocusNode: _contentFocusNode),
])),
]))));
}