diff --git a/lib/main.dart b/lib/main.dart index 37638d61..9a6e7d55 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -169,6 +169,7 @@ class FlwtchState extends State { var args = jsonDecode(call.arguments); var profile = profs.getProfile(args["ProfileOnion"])!; var convo = profile.contactList.getContact(args["Handle"])!; + var initialIndex = convo.unreadMessages; convo.unreadMessages = 0; // single pane mode pushes; double pane mode reads AppState.selectedProfile/Conversation @@ -186,7 +187,7 @@ class FlwtchState extends State { ChangeNotifierProvider.value(value: profile), ChangeNotifierProvider.value(value: convo), ], - builder: (context, child) => MessageView(), + builder: (context, child) => MessageView(initialIndex), ); }, ), diff --git a/lib/model.dart b/lib/model.dart index 1baa71b0..39fe3e3a 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -3,13 +3,6 @@ import 'dart:convert'; import 'package:cwtch/widgets/messagerow.dart'; import 'package:flutter/cupertino.dart'; import 'package:cwtch/models/servers.dart'; -import 'package:cwtch/widgets/messagebubble.dart'; -import 'package:provider/provider.dart'; -import 'dart:async'; -import 'dart:collection'; - -import 'cwtch/cwtch.dart'; -import 'main.dart'; //////////////////// /// UI State /// @@ -31,6 +24,52 @@ class ChatMessage { }; } +class AppState extends ChangeNotifier { + bool cwtchInit = false; + bool cwtchIsClosing = false; + String appError = ""; + String? _selectedProfile; + String? _selectedConversation; + int? _selectedIndex; + bool _unreadMessagesBelow = false; + + void SetCwtchInit() { + cwtchInit = true; + notifyListeners(); + } + + void SetAppError(String error) { + appError = error; + notifyListeners(); + } + + String? get selectedProfile => _selectedProfile; + set selectedProfile(String? newVal) { + this._selectedProfile = newVal; + notifyListeners(); + } + + String? get selectedConversation => _selectedConversation; + set selectedConversation(String? newVal) { + this._selectedConversation = newVal; + notifyListeners(); + } + + int? get selectedIndex => _selectedIndex; + set selectedIndex(int? newVal) { + this._selectedIndex = newVal; + notifyListeners(); + } + + bool get unreadMessagesBelow => _unreadMessagesBelow; + set unreadMessagesBelow(bool newVal) { + this._unreadMessagesBelow = newVal; + notifyListeners(); + } + + bool isLandscape(BuildContext c) => MediaQuery.of(c).size.width > MediaQuery.of(c).size.height; +} + /////////////////// /// Providers /// /////////////////// @@ -62,45 +101,6 @@ class ProfileListState extends ChangeNotifier { } } -class AppState extends ChangeNotifier { - bool cwtchInit = false; - bool cwtchIsClosing = false; - String appError = ""; - String? _selectedProfile; - String? _selectedConversation; - int? _selectedIndex; - - void SetCwtchInit() { - cwtchInit = true; - notifyListeners(); - } - - void SetAppError(String error) { - appError = error; - notifyListeners(); - } - - String? get selectedProfile => _selectedProfile; - set selectedProfile(String? newVal) { - this._selectedProfile = newVal; - notifyListeners(); - } - - String? get selectedConversation => _selectedConversation; - set selectedConversation(String? newVal) { - this._selectedConversation = newVal; - notifyListeners(); - } - - int? get selectedIndex => _selectedIndex; - set selectedIndex(int? newVal) { - this._selectedIndex = newVal; - notifyListeners(); - } - - bool isLandscape(BuildContext c) => MediaQuery.of(c).size.width > MediaQuery.of(c).size.height; -} - class ContactListState extends ChangeNotifier { List _contacts = []; String _filter = ""; diff --git a/lib/views/contactsview.dart b/lib/views/contactsview.dart index fb1af6c3..dbb59909 100644 --- a/lib/views/contactsview.dart +++ b/lib/views/contactsview.dart @@ -23,6 +23,7 @@ class ContactsView extends StatefulWidget { // selectConversation can be called from anywhere to set the active conversation void selectConversation(BuildContext context, String handle) { + var initialIndex = Provider.of(context, listen: false).contactList.getContact(handle)!.unreadMessages; // requery instead of using contactinfostate directly because sometimes listview gets confused about data that resorts Provider.of(context, listen: false).contactList.getContact(handle)!.unreadMessages = 0; // triggers update in Double/TripleColumnView @@ -30,10 +31,10 @@ void selectConversation(BuildContext context, String handle) { Provider.of(context, listen: false).selectedIndex = null; // if in singlepane mode, push to the stack var isLandscape = Provider.of(context, listen: false).isLandscape(context); - if (Provider.of(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(context, handle); + if (Provider.of(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(context, handle, initialIndex); } -void _pushMessageView(BuildContext context, String handle) { +void _pushMessageView(BuildContext context, String handle, int initialIndex) { var profileOnion = Provider.of(context, listen: false).onion; Navigator.of(context).push( MaterialPageRoute( @@ -46,7 +47,7 @@ void _pushMessageView(BuildContext context, String handle) { ChangeNotifierProvider.value(value: profile), ChangeNotifierProvider.value(value: profile.contactList.getContact(handle)!), ], - builder: (context, child) => MessageView(), + builder: (context, child) => MessageView(initialIndex), ); }, ), diff --git a/lib/views/doublecolview.dart b/lib/views/doublecolview.dart index ebd7ffff..9e163b74 100644 --- a/lib/views/doublecolview.dart +++ b/lib/views/doublecolview.dart @@ -17,6 +17,7 @@ class _DoubleColumnViewState extends State { Widget build(BuildContext context) { var flwtch = Provider.of(context); var cols = Provider.of(context).uiColumns(true); + var initialIndex = flwtch.selectedConversation == null ? 0 : Provider.of(context, listen: false).contactList.getContact(flwtch.selectedConversation!)!.unreadMessages; return Flex( direction: Axis.horizontal, children: [ @@ -35,7 +36,7 @@ class _DoubleColumnViewState extends State { ChangeNotifierProvider.value(value: Provider.of(context)), ChangeNotifierProvider.value( value: flwtch.selectedConversation != null ? Provider.of(context).contactList.getContact(flwtch.selectedConversation!)! : ContactInfoState("", "")), - ], child: Container(child: MessageView())), + ], child: Container(child: MessageView(initialIndex))), ), ], ); diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 000e327e..b1bb5f09 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -4,7 +4,6 @@ import 'package:crypto/crypto.dart'; import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/messages/quotedmessage.dart'; -import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/messageloadingbubble.dart'; import 'package:cwtch/widgets/profileimage.dart'; import 'package:flutter/cupertino.dart'; @@ -14,6 +13,7 @@ import 'package:cwtch/widgets/DropdownContacts.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import '../main.dart'; import '../model.dart'; @@ -22,6 +22,9 @@ import '../widgets/messagelist.dart'; import 'groupsettingsview.dart'; class MessageView extends StatefulWidget { + int initialIndex; + MessageView(this.initialIndex); + @override _MessageViewState createState() => _MessageViewState(); } @@ -30,14 +33,28 @@ class _MessageViewState extends State { final ctrlrCompose = TextEditingController(); final focusNode = FocusNode(); String selectedContact = ""; + ItemPositionsListener scrollListener = ItemPositionsListener.create(); + ItemScrollController scrollController = ItemScrollController(); - // @override - // void didChangeDependencies() { - // super.didChangeDependencies(); - // if (Provider.of(context, listen: false).unreadMessages > 0) { - // Provider.of(context, listen: false).unreadMessages = 0; - // } - // } + @override + void initState() { + // using "8" because "# of messages that fit on one screen" isnt trivial to calculate at this point + if (widget.initialIndex > 8) { + WidgetsFlutterBinding.ensureInitialized().addPostFrameCallback((timeStamp) { + Provider.of(context, listen: false).unreadMessagesBelow = true; + }); + } + + scrollListener.itemPositions.addListener(() { + var first = scrollListener.itemPositions.value.first.index; + var last = scrollListener.itemPositions.value.last.index; + // sometimes these go hi->lo and sometimes they go lo->hi because [who tf knows] + if (first == 0 || last == 0) { + Provider.of(context, listen: false).unreadMessagesBelow = false; + } + }); + super.initState(); + } @override void dispose() { @@ -57,6 +74,9 @@ class _MessageViewState extends State { return WillPopScope( onWillPop: _onWillPop, child: Scaffold( + floatingActionButton: appState.unreadMessagesBelow ? FloatingActionButton(child: Icon(Icons.arrow_downward), onPressed: (){ + scrollController.scrollTo(index: 0, duration: Duration(milliseconds: 600)); + }) : null, appBar: AppBar( // setting leading to null makes it do the default behaviour; container() hides it leading: Provider.of(context).uiColumns(appState.isLandscape(context)).length > 1 ? Container() : null, @@ -87,13 +107,14 @@ class _MessageViewState extends State { onPressed: _pushContactSettings), ], ), - body: Padding(padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 108.0), child: MessageList()), + body: Padding(padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 108.0), child: MessageList(widget.initialIndex, scrollController, scrollListener)), bottomSheet: _buildComposeBox(), )); } Future _onWillPop() async { Provider.of(context, listen: false).unreadMessages = 0; + Provider.of(context, listen: false).selectedConversation = null; return true; } diff --git a/lib/views/triplecolview.dart b/lib/views/triplecolview.dart index 4156c738..b666e9b1 100644 --- a/lib/views/triplecolview.dart +++ b/lib/views/triplecolview.dart @@ -35,7 +35,7 @@ class _TripleColumnViewState extends State { child: appState.selectedConversation == null ? Center(child: Text(AppLocalizations.of(context)!.addContactFirst)) : //dev - Container(child: MessageView()), + Container(child: MessageView(0/*todo:setme*/)), ), ]); } diff --git a/lib/widgets/messagebubbledecorations.dart b/lib/widgets/messagebubbledecorations.dart index 9b0591b3..22ba1e91 100644 --- a/lib/widgets/messagebubbledecorations.dart +++ b/lib/widgets/messagebubbledecorations.dart @@ -45,6 +45,5 @@ class _MessageBubbleDecoration extends State { child: Icon(Icons.hourglass_bottom_outlined, color: Provider.of(context).theme.messageFromMeTextColor(), size: 16)))) ], )); - ; } } diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index 6038e213..defa3a19 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -1,24 +1,24 @@ import 'package:cwtch/models/message.dart'; -import 'package:cwtch/models/messages/malformedmessage.dart'; -import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/messageloadingbubble.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import '../main.dart'; import '../model.dart'; import '../settings.dart'; -import 'messagerow.dart'; class MessageList extends StatefulWidget { + int initialIndex; + ItemScrollController scrollController; + ItemPositionsListener scrollListener; + MessageList(this.initialIndex, this.scrollController, this.scrollListener); + @override _MessageListState createState() => _MessageListState(); } class _MessageListState extends State { - ScrollController ctrlr1 = ScrollController(); - @override Widget build(BuildContext outerContext) { bool isP2P = !Provider.of(context).isGroup; @@ -52,12 +52,11 @@ class _MessageListState extends State { : (showEphemeralWarning ? Text(AppLocalizations.of(context)!.chatHistoryDefault, textAlign: TextAlign.center) : - // We are not allowed to put null here, so put an empty text widge + // We are not allowed to put null here, so put an empty text widget Text("")), ))), Expanded( child: Scrollbar( - controller: ctrlr1, child: Container( // Only show broken heart is the contact is offline... decoration: BoxDecoration( @@ -70,8 +69,10 @@ class _MessageListState extends State { colorFilter: ColorFilter.mode(Provider.of(context).theme.hilightElementTextColor(), BlendMode.srcIn))), // Don't load messages for syncing server... child: loadMessages - ? ListView.builder( - controller: ctrlr1, + ? ScrollablePositionedList.builder( + itemPositionsListener: widget.scrollListener, + itemScrollController: widget.scrollController, + initialScrollIndex: widget.initialIndex, itemCount: Provider.of(outerContext).totalMessages, reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction... itemBuilder: (itemBuilderContext, index) { diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index c71b7afb..8a94837c 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:io'; import 'package:cwtch/cwtch_icons_icons.dart'; @@ -50,7 +49,7 @@ class MessageRowState extends State { child: IconButton( tooltip: AppLocalizations.of(context)!.tooltipReplyToThisMessage, onPressed: () { - Provider.of(context, listen: false).selectedIndex = Provider.of(context).messageIndex; + Provider.of(context, listen: false).selectedIndex = Provider.of(context, listen: false).messageIndex; }, icon: Icon(Icons.reply, color: Provider.of(context).theme.dropShadowColor()))); Widget wdgSpacer = Expanded(child: SizedBox(width: 60, height: 10)); diff --git a/pubspec.yaml b/pubspec.yaml index 5213118a..5d33a9c2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,7 @@ dependencies: glob: any flutter_test: sdk: flutter + scrollable_positioned_list: ^0.2.0-nullsafety.0 dev_dependencies: msix: ^2.1.3