diff --git a/lib/src/components/media_view_sizing.dart b/lib/src/components/media_view_sizing.dart index ebb06f5..8abfa7f 100644 --- a/lib/src/components/media_view_sizing.dart +++ b/lib/src/components/media_view_sizing.dart @@ -1,30 +1,72 @@ import 'package:flutter/material.dart'; -class MediaViewSizing extends StatelessWidget { +class MediaViewSizing extends StatefulWidget { + const MediaViewSizing( + {super.key, + this.requiredHeight, + required this.child, + this.bottomNavigation}); + + final double? requiredHeight; + final Widget? bottomNavigation; final Widget child; - const MediaViewSizing(this.child, {super.key}); + @override + State createState() => _MediaViewSizingState(); +} +class _MediaViewSizingState extends State { @override Widget build(BuildContext context) { + bool needToDownSizeImage = false; + + if (widget.requiredHeight != null) { + // Get the screen size and safe area padding + final screenSize = MediaQuery.of(context).size; + final safeAreaPadding = MediaQuery.of(context).padding; + + // Calculate the available width and height + final availableWidth = screenSize.width; + final availableHeight = + screenSize.height - safeAreaPadding.top - safeAreaPadding.bottom; + + var aspectRatioWidth = availableWidth; + var aspectRatioHeight = (aspectRatioWidth * 16) / 9; + if (aspectRatioHeight < availableHeight) { + if ((screenSize.height - widget.requiredHeight!) < aspectRatioHeight) { + needToDownSizeImage = true; + } + } + } + + Widget imageChild = Align( + alignment: Alignment.topCenter, + child: SizedBox( + child: AspectRatio( + aspectRatio: 9 / 16, + child: ClipRRect( + borderRadius: BorderRadius.circular(22), + child: widget.child, + ), + ), + ), + ); + + Widget bottomNavigation = Container(); + + if (widget.bottomNavigation != null) { + if (needToDownSizeImage) { + imageChild = Expanded(child: imageChild); + bottomNavigation = widget.bottomNavigation!; + } else { + bottomNavigation = Expanded(child: widget.bottomNavigation!); + } + } + return SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded( - child: Align( - alignment: Alignment.topCenter, - child: AspectRatio( - aspectRatio: 9 / 16, - // padding: EdgeInsets.symmetric(vertical: 50, horizontal: 0), - child: ClipRRect( - borderRadius: BorderRadius.circular(22), - child: child, - ), - ), - ), - ), - ], + children: [imageChild, bottomNavigation], ), ); } diff --git a/lib/src/views/camera_to_share/camera_preview_view.dart b/lib/src/views/camera_to_share/camera_preview_view.dart index 3124452..19d5467 100644 --- a/lib/src/views/camera_to_share/camera_preview_view.dart +++ b/lib/src/views/camera_to_share/camera_preview_view.dart @@ -200,7 +200,7 @@ class _CameraPreviewViewState extends State { ); } return MediaViewSizing( - Stack( + child: Stack( children: [ ClipRRect( borderRadius: BorderRadius.circular(22), diff --git a/lib/src/views/camera_to_share/share_image_editor_view.dart b/lib/src/views/camera_to_share/share_image_editor_view.dart index 1e37316..248fdae 100644 --- a/lib/src/views/camera_to_share/share_image_editor_view.dart +++ b/lib/src/views/camera_to_share/share_image_editor_view.dart @@ -294,7 +294,7 @@ class _ShareImageEditorView extends State { setState(() {}); }, child: MediaViewSizing( - SizedBox( + child: SizedBox( height: currentImage.height / pixelRatio, width: currentImage.width / pixelRatio, child: Screenshot( diff --git a/lib/src/views/chats/chat_item_details_view.dart b/lib/src/views/chats/chat_item_details_view.dart index 1e5a8ba..44a4656 100644 --- a/lib/src/views/chats/chat_item_details_view.dart +++ b/lib/src/views/chats/chat_item_details_view.dart @@ -22,6 +22,27 @@ import 'package:twonly/src/views/chats/media_viewer_view.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/contact/contact_view.dart'; +InputDecoration inputTextMessageDeco(BuildContext context) { + return InputDecoration( + hintText: context.lang.chatListDetailInput, + contentPadding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(20), + borderSide: + BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(20.0), + borderSide: + BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(20.0), + borderSide: BorderSide(color: Colors.grey, width: 2.0), + ), + ); +} + class ChatListEntry extends StatelessWidget { const ChatListEntry( this.message, this.contact, this.lastMessageFromSameUser, this.reactions, @@ -61,6 +82,7 @@ class ChatListEntry extends StatelessWidget { if (content is TextMessageContent) { hasOneTextReaction = true; + if (!isEmoji(content.text)) continue; late Widget child; if (EmojiAnimation.animatedIcons.containsKey(content.text)) { child = SizedBox( @@ -90,6 +112,72 @@ class ChatListEntry extends StatelessWidget { ); } + Widget getTextResponseColumns(BuildContext context, bool right) { + List children = []; + for (final reaction in reactions) { + MessageContent? content = MessageContent.fromJson( + reaction.kind, jsonDecode(reaction.contentJson!)); + + if (content is TextMessageContent) { + if (content.text.length <= 1) continue; + if (isEmoji(content.text)) continue; + var entries = [ + FaIcon( + FontAwesomeIcons.reply, + size: 10, + ), + SizedBox(width: 5), + Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.5, + ), + child: Text( + content.text, + style: TextStyle(fontSize: 14), + textAlign: right ? TextAlign.left : TextAlign.right, + )), + ]; + if (!right) { + entries = entries.reversed.toList(); + } + + children.insert( + 0, + Container( + padding: EdgeInsets.only(top: 5, bottom: 0, right: 10, left: 10), + child: Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.8, + ), + padding: EdgeInsets.symmetric(vertical: 1, horizontal: 10), + decoration: BoxDecoration( + color: right + ? const Color.fromARGB(107, 124, 77, 255) + : const Color.fromARGB(83, 68, 137, 255), + borderRadius: BorderRadius.circular(12.0), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: entries, + ), + ), + ), + ); + } + } + + if (children.isEmpty) return Container(); + + return Column( + // mainAxisAlignment: message.messageOtherId == null + // ? MainAxisAlignment.start + // : MainAxisAlignment.end, + crossAxisAlignment: + right ? CrossAxisAlignment.start : CrossAxisAlignment.end, + children: children, + ); + } + @override Widget build(BuildContext context) { bool right = message.messageOtherId == null; @@ -194,11 +282,21 @@ class ChatListEntry extends StatelessWidget { padding: lastMessageFromSameUser ? EdgeInsets.only(top: 5, bottom: 0, right: 10, left: 10) : EdgeInsets.only(top: 5, bottom: 20, right: 10, left: 10), - child: Stack( - alignment: right ? Alignment.centerRight : Alignment.centerLeft, + child: Column( + mainAxisAlignment: + right ? MainAxisAlignment.end : MainAxisAlignment.start, + crossAxisAlignment: + right ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - child, - Positioned(bottom: 5, left: 5, right: 5, child: getReactionRow()), + Stack( + alignment: right ? Alignment.centerRight : Alignment.centerLeft, + children: [ + child, + Positioned( + bottom: 5, left: 5, right: 5, child: getReactionRow()), + ], + ), + getTextResponseColumns(context, !right) ], ), ), @@ -406,28 +504,7 @@ class _ChatItemDetailsViewState extends State { onSubmitted: (_) { _sendMessage(); }, - decoration: InputDecoration( - hintText: context.lang.chatListDetailInput, - contentPadding: - EdgeInsets.symmetric(horizontal: 20, vertical: 10), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(20), - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, - width: 2.0), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(20.0), - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, - width: 2.0), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(20.0), - borderSide: - BorderSide(color: Colors.grey, width: 2.0), - ), - ), + decoration: inputTextMessageDeco(context), ), ), SizedBox(width: 8), diff --git a/lib/src/views/chats/media_viewer_view.dart b/lib/src/views/chats/media_viewer_view.dart index cffb425..667f7f0 100644 --- a/lib/src/views/chats/media_viewer_view.dart +++ b/lib/src/views/chats/media_viewer_view.dart @@ -33,7 +33,6 @@ class _MediaViewerViewState extends State { Timer? progressTimer; bool showShortReactions = false; - int selectedShortReaction = -1; // current image related Uint8List? imageBytes; @@ -42,12 +41,14 @@ class _MediaViewerViewState extends State { double progress = 0; bool isRealTwonly = false; bool isDownloading = false; + bool showSendTextMessageInput = false; bool imageSaved = false; bool imageSaving = false; List allMediaFiles = []; late StreamSubscription> _subscription; + TextEditingController textMessageController = TextEditingController(); @override void initState() { @@ -116,6 +117,7 @@ class _MediaViewerViewState extends State { progress = 0; isDownloading = false; isRealTwonly = false; + showSendTextMessageInput = false; }); if (content.isRealTwonly) { @@ -206,6 +208,134 @@ class _MediaViewerViewState extends State { _subscription.cancel(); } + Future onPressedSaveToGallery() async { + if (allMediaFiles.first.messageOtherId == null) { + return; // should not be possible + } + setState(() { + imageSaving = true; + }); + encryptAndSendMessage( + null, + widget.contact.userId, + MessageJson( + kind: MessageKind.storedMediaFile, + messageId: allMediaFiles.first.messageId, + content: StoredMediaFileContent( + messageId: allMediaFiles.first.messageOtherId!, + ), + timestamp: DateTime.now(), + ), + pushKind: PushKind.storedMediaFile, + ); + final res = await saveImageToGallery(imageBytes!); + if (res == null) { + setState(() { + imageSaving = false; + imageSaved = true; + }); + } + } + + Widget bottomNavigation() { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (maxShowTime == 999999) + OutlinedButton( + style: OutlinedButton.styleFrom( + iconColor: imageSaved + ? Theme.of(context).colorScheme.outline + : Theme.of(context).colorScheme.primary, + foregroundColor: imageSaved + ? Theme.of(context).colorScheme.outline + : Theme.of(context).colorScheme.primary, + ), + onPressed: onPressedSaveToGallery, + child: Row( + children: [ + imageSaving + ? SizedBox( + width: 10, + height: 10, + child: CircularProgressIndicator(strokeWidth: 1)) + : imageSaved + ? Icon(Icons.check) + : FaIcon(FontAwesomeIcons.floppyDisk), + ], + ), + ), + SizedBox(width: 10), + IconButton( + icon: SizedBox( + width: 30, + height: 30, + child: GridView.count( + crossAxisCount: 2, + children: List.generate( + 4, + (index) { + return SizedBox( + width: 8, + height: 8, + child: Center( + child: EmojiAnimation( + emoji: + EmojiAnimation.animatedIcons.keys.toList()[index], + ), + ), + ); + }, + ), + ), + ), + onPressed: () async { + setState(() { + showShortReactions = !showShortReactions; + }); + }, + style: ButtonStyle( + padding: WidgetStateProperty.all( + EdgeInsets.symmetric(vertical: 10, horizontal: 20), + ), + ), + ), + SizedBox(width: 10), + IconButton.outlined( + icon: FaIcon(FontAwesomeIcons.message), + onPressed: () async { + setState(() { + showSendTextMessageInput = true; + showShortReactions = true; + }); + }, + style: ButtonStyle( + padding: WidgetStateProperty.all( + EdgeInsets.symmetric(vertical: 10, horizontal: 20), + ), + ), + ), + SizedBox(width: 10), + IconButton.outlined( + icon: FaIcon(FontAwesomeIcons.camera), + onPressed: () async { + await Navigator.push(context, MaterialPageRoute( + builder: (context) { + return CameraSendToView(widget.contact); + }, + )); + }, + style: ButtonStyle( + padding: WidgetStateProperty.all( + EdgeInsets.symmetric(vertical: 10, horizontal: 20), + ), + ), + ), + ], + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -216,10 +346,19 @@ class _MediaViewerViewState extends State { if (imageBytes != null && (canBeSeenUntil == null || progress >= 0)) GestureDetector( onTap: () { + if (showSendTextMessageInput) { + setState(() { + showShortReactions = false; + showSendTextMessageInput = false; + }); + return; + } nextMediaOrExit(); }, child: MediaViewSizing( - Image.memory( + bottomNavigation: bottomNavigation(), + requiredHeight: 50, + child: Image.memory( imageBytes!, fit: BoxFit.contain, frameBuilder: @@ -233,7 +372,7 @@ class _MediaViewerViewState extends State { height: 60, width: 60, child: - CircularProgressIndicator(strokeWidth: 6), + CircularProgressIndicator(strokeWidth: 2), ), ); }), @@ -303,206 +442,77 @@ class _MediaViewerViewState extends State { ], ), ), - AnimatedPositioned( - duration: Duration(milliseconds: 200), // Animation duration - bottom: showShortReactions ? 100 : 90, - left: showShortReactions ? 0 : 150, - right: showShortReactions ? 0 : 150, - curve: Curves.linearToEaseOut, - child: AnimatedOpacity( - opacity: showShortReactions ? 1.0 : 0.0, // Fade in/out - duration: Duration(milliseconds: 150), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.end, - children: List.generate( - 6, - (index) { - final emoji = - EmojiAnimation.animatedIcons.keys.toList()[index]; - return AnimatedSize( - duration: - Duration(milliseconds: 200), // Animation duration - curve: Curves.linearToEaseOut, - child: GestureDetector( - onTap: () { + if (showSendTextMessageInput) + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + color: context.color.surface, + padding: const EdgeInsets.only( + bottom: 10, left: 20, right: 20, top: 10), + child: Row( + children: [ + IconButton( + icon: FaIcon(FontAwesomeIcons.xmark), + onPressed: () { + setState(() { + showShortReactions = false; + showSendTextMessageInput = false; + }); + }, + ), + Expanded( + child: Container( + child: TextField( + autofocus: true, + controller: textMessageController, + onEditingComplete: () { + setState(() { + showSendTextMessageInput = false; + showShortReactions = false; + }); + }, + decoration: inputTextMessageDeco(context), + ), + ), + ), + IconButton( + icon: FaIcon(FontAwesomeIcons.solidPaperPlane), + onPressed: () { + if (textMessageController.text.isNotEmpty) { sendTextMessage( widget.contact.userId, TextMessageContent( - text: emoji, + text: textMessageController.text, responseToMessageId: allMediaFiles.first.messageOtherId, ), PushKind.reaction, ); - setState(() { - selectedShortReaction = index; - }); - Future.delayed(Duration(milliseconds: 300), () { - setState(() { - showShortReactions = false; - }); - }); - }, - child: (selectedShortReaction == index) - ? EmojiAnimationFlying( - emoji: emoji, - duration: Duration(milliseconds: 300), - startPosition: 0.0, - size: (showShortReactions) ? 40 : 10) - : AnimatedOpacity( - opacity: (selectedShortReaction == -1) - ? 1 - : 0, // Fade in/out - duration: Duration(milliseconds: 150), - child: SizedBox( - width: showShortReactions ? 40 : 10, - child: Center( - child: EmojiAnimation( - emoji: emoji, - ), - ), - ), - ), - ), - ); - }, + textMessageController.clear(); + } + setState(() { + showSendTextMessageInput = false; + showShortReactions = false; + }); + }, + ) + ], ), ), ), - ), - if (imageBytes != null) - Positioned( - bottom: 30, - left: 0, - right: 0, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (maxShowTime == 999999) - OutlinedButton( - style: OutlinedButton.styleFrom( - iconColor: imageSaved - ? Theme.of(context).colorScheme.outline - : Theme.of(context).colorScheme.primary, - foregroundColor: imageSaved - ? Theme.of(context).colorScheme.outline - : Theme.of(context).colorScheme.primary, - ), - onPressed: () async { - if (allMediaFiles.first.messageOtherId == null) { - return; // should not be possible - } - setState(() { - imageSaving = true; - }); - encryptAndSendMessage( - null, - widget.contact.userId, - MessageJson( - kind: MessageKind.storedMediaFile, - messageId: allMediaFiles.first.messageId, - content: StoredMediaFileContent( - messageId: allMediaFiles.first.messageOtherId!, - ), - timestamp: DateTime.now(), - ), - pushKind: PushKind.storedMediaFile, - ); - final res = await saveImageToGallery(imageBytes!); - if (res == null) { - setState(() { - imageSaving = false; - imageSaved = true; - }); - } - }, - child: Row( - children: [ - imageSaving - ? SizedBox( - width: 10, - height: 10, - child: CircularProgressIndicator( - strokeWidth: 1)) - : imageSaved - ? Icon(Icons.check) - : FaIcon(FontAwesomeIcons.floppyDisk), - ], - ), - ), - SizedBox(width: 10), - IconButton( - icon: SizedBox( - width: 30, - height: 30, - child: GridView.count( - crossAxisCount: 2, - children: List.generate( - 4, - (index) { - return SizedBox( - width: 8, - height: 8, - child: Center( - child: EmojiAnimation( - emoji: EmojiAnimation.animatedIcons.keys - .toList()[index], - ), - ), - ); - }, - ), - ), - ), - onPressed: () async { - setState(() { - showShortReactions = !showShortReactions; - selectedShortReaction = -1; - }); - }, - style: ButtonStyle( - padding: WidgetStateProperty.all( - EdgeInsets.symmetric(vertical: 10, horizontal: 20), - ), - ), - ), - SizedBox(width: 10), - IconButton.outlined( - icon: FaIcon(FontAwesomeIcons.message), - onPressed: () async { - Navigator.popUntil(context, (route) => route.isFirst); - Navigator.push( - context, - MaterialPageRoute(builder: (context) { - return ChatItemDetailsView(widget.contact); - }), - ); - }, - style: ButtonStyle( - padding: WidgetStateProperty.all( - EdgeInsets.symmetric(vertical: 10, horizontal: 20), - ), - ), - ), - SizedBox(width: 10), - IconButton.outlined( - icon: FaIcon(FontAwesomeIcons.camera), - onPressed: () async { - await Navigator.push(context, MaterialPageRoute( - builder: (context) { - return CameraSendToView(widget.contact); - }, - )); - }, - style: ButtonStyle( - padding: WidgetStateProperty.all( - EdgeInsets.symmetric(vertical: 10, horizontal: 20), - ), - ), - ), - ], - ), + if (allMediaFiles.isNotEmpty) + ReactionButtons( + show: showShortReactions, + userId: widget.contact.userId, + responseToMessageId: allMediaFiles.first.messageOtherId!, + hide: () { + setState(() { + showShortReactions = false; + showSendTextMessageInput = false; + }); + }, ), ], ), @@ -510,3 +520,93 @@ class _MediaViewerViewState extends State { ); } } + +class ReactionButtons extends StatefulWidget { + const ReactionButtons( + {super.key, + required this.show, + required this.userId, + required this.responseToMessageId, + required this.hide}); + + final bool show; + final int userId; + final int responseToMessageId; + final Function() hide; + + @override + State createState() => _ReactionButtonsState(); +} + +class _ReactionButtonsState extends State { + int selectedShortReaction = -1; + @override + Widget build(BuildContext context) { + return AnimatedPositioned( + duration: Duration(milliseconds: 200), // Animation duration + bottom: widget.show ? 100 : 90, + left: widget.show ? 0 : 150, + right: widget.show ? 0 : 150, + curve: Curves.linearToEaseOut, + child: AnimatedOpacity( + opacity: widget.show ? 1.0 : 0.0, // Fade in/out + duration: Duration(milliseconds: 150), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.end, + children: List.generate( + 6, + (index) { + final emoji = EmojiAnimation.animatedIcons.keys.toList()[index]; + return AnimatedSize( + duration: Duration(milliseconds: 200), // Animation duration + curve: Curves.linearToEaseOut, + child: GestureDetector( + onTap: () { + sendTextMessage( + widget.userId, + TextMessageContent( + text: emoji, + responseToMessageId: widget.responseToMessageId, + ), + PushKind.reaction, + ); + setState(() { + selectedShortReaction = index; + }); + Future.delayed(Duration(milliseconds: 300), () { + setState(() { + widget.hide(); + selectedShortReaction = -1; + }); + }); + }, + child: (selectedShortReaction == index) + ? EmojiAnimationFlying( + emoji: emoji, + duration: Duration(milliseconds: 300), + startPosition: 0.0, + size: (widget.show) ? 40 : 10) + : AnimatedOpacity( + opacity: (selectedShortReaction == -1) + ? 1 + : 0, // Fade in/out + duration: Duration(milliseconds: 150), + child: SizedBox( + width: widget.show ? 40 : 10, + child: Center( + child: EmojiAnimation( + emoji: emoji, + ), + ), + ), + ), + ), + ); + }, + ), + ), + ), + ); + } +}