feat: implement an emoji keyboard

- add button to show emoji keyboard
- change database directory for debug builds

Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
This commit is contained in:
TheOneWithTheBraid 2022-02-14 13:49:46 +01:00
parent 0eba2ae859
commit 30ce5c7f57
9 changed files with 117 additions and 11 deletions

View File

@ -2711,7 +2711,6 @@
"markAsRead": "Mark as read", "markAsRead": "Mark as read",
"reportUser": "Report user", "reportUser": "Report user",
"dismiss": "Dismiss", "dismiss": "Dismiss",
"markAsRead": "Mark as read",
"matrixWidgets": "Matrix Widgets", "matrixWidgets": "Matrix Widgets",
"integrationsNotImplemented": "Editing widgets and integrations is not possible yet.", "integrationsNotImplemented": "Editing widgets and integrations is not possible yet.",
"editIntegrations": "Edit widgets and integrations", "editIntegrations": "Edit widgets and integrations",
@ -2725,5 +2724,6 @@
}, },
"pinMessage": "Pin to room", "pinMessage": "Pin to room",
"pinnedEventsError": "Error loading pinned messages", "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"
} }

View File

@ -122,7 +122,7 @@ To run code after the widget was created first we use the WidgetBindings in the
```dart ```dart
@override @override
void initState() { void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance!.addPostFrameCallback((_) {
// Do something when build is finished // Do something when build is finished
}); });
super.initState(); super.initState();

View File

@ -8,6 +8,7 @@ import 'package:flutter/services.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:desktop_drop/desktop_drop.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:file_picker_cross/file_picker_cross.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
@ -125,6 +126,8 @@ class ChatController extends State<Chat> {
bool showEmojiPicker = false; bool showEmojiPicker = false;
EmojiPickerType emojiPickerType = EmojiPickerType.keyboard;
void startCallAction() async { void startCallAction() async {
final url = final url =
'${AppConfig.jitsiInstance}${Uri.encodeComponent(Matrix.of(context).client.generateUniqueTransactionId())}'; '${AppConfig.jitsiInstance}${Uri.encodeComponent(Matrix.of(context).client.generateUniqueTransactionId())}';
@ -178,6 +181,8 @@ class ChatController extends State<Chat> {
@override @override
void initState() { void initState() {
scrollController.addListener(_updateScrollController); scrollController.addListener(_updateScrollController);
inputFocus.addListener(_inputFocusListener);
super.initState(); super.initState();
} }
@ -239,6 +244,7 @@ class ChatController extends State<Chat> {
void dispose() { void dispose() {
timeline?.cancelSubscriptions(); timeline?.cancelSubscriptions();
timeline = null; timeline = null;
inputFocus.removeListener(_inputFocusListener);
super.dispose(); super.dispose();
} }
@ -426,6 +432,20 @@ class ChatController extends State<Chat> {
}); });
} }
void emojiPickerAction() {
emojiPickerType = EmojiPickerType.keyboard;
setState(() => showEmojiPicker = !showEmojiPicker);
_inputFocusListener();
}
void _inputFocusListener() {
if (showEmojiPicker) {
inputFocus.unfocus();
} else {
inputFocus.requestFocus();
}
}
void sendLocationAction() async { void sendLocationAction() async {
await showDialog( await showDialog(
context: context, context: context,
@ -668,7 +688,18 @@ class ChatController extends State<Chat> {
void scrollDown() => scrollController.jumpTo(0); 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); setState(() => showEmojiPicker = false);
if (emoji == null) return; if (emoji == null) return;
// make sure we don't send the same emoji twice // make sure we don't send the same emoji twice
@ -677,12 +708,41 @@ class ChatController extends State<Chat> {
return sendEmojiAction(emoji.emoji); 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<Event> _allReactionEvents; late Iterable<Event> _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<Event> allReactionEvents) async { void pickEmojiReactionAction(Iterable<Event> allReactionEvents) async {
_allReactionEvents = allReactionEvents; _allReactionEvents = allReactionEvents;
emojiPickerType = EmojiPickerType.reaction;
setState(() => showEmojiPicker = true); setState(() => showEmojiPicker = true);
} }
@ -902,3 +962,5 @@ class ChatController extends State<Chat> {
@override @override
Widget build(BuildContext context) => ChatView(this); Widget build(BuildContext context) => ChatView(this);
} }
enum EmojiPickerType { reaction, keyboard }

View File

@ -18,7 +18,7 @@ class ChatEmojiPicker extends StatelessWidget {
child: controller.showEmojiPicker child: controller.showEmojiPicker
? EmojiPicker( ? EmojiPicker(
onEmojiSelected: controller.onEmojiSelected, onEmojiSelected: controller.onEmojiSelected,
onBackspacePressed: controller.cancelEmojiPicker, onBackspacePressed: controller.emojiPickerBackspace,
) )
: null, : null,
); );

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:animations/animations.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.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/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import 'chat.dart'; import 'chat.dart';
import 'encryption_button.dart';
import 'input_bar.dart'; import 'input_bar.dart';
class ChatInputRow extends StatelessWidget { class ChatInputRow extends StatelessWidget {
final ChatController controller; final ChatController controller;
const ChatInputRow(this.controller, {Key? key}) : super(key: key); const ChatInputRow(this.controller, {Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (controller.showEmojiPicker) return Container(); if (controller.showEmojiPicker &&
controller.emojiPickerType == EmojiPickerType.reaction) {
return Container();
}
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -176,7 +180,31 @@ class ChatInputRow extends StatelessWidget {
Container( Container(
height: 56, height: 56,
alignment: Alignment.center, alignment: Alignment.center,
child: EncryptionButton(controller.room!), child: IconButton(
tooltip: L10n.of(context)!.emojis,
icon: PageTransitionSwitcher(
transitionBuilder: (
Widget child,
Animation<double> primaryAnimation,
Animation<double> 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 && if (controller.matrix!.isMultiAccount &&
controller.matrix!.hasComplexBundles && controller.matrix!.hasComplexBundles &&

View File

@ -12,6 +12,7 @@ import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/chat_app_bar_title.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/pinned_events.dart';
import 'package:fluffychat/pages/chat/reactions_picker.dart'; import 'package:fluffychat/pages/chat/reactions_picker.dart';
import 'package:fluffychat/pages/chat/reply_display.dart'; import 'package:fluffychat/pages/chat/reply_display.dart';
@ -119,6 +120,7 @@ class ChatView extends StatelessWidget {
icon: const Icon(Icons.widgets), icon: const Icon(Icons.widgets),
tooltip: L10n.of(context)!.matrixWidgets, tooltip: L10n.of(context)!.matrixWidgets,
), ),
EncryptionButton(controller.room!),
ChatSettingsPopupMenu(controller.room!, !controller.room!.isDirectChat), ChatSettingsPopupMenu(controller.room!, !controller.room!.isDirectChat),
]; ];
} }

View File

@ -77,7 +77,8 @@ class ReactionsPicker extends StatelessWidget {
), ),
child: const Icon(Icons.add_outlined), child: const Icon(Icons.add_outlined),
), ),
onTap: () => controller.pickEmojiAction(allReactionEvents)) onTap: () =>
controller.pickEmojiReactionAction(allReactionEvents))
]); ]);
}), }),
), ),

View File

@ -94,6 +94,11 @@ class FlutterFluffyBoxDatabase extends FluffyBoxDatabase {
directory = Directory.current; 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; path = directory.path;
} }
return path; return path;

View File

@ -9,6 +9,9 @@ list(APPEND FLUTTER_PLUGIN_LIST
url_launcher_windows url_launcher_windows
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST}) foreach(plugin ${FLUTTER_PLUGIN_LIST})
@ -17,3 +20,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST})
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>) list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin) 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)