opening a chat with unread messages should go to last read. fixes #48
continuous-integration/drone/pr Build is failing Details

This commit is contained in:
erinn 2021-07-23 14:50:21 -07:00
parent 8668211e1d
commit 08fcbc6a10
10 changed files with 98 additions and 74 deletions

View File

@ -169,6 +169,7 @@ class FlwtchState extends State<Flwtch> {
var args = jsonDecode(call.arguments); var args = jsonDecode(call.arguments);
var profile = profs.getProfile(args["ProfileOnion"])!; var profile = profs.getProfile(args["ProfileOnion"])!;
var convo = profile.contactList.getContact(args["Handle"])!; var convo = profile.contactList.getContact(args["Handle"])!;
var initialIndex = convo.unreadMessages;
convo.unreadMessages = 0; convo.unreadMessages = 0;
// single pane mode pushes; double pane mode reads AppState.selectedProfile/Conversation // single pane mode pushes; double pane mode reads AppState.selectedProfile/Conversation
@ -186,7 +187,7 @@ class FlwtchState extends State<Flwtch> {
ChangeNotifierProvider.value(value: profile), ChangeNotifierProvider.value(value: profile),
ChangeNotifierProvider.value(value: convo), ChangeNotifierProvider.value(value: convo),
], ],
builder: (context, child) => MessageView(), builder: (context, child) => MessageView(initialIndex),
); );
}, },
), ),

View File

@ -3,13 +3,6 @@ import 'dart:convert';
import 'package:cwtch/widgets/messagerow.dart'; import 'package:cwtch/widgets/messagerow.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:cwtch/models/servers.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 /// /// 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 /// /// 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 { class ContactListState extends ChangeNotifier {
List<ContactInfoState> _contacts = []; List<ContactInfoState> _contacts = [];
String _filter = ""; String _filter = "";

View File

@ -23,6 +23,7 @@ class ContactsView extends StatefulWidget {
// selectConversation can be called from anywhere to set the active conversation // selectConversation can be called from anywhere to set the active conversation
void selectConversation(BuildContext context, String handle) { void selectConversation(BuildContext context, String handle) {
var initialIndex = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages;
// requery instead of using contactinfostate directly because sometimes listview gets confused about data that resorts // requery instead of using contactinfostate directly because sometimes listview gets confused about data that resorts
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages = 0; Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages = 0;
// triggers update in Double/TripleColumnView // triggers update in Double/TripleColumnView
@ -30,10 +31,10 @@ void selectConversation(BuildContext context, String handle) {
Provider.of<AppState>(context, listen: false).selectedIndex = null; Provider.of<AppState>(context, listen: false).selectedIndex = null;
// if in singlepane mode, push to the stack // if in singlepane mode, push to the stack
var isLandscape = Provider.of<AppState>(context, listen: false).isLandscape(context); var isLandscape = Provider.of<AppState>(context, listen: false).isLandscape(context);
if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(context, handle); if (Provider.of<Settings>(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<ProfileInfoState>(context, listen: false).onion; var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute<void>( MaterialPageRoute<void>(
@ -46,7 +47,7 @@ void _pushMessageView(BuildContext context, String handle) {
ChangeNotifierProvider.value(value: profile), ChangeNotifierProvider.value(value: profile),
ChangeNotifierProvider.value(value: profile.contactList.getContact(handle)!), ChangeNotifierProvider.value(value: profile.contactList.getContact(handle)!),
], ],
builder: (context, child) => MessageView(), builder: (context, child) => MessageView(initialIndex),
); );
}, },
), ),

View File

@ -17,6 +17,7 @@ class _DoubleColumnViewState extends State<DoubleColumnView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
var flwtch = Provider.of<AppState>(context); var flwtch = Provider.of<AppState>(context);
var cols = Provider.of<Settings>(context).uiColumns(true); var cols = Provider.of<Settings>(context).uiColumns(true);
var initialIndex = flwtch.selectedConversation == null ? 0 : Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(flwtch.selectedConversation!)!.unreadMessages;
return Flex( return Flex(
direction: Axis.horizontal, direction: Axis.horizontal,
children: <Widget>[ children: <Widget>[
@ -35,7 +36,7 @@ class _DoubleColumnViewState extends State<DoubleColumnView> {
ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context)), ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context)),
ChangeNotifierProvider.value( ChangeNotifierProvider.value(
value: flwtch.selectedConversation != null ? Provider.of<ProfileInfoState>(context).contactList.getContact(flwtch.selectedConversation!)! : ContactInfoState("", "")), value: flwtch.selectedConversation != null ? Provider.of<ProfileInfoState>(context).contactList.getContact(flwtch.selectedConversation!)! : ContactInfoState("", "")),
], child: Container(child: MessageView())), ], child: Container(child: MessageView(initialIndex))),
), ),
], ],
); );

View File

@ -4,7 +4,6 @@ import 'package:crypto/crypto.dart';
import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messages/quotedmessage.dart'; import 'package:cwtch/models/messages/quotedmessage.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:cwtch/widgets/messageloadingbubble.dart'; import 'package:cwtch/widgets/messageloadingbubble.dart';
import 'package:cwtch/widgets/profileimage.dart'; import 'package:cwtch/widgets/profileimage.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
@ -14,6 +13,7 @@ import 'package:cwtch/widgets/DropdownContacts.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import '../main.dart'; import '../main.dart';
import '../model.dart'; import '../model.dart';
@ -22,6 +22,9 @@ import '../widgets/messagelist.dart';
import 'groupsettingsview.dart'; import 'groupsettingsview.dart';
class MessageView extends StatefulWidget { class MessageView extends StatefulWidget {
int initialIndex;
MessageView(this.initialIndex);
@override @override
_MessageViewState createState() => _MessageViewState(); _MessageViewState createState() => _MessageViewState();
} }
@ -30,14 +33,28 @@ class _MessageViewState extends State<MessageView> {
final ctrlrCompose = TextEditingController(); final ctrlrCompose = TextEditingController();
final focusNode = FocusNode(); final focusNode = FocusNode();
String selectedContact = ""; String selectedContact = "";
ItemPositionsListener scrollListener = ItemPositionsListener.create();
ItemScrollController scrollController = ItemScrollController();
// @override @override
// void didChangeDependencies() { void initState() {
// super.didChangeDependencies(); // using "8" because "# of messages that fit on one screen" isnt trivial to calculate at this point
// if (Provider.of<ContactInfoState>(context, listen: false).unreadMessages > 0) { if (widget.initialIndex > 8) {
// Provider.of<ContactInfoState>(context, listen: false).unreadMessages = 0; WidgetsFlutterBinding.ensureInitialized().addPostFrameCallback((timeStamp) {
// } Provider.of<AppState>(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<AppState>(context, listen: false).unreadMessagesBelow = false;
}
});
super.initState();
}
@override @override
void dispose() { void dispose() {
@ -57,6 +74,9 @@ class _MessageViewState extends State<MessageView> {
return WillPopScope( return WillPopScope(
onWillPop: _onWillPop, onWillPop: _onWillPop,
child: Scaffold( child: Scaffold(
floatingActionButton: appState.unreadMessagesBelow ? FloatingActionButton(child: Icon(Icons.arrow_downward), onPressed: (){
scrollController.scrollTo(index: 0, duration: Duration(milliseconds: 600));
}) : null,
appBar: AppBar( appBar: AppBar(
// setting leading to null makes it do the default behaviour; container() hides it // setting leading to null makes it do the default behaviour; container() hides it
leading: Provider.of<Settings>(context).uiColumns(appState.isLandscape(context)).length > 1 ? Container() : null, leading: Provider.of<Settings>(context).uiColumns(appState.isLandscape(context)).length > 1 ? Container() : null,
@ -87,13 +107,14 @@ class _MessageViewState extends State<MessageView> {
onPressed: _pushContactSettings), 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(), bottomSheet: _buildComposeBox(),
)); ));
} }
Future<bool> _onWillPop() async { Future<bool> _onWillPop() async {
Provider.of<ContactInfoState>(context, listen: false).unreadMessages = 0; Provider.of<ContactInfoState>(context, listen: false).unreadMessages = 0;
Provider.of<AppState>(context, listen: false).selectedConversation = null;
return true; return true;
} }

View File

@ -35,7 +35,7 @@ class _TripleColumnViewState extends State<TripleColumnView> {
child: appState.selectedConversation == null child: appState.selectedConversation == null
? Center(child: Text(AppLocalizations.of(context)!.addContactFirst)) ? Center(child: Text(AppLocalizations.of(context)!.addContactFirst))
: //dev : //dev
Container(child: MessageView()), Container(child: MessageView(0/*todo:setme*/)),
), ),
]); ]);
} }

View File

@ -45,6 +45,5 @@ class _MessageBubbleDecoration extends State<MessageBubbleDecoration> {
child: Icon(Icons.hourglass_bottom_outlined, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 16)))) child: Icon(Icons.hourglass_bottom_outlined, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 16))))
], ],
)); ));
;
} }
} }

View File

@ -1,24 +1,24 @@
import 'package:cwtch/models/message.dart'; 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:cwtch/widgets/messageloadingbubble.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:provider/provider.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 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../main.dart';
import '../model.dart'; import '../model.dart';
import '../settings.dart'; import '../settings.dart';
import 'messagerow.dart';
class MessageList extends StatefulWidget { class MessageList extends StatefulWidget {
int initialIndex;
ItemScrollController scrollController;
ItemPositionsListener scrollListener;
MessageList(this.initialIndex, this.scrollController, this.scrollListener);
@override @override
_MessageListState createState() => _MessageListState(); _MessageListState createState() => _MessageListState();
} }
class _MessageListState extends State<MessageList> { class _MessageListState extends State<MessageList> {
ScrollController ctrlr1 = ScrollController();
@override @override
Widget build(BuildContext outerContext) { Widget build(BuildContext outerContext) {
bool isP2P = !Provider.of<ContactInfoState>(context).isGroup; bool isP2P = !Provider.of<ContactInfoState>(context).isGroup;
@ -52,12 +52,11 @@ class _MessageListState extends State<MessageList> {
: (showEphemeralWarning : (showEphemeralWarning
? Text(AppLocalizations.of(context)!.chatHistoryDefault, textAlign: TextAlign.center) ? 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("")), Text("")),
))), ))),
Expanded( Expanded(
child: Scrollbar( child: Scrollbar(
controller: ctrlr1,
child: Container( child: Container(
// Only show broken heart is the contact is offline... // Only show broken heart is the contact is offline...
decoration: BoxDecoration( decoration: BoxDecoration(
@ -70,8 +69,10 @@ class _MessageListState extends State<MessageList> {
colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.hilightElementTextColor(), BlendMode.srcIn))), colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.hilightElementTextColor(), BlendMode.srcIn))),
// Don't load messages for syncing server... // Don't load messages for syncing server...
child: loadMessages child: loadMessages
? ListView.builder( ? ScrollablePositionedList.builder(
controller: ctrlr1, itemPositionsListener: widget.scrollListener,
itemScrollController: widget.scrollController,
initialScrollIndex: widget.initialIndex,
itemCount: Provider.of<ContactInfoState>(outerContext).totalMessages, itemCount: Provider.of<ContactInfoState>(outerContext).totalMessages,
reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction... 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) { itemBuilder: (itemBuilderContext, index) {

View File

@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/cwtch_icons_icons.dart';
@ -50,7 +49,7 @@ class MessageRowState extends State<MessageRow> {
child: IconButton( child: IconButton(
tooltip: AppLocalizations.of(context)!.tooltipReplyToThisMessage, tooltip: AppLocalizations.of(context)!.tooltipReplyToThisMessage,
onPressed: () { onPressed: () {
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context).messageIndex; Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
}, },
icon: Icon(Icons.reply, color: Provider.of<Settings>(context).theme.dropShadowColor()))); icon: Icon(Icons.reply, color: Provider.of<Settings>(context).theme.dropShadowColor())));
Widget wdgSpacer = Expanded(child: SizedBox(width: 60, height: 10)); Widget wdgSpacer = Expanded(child: SizedBox(width: 60, height: 10));

View File

@ -40,6 +40,7 @@ dependencies:
glob: any glob: any
flutter_test: flutter_test:
sdk: flutter sdk: flutter
scrollable_positioned_list: ^0.2.0-nullsafety.0
dev_dependencies: dev_dependencies:
msix: ^2.1.3 msix: ^2.1.3