diff --git a/CHANGELOG.md b/CHANGELOG.md index 51a6183..2b9e6a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ - Support for groups - Edit & Delete messages - Switched to FFmpeg for improved video compression +- Create images using volume buttons - Video max. length increased to 60 seconds +- New and improved emoji picker - Removing audio after recording is possible - Edited image is now embedded into the video - New context menu and other UI enhancements diff --git a/lib/app.dart b/lib/app.dart index 0025085..5b430a7 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -118,6 +118,8 @@ class _AppState extends State with WidgetsBindingObserver { colorScheme: ColorScheme.fromSeed( brightness: Brightness.dark, seedColor: const Color(0xFF57CC99), + surface: const Color.fromARGB(255, 20, 18, 23), + surfaceContainer: const Color.fromARGB(255, 33, 30, 39), ), inputDecorationTheme: const InputDecorationTheme( border: OutlineInputBorder(), diff --git a/lib/src/model/json/userdata.dart b/lib/src/model/json/userdata.dart index 2a921f3..604c273 100644 --- a/lib/src/model/json/userdata.dart +++ b/lib/src/model/json/userdata.dart @@ -64,8 +64,6 @@ class UserData { @JsonKey(defaultValue: false) bool storeMediaFilesInGallery = false; - List? lastUsedEditorEmojis; - String? lastPlanBallance; String? additionalUserInvites; diff --git a/lib/src/model/json/userdata.g.dart b/lib/src/model/json/userdata.g.dart index 17cf5cf..6de5cbd 100644 --- a/lib/src/model/json/userdata.g.dart +++ b/lib/src/model/json/userdata.g.dart @@ -40,9 +40,6 @@ UserData _$UserDataFromJson(Map json) => UserData( ) ..storeMediaFilesInGallery = json['storeMediaFilesInGallery'] as bool? ?? false - ..lastUsedEditorEmojis = (json['lastUsedEditorEmojis'] as List?) - ?.map((e) => e as String) - .toList() ..lastPlanBallance = json['lastPlanBallance'] as String? ..additionalUserInvites = json['additionalUserInvites'] as String? ..tutorialDisplayed = (json['tutorialDisplayed'] as List?) @@ -93,7 +90,6 @@ Map _$UserDataToJson(UserData instance) => { 'preSelectedEmojies': instance.preSelectedEmojies, 'autoDownloadOptions': instance.autoDownloadOptions, 'storeMediaFilesInGallery': instance.storeMediaFilesInGallery, - 'lastUsedEditorEmojis': instance.lastUsedEditorEmojis, 'lastPlanBallance': instance.lastPlanBallance, 'additionalUserInvites': instance.additionalUserInvites, 'tutorialDisplayed': instance.tutorialDisplayed, diff --git a/lib/src/views/camera/image_editor/layers/text_layer.dart b/lib/src/views/camera/image_editor/layers/text_layer.dart index 5cff596..72dc428 100755 --- a/lib/src/views/camera/image_editor/layers/text_layer.dart +++ b/lib/src/views/camera/image_editor/layers/text_layer.dart @@ -24,6 +24,7 @@ class TextLayer extends StatefulWidget { class _TextViewState extends State { double initialRotation = 0; bool deleteLayer = false; + double localBottom = 0; bool isDeleted = false; bool elementIsScaled = false; final GlobalKey _widgetKey = GlobalKey(); // Create a GlobalKey @@ -35,9 +36,19 @@ class _TextViewState extends State { textController.text = widget.layerData.text; - if (widget.layerData.offset.dy == 0) { - // Set the initial offset to the center of the screen - WidgetsBinding.instance.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) { + final mq = MediaQuery.of(context); + final globalDesiredBottom = mq.viewInsets.bottom + mq.viewPadding.bottom; + final parentBox = context.findRenderObject() as RenderBox?; + if (parentBox != null) { + final parentTopGlobal = parentBox.localToGlobal(Offset.zero).dy; + final screenHeight = mq.size.height; + localBottom = (screenHeight - globalDesiredBottom) - + parentTopGlobal - + (parentBox.size.height); + } + + if (widget.layerData.offset.dy == 0) { setState(() { widget.layerData.offset = Offset( 0, @@ -47,17 +58,20 @@ class _TextViewState extends State { ); textController.text = widget.layerData.text; }); - }); - } + } + }); } @override Widget build(BuildContext context) { if (widget.layerData.isDeleted) return Container(); + final bottom = MediaQuery.of(context).viewInsets.bottom + + MediaQuery.of(context).viewPadding.bottom; + if (widget.layerData.isEditing) { return Positioned( - bottom: MediaQuery.of(context).viewInsets.bottom - 100, + bottom: bottom - localBottom, left: 0, right: 0, child: Container( diff --git a/lib/src/views/camera/image_editor/modules/all_emojis.dart b/lib/src/views/camera/image_editor/modules/all_emojis.dart index 423342a..3ab1180 100755 --- a/lib/src/views/camera/image_editor/modules/all_emojis.dart +++ b/lib/src/views/camera/image_editor/modules/all_emojis.dart @@ -1,107 +1,83 @@ -import 'dart:async'; - +import 'dart:io'; +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:flutter/material.dart'; -import 'package:twonly/globals.dart'; -import 'package:twonly/src/utils/storage.dart'; -import 'package:twonly/src/views/camera/image_editor/data/data.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; -class Emojis extends StatefulWidget { - const Emojis({super.key}); - - @override - State createState() => _EmojisState(); -} - -class _EmojisState extends State { - List lastUsed = emojis; - - @override - void initState() { - super.initState(); - unawaited(initAsync()); - } - - Future initAsync() async { - setState(() { - lastUsed = gUser.lastUsedEditorEmojis ?? []; - lastUsed.addAll(emojis); - }); - } - - Future selectEmojis(String emoji) async { - await updateUserdata((user) { - if (user.lastUsedEditorEmojis == null) { - user.lastUsedEditorEmojis = [emoji]; - } else { - if (user.lastUsedEditorEmojis!.contains(emoji)) { - user.lastUsedEditorEmojis!.remove(emoji); - } - user.lastUsedEditorEmojis!.insert(0, emoji); - if (user.lastUsedEditorEmojis!.length > 12) { - user.lastUsedEditorEmojis = user.lastUsedEditorEmojis!.sublist(0, 12); - } - user.lastUsedEditorEmojis!.toSet().toList(); - } - return user; - }); - if (!mounted) return; - Navigator.pop( - context, - EmojiLayerData( - text: emoji, - ), - ); - } +class EmojiPickerBottom extends StatelessWidget { + const EmojiPickerBottom({super.key}); @override Widget build(BuildContext context) { return SingleChildScrollView( child: Container( padding: EdgeInsets.zero, - height: 400, - decoration: const BoxDecoration( - borderRadius: BorderRadius.only( + height: 450, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( topLeft: Radius.circular(32), topRight: Radius.circular(32), ), - color: Colors.black, + color: context.color.surfaceContainer, boxShadow: [ BoxShadow( blurRadius: 10.9, - color: Color.fromRGBO(0, 0, 0, 0.1), + color: context.color.surfaceContainer.withAlpha(25), ), ], ), child: Column( children: [ - const SizedBox(height: 16), Container( - height: 315, - padding: EdgeInsets.zero, - child: GridView( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 60, - ), - children: lastUsed.map((String emoji) { - return GridTile( - child: GestureDetector( - onTap: () async { - await selectEmojis(emoji); - }, - child: Container( - padding: EdgeInsets.zero, - alignment: Alignment.center, - child: Text( - emoji, - style: const TextStyle(fontSize: 35), - ), - ), + margin: const EdgeInsets.all(30), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32), + color: Colors.grey, + ), + height: 3, + width: 60, + ), + Expanded( + child: EmojiPicker( + onEmojiSelected: (category, emoji) { + Navigator.pop( + context, + EmojiLayerData( + text: emoji.emoji, ), ); - }).toList(), + }, + // textEditingController: _textFieldController, + config: Config( + height: 400, + locale: Localizations.localeOf(context), + viewOrderConfig: const ViewOrderConfig( + top: EmojiPickerItem.searchBar, + // middle: EmojiPickerItem.emojiView, + bottom: EmojiPickerItem.categoryBar, + ), + emojiTextStyle: + TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)), + emojiViewConfig: EmojiViewConfig( + backgroundColor: context.color.surfaceContainer, + ), + searchViewConfig: SearchViewConfig( + backgroundColor: context.color.surfaceContainer, + buttonIconColor: Colors.white, + ), + categoryViewConfig: CategoryViewConfig( + backgroundColor: context.color.surfaceContainer, + dividerColor: Colors.white, + indicatorColor: context.color.primary, + iconColorSelected: context.color.primary, + iconColor: context.color.secondary, + ), + bottomActionBarConfig: BottomActionBarConfig( + backgroundColor: context.color.surfaceContainer, + buttonColor: context.color.surfaceContainer, + buttonIconColor: context.color.secondary, + ), + ), ), ), ], diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index d1b0112..bfa5c3b 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -167,7 +167,7 @@ class _ShareImageEditorView extends State { context: context, backgroundColor: Colors.black, builder: (BuildContext context) { - return const Emojis(); + return const EmojiPickerBottom(); }, ) as Layer?; if (layer == null) return; diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index da3bc96..5b48640 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -11,11 +11,10 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart'; -import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/camera/camera_send_to_view.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_date_chip.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_group_action.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/message_input.dart'; import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/flame.dart'; @@ -70,10 +69,8 @@ class ChatMessagesView extends StatefulWidget { } class _ChatMessagesViewState extends State { - TextEditingController newMessageController = TextEditingController(); HashSet alreadyReportedOpened = HashSet(); late Group group; - String currentInputText = ''; late StreamSubscription userSub; late StreamSubscription> messageSub; StreamSubscription>? groupActionsSub; @@ -267,21 +264,6 @@ class _ChatMessagesViewState extends State { setState(() {}); } - Future _sendMessage() async { - if (newMessageController.text == '') return; - - await insertAndSendTextMessage( - group.groupId, - newMessageController.text, - quotesMessage?.messageId, - ); - - newMessageController.clear(); - currentInputText = ''; - quotesMessage = null; - setState(() {}); - } - Future scrollToMessage(String messageId) async { final index = messages.indexWhere( (x) => x.isMessage && x.message!.messageId == messageId, @@ -464,100 +446,15 @@ class _ChatMessagesViewState extends State { ), ), if (!group.leftGroup) - Padding( - padding: const EdgeInsets.only( - bottom: 30, - left: 20, - right: 20, - top: 10, - ), - child: Row( - children: [ - Expanded( - child: Container( - color: Colors.grey, - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 10, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: Theme.of(context).colorScheme.primary, - width: 2, - ), - ), - child: Row( - children: [ - const FaIcon(FontAwesomeIcons.faceSmile), - Expanded( - child: TextField( - controller: newMessageController, - focusNode: textFieldFocus, - keyboardType: TextInputType.multiline, - maxLines: 4, - minLines: 1, - onChanged: (value) { - currentInputText = value; - setState(() {}); - }, - onSubmitted: (_) { - _sendMessage(); - }, - decoration: InputDecoration( - hintText: context.lang.chatListDetailInput, - // contentPadding: const EdgeInsets.symmetric( - // horizontal: 20, - // vertical: 10, - // ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(20), - borderSide: BorderSide( - color: Theme.of(context) - .colorScheme - .primary, - width: 2, - ), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(20), - borderSide: const BorderSide( - color: Colors.grey, - width: 2, - ), - ), - ), - ), - ), - ], - ), - ), - ), - if (currentInputText != '') - IconButton( - padding: const EdgeInsets.all(15), - icon: const FaIcon( - FontAwesomeIcons.solidPaperPlane, - ), - onPressed: _sendMessage, - ) - else - IconButton( - icon: const FaIcon(FontAwesomeIcons.camera), - padding: const EdgeInsets.all(15), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return CameraSendToView(widget.group); - }, - ), - ); - }, - ), - ], - ), + MessageInput( + group: group, + quotesMessage: quotesMessage, + textFieldFocus: textFieldFocus, + onMessageSend: () { + setState(() { + quotesMessage = null; + }); + }, ), ], ), diff --git a/lib/src/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/views/chats/chat_messages_components/message_context_menu.dart index 3a95cc1..88903f8 100644 --- a/lib/src/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/views/chats/chat_messages_components/message_context_menu.dart @@ -45,7 +45,7 @@ class MessageContextMenu extends StatelessWidget { context: context, backgroundColor: Colors.black, builder: (BuildContext context) { - return const Emojis(); + return const EmojiPickerBottom(); }, ) as EmojiLayerData?; if (layer == null) return; diff --git a/lib/src/views/chats/chat_messages_components/message_input.dart b/lib/src/views/chats/chat_messages_components/message_input.dart new file mode 100644 index 0000000..d989a62 --- /dev/null +++ b/lib/src/views/chats/chat_messages_components/message_input.dart @@ -0,0 +1,216 @@ +import 'dart:io'; +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/camera/camera_send_to_view.dart'; + +class MessageInput extends StatefulWidget { + const MessageInput({ + required this.group, + required this.quotesMessage, + required this.textFieldFocus, + required this.onMessageSend, + super.key, + }); + + final Group group; + final FocusNode textFieldFocus; + final Message? quotesMessage; + final VoidCallback onMessageSend; + + @override + State createState() => _MessageInputState(); +} + +class _MessageInputState extends State { + late final TextEditingController _textFieldController; + final bool isApple = Platform.isIOS; + bool _emojiShowing = false; + + Future _sendMessage() async { + if (_textFieldController.text == '') return; + + await insertAndSendTextMessage( + widget.group.groupId, + _textFieldController.text, + widget.quotesMessage?.messageId, + ); + + _textFieldController.clear(); + _emojiShowing = false; + widget.onMessageSend(); + setState(() {}); + } + + @override + void initState() { + _textFieldController = TextEditingController(); + widget.textFieldFocus.addListener(_handleTextFocusChange); + super.initState(); + } + + @override + void dispose() { + widget.textFieldFocus.removeListener(_handleTextFocusChange); + widget.textFieldFocus.dispose(); + super.dispose(); + } + + void _handleTextFocusChange() { + if (widget.textFieldFocus.hasFocus) { + setState(() { + _emojiShowing = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.only( + bottom: 10, + left: 10, + top: 10, + ), + child: Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 3, + ), + decoration: BoxDecoration( + color: context.color.surfaceContainer, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + GestureDetector( + onTap: () { + setState(() { + _emojiShowing = !_emojiShowing; + if (_emojiShowing) { + widget.textFieldFocus.unfocus(); + } else { + widget.textFieldFocus.requestFocus(); + } + }); + }, + child: Padding( + padding: const EdgeInsets.only( + top: 8, + bottom: 8, + left: 12, + right: 8, + ), + child: FaIcon( + size: 20, + _emojiShowing + ? FontAwesomeIcons.keyboard + : FontAwesomeIcons.faceSmile, + ), + ), + ), + Expanded( + child: TextField( + controller: _textFieldController, + focusNode: widget.textFieldFocus, + keyboardType: TextInputType.multiline, + maxLines: 4, + minLines: 1, + onChanged: (value) { + setState(() {}); + }, + onSubmitted: (_) { + _sendMessage(); + }, + style: const TextStyle(fontSize: 17), + decoration: InputDecoration( + hintText: context.lang.chatListDetailInput, + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + ), + ), + ), + ], + ), + ), + ), + if (_textFieldController.text != '') + IconButton( + padding: const EdgeInsets.all(15), + icon: FaIcon( + color: context.color.primary, + FontAwesomeIcons.solidPaperPlane, + ), + onPressed: _sendMessage, + ) + else + IconButton( + icon: const FaIcon(FontAwesomeIcons.camera), + padding: const EdgeInsets.all(15), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return CameraSendToView(widget.group); + }, + ), + ); + }, + ), + ], + ), + ), + Offstage( + offstage: !_emojiShowing, + child: EmojiPicker( + textEditingController: _textFieldController, + onEmojiSelected: (category, emoji) { + setState(() {}); + }, + onBackspacePressed: () { + setState(() {}); + }, + config: Config( + height: 300, + locale: Localizations.localeOf(context), + viewOrderConfig: const ViewOrderConfig( + top: EmojiPickerItem.searchBar, + // middle: EmojiPickerItem.emojiView, + bottom: EmojiPickerItem.categoryBar, + ), + emojiTextStyle: + TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)), + emojiViewConfig: EmojiViewConfig( + backgroundColor: context.color.surfaceContainer, + ), + searchViewConfig: SearchViewConfig( + backgroundColor: context.color.surfaceContainer, + buttonIconColor: Colors.white, + ), + categoryViewConfig: CategoryViewConfig( + backgroundColor: context.color.surfaceContainer, + dividerColor: Colors.white, + indicatorColor: context.color.primary, + iconColorSelected: context.color.primary, + iconColor: context.color.secondary, + ), + bottomActionBarConfig: BottomActionBarConfig( + backgroundColor: context.color.surfaceContainer, + buttonColor: context.color.surfaceContainer, + buttonIconColor: context.color.secondary, + ), + ), + ), + ), + ], + ); + } +}