From f02acf93fe50c3b52778da2e1011e63ac1048750 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sun, 8 Oct 2023 22:41:26 +0700 Subject: [PATCH 01/12] TW-731: add packages for copy/paste image --- android/app/build.gradle | 3 +- android/app/src/main/AndroidManifest.xml | 8 +++- pubspec.lock | 51 +++++++++++++++++++++--- pubspec.yaml | 7 +++- 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 23419542c7..a985cab9c1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -33,6 +33,7 @@ if (keystorePropertiesFile.exists()) { android { compileSdkVersion 33 + ndkVersion flutter.ndkVersion sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -44,7 +45,7 @@ android { defaultConfig { applicationId "com.twake.twake" - minSdkVersion 21 + minSdkVersion 23 targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 78d5c7eb27..700a8dbbcd 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -40,7 +40,7 @@ android:requestLegacyExternalStorage="true" android:allowBackup="false" android:fullBackupContent="false" - > + > + + diff --git a/pubspec.lock b/pubspec.lock index f2b9fb4f9d..33d13ac520 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1043,11 +1043,12 @@ packages: flutter_typeahead: dependency: "direct main" description: - name: flutter_typeahead - sha256: f3a5f79d9a056e5108452dbec31d12bbd7f6d25e9097bf0f956e3f8d024e1747 - url: "https://pub.dev" - source: hosted - version: "4.7.0" + path: "." + ref: twake-supported + resolved-ref: "4d79401802bd3528ab1b27ff865916269bb416c8" + url: "https://github.com/linagora/flutter_typeahead.git" + source: git + version: "4.8.0" flutter_web_auth: dependency: "direct main" description: @@ -1367,6 +1368,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + irondash_engine_context: + dependency: transitive + description: + name: irondash_engine_context + sha256: fea21bff36d44a5955beba90619f54b9169884014128ae75f50eba2db03c24a0 + url: "https://pub.dev" + source: hosted + version: "0.3.1" + irondash_message_channel: + dependency: transitive + description: + name: irondash_message_channel + sha256: "500daa1fbe679f7d28a5258df3ff47dab6de352e680dc93c1ca9eae1555d8db5" + url: "https://pub.dev" + source: hosted + version: "0.3.1" isolate: dependency: transitive description: @@ -1883,6 +1900,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.0" + pixel_snap: + dependency: transitive + description: + name: pixel_snap + sha256: "5de3662b926c9bc189578cf90f9d5b350ee61bc8e20e8a91fa1dfdd26c9f5ece" + url: "https://pub.dev" + source: hosted + version: "0.1.2" platform: dependency: transitive description: @@ -2425,6 +2450,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + super_clipboard: + dependency: "direct main" + description: + name: super_clipboard + sha256: "548642d62d691d2ba00850efe0f7a11ce5696e59111658abd4c6cb76b49aa61d" + url: "https://pub.dev" + source: hosted + version: "0.6.4" + super_native_extensions: + dependency: transitive + description: + name: super_native_extensions + sha256: "3bbb95899f848617b819eec1fa00c80a617f1bd469a480b1f0c4dd383da4c5ec" + url: "https://pub.dev" + source: hosted + version: "0.6.4" sync_http: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 046f06cbc0..be77f5a1fa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,7 +43,11 @@ dependencies: flutter_ringtone_player: ^3.1.1 flutter_secure_storage: ^7.0.1 flutter_svg: ^0.22.0 - flutter_typeahead: ^4.7.0 + # FIXME: change to upstream when https://github.com/AbdulRahmanAlHamali/flutter_typeahead/pull/528 is merge + flutter_typeahead: + git: + url: https://github.com/linagora/flutter_typeahead.git + ref: twake-supported flutter_web_auth: ^0.5.0 # flutter_webrtc: # Until https://github.com/flutter-webrtc/flutter-webrtc/issues/1212 is fixed # git: https://github.com/radzio-it/flutter-webrtc.git @@ -149,6 +153,7 @@ dependencies: media_kit_libs_video: ^1.0.1 video_player: ^2.7.2 js: ^0.6.7 + super_clipboard: ^0.6.4 dev_dependencies: build_runner: ^2.3.3 dart_code_metrics: ^5.7.3 From c47577a81f9d904a1e1da26ffd9335368138db45 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sun, 8 Oct 2023 22:42:13 +0700 Subject: [PATCH 02/12] TW-731: add generated plugins for other platforms --- linux/flutter/generated_plugin_registrant.cc | 8 ++++++++ linux/flutter/generated_plugins.cmake | 2 ++ macos/Flutter/GeneratedPluginRegistrant.swift | 4 ++++ windows/flutter/generated_plugin_registrant.cc | 6 ++++++ windows/flutter/generated_plugins.cmake | 2 ++ 5 files changed, 22 insertions(+) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e3fc573553..629505e486 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -15,9 +15,11 @@ #include #include #include +#include #include #include #include +#include #include void fl_register_plugins(FlPluginRegistry* registry) { @@ -48,6 +50,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) handy_window_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "HandyWindowPlugin"); handy_window_plugin_register_with_registrar(handy_window_registrar); + g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin"); + irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar); g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); @@ -57,6 +62,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) record_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin"); record_linux_plugin_register_with_registrar(record_linux_registrar); + g_autoptr(FlPluginRegistrar) super_native_extensions_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "SuperNativeExtensionsPlugin"); + super_native_extensions_plugin_register_with_registrar(super_native_extensions_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 591a12a865..92d5322adf 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -12,9 +12,11 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_secure_storage_linux flutter_webrtc handy_window + irondash_engine_context media_kit_libs_linux media_kit_video record_linux + super_native_extensions url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index f0c9c8e51f..c2b025e778 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -21,6 +21,7 @@ import flutter_secure_storage_macos import flutter_web_auth import flutter_webrtc import geolocator_apple +import irondash_engine_context import just_audio import macos_ui import macos_window_utils @@ -34,6 +35,7 @@ import screen_brightness_macos import share_plus_macos import shared_preferences_macos import sqflite +import super_native_extensions import url_launcher_macos import video_compress import wakelock_macos @@ -56,6 +58,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterWebAuthPlugin.register(with: registry.registrar(forPlugin: "FlutterWebAuthPlugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) + IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) MacOSUiPlugin.register(with: registry.registrar(forPlugin: "MacOSUiPlugin")) MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin")) @@ -69,6 +72,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin")) WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 7c2d17617d..d8a27d3b42 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -14,11 +14,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -38,6 +40,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterWebRTCPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); + IrondashEngineContextPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("IrondashEngineContextPluginCApi")); MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); MediaKitVideoPluginCApiRegisterWithRegistrar( @@ -48,6 +52,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("RecordWindowsPluginCApi")); ScreenBrightnessWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin")); + SuperNativeExtensionsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SuperNativeExtensionsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 2d0ed4fc1c..30d5e717d8 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -11,11 +11,13 @@ list(APPEND FLUTTER_PLUGIN_LIST file_saver file_selector_windows flutter_webrtc + irondash_engine_context media_kit_libs_windows_video media_kit_video permission_handler_windows record_windows screen_brightness_windows + super_native_extensions url_launcher_windows ) From d4777ec9f8b8956e907771d446125fd51d271cea Mon Sep 17 00:00:00 2001 From: sherlock Date: Wed, 11 Oct 2023 11:18:54 +0700 Subject: [PATCH 03/12] TW-731: add clipboard service --- .../model/clipboard/clipboard_image_info.dart | 19 ++ lib/utils/clipboard.dart | 173 ++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 lib/presentation/model/clipboard/clipboard_image_info.dart create mode 100644 lib/utils/clipboard.dart diff --git a/lib/presentation/model/clipboard/clipboard_image_info.dart b/lib/presentation/model/clipboard/clipboard_image_info.dart new file mode 100644 index 0000000000..a172e3183f --- /dev/null +++ b/lib/presentation/model/clipboard/clipboard_image_info.dart @@ -0,0 +1,19 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/services.dart'; + +class ClipboardImageInfo with EquatableMixin { + final Stream stream; + + final String? fileName; + + final int? fileSize; + + ClipboardImageInfo({ + required this.stream, + this.fileName, + this.fileSize, + }); + + @override + List get props => [stream, fileName, fileSize]; +} diff --git a/lib/utils/clipboard.dart b/lib/utils/clipboard.dart new file mode 100644 index 0000000000..8e104f31c2 --- /dev/null +++ b/lib/utils/clipboard.dart @@ -0,0 +1,173 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:fluffychat/presentation/model/clipboard/clipboard_image_info.dart'; +import 'package:flutter/services.dart'; +import 'package:matrix/matrix.dart'; +import 'package:mime/mime.dart'; +import 'package:super_clipboard/super_clipboard.dart'; + +class Clipboard { + static final _clipboard = Clipboard._(); + + Clipboard._(); + + ClipboardReader? _reader; + + static Clipboard get instance => _clipboard; + + static const allImageFormatsSupported = [ + Formats.png, + Formats.jpeg, + Formats.heic, + Formats.heif, + Formats.svg, + ]; + + Future copyImageAsStream(File image, {String? mimeType}) async { + final item = DataWriterItem(suggestedName: image.path); + final imageStream = image.openRead(); + final mime = mimeType ?? lookupMimeType(image.path); + await imageStream.forEach((data) { + item.add(getFormatFrom(mime)(Uint8List.fromList(data))); + }); + try { + await ClipboardWriter.instance.write([item]); + } catch (e) { + Logs().e('Clipboard::copyImageAsStream(): $e'); + } + } + + Future copyImageAsBytes(Uint8List data, {String? mimeType}) async { + try { + final item = DataWriterItem(); + item.add(getFormatFrom(mimeType)(data)); + await ClipboardWriter.instance.write([item]); + } catch (e) { + Logs().e('Clipboard::copyImageAsBytes(): $e'); + } + } + + Future initReader() async { + _reader = await ClipboardReader.readClipboard(); + } + + Future pasteImageUsingStream() async { + _reader ??= await ClipboardReader.readClipboard(); + ClipboardImageInfo? imageInfo; + + final readableFormats = _reader!.getFormats(allImageFormatsSupported); + if (readableFormats.isEmpty != false && + readableFormats.first is! SimpleFileFormat) { + return imageInfo; + } + + final c = Completer(); + final progress = _reader!.getFile( + readableFormats.first as SimpleFileFormat, + (file) async { + try { + imageInfo = ClipboardImageInfo( + stream: file.getStream(), + fileName: file.fileName, + fileSize: file.fileSize, + ); + c.complete(imageInfo); + } catch (e) { + Logs().e('Clipboard::pasteImageUsingBytes(): $e'); + c.completeError(e); + } + }, + onError: (e) { + Logs().e('Clipboard::pasteImageUsingBytes(): $e'); + c.completeError(e); + }, + ); + if (progress == null) { + c.complete(null); + } + return c.future; + } + + Future? pasteImageUsingBytes() async { + _reader ??= await ClipboardReader.readClipboard(); + final readableFormats = _reader!.getFormats(allImageFormatsSupported); + if (readableFormats.isEmpty != false && + readableFormats.first is! SimpleFileFormat) { + return null; + } + + final c = Completer(); + final progress = _reader!.getFile( + readableFormats.first as SimpleFileFormat, + (file) async { + try { + final all = await file.readAll(); + c.complete(all); + } catch (e) { + Logs().e('Clipboard::pasteImageUsingBytes(): $e'); + c.completeError(e); + } + }, + onError: (e) { + Logs().e('Clipboard::pasteImageUsingBytes(): $e'); + c.completeError(e); + }, + ); + if (progress == null) { + c.complete(null); + } + return c.future; + } + + Future isReadableImageFormat() async { + _reader ??= await ClipboardReader.readClipboard(); + return _reader!.canProvide(Formats.png) || + _reader!.canProvide(Formats.jpeg) || + _reader!.canProvide(Formats.heic) || + _reader!.canProvide(Formats.heif) || + _reader!.canProvide(Formats.svg); + } + + Future pasteText() async { + _reader ??= await ClipboardReader.readClipboard(); + String? copied; + + final readersFormat = _reader!.getFormats(Formats.standardFormats); + if (readersFormat.isEmpty) { + return copied; + } + + final c = Completer(); + final progress = _reader!.getValue( + Formats.plainText, + (value) { + copied = value; + c.complete(copied); + }, + onError: (error) { + Logs().e('Clipboard::readText(): $error'); + c.completeError(error); + }, + ); + if (progress == null) { + c.completeError('Clipboard::readText(): error'); + } + return c.future; + } + + SimpleFileFormat getFormatFrom(String? mimeType) { + switch (mimeType) { + case 'image/png': + return Formats.png; + case 'image/jpeg': + return Formats.jpeg; + case 'image/heic': + return Formats.heic; + case 'image/heif': + return Formats.heif; + default: + return Formats.plainTextFile; + } + } +} From bb7b37425ce5cf992b94df821debec9743e1057c Mon Sep 17 00:00:00 2001 From: sherlock Date: Wed, 11 Oct 2023 11:19:23 +0700 Subject: [PATCH 04/12] TW-731: support copy images in chat --- lib/pages/chat/chat.dart | 42 ++++++++++++++++++++++++++++++++--- lib/pages/chat/chat_view.dart | 2 +- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index bbace50d3a..4c385d36e6 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -31,6 +31,7 @@ import 'package:fluffychat/presentation/mixins/media_picker_mixin.dart'; import 'package:fluffychat/presentation/mixins/send_files_mixin.dart'; import 'package:fluffychat/presentation/model/forward/forward_argument.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; +import 'package:fluffychat/utils/clipboard.dart'; import 'package:fluffychat/utils/extension/build_context_extension.dart'; import 'package:fluffychat/utils/extension/value_notifier_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; @@ -47,7 +48,7 @@ import 'package:fluffychat/widgets/mixins/popup_context_menu_action_mixin.dart'; import 'package:fluffychat/widgets/mixins/popup_menu_widget_mixin.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; +import 'package:flutter/services.dart' as flutter; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; @@ -776,8 +777,43 @@ class ChatController extends State return copyString; } + void copySingleEventAction() async { + if (selectedEvents.length == 1) { + final event = selectedEvents.first; + final matrixFile = event.getMatrixFile() ?? + await event.downloadAndDecryptAttachment( + getThumbnail: true, + ); + if (event.messageType == MessageTypes.Image) { + if (matrixFile.filePath != null) { + Clipboard.instance.copyImageAsStream( + File(matrixFile.filePath!), + mimeType: event.mimeType, + ); + } else if (matrixFile.bytes != null) { + Clipboard.instance.copyImageAsBytes( + matrixFile.bytes!, + mimeType: event.mimeType, + ); + } else { + TwakeSnackBar.show(context, L10n.of(context)!.copyImageFailed); + Logs().e( + 'copySingleEventAction(): failed to copy file ${matrixFile.name}', + ); + } + } else { + copyEventsAction(); + } + } + } + void copyEventsAction() { - Clipboard.setData(ClipboardData(text: _getSelectedEventString())); + flutter.Clipboard.setData( + flutter.ClipboardData( + text: _getSelectedEventString(), + ), + ); + showEmojiPickerNotifier.value = false; setState(() { selectedEvents.clear(); @@ -1467,7 +1503,7 @@ class ChatController extends State break; case ChatContextMenuActions.copyMessage: onSelectMessage(event); - copyEventsAction(); + copySingleEventAction(); break; case ChatContextMenuActions.pinMessage: onSelectMessage(event); diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 24a9b50775..f45e3a29e1 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -38,7 +38,7 @@ class ChatView extends StatelessWidget { icon: Icons.copy_outlined, tooltip: L10n.of(context)!.copy, onTap: () => controller - .actionWithClearSelections(controller.copyEventsAction), + .actionWithClearSelections(controller.copySingleEventAction), ), if (controller.canRedactSelectedEvents) TwakeIconButton( From 427dafff2c1d89451cec43eefdf5a131061713ae Mon Sep 17 00:00:00 2001 From: sherlock Date: Wed, 11 Oct 2023 11:20:07 +0700 Subject: [PATCH 05/12] TW-731: support paste image in web and mobile --- .../mixins/paste_image_mixin.dart | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 lib/presentation/mixins/paste_image_mixin.dart diff --git a/lib/presentation/mixins/paste_image_mixin.dart b/lib/presentation/mixins/paste_image_mixin.dart new file mode 100644 index 0000000000..3444b02b9e --- /dev/null +++ b/lib/presentation/mixins/paste_image_mixin.dart @@ -0,0 +1,57 @@ +import 'dart:typed_data'; + +import 'package:fluffychat/pages/chat/send_file_dialog.dart'; +import 'package:fluffychat/presentation/model/clipboard/clipboard_image_info.dart'; +import 'package:fluffychat/utils/clipboard.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/twake_snackbar.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +mixin PasteImageMixin { + Future pasteImage(BuildContext context, Room room) async { + await Clipboard.instance.initReader(); + if (!(await Clipboard.instance.isReadableImageFormat())) { + Logs().e('PasteImageMixin::pasteImage(): not readable image format'); + return; + } + Uint8List? imageData; + ClipboardImageInfo? imageClipboard; + if (PlatformInfos.isWeb) { + imageData = await Clipboard.instance.pasteImageUsingBytes(); + } else { + imageClipboard = await Clipboard.instance.pasteImageUsingStream(); + if (imageClipboard == null) { + TwakeSnackBar.show(context, L10n.of(context)!.pasteImageFailed); + return; + } + // FIXME: need to update the SendFileDialog to have FileInfo inside + // after update we can use stream to read files instead of convert into raw image data + final data = await imageClipboard.stream.toList(); + imageData = Uint8List.fromList( + data.expand((Uint8List uint8List) => uint8List).toList(), + ); + } + if (imageData == null || imageData.isEmpty) { + TwakeSnackBar.show(context, L10n.of(context)!.pasteImageFailed); + return; + } + + await showDialog( + context: context, + useRootNavigator: PlatformInfos.isWeb, + builder: (context) { + return SendFileDialog( + room: room, + files: [ + MatrixImageFile( + name: imageClipboard?.fileName ?? 'copied', + bytes: imageData, + ) + ], + ); + }, + ); + } +} From 1276cee02436419020d9befde6f38c881dde1e59 Mon Sep 17 00:00:00 2001 From: sherlock Date: Wed, 11 Oct 2023 11:26:24 +0700 Subject: [PATCH 06/12] TW-731: support context menu inputbar in web --- lib/pages/chat/input_bar.dart | 98 +++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 40 deletions(-) diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index bce81fe17c..427b710bcb 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -311,47 +311,65 @@ class InputBar extends StatelessWidget { onSubmitted?.call(controller?.text ?? ''); } }, - child: TypeAheadField>( - direction: AxisDirection.up, - hideOnEmpty: true, - hideOnLoading: true, - keepSuggestionsOnSuggestionSelected: true, - debounceDuration: const Duration(milliseconds: 50), - // show suggestions after 50ms idle time (default is 300) - textFieldConfiguration: TextFieldConfiguration( - minLines: minLines, - maxLines: maxLines, - keyboardType: keyboardType!, - textInputAction: textInputAction, - autofocus: autofocus!, - style: InputBarStyle.getTypeAheadTextStyle(context), - onSubmitted: (text) { - // fix for library for now - // it sets the types for the callback incorrectly - onSubmitted!(text); - }, - controller: controller, - decoration: decoration!, - focusNode: focusNode, - onChanged: (text) { - // fix for the library for now - // it sets the types for the callback incorrectly - onChanged!(text); + child: Listener( + onPointerDown: (PointerDownEvent event) async { + if (event.kind == PointerDeviceKind.mouse && + event.buttons == kSecondaryMouseButton) { + // FIXME: the contextMenuBuilder.editable can do this but its style in web is not customizable + // currently this is only solution + final screenSize = MediaQuery.of(context).size; + final offset = event.position; + final position = RelativeRect.fromLTRB( + offset.dx, + offset.dy, + screenSize.width - offset.dx, + screenSize.height - offset.dy, + ); + final menuItem = await showMenu( + useRootNavigator: PlatformInfos.isWeb, + context: context, + items: [ + PopupMenuItem( + value: InputBarContextMenu.copy, + child: Text(L10n.of(context)!.copy), + ), + PopupMenuItem( + value: InputBarContextMenu.cut, + child: Text(L10n.of(context)!.cut), + ), + PopupMenuItem( + value: InputBarContextMenu.paste, + child: Text(L10n.of(context)!.paste), + ), + ], + position: position, + ); + + if (menuItem == null) { + return; + } + + if (controller == null) { + return; + } + + switch (menuItem) { + case InputBarContextMenu.copy: + controller!.copyText(); + break; + case InputBarContextMenu.cut: + controller!.cutText(); + break; + case InputBarContextMenu.paste: + if (await Clipboard.instance.isReadableImageFormat()) { + await pasteImage(context, room!); + } else { + await controller!.pasteText(); + } + break; + } + } }, - textCapitalization: TextCapitalization.sentences, - ), - suggestionsCallback: getSuggestions, - itemBuilder: (context, suggestion) => SuggestionTile( - suggestion: suggestion, - client: Matrix.of(context).client, - ), - onSuggestionSelected: (Map suggestion) => - insertSuggestion(context, suggestion), - errorBuilder: (BuildContext context, Object? error) => Container(), - loadingBuilder: (BuildContext context) => Container(), - // fix loading briefly flickering a dark box - noItemsFoundBuilder: (BuildContext context) => - Container(), // fix loading briefly showing no suggestions ), ); } From 10ac32c5c174d4e1c218172fbbb6e57616fc158a Mon Sep 17 00:00:00 2001 From: sherlock Date: Wed, 11 Oct 2023 11:27:16 +0700 Subject: [PATCH 07/12] TW-731: add shortcut copy/paste for web --- lib/pages/chat/input_bar.dart | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index 427b710bcb..8659b42731 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -311,6 +311,27 @@ class InputBar extends StatelessWidget { onSubmitted?.call(controller?.text ?? ''); } }, + child: CallbackShortcuts( + bindings: { + const SingleActivator( + flutter.LogicalKeyboardKey.keyV, + meta: true, + ): () async { + if (await Clipboard.instance.isReadableImageFormat()) { + await pasteImage(context, room!); + } else if (controller != null) { + await controller!.pasteText(); + } + }, + const SingleActivator( + flutter.LogicalKeyboardKey.keyC, + meta: true, + ): () { + if (controller != null) { + controller!.copyText(); + } + }, + }, child: Listener( onPointerDown: (PointerDownEvent event) async { if (event.kind == PointerDeviceKind.mouse && @@ -375,6 +396,14 @@ class InputBar extends StatelessWidget { } } +class PasteIntent extends Intent { + const PasteIntent(); +} + +class CopyIntent extends Intent { + const CopyIntent(); +} + class NewLineIntent extends Intent {} class SubmitLineIntent extends Intent {} From e76308f302931372e91044f27b883b9effa89040 Mon Sep 17 00:00:00 2001 From: sherlock Date: Wed, 11 Oct 2023 11:27:46 +0700 Subject: [PATCH 08/12] TW-731: supprt context menu for mobile --- lib/pages/chat/input_bar.dart | 110 +++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index 8659b42731..6f3fa26fcd 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -1,7 +1,14 @@ +import 'package:fluffychat/pages/chat/send_file_dialog.dart'; +import 'package:fluffychat/presentation/enum/chat/popup_menu_item_web_enum.dart'; +import 'package:fluffychat/presentation/extensions/text_editting_controller_extension.dart'; +import 'package:fluffychat/presentation/mixins/paste_image_mixin.dart'; +import 'package:fluffychat/utils/clipboard.dart'; import 'package:fluffychat/utils/extension/raw_key_event_extension.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' as flutter; import 'package:emojis/emoji.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -15,7 +22,7 @@ import 'package:fluffychat/widgets/mxc_image.dart'; import 'command_hints.dart'; -class InputBar extends StatelessWidget { +class InputBar extends StatelessWidget with PasteImageMixin { final Room? room; final int? minLines; final int? maxLines; @@ -391,6 +398,107 @@ class InputBar extends StatelessWidget { } } }, + child: TypeAheadField>( + direction: AxisDirection.up, + hideOnEmpty: true, + hideOnLoading: true, + keepSuggestionsOnSuggestionSelected: true, + debounceDuration: const Duration(milliseconds: 50), + // show suggestions after 50ms idle time (default is 300) + textFieldConfiguration: TextFieldConfiguration( + minLines: minLines, + maxLines: maxLines, + keyboardType: keyboardType!, + textInputAction: textInputAction, + autofocus: autofocus!, + style: InputBarStyle.getTypeAheadTextStyle(context), + onSubmitted: (text) { + // fix for library for now + // it sets the types for the callback incorrectly + onSubmitted!(text); + }, + controller: controller, + decoration: decoration!, + focusNode: focusNode, + onChanged: (text) { + // fix for the library for now + // it sets the types for the callback incorrectly + onChanged!(text); + }, + textCapitalization: TextCapitalization.sentences, + contentInsertionConfiguration: ContentInsertionConfiguration( + onContentInserted: (keyboardInsertContent) async { + if (room == null || !keyboardInsertContent.hasData) { + return; + } + await showDialog( + context: context, + useRootNavigator: PlatformInfos.isWeb, + builder: (context) { + return SendFileDialog( + room: room!, + files: [ + MatrixImageFile( + name: keyboardInsertContent.uri, + bytes: keyboardInsertContent.data, + ) + ], + ); + }, + ); + }, + ), + contextMenuBuilder: ( + BuildContext contextMenucontext, + EditableTextState editableTextState, + ) { + return AdaptiveTextSelectionToolbar.editable( + anchors: editableTextState.contextMenuAnchors, + clipboardStatus: ClipboardStatus.pasteable, + onPaste: !PlatformInfos.isWeb + ? () async { + if (room == null) { + // FIXME: need to handle the case when in draft chat + return; + } + + if (await Clipboard.instance + .isReadableImageFormat()) { + await pasteImage(context, room!); + } else { + editableTextState + .pasteText(SelectionChangedCause.toolbar); + } + } + : null, + onCopy: () { + editableTextState + .copySelection(SelectionChangedCause.toolbar); + }, + onCut: () { + editableTextState + .cutSelection(SelectionChangedCause.toolbar); + }, + onSelectAll: () { + editableTextState.selectAll(SelectionChangedCause.toolbar); + }, + ); + }, + ), + suggestionsCallback: getSuggestions, + itemBuilder: (context, suggestion) => SuggestionTile( + suggestion: suggestion, + client: Matrix.of(context).client, + ), + onSuggestionSelected: (Map suggestion) => + insertSuggestion(context, suggestion), + errorBuilder: (BuildContext context, Object? error) => Container(), + loadingBuilder: (BuildContext context) => Container(), + // fix loading briefly flickering a dark box + noItemsFoundBuilder: (BuildContext context) => + Container(), // fix loading briefly showing no suggestions + ), + ), ), ); } From 3cbc18e6f8567d3fd6680b19a460a1b356145661 Mon Sep 17 00:00:00 2001 From: sherlock Date: Wed, 11 Oct 2023 11:28:22 +0700 Subject: [PATCH 09/12] TW-731: rebuild copy/paste text --- assets/l10n/intl_en.arb | 6 ++- .../enum/chat/popup_menu_item_web_enum.dart | 5 +++ .../text_editting_controller_extension.dart | 40 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 lib/presentation/enum/chat/popup_menu_item_web_enum.dart create mode 100644 lib/presentation/extensions/text_editting_controller_extension.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 0be0f07118..353097a77d 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2760,5 +2760,9 @@ "thisMessageHasBeenEncrypted": "This message has been encrypted", "roomCreationFailed": "Room creation failed", "errorGettingPdf": "Error getting PDF", - "errorPreviewingFile": "Error previewing file" + "errorPreviewingFile": "Error previewing file", + "paste": "Paste", + "cut": "Cut", + "pasteImageFailed": "Paste image failed", + "copyImageFailed": "Copy image failed" } \ No newline at end of file diff --git a/lib/presentation/enum/chat/popup_menu_item_web_enum.dart b/lib/presentation/enum/chat/popup_menu_item_web_enum.dart new file mode 100644 index 0000000000..f34755c591 --- /dev/null +++ b/lib/presentation/enum/chat/popup_menu_item_web_enum.dart @@ -0,0 +1,5 @@ +enum InputBarContextMenu { + copy, + cut, + paste, +} diff --git a/lib/presentation/extensions/text_editting_controller_extension.dart b/lib/presentation/extensions/text_editting_controller_extension.dart new file mode 100644 index 0000000000..2d0c47aa54 --- /dev/null +++ b/lib/presentation/extensions/text_editting_controller_extension.dart @@ -0,0 +1,40 @@ +import 'package:fluffychat/utils/clipboard.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' as flutter; + +extension TextEdittingControllerExtension on TextEditingController { + Future pasteText() async { + final start = selection.start; + final end = selection.end; + final pastedText = await Clipboard.instance.pasteText(); + if (pastedText != null) { + if (start == -1 || end == -1) { + text = pastedText + text; + return; + } + if (start == end) { + final startText = text.substring(0, start); + final trailingText = text.substring(end, text.length); + text = startText + pastedText + trailingText; + } else { + text = text.replaceRange(start, end, pastedText); + } + } + } + + Future copyText() async { + final start = selection.start; + final end = selection.end; + if (start < end) { + await flutter.Clipboard.setData( + flutter.ClipboardData( + text: text.substring(start, end), + ), + ); + } + } + + Future cutText() async { + //TO-DO: + } +} From 3b49383e3e0452659bf32500951725391a590916 Mon Sep 17 00:00:00 2001 From: sherlock Date: Wed, 11 Oct 2023 12:57:05 +0700 Subject: [PATCH 10/12] TW-731: support copy/paste image for ios --- assets/l10n/intl_en.arb | 4 ++- lib/pages/chat/chat.dart | 10 +++++-- lib/pages/chat/input_bar.dart | 2 +- lib/pages/chat/send_file_dialog.dart | 26 ++++++++++--------- .../mixins/paste_image_mixin.dart | 1 + 5 files changed, 27 insertions(+), 16 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 353097a77d..d8bc462966 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2764,5 +2764,7 @@ "paste": "Paste", "cut": "Cut", "pasteImageFailed": "Paste image failed", - "copyImageFailed": "Copy image failed" + "copyImageFailed": "Copy image failed", + "fileFormatNotSupported": "File format not supported", + "copyImageSuccess": "Image copied to clipboard" } \ No newline at end of file diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 4c385d36e6..5ebb7f0c9b 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -786,15 +786,21 @@ class ChatController extends State ); if (event.messageType == MessageTypes.Image) { if (matrixFile.filePath != null) { - Clipboard.instance.copyImageAsStream( + await Clipboard.instance.copyImageAsStream( File(matrixFile.filePath!), mimeType: event.mimeType, ); + if (!PlatformInfos.isAndroid) { + TwakeSnackBar.show(context, L10n.of(context)!.copyImageSuccess); + } } else if (matrixFile.bytes != null) { - Clipboard.instance.copyImageAsBytes( + await Clipboard.instance.copyImageAsBytes( matrixFile.bytes!, mimeType: event.mimeType, ); + if (!PlatformInfos.isAndroid) { + TwakeSnackBar.show(context, L10n.of(context)!.copyImageSuccess); + } } else { TwakeSnackBar.show(context, L10n.of(context)!.copyImageFailed); Logs().e( diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index 6f3fa26fcd..1143a471d3 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -461,7 +461,7 @@ class InputBar extends StatelessWidget with PasteImageMixin { // FIXME: need to handle the case when in draft chat return; } - + await Clipboard.instance.initReader(); if (await Clipboard.instance .isReadableImageFormat()) { await pasteImage(context, room!); diff --git a/lib/pages/chat/send_file_dialog.dart b/lib/pages/chat/send_file_dialog.dart index 42bf1949e9..33893a16d7 100644 --- a/lib/pages/chat/send_file_dialog.dart +++ b/lib/pages/chat/send_file_dialog.dart @@ -1,3 +1,5 @@ +import 'package:fluffychat/presentation/extensions/send_file_web_extension.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -43,18 +45,18 @@ class SendFileDialogState extends State { } // ignore: unused_local_variable final scaffoldMessenger = ScaffoldMessenger.of(context); - // widget.room - // .sendFileEvent( - // file, - // thumbnail: thumbnail, - // shrinkImageMaxDimension: origImage ? null : 1600, - // ) - // .catchError((e) { - // scaffoldMessenger.showSnackBar( - // SnackBar(content: Text((e as Object).toLocalizedString(context))), - // ); - // return null; - // }); + widget.room + .sendFileOnWebEvent( + file, + thumbnail: thumbnail, + shrinkImageMaxDimension: origImage ? null : 1600, + ) + .catchError((e) { + scaffoldMessenger.showSnackBar( + SnackBar(content: Text((e as Object).toLocalizedString(context))), + ); + return null; + }); } Navigator.of(context, rootNavigator: false).pop(); diff --git a/lib/presentation/mixins/paste_image_mixin.dart b/lib/presentation/mixins/paste_image_mixin.dart index 3444b02b9e..e2271c79ba 100644 --- a/lib/presentation/mixins/paste_image_mixin.dart +++ b/lib/presentation/mixins/paste_image_mixin.dart @@ -13,6 +13,7 @@ mixin PasteImageMixin { Future pasteImage(BuildContext context, Room room) async { await Clipboard.instance.initReader(); if (!(await Clipboard.instance.isReadableImageFormat())) { + TwakeSnackBar.show(context, L10n.of(context)!.fileFormatNotSupported); Logs().e('PasteImageMixin::pasteImage(): not readable image format'); return; } From 0b0dbaba317847016d92de256797e2a0017bb3e2 Mon Sep 17 00:00:00 2001 From: sherlock Date: Wed, 11 Oct 2023 13:51:32 +0700 Subject: [PATCH 11/12] TW-731: support copy/paste in windows, linux platform --- lib/pages/chat/input_bar.dart | 5 +++-- lib/utils/platform_infos.dart | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index 1143a471d3..166877a8c1 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -320,9 +320,10 @@ class InputBar extends StatelessWidget with PasteImageMixin { }, child: CallbackShortcuts( bindings: { - const SingleActivator( + SingleActivator( flutter.LogicalKeyboardKey.keyV, - meta: true, + meta: PlatformInfos.isMacKeyboardPlatform, + control: !PlatformInfos.isMacKeyboardPlatform, ): () async { if (await Clipboard.instance.isReadableImageFormat()) { await pasteImage(context, room!); diff --git a/lib/utils/platform_infos.dart b/lib/utils/platform_infos.dart index 0ec701355e..8c8cd1a97c 100644 --- a/lib/utils/platform_infos.dart +++ b/lib/utils/platform_infos.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:universal_html/html.dart' as html; import '../config/app_config.dart'; abstract class PlatformInfos { @@ -28,6 +29,12 @@ abstract class PlatformInfos { static bool get platformCanRecord => (isMobile || isMacOS); + static bool get isMacKeyboardPlatform => + isMacOS && + (kIsWeb && + html.window.navigator.platform != null && + html.window.navigator.platform!.contains('mac')); + static String get clientName => '${AppConfig.applicationName} ${isWeb ? 'web' : Platform.operatingSystem}${kReleaseMode ? '' : 'Debug'}'; From 96cde018d80c0eddb0a19e1747cddae79bde6fc9 Mon Sep 17 00:00:00 2001 From: sherlock Date: Wed, 11 Oct 2023 15:28:42 +0700 Subject: [PATCH 12/12] TW-731: remove copy/paste images in mobile for now because of load image in memory --- lib/pages/chat/chat.dart | 45 ++++----- lib/pages/chat/input_bar.dart | 92 +++++++------------ lib/pages/chat/send_file_dialog.dart | 17 +--- .../text_editting_controller_extension.dart | 10 +- .../mixins/paste_image_mixin.dart | 1 - lib/utils/clipboard.dart | 30 +++--- lib/utils/platform_infos.dart | 4 +- 7 files changed, 73 insertions(+), 126 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 5ebb7f0c9b..f35800111c 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -48,7 +48,6 @@ import 'package:fluffychat/widgets/mixins/popup_context_menu_action_mixin.dart'; import 'package:fluffychat/widgets/mixins/popup_menu_widget_mixin.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart' as flutter; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; @@ -780,28 +779,24 @@ class ChatController extends State void copySingleEventAction() async { if (selectedEvents.length == 1) { final event = selectedEvents.first; - final matrixFile = event.getMatrixFile() ?? - await event.downloadAndDecryptAttachment( - getThumbnail: true, - ); - if (event.messageType == MessageTypes.Image) { - if (matrixFile.filePath != null) { - await Clipboard.instance.copyImageAsStream( - File(matrixFile.filePath!), - mimeType: event.mimeType, - ); - if (!PlatformInfos.isAndroid) { - TwakeSnackBar.show(context, L10n.of(context)!.copyImageSuccess); - } - } else if (matrixFile.bytes != null) { - await Clipboard.instance.copyImageAsBytes( - matrixFile.bytes!, - mimeType: event.mimeType, - ); - if (!PlatformInfos.isAndroid) { - TwakeSnackBar.show(context, L10n.of(context)!.copyImageSuccess); + if (event.messageType == MessageTypes.Image && PlatformInfos.isWeb) { + final matrixFile = event.getMatrixFile() ?? + await event.downloadAndDecryptAttachment( + getThumbnail: true, + ); + try { + if (matrixFile.filePath != null) { + await Clipboard.instance.copyImageAsStream( + File(matrixFile.filePath!), + mimeType: event.mimeType, + ); + } else if (matrixFile.bytes != null) { + await Clipboard.instance.copyImageAsBytes( + matrixFile.bytes!, + mimeType: event.mimeType, + ); } - } else { + } catch (e) { TwakeSnackBar.show(context, L10n.of(context)!.copyImageFailed); Logs().e( 'copySingleEventAction(): failed to copy file ${matrixFile.name}', @@ -814,11 +809,7 @@ class ChatController extends State } void copyEventsAction() { - flutter.Clipboard.setData( - flutter.ClipboardData( - text: _getSelectedEventString(), - ), - ); + Clipboard.instance.copyText(_getSelectedEventString()); showEmojiPickerNotifier.value = false; setState(() { diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index 166877a8c1..aee2996bc6 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -1,4 +1,3 @@ -import 'package:fluffychat/pages/chat/send_file_dialog.dart'; import 'package:fluffychat/presentation/enum/chat/popup_menu_item_web_enum.dart'; import 'package:fluffychat/presentation/extensions/text_editting_controller_extension.dart'; import 'package:fluffychat/presentation/mixins/paste_image_mixin.dart'; @@ -426,65 +425,40 @@ class InputBar extends StatelessWidget with PasteImageMixin { // it sets the types for the callback incorrectly onChanged!(text); }, - textCapitalization: TextCapitalization.sentences, - contentInsertionConfiguration: ContentInsertionConfiguration( - onContentInserted: (keyboardInsertContent) async { - if (room == null || !keyboardInsertContent.hasData) { - return; - } - await showDialog( - context: context, - useRootNavigator: PlatformInfos.isWeb, - builder: (context) { - return SendFileDialog( - room: room!, - files: [ - MatrixImageFile( - name: keyboardInsertContent.uri, - bytes: keyboardInsertContent.data, - ) - ], + contextMenuBuilder: !PlatformInfos.isWeb + ? ( + BuildContext contextMenucontext, + EditableTextState editableTextState, + ) { + return AdaptiveTextSelectionToolbar.editable( + anchors: editableTextState.contextMenuAnchors, + clipboardStatus: ClipboardStatus.pasteable, + onPaste: !PlatformInfos.isWeb + ? () async { + if (room == null) { + // FIXME: need to handle the case when in draft chat + return; + } + editableTextState + .pasteText(SelectionChangedCause.toolbar); + } + : null, + onCopy: () { + editableTextState + .copySelection(SelectionChangedCause.toolbar); + }, + onCut: () { + editableTextState + .cutSelection(SelectionChangedCause.toolbar); + }, + onSelectAll: () { + editableTextState + .selectAll(SelectionChangedCause.toolbar); + }, ); - }, - ); - }, - ), - contextMenuBuilder: ( - BuildContext contextMenucontext, - EditableTextState editableTextState, - ) { - return AdaptiveTextSelectionToolbar.editable( - anchors: editableTextState.contextMenuAnchors, - clipboardStatus: ClipboardStatus.pasteable, - onPaste: !PlatformInfos.isWeb - ? () async { - if (room == null) { - // FIXME: need to handle the case when in draft chat - return; - } - await Clipboard.instance.initReader(); - if (await Clipboard.instance - .isReadableImageFormat()) { - await pasteImage(context, room!); - } else { - editableTextState - .pasteText(SelectionChangedCause.toolbar); - } - } - : null, - onCopy: () { - editableTextState - .copySelection(SelectionChangedCause.toolbar); - }, - onCut: () { - editableTextState - .cutSelection(SelectionChangedCause.toolbar); - }, - onSelectAll: () { - editableTextState.selectAll(SelectionChangedCause.toolbar); - }, - ); - }, + } + : null, + textCapitalization: TextCapitalization.sentences, ), suggestionsCallback: getSuggestions, itemBuilder: (context, suggestion) => SuggestionTile( diff --git a/lib/pages/chat/send_file_dialog.dart b/lib/pages/chat/send_file_dialog.dart index 33893a16d7..0541f60f1f 100644 --- a/lib/pages/chat/send_file_dialog.dart +++ b/lib/pages/chat/send_file_dialog.dart @@ -1,5 +1,6 @@ import 'package:fluffychat/presentation/extensions/send_file_web_extension.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -52,9 +53,7 @@ class SendFileDialogState extends State { shrinkImageMaxDimension: origImage ? null : 1600, ) .catchError((e) { - scaffoldMessenger.showSnackBar( - SnackBar(content: Text((e as Object).toLocalizedString(context))), - ); + TwakeSnackBar.show(context, (e as Object).toLocalizedString(context)); return null; }); } @@ -93,18 +92,6 @@ class SendFileDialogState extends State { fit: BoxFit.contain, ), ), - Row( - children: [ - Checkbox( - value: origImage, - onChanged: (v) => setState(() => origImage = v ?? false), - ), - InkWell( - onTap: () => setState(() => origImage = !origImage), - child: Text('${L10n.of(context)!.sendOriginal} ($sizeString)'), - ), - ], - ) ], ); } else { diff --git a/lib/presentation/extensions/text_editting_controller_extension.dart b/lib/presentation/extensions/text_editting_controller_extension.dart index 2d0c47aa54..c01d7290b7 100644 --- a/lib/presentation/extensions/text_editting_controller_extension.dart +++ b/lib/presentation/extensions/text_editting_controller_extension.dart @@ -1,15 +1,16 @@ import 'package:fluffychat/utils/clipboard.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart' as flutter; extension TextEdittingControllerExtension on TextEditingController { Future pasteText() async { final start = selection.start; final end = selection.end; + Clipboard.instance.initReader(); final pastedText = await Clipboard.instance.pasteText(); if (pastedText != null) { if (start == -1 || end == -1) { text = pastedText + text; + selection = TextSelection.collapsed(offset: text.length); return; } if (start == end) { @@ -19,6 +20,7 @@ extension TextEdittingControllerExtension on TextEditingController { } else { text = text.replaceRange(start, end, pastedText); } + selection = TextSelection.collapsed(offset: end + pastedText.length); } } @@ -26,11 +28,7 @@ extension TextEdittingControllerExtension on TextEditingController { final start = selection.start; final end = selection.end; if (start < end) { - await flutter.Clipboard.setData( - flutter.ClipboardData( - text: text.substring(start, end), - ), - ); + await Clipboard.instance.copyText(text.substring(start, end)); } } diff --git a/lib/presentation/mixins/paste_image_mixin.dart b/lib/presentation/mixins/paste_image_mixin.dart index e2271c79ba..99ad7f1056 100644 --- a/lib/presentation/mixins/paste_image_mixin.dart +++ b/lib/presentation/mixins/paste_image_mixin.dart @@ -11,7 +11,6 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; mixin PasteImageMixin { Future pasteImage(BuildContext context, Room room) async { - await Clipboard.instance.initReader(); if (!(await Clipboard.instance.isReadableImageFormat())) { TwakeSnackBar.show(context, L10n.of(context)!.fileFormatNotSupported); Logs().e('PasteImageMixin::pasteImage(): not readable image format'); diff --git a/lib/utils/clipboard.dart b/lib/utils/clipboard.dart index 8e104f31c2..95f9859aff 100644 --- a/lib/utils/clipboard.dart +++ b/lib/utils/clipboard.dart @@ -24,6 +24,12 @@ class Clipboard { Formats.svg, ]; + Future copyText(String text) async { + final item = DataWriterItem(); + item.add(Formats.plainText(text)); + await ClipboardWriter.instance.write([item]); + } + Future copyImageAsStream(File image, {String? mimeType}) async { final item = DataWriterItem(suggestedName: image.path); final imageStream = image.openRead(); @@ -31,21 +37,13 @@ class Clipboard { await imageStream.forEach((data) { item.add(getFormatFrom(mime)(Uint8List.fromList(data))); }); - try { - await ClipboardWriter.instance.write([item]); - } catch (e) { - Logs().e('Clipboard::copyImageAsStream(): $e'); - } + await ClipboardWriter.instance.write([item]); } Future copyImageAsBytes(Uint8List data, {String? mimeType}) async { - try { - final item = DataWriterItem(); - item.add(getFormatFrom(mimeType)(data)); - await ClipboardWriter.instance.write([item]); - } catch (e) { - Logs().e('Clipboard::copyImageAsBytes(): $e'); - } + final item = DataWriterItem(); + item.add(getFormatFrom(mimeType)(data)); + await ClipboardWriter.instance.write([item]); } Future initReader() async { @@ -53,7 +51,7 @@ class Clipboard { } Future pasteImageUsingStream() async { - _reader ??= await ClipboardReader.readClipboard(); + _reader = await ClipboardReader.readClipboard(); ClipboardImageInfo? imageInfo; final readableFormats = _reader!.getFormats(allImageFormatsSupported); @@ -90,7 +88,7 @@ class Clipboard { } Future? pasteImageUsingBytes() async { - _reader ??= await ClipboardReader.readClipboard(); + _reader = await ClipboardReader.readClipboard(); final readableFormats = _reader!.getFormats(allImageFormatsSupported); if (readableFormats.isEmpty != false && readableFormats.first is! SimpleFileFormat) { @@ -121,7 +119,7 @@ class Clipboard { } Future isReadableImageFormat() async { - _reader ??= await ClipboardReader.readClipboard(); + _reader = await ClipboardReader.readClipboard(); return _reader!.canProvide(Formats.png) || _reader!.canProvide(Formats.jpeg) || _reader!.canProvide(Formats.heic) || @@ -130,7 +128,7 @@ class Clipboard { } Future pasteText() async { - _reader ??= await ClipboardReader.readClipboard(); + _reader = await ClipboardReader.readClipboard(); String? copied; final readersFormat = _reader!.getFormats(Formats.standardFormats); diff --git a/lib/utils/platform_infos.dart b/lib/utils/platform_infos.dart index 8c8cd1a97c..a86bc94aba 100644 --- a/lib/utils/platform_infos.dart +++ b/lib/utils/platform_infos.dart @@ -30,10 +30,10 @@ abstract class PlatformInfos { static bool get platformCanRecord => (isMobile || isMacOS); static bool get isMacKeyboardPlatform => - isMacOS && + isMacOS || (kIsWeb && html.window.navigator.platform != null && - html.window.navigator.platform!.contains('mac')); + html.window.navigator.platform!.toLowerCase().contains('mac')); static String get clientName => '${AppConfig.applicationName} ${isWeb ? 'web' : Platform.operatingSystem}${kReleaseMode ? '' : 'Debug'}';