diff --git a/lib/screens/channel/chat/chat.dart b/lib/screens/channel/chat/chat.dart index ae2ae82d..ba32c99c 100644 --- a/lib/screens/channel/chat/chat.dart +++ b/lib/screens/channel/chat/chat.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:frosty/screens/channel/chat/emote_menu/emote_menu.dart'; import 'package:frosty/screens/channel/chat/widgets/chat_bottom_bar.dart'; @@ -16,99 +17,113 @@ class Chat extends StatelessWidget { Widget build(BuildContext context) { return Observer( builder: (context) { - return Stack( + return Column( children: [ - Column( - children: [ - Expanded( - child: GestureDetector( - onTap: () { - if (chatStore.assetsStore.showEmoteMenu) { - chatStore.assetsStore.showEmoteMenu = false; - } else if (chatStore.textFieldFocusNode.hasFocus) { - chatStore.textFieldFocusNode.unfocus(); - } - }, - child: Stack( - alignment: AlignmentDirectional.bottomCenter, - children: [ - MediaQuery( - data: MediaQuery.of(context).copyWith(textScaleFactor: chatStore.settings.messageScale), - child: DefaultTextStyle( - style: DefaultTextStyle.of(context).style.copyWith(fontSize: chatStore.settings.fontSize), - child: Observer( - builder: (context) { - return ListView.builder( - reverse: true, - padding: EdgeInsets.zero, - addAutomaticKeepAlives: false, - controller: chatStore.scrollController, - itemCount: chatStore.renderMessages.length, - itemBuilder: (context, index) => ChatMessage( - ircMessage: chatStore.renderMessages.reversed.toList()[index], - chatStore: chatStore, - ), - ); - }, - ), - ), - ), - Padding( - padding: const EdgeInsets.all(5.0), - child: Observer( - builder: (_) => AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - switchInCurve: Curves.easeOutCubic, - switchOutCurve: Curves.easeInCubic, - child: chatStore.autoScroll - ? null - : Button( - padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), - onPressed: chatStore.resumeScroll, - icon: const Icon(Icons.arrow_circle_down), - child: const Text('Resume Scroll'), - ), - ), - ), - ), - ], - ), - ), - ), - if (chatStore.settings.showBottomBar) ChatBottomBar(chatStore: chatStore), - AnimatedContainer( - curve: Curves.ease, - duration: const Duration(milliseconds: 200), - height: chatStore.assetsStore.showEmoteMenu ? MediaQuery.of(context).size.height / 3 : 0, - child: AnimatedOpacity( - curve: Curves.ease, - opacity: chatStore.assetsStore.showEmoteMenu ? 1 : 0, - duration: const Duration(milliseconds: 200), - child: EmoteMenu(chatStore: chatStore), - ), - ), - ], - ), - AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: chatStore.notification != null - ? Align( - alignment: Alignment.topCenter, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary.withOpacity(0.8), - borderRadius: BorderRadius.circular(30.0), - ), - width: double.infinity, - padding: const EdgeInsets.all(10.0), - margin: const EdgeInsets.all(10.0), - child: AlertMessage( - message: chatStore.notification!, - color: Colors.white, + Expanded( + child: GestureDetector( + onTap: () { + if (chatStore.assetsStore.showEmoteMenu) { + chatStore.assetsStore.showEmoteMenu = false; + } else if (chatStore.textFieldFocusNode.hasFocus) { + chatStore.textFieldFocusNode.unfocus(); + } + }, + child: Stack( + alignment: AlignmentDirectional.bottomCenter, + children: [ + MediaQuery( + data: MediaQuery.of(context).copyWith(textScaleFactor: chatStore.settings.messageScale), + child: DefaultTextStyle( + style: DefaultTextStyle.of(context).style.copyWith(fontSize: chatStore.settings.fontSize), + child: Observer( + builder: (context) { + return ListView.builder( + reverse: true, + padding: EdgeInsets.zero, + addAutomaticKeepAlives: false, + controller: chatStore.scrollController, + itemCount: chatStore.renderMessages.length, + itemBuilder: (context, index) => ChatMessage( + ircMessage: chatStore.renderMessages.reversed.toList()[index], + chatStore: chatStore, + ), + ); + }, ), ), - ) - : null, + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: chatStore.notification != null + ? Align( + alignment: chatStore.settings.chatNotificationsOnBottom ? Alignment.bottomCenter : Alignment.topCenter, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary.withOpacity(0.8), + borderRadius: BorderRadius.circular(30.0), + ), + margin: const EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: AlertMessage( + message: chatStore.notification!, + color: Colors.white, + ), + ), + Button( + onPressed: () async { + // Paste clipboard text into the text controller. + final data = await Clipboard.getData(Clipboard.kTextPlain); + + if (data != null) chatStore.textController.text = data.text!; + + chatStore.notification = null; + }, + fill: false, + child: const Text('Paste'), + ), + ], + ), + ), + ) + : null, + ), + Padding( + padding: const EdgeInsets.all(5.0), + child: Observer( + builder: (_) => AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.easeOutCubic, + switchOutCurve: Curves.easeInCubic, + child: chatStore.autoScroll + ? null + : Button( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), + onPressed: chatStore.resumeScroll, + icon: const Icon(Icons.arrow_circle_down), + child: const Text('Resume Scroll'), + ), + ), + ), + ), + ], + ), + ), + ), + if (chatStore.settings.showBottomBar) ChatBottomBar(chatStore: chatStore), + AnimatedContainer( + curve: Curves.ease, + duration: const Duration(milliseconds: 200), + height: chatStore.assetsStore.showEmoteMenu ? MediaQuery.of(context).size.height / 3 : 0, + child: AnimatedOpacity( + curve: Curves.ease, + opacity: chatStore.assetsStore.showEmoteMenu ? 1 : 0, + duration: const Duration(milliseconds: 200), + child: EmoteMenu(chatStore: chatStore), + ), ), ], ); diff --git a/lib/screens/channel/chat/widgets/chat_message.dart b/lib/screens/channel/chat/widgets/chat_message.dart index 2b99dcb7..8d6b312b 100644 --- a/lib/screens/channel/chat/widgets/chat_message.dart +++ b/lib/screens/channel/chat/widgets/chat_message.dart @@ -41,10 +41,10 @@ class ChatMessage extends StatelessWidget { ); } - void onLongPressMessage() { + Future onLongPressMessage() async { HapticFeedback.lightImpact(); - Clipboard.setData(ClipboardData(text: ircMessage.message)); + await Clipboard.setData(ClipboardData(text: ircMessage.message)); chatStore.notification = 'Message copied'; } diff --git a/lib/screens/settings/sections/chat_settings.dart b/lib/screens/settings/sections/chat_settings.dart index 52026702..905f32b2 100644 --- a/lib/screens/settings/sections/chat_settings.dart +++ b/lib/screens/settings/sections/chat_settings.dart @@ -94,6 +94,13 @@ class _ChatSettingsState extends State { value: settingsStore.landscapeChatLeftSide, onChanged: (newValue) => settingsStore.landscapeChatLeftSide = newValue, ), + SwitchListTile.adaptive( + isThreeLine: true, + title: const Text('Notifications on bottom'), + subtitle: const Text('Shows notifications (e.g., "Message copied") on the bottom of the chat.'), + value: settingsStore.chatNotificationsOnBottom, + onChanged: (newValue) => settingsStore.chatNotificationsOnBottom = newValue, + ), ListTile( isThreeLine: true, title: const Text('Landscape fill cutout side'), diff --git a/lib/screens/settings/sections/video_settings.dart b/lib/screens/settings/sections/video_settings.dart index f5872bea..7e8dd238 100644 --- a/lib/screens/settings/sections/video_settings.dart +++ b/lib/screens/settings/sections/video_settings.dart @@ -34,7 +34,7 @@ class VideoSettings extends StatelessWidget { ), SwitchListTile.adaptive( isThreeLine: true, - title: const Text('Picture-in-picture Button (experimental)'), + title: const Text('Picture-in-picture button (experimental)'), subtitle: const Text('Adds a button to enter PiP mode on the bottom right of the overlay (may cause freezes/crashes).'), value: settingsStore.pictureInPicture, onChanged: settingsStore.showVideo && settingsStore.showOverlay ? (newValue) => settingsStore.pictureInPicture = newValue : null, diff --git a/lib/screens/settings/stores/settings_store.dart b/lib/screens/settings/stores/settings_store.dart index 8480ec63..2444f905 100644 --- a/lib/screens/settings/stores/settings_store.dart +++ b/lib/screens/settings/stores/settings_store.dart @@ -92,6 +92,7 @@ abstract class _SettingsStoreBase with Store { static const defaultShowBottomBar = true; static const defaultLandscapeChatLeftSide = false; + static const defaultChatNotificationsOnBottom = false; static const defaultChatWidth = 0.3; static const defaultFullScreenChatOverlayOpacity = 0.5; @@ -128,6 +129,10 @@ abstract class _SettingsStoreBase with Store { @observable var landscapeChatLeftSide = defaultLandscapeChatLeftSide; + @JsonKey(defaultValue: defaultChatNotificationsOnBottom) + @observable + var chatNotificationsOnBottom = defaultChatNotificationsOnBottom; + @JsonKey(defaultValue: defaultLandscapeCutout) @observable var landscapeCutout = defaultLandscapeCutout; @@ -187,6 +192,7 @@ abstract class _SettingsStoreBase with Store { autocomplete = defaultAutocomplete; showBottomBar = defaultShowBottomBar; landscapeChatLeftSide = defaultLandscapeChatLeftSide; + chatNotificationsOnBottom = defaultChatNotificationsOnBottom; landscapeCutout = defaultLandscapeCutout; chatWidth = defaultChatWidth; fullScreenChatOverlayOpacity = defaultFullScreenChatOverlayOpacity; diff --git a/lib/screens/settings/stores/settings_store.g.dart b/lib/screens/settings/stores/settings_store.g.dart index 3b8f015e..2ded7c09 100644 --- a/lib/screens/settings/stores/settings_store.g.dart +++ b/lib/screens/settings/stores/settings_store.g.dart @@ -25,6 +25,8 @@ SettingsStore _$SettingsStoreFromJson(Map json) => ..autocomplete = json['autocomplete'] as bool? ?? true ..showBottomBar = json['showBottomBar'] as bool? ?? true ..landscapeChatLeftSide = json['landscapeChatLeftSide'] as bool? ?? false + ..chatNotificationsOnBottom = + json['chatNotificationsOnBottom'] as bool? ?? false ..landscapeCutout = $enumDecodeNullable( _$LandscapeCutoutTypeEnumMap, json['landscapeCutout']) ?? LandscapeCutoutType.none @@ -67,6 +69,7 @@ Map _$SettingsStoreToJson(SettingsStore instance) => 'autocomplete': instance.autocomplete, 'showBottomBar': instance.showBottomBar, 'landscapeChatLeftSide': instance.landscapeChatLeftSide, + 'chatNotificationsOnBottom': instance.chatNotificationsOnBottom, 'landscapeCutout': _$LandscapeCutoutTypeEnumMap[instance.landscapeCutout]!, 'chatWidth': instance.chatWidth, @@ -356,6 +359,23 @@ mixin _$SettingsStore on _SettingsStoreBase, Store { }); } + late final _$chatNotificationsOnBottomAtom = Atom( + name: '_SettingsStoreBase.chatNotificationsOnBottom', context: context); + + @override + bool get chatNotificationsOnBottom { + _$chatNotificationsOnBottomAtom.reportRead(); + return super.chatNotificationsOnBottom; + } + + @override + set chatNotificationsOnBottom(bool value) { + _$chatNotificationsOnBottomAtom + .reportWrite(value, super.chatNotificationsOnBottom, () { + super.chatNotificationsOnBottom = value; + }); + } + late final _$landscapeCutoutAtom = Atom(name: '_SettingsStoreBase.landscapeCutout', context: context); @@ -719,6 +739,7 @@ chatOnlyPreventSleep: ${chatOnlyPreventSleep}, autocomplete: ${autocomplete}, showBottomBar: ${showBottomBar}, landscapeChatLeftSide: ${landscapeChatLeftSide}, +chatNotificationsOnBottom: ${chatNotificationsOnBottom}, landscapeCutout: ${landscapeCutout}, chatWidth: ${chatWidth}, fullScreenChatOverlayOpacity: ${fullScreenChatOverlayOpacity}, diff --git a/lib/widgets/alert_message.dart b/lib/widgets/alert_message.dart index 613ed9c2..619772ac 100644 --- a/lib/widgets/alert_message.dart +++ b/lib/widgets/alert_message.dart @@ -16,6 +16,7 @@ class AlertMessage extends StatelessWidget { @override Widget build(BuildContext context) { return Row( + mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(