diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 3d36c2e8..02df5f19 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2711,7 +2711,6 @@ "markAsRead": "Mark as read", "reportUser": "Report user", "dismiss": "Dismiss", - "markAsRead": "Mark as read", "matrixWidgets": "Matrix Widgets", "integrationsNotImplemented": "Editing widgets and integrations is not possible yet.", "editIntegrations": "Edit widgets and integrations", @@ -2725,5 +2724,6 @@ }, "pinMessage": "Pin to room", "pinnedEventsError": "Error loading pinned messages", - "confirmEventUnpin": "Are you sure to permanently unpin the event?" + "confirmEventUnpin": "Are you sure to permanently unpin the event?", + "emojis": "Emojis" } diff --git a/docs/code_style.md b/docs/code_style.md index 88d7eb38..712830d5 100644 --- a/docs/code_style.md +++ b/docs/code_style.md @@ -122,7 +122,7 @@ To run code after the widget was created first we use the WidgetBindings in the ```dart @override void initState() { - WidgetsBinding.instance.addPostFrameCallback((_) { + WidgetsBinding.instance!.addPostFrameCallback((_) { // Do something when build is finished }); super.initState(); diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index d10267db..81f18d93 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -8,6 +8,7 @@ import 'package:flutter/services.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:desktop_drop/desktop_drop.dart'; +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:file_picker_cross/file_picker_cross.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; @@ -125,6 +126,8 @@ class ChatController extends State { bool showEmojiPicker = false; + EmojiPickerType emojiPickerType = EmojiPickerType.keyboard; + void startCallAction() async { final url = '${AppConfig.jitsiInstance}${Uri.encodeComponent(Matrix.of(context).client.generateUniqueTransactionId())}'; @@ -178,6 +181,8 @@ class ChatController extends State { @override void initState() { scrollController.addListener(_updateScrollController); + + inputFocus.addListener(_inputFocusListener); super.initState(); } @@ -239,6 +244,7 @@ class ChatController extends State { void dispose() { timeline?.cancelSubscriptions(); timeline = null; + inputFocus.removeListener(_inputFocusListener); super.dispose(); } @@ -426,6 +432,20 @@ class ChatController extends State { }); } + void emojiPickerAction() { + emojiPickerType = EmojiPickerType.keyboard; + setState(() => showEmojiPicker = !showEmojiPicker); + _inputFocusListener(); + } + + void _inputFocusListener() { + if (showEmojiPicker) { + inputFocus.unfocus(); + } else { + inputFocus.requestFocus(); + } + } + void sendLocationAction() async { await showDialog( context: context, @@ -668,7 +688,18 @@ class ChatController extends State { void scrollDown() => scrollController.jumpTo(0); - void onEmojiSelected(_, emoji) { + void onEmojiSelected(_, Emoji? emoji) { + switch (emojiPickerType) { + case EmojiPickerType.reaction: + senEmojiReaction(emoji); + break; + case EmojiPickerType.keyboard: + typeEmoji(emoji); + break; + } + } + + void senEmojiReaction(Emoji? emoji) { setState(() => showEmojiPicker = false); if (emoji == null) return; // make sure we don't send the same emoji twice @@ -677,12 +708,41 @@ class ChatController extends State { return sendEmojiAction(emoji.emoji); } + void typeEmoji(Emoji? emoji) { + if (emoji == null) return; + final text = sendController.text; + final selection = sendController.selection; + final newText = sendController.text.isEmpty + ? emoji.emoji + : text.replaceRange(selection.start, selection.end, emoji.emoji); + sendController.value = TextEditingValue( + text: newText, + selection: TextSelection.collapsed( + // don't forget an UTF-8 combined emoji might have a length > 1 + offset: selection.baseOffset + emoji.emoji.length, + ), + ); + } + late Iterable _allReactionEvents; - void cancelEmojiPicker() => setState(() => showEmojiPicker = false); + void emojiPickerBackspace() { + switch (emojiPickerType) { + case EmojiPickerType.reaction: + setState(() => showEmojiPicker = false); + break; + case EmojiPickerType.keyboard: + sendController + ..text = sendController.text.characters.skipLast(1).toString() + ..selection = TextSelection.fromPosition( + TextPosition(offset: sendController.text.length)); + break; + } + } - void pickEmojiAction(Iterable allReactionEvents) async { + void pickEmojiReactionAction(Iterable allReactionEvents) async { _allReactionEvents = allReactionEvents; + emojiPickerType = EmojiPickerType.reaction; setState(() => showEmojiPicker = true); } @@ -902,3 +962,5 @@ class ChatController extends State { @override Widget build(BuildContext context) => ChatView(this); } + +enum EmojiPickerType { reaction, keyboard } diff --git a/lib/pages/chat/chat_emoji_picker.dart b/lib/pages/chat/chat_emoji_picker.dart index 6d5ffd31..069c4fc1 100644 --- a/lib/pages/chat/chat_emoji_picker.dart +++ b/lib/pages/chat/chat_emoji_picker.dart @@ -18,7 +18,7 @@ class ChatEmojiPicker extends StatelessWidget { child: controller.showEmojiPicker ? EmojiPicker( onEmojiSelected: controller.onEmojiSelected, - onBackspacePressed: controller.cancelEmojiPicker, + onBackspacePressed: controller.emojiPickerBackspace, ) : null, ); diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 2671e45b..a66ace67 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:animations/animations.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; @@ -8,16 +9,19 @@ import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'chat.dart'; -import 'encryption_button.dart'; import 'input_bar.dart'; class ChatInputRow extends StatelessWidget { final ChatController controller; + const ChatInputRow(this.controller, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { - if (controller.showEmojiPicker) return Container(); + if (controller.showEmojiPicker && + controller.emojiPickerType == EmojiPickerType.reaction) { + return Container(); + } return Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -176,7 +180,31 @@ class ChatInputRow extends StatelessWidget { Container( height: 56, alignment: Alignment.center, - child: EncryptionButton(controller.room!), + child: IconButton( + tooltip: L10n.of(context)!.emojis, + icon: PageTransitionSwitcher( + transitionBuilder: ( + Widget child, + Animation primaryAnimation, + Animation secondaryAnimation, + ) { + return SharedAxisTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.scaled, + child: child, + fillColor: Colors.transparent, + ); + }, + child: Icon( + controller.showEmojiPicker + ? Icons.keyboard + : Icons.emoji_emotions_outlined, + key: ValueKey(controller.showEmojiPicker), + ), + ), + onPressed: controller.emojiPickerAction, + ), ), if (controller.matrix!.isMultiAccount && controller.matrix!.hasComplexBundles && diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 69bf03de..67e03610 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -12,6 +12,7 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/chat_app_bar_title.dart'; +import 'package:fluffychat/pages/chat/encryption_button.dart'; import 'package:fluffychat/pages/chat/pinned_events.dart'; import 'package:fluffychat/pages/chat/reactions_picker.dart'; import 'package:fluffychat/pages/chat/reply_display.dart'; @@ -119,6 +120,7 @@ class ChatView extends StatelessWidget { icon: const Icon(Icons.widgets), tooltip: L10n.of(context)!.matrixWidgets, ), + EncryptionButton(controller.room!), ChatSettingsPopupMenu(controller.room!, !controller.room!.isDirectChat), ]; } diff --git a/lib/pages/chat/reactions_picker.dart b/lib/pages/chat/reactions_picker.dart index b43edb36..ab70d548 100644 --- a/lib/pages/chat/reactions_picker.dart +++ b/lib/pages/chat/reactions_picker.dart @@ -77,7 +77,8 @@ class ReactionsPicker extends StatelessWidget { ), child: const Icon(Icons.add_outlined), ), - onTap: () => controller.pickEmojiAction(allReactionEvents)) + onTap: () => + controller.pickEmojiReactionAction(allReactionEvents)) ]); }), ), diff --git a/lib/utils/matrix_sdk_extensions.dart/fluffybox_database.dart b/lib/utils/matrix_sdk_extensions.dart/fluffybox_database.dart index 12a906a5..ef7bd00a 100644 --- a/lib/utils/matrix_sdk_extensions.dart/fluffybox_database.dart +++ b/lib/utils/matrix_sdk_extensions.dart/fluffybox_database.dart @@ -94,6 +94,11 @@ class FlutterFluffyBoxDatabase extends FluffyBoxDatabase { directory = Directory.current; } } + // do not destroy your stable FluffyChat in debug mode + if (kDebugMode) { + directory = Directory(directory.uri.resolve('debug').toFilePath()); + directory.create(recursive: true); + } path = directory.path; } return path; diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index acf95a15..f4b94444 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -9,6 +9,9 @@ list(APPEND FLUTTER_PLUGIN_LIST url_launcher_windows ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -17,3 +20,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin)