Skip to content

Commit 56008b1

Browse files
committed
compose: Support images from keyboard for Android
Fixes: zulip#419 Fixes: zulip#1173 Signed-off-by: Zixuan James Li <[email protected]>
1 parent de2b4f6 commit 56008b1

11 files changed

+199
-0
lines changed

assets/l10n/app_en.arb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,14 @@
522522
"@topicValidationErrorMandatoryButEmpty": {
523523
"description": "Topic validation error when topic is required but was empty."
524524
},
525+
"errorContentNotInsertedTitle": "Content not inserted",
526+
"@errorContentNotInsertedTitle": {
527+
"description": "Title for error dialog when an attempt to insert rich content failed."
528+
},
529+
"errorContentToInsertIsEmpty": "The file to be inserted is empty or cannot be accessed.",
530+
"@errorContentToInsertIsEmpty": {
531+
"description": "Error message when the rich content to be inserted is empty or cannot be accessed."
532+
},
525533
"errorInvalidApiKeyMessage": "Your account at {url} could not be authenticated. Please try logging in again or use another account.",
526534
"@errorInvalidApiKeyMessage": {
527535
"description": "Error message in the dialog for invalid API key.",

lib/generated/l10n/zulip_localizations.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,18 @@ abstract class ZulipLocalizations {
801801
/// **'Topics are required in this organization.'**
802802
String get topicValidationErrorMandatoryButEmpty;
803803

804+
/// Title for error dialog when an attempt to insert rich content failed.
805+
///
806+
/// In en, this message translates to:
807+
/// **'Content not inserted'**
808+
String get errorContentNotInsertedTitle;
809+
810+
/// Error message when the rich content to be inserted is empty or cannot be accessed.
811+
///
812+
/// In en, this message translates to:
813+
/// **'The file to be inserted is empty or cannot be accessed.'**
814+
String get errorContentToInsertIsEmpty;
815+
804816
/// Error message in the dialog for invalid API key.
805817
///
806818
/// In en, this message translates to:

lib/generated/l10n/zulip_localizations_ar.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,12 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
404404
@override
405405
String get topicValidationErrorMandatoryButEmpty => 'Topics are required in this organization.';
406406

407+
@override
408+
String get errorContentNotInsertedTitle => 'Content not inserted';
409+
410+
@override
411+
String get errorContentToInsertIsEmpty => 'The file to be inserted is empty or cannot be accessed.';
412+
407413
@override
408414
String errorInvalidApiKeyMessage(String url) {
409415
return 'Your account at $url could not be authenticated. Please try logging in again or use another account.';

lib/generated/l10n/zulip_localizations_en.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,12 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
404404
@override
405405
String get topicValidationErrorMandatoryButEmpty => 'Topics are required in this organization.';
406406

407+
@override
408+
String get errorContentNotInsertedTitle => 'Content not inserted';
409+
410+
@override
411+
String get errorContentToInsertIsEmpty => 'The file to be inserted is empty or cannot be accessed.';
412+
407413
@override
408414
String errorInvalidApiKeyMessage(String url) {
409415
return 'Your account at $url could not be authenticated. Please try logging in again or use another account.';

lib/generated/l10n/zulip_localizations_ja.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,12 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
404404
@override
405405
String get topicValidationErrorMandatoryButEmpty => 'Topics are required in this organization.';
406406

407+
@override
408+
String get errorContentNotInsertedTitle => 'Content not inserted';
409+
410+
@override
411+
String get errorContentToInsertIsEmpty => 'The file to be inserted is empty or cannot be accessed.';
412+
407413
@override
408414
String errorInvalidApiKeyMessage(String url) {
409415
return 'Your account at $url could not be authenticated. Please try logging in again or use another account.';

lib/generated/l10n/zulip_localizations_nb.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,12 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
404404
@override
405405
String get topicValidationErrorMandatoryButEmpty => 'Topics are required in this organization.';
406406

407+
@override
408+
String get errorContentNotInsertedTitle => 'Content not inserted';
409+
410+
@override
411+
String get errorContentToInsertIsEmpty => 'The file to be inserted is empty or cannot be accessed.';
412+
407413
@override
408414
String errorInvalidApiKeyMessage(String url) {
409415
return 'Your account at $url could not be authenticated. Please try logging in again or use another account.';

lib/generated/l10n/zulip_localizations_pl.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,12 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
404404
@override
405405
String get topicValidationErrorMandatoryButEmpty => 'Wątki są wymagane przez tę organizację.';
406406

407+
@override
408+
String get errorContentNotInsertedTitle => 'Content not inserted';
409+
410+
@override
411+
String get errorContentToInsertIsEmpty => 'The file to be inserted is empty or cannot be accessed.';
412+
407413
@override
408414
String errorInvalidApiKeyMessage(String url) {
409415
return 'Your account at $url could not be authenticated. Please try logging in again or use another account.';

lib/generated/l10n/zulip_localizations_ru.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,12 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
404404
@override
405405
String get topicValidationErrorMandatoryButEmpty => 'Темы обязательны в этой организации.';
406406

407+
@override
408+
String get errorContentNotInsertedTitle => 'Content not inserted';
409+
410+
@override
411+
String get errorContentToInsertIsEmpty => 'The file to be inserted is empty or cannot be accessed.';
412+
407413
@override
408414
String errorInvalidApiKeyMessage(String url) {
409415
return 'Your account at $url could not be authenticated. Please try logging in again or use another account.';

lib/generated/l10n/zulip_localizations_sk.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,12 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
404404
@override
405405
String get topicValidationErrorMandatoryButEmpty => 'Topics are required in this organization.';
406406

407+
@override
408+
String get errorContentNotInsertedTitle => 'Content not inserted';
409+
410+
@override
411+
String get errorContentToInsertIsEmpty => 'The file to be inserted is empty or cannot be accessed.';
412+
407413
@override
408414
String errorInvalidApiKeyMessage(String url) {
409415
return 'Your account at $url could not be authenticated. Please try logging in again or use another account.';

lib/widgets/compose_box.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:app_settings/app_settings.dart';
44
import 'package:flutter/material.dart';
55
import 'package:flutter/services.dart';
66
import 'package:mime/mime.dart';
7+
import 'package:path/path.dart' as path;
78

89
import '../api/exception.dart';
910
import '../api/model/model.dart';
@@ -465,6 +466,39 @@ class _ContentInputState extends State<_ContentInput> with WidgetsBindingObserve
465466
}
466467
}
467468

469+
void _handleContentInserted(KeyboardInsertedContent content) async {
470+
if (content.data == null || content.data!.isEmpty) {
471+
// As of writing, the engine implementation never leaves `content.data` as
472+
// `null`, but ideally it should be when the data cannot be read for
473+
// errors.
474+
//
475+
// When `content.data` is empty, the data is not literally empty — this
476+
// can also happen when the data can't be read from the input stream
477+
// provided by the Android SDK because of an IO exception.
478+
//
479+
// See Flutter engine implementation that prepares this data:
480+
// https://github.com/flutter/flutter/blob/0ffc4ce00/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java#L497-L548
481+
// TODO(upstream): improve the API for this
482+
final zulipLocalizations = ZulipLocalizations.of(context);
483+
showErrorDialog(context: context,
484+
title: zulipLocalizations.errorContentNotInsertedTitle,
485+
message: zulipLocalizations.errorContentToInsertIsEmpty);
486+
return;
487+
}
488+
489+
final file = _File(
490+
content: Stream.fromIterable([content.data!]),
491+
length: content.data!.length,
492+
filename: path.basename(content.uri),
493+
mimeType: content.mimeType);
494+
495+
await _uploadFiles(
496+
context: context,
497+
contentController: widget.controller.content,
498+
contentFocusNode: widget.controller.contentFocusNode,
499+
files: [file]);
500+
}
501+
468502
static double maxHeight(BuildContext context) {
469503
final clampingTextScaler = MediaQuery.textScalerOf(context)
470504
.clamp(maxScaleFactor: 1.5);
@@ -508,6 +542,8 @@ class _ContentInputState extends State<_ContentInput> with WidgetsBindingObserve
508542
child: TextField(
509543
controller: widget.controller.content,
510544
focusNode: widget.controller.contentFocusNode,
545+
contentInsertionConfiguration: ContentInsertionConfiguration(
546+
onContentInserted: _handleContentInserted),
511547
// Let the content show through the `contentPadding` so that
512548
// our [InsetShadowBox] can fade it smoothly there.
513549
clipBehavior: Clip.none,

0 commit comments

Comments
 (0)