From 5d5b01b8a9f14ab14203334944573038ea94b189 Mon Sep 17 00:00:00 2001 From: Nutcake Date: Tue, 16 May 2023 15:57:44 +0200 Subject: [PATCH] Improve error handling in message view --- lib/clients/api_client.dart | 7 +- lib/clients/messaging_client.dart | 4 + lib/models/message.dart | 13 +- lib/widgets/messages/messages_list.dart | 408 +++++++++++++----------- pubspec.yaml | 2 +- 5 files changed, 232 insertions(+), 202 deletions(-) diff --git a/lib/clients/api_client.dart b/lib/clients/api_client.dart index 3d6852a..4b20ef2 100644 --- a/lib/clients/api_client.dart +++ b/lib/clients/api_client.dart @@ -124,10 +124,13 @@ class ApiClient { if (response.statusCode == 403) { tryCachedLogin(); // TODO: Show the login screen again if cached login was unsuccessful. - throw "You are not authorized to do that."; + throw "You are not authorized to do that"; + } + if (response.statusCode == 500) { + throw "Internal server error"; } if (response.statusCode != 200) { - throw "Unknown Error${kDebugMode ? ": ${response.statusCode}|${response.body}" : ""}"; + throw "Unknown Error: ${response.statusCode}${kDebugMode ? "|${response.body}" : ""}"; } } diff --git a/lib/clients/messaging_client.dart b/lib/clients/messaging_client.dart index 9225d7b..b6187d4 100644 --- a/lib/clients/messaging_client.dart +++ b/lib/clients/messaging_client.dart @@ -196,6 +196,10 @@ class MessagingClient extends ChangeNotifier { MessageCache _createUserMessageCache(String userId) => MessageCache(apiClient: _apiClient, userId: userId); + void deleteUserMessageCache(String userId) { + _messageCache.remove(userId); + } + Future loadUserMessageCache(String userId) async { final cache = getUserMessageCache(userId) ?? _createUserMessageCache(userId); await cache.loadMessages(); diff --git a/lib/models/message.dart b/lib/models/message.dart index 9357d74..4afd900 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -114,7 +114,7 @@ class MessageCache { final List _messages = []; final ApiClient _apiClient; final String _userId; - Future? currentOperation; + Object? error; List get messages => _messages; @@ -155,9 +155,14 @@ class MessageCache { } Future loadMessages() async { - final messages = await MessageApi.getUserMessages(_apiClient, userId: _userId); - _messages.addAll(messages); - _ensureIntegrity(); + error = null; + try { + final messages = await MessageApi.getUserMessages(_apiClient, userId: _userId); + _messages.addAll(messages); + _ensureIntegrity(); + } catch (e) { + error = e; + } } void _ensureIntegrity() { diff --git a/lib/widgets/messages/messages_list.dart b/lib/widgets/messages/messages_list.dart index 09a3720..69c5c4c 100644 --- a/lib/widgets/messages/messages_list.dart +++ b/lib/widgets/messages/messages_list.dart @@ -2,6 +2,7 @@ import 'package:contacts_plus_plus/client_holder.dart'; import 'package:contacts_plus_plus/clients/messaging_client.dart'; import 'package:contacts_plus_plus/models/friend.dart'; import 'package:contacts_plus_plus/models/message.dart'; +import 'package:contacts_plus_plus/widgets/default_error_widget.dart'; import 'package:contacts_plus_plus/widgets/friends/friend_online_status_indicator.dart'; import 'package:contacts_plus_plus/widgets/messages/messages_session_header.dart'; import 'package:flutter/material.dart'; @@ -78,213 +79,230 @@ class _MessagesListState extends State with SingleTickerProviderSt .of(context) .colorScheme .surfaceVariant; - return Scaffold( - appBar: AppBar( - title: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - FriendOnlineStatusIndicator(userStatus: widget.friend.userStatus), - const SizedBox(width: 8,), - Text(widget.friend.username), - if (widget.friend.isHeadless) Padding( - padding: const EdgeInsets.only(left: 12), - child: Icon(Icons.dns, size: 18, color: Theme - .of(context) - .colorScheme - .onSecondaryContainer - .withAlpha(150),), - ), - ], - ), - scrolledUnderElevation: 0.0, - backgroundColor: appBarColor, - ), - body: Column( - children: [ - if (sessions.isNotEmpty) Container( - constraints: const BoxConstraints(maxHeight: 64), - decoration: BoxDecoration( - color: appBarColor, - border: const Border(top: BorderSide(width: 1, color: Colors.black26),) - ), - child: Stack( + return Consumer( + builder: (context, mClient, _) { + final cache = mClient.getUserMessageCache(widget.friend.id); + return Scaffold( + appBar: AppBar( + title: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - ListView.builder( - controller: _sessionListScrollController, - scrollDirection: Axis.horizontal, - itemCount: sessions.length, - itemBuilder: (context, index) => SessionTile(session: sessions[index]), + FriendOnlineStatusIndicator(userStatus: widget.friend.userStatus), + const SizedBox(width: 8,), + Text(widget.friend.username), + if (widget.friend.isHeadless) Padding( + padding: const EdgeInsets.only(left: 12), + child: Icon(Icons.dns, size: 18, color: Theme + .of(context) + .colorScheme + .onSecondaryContainer + .withAlpha(150),), ), - AnimatedOpacity( - opacity: _shevronOpacity, - curve: Curves.easeOut, - duration: const Duration(milliseconds: 200), - child: Align( - alignment: Alignment.centerRight, - child: Container( - padding: const EdgeInsets.only(left: 16, right: 4, top: 1, bottom: 1), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [ - appBarColor.withOpacity(0), - appBarColor, - appBarColor, - ], + ], + ), + scrolledUnderElevation: 0.0, + backgroundColor: appBarColor, + ), + body: Column( + children: [ + if (sessions.isNotEmpty) Container( + constraints: const BoxConstraints(maxHeight: 64), + decoration: BoxDecoration( + color: appBarColor, + border: const Border(top: BorderSide(width: 1, color: Colors.black26),) + ), + child: Stack( + children: [ + ListView.builder( + controller: _sessionListScrollController, + scrollDirection: Axis.horizontal, + itemCount: sessions.length, + itemBuilder: (context, index) => SessionTile(session: sessions[index]), + ), + AnimatedOpacity( + opacity: _shevronOpacity, + curve: Curves.easeOut, + duration: const Duration(milliseconds: 200), + child: Align( + alignment: Alignment.centerRight, + child: Container( + padding: const EdgeInsets.only(left: 16, right: 4, top: 1, bottom: 1), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + appBarColor.withOpacity(0), + appBarColor, + appBarColor, + ], + ), + ), + height: double.infinity, + child: const Icon(Icons.chevron_right), ), ), - height: double.infinity, - child: const Icon(Icons.chevron_right), - ), - ), - ) - ], - ), - ), - Expanded( - child: Consumer( - builder: (context, mClient, _) { - final cache = mClient.getUserMessageCache(widget.friend.id); - if (cache == null) { - return const Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - LinearProgressIndicator() - ], - ); - } - if (cache.messages.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.message_outlined), - Padding( - padding: const EdgeInsets.symmetric(vertical: 24), - child: Text( - "There are no messages here\nWhy not say hello?", - textAlign: TextAlign.center, - style: Theme - .of(context) - .textTheme - .titleMedium, - ), - ) - ], - ), - ); - } - return ListView.builder( - controller: _messageScrollController, - reverse: true, - itemCount: cache.messages.length, - itemBuilder: (context, index) { - final entry = cache.messages[index]; - if (index == cache.messages.length - 1) { - return Padding( - padding: const EdgeInsets.only(top: 12), - child: MessageBubble(message: entry,), + ) + ], + ), + ), + Expanded( + child: Builder( + builder: (context) { + if (cache == null) { + return const Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + LinearProgressIndicator() + ], + ); + } + if (cache.error != null) { + return DefaultErrorWidget( + message: cache.error.toString(), + onRetry: () { + setState(() { + mClient.deleteUserMessageCache(widget.friend.id); + }); + mClient.loadUserMessageCache(widget.friend.id); + }, ); } - return MessageBubble(message: entry,); - }, - ); - }, - ), - ), - AnimatedContainer( - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - blurRadius: _showBottomBarShadow ? 8 : 0, - color: Theme.of(context).shadowColor, - offset: const Offset(0, 4), - ), - ], - color: Theme.of(context).colorScheme.background, - ), - padding: const EdgeInsets.symmetric(horizontal: 4), - duration: const Duration(milliseconds: 250), - child: Row( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.all(8), - child: TextField( - autocorrect: true, - controller: _messageTextController, - maxLines: 4, - minLines: 1, - onChanged: (text) { - if (text.isNotEmpty && !_isSendable) { - setState(() { - _isSendable = true; - }); - } else if (text.isEmpty && _isSendable) { - setState(() { - _isSendable = false; - }); + if (cache.messages.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.message_outlined), + Padding( + padding: const EdgeInsets.symmetric(vertical: 24), + child: Text( + "There are no messages here\nWhy not say hello?", + textAlign: TextAlign.center, + style: Theme + .of(context) + .textTheme + .titleMedium, + ), + ) + ], + ), + ); + } + return ListView.builder( + controller: _messageScrollController, + reverse: true, + itemCount: cache.messages.length, + itemBuilder: (context, index) { + final entry = cache.messages[index]; + if (index == cache.messages.length - 1) { + return Padding( + padding: const EdgeInsets.only(top: 12), + child: MessageBubble(message: entry,), + ); } + return MessageBubble(message: entry,); }, - decoration: InputDecoration( - isDense: true, - hintText: "Send a message to ${widget.friend - .username}...", - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(24) - ) + ); + }, + ), + ), + AnimatedContainer( + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + blurRadius: _showBottomBarShadow ? 8 : 0, + color: Theme.of(context).shadowColor, + offset: const Offset(0, 4), + ), + ], + color: Theme.of(context).colorScheme.background, + ), + padding: const EdgeInsets.symmetric(horizontal: 4), + duration: const Duration(milliseconds: 250), + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(8), + child: TextField( + enabled: cache != null && cache.error == null, + autocorrect: true, + controller: _messageTextController, + maxLines: 4, + minLines: 1, + onChanged: (text) { + if (text.isNotEmpty && !_isSendable) { + setState(() { + _isSendable = true; + }); + } else if (text.isEmpty && _isSendable) { + setState(() { + _isSendable = false; + }); + } + }, + decoration: InputDecoration( + isDense: true, + hintText: "Message ${widget.friend + .username}...", + hintMaxLines: 1, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(24) + ) + ), + ), ), ), - ), - ), - Padding( - padding: const EdgeInsets.only(left: 8, right: 4.0), - child: Consumer( - builder: (context, mClient, _) { - return IconButton( - splashRadius: 24, - onPressed: _isSendable ? () async { - setState(() { - _isSendable = false; - }); - final message = Message( - id: Message.generateId(), - recipientId: widget.friend.id, - senderId: apiClient.userId, - type: MessageType.text, - content: _messageTextController.text, - sendTime: DateTime.now().toUtc(), + Padding( + padding: const EdgeInsets.only(left: 8, right: 4.0), + child: Consumer( + builder: (context, mClient, _) { + return IconButton( + splashRadius: 24, + onPressed: _isSendable ? () async { + setState(() { + _isSendable = false; + }); + final message = Message( + id: Message.generateId(), + recipientId: widget.friend.id, + senderId: apiClient.userId, + type: MessageType.text, + content: _messageTextController.text, + sendTime: DateTime.now().toUtc(), + ); + try { + mClient.sendMessage(message); + _messageTextController.clear(); + setState(() {}); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Failed to send message\n$e", + maxLines: null, + ), + ), + ); + setState(() { + _isSendable = true; + }); + } + } : null, + iconSize: 28, + icon: const Icon(Icons.send), ); - try { - mClient.sendMessage(message); - _messageTextController.clear(); - setState(() {}); - } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Failed to send message\n$e", - maxLines: null, - ), - ), - ); - setState(() { - _isSendable = true; - }); - } - } : null, - iconSize: 28, - icon: const Icon(Icons.send), - ); - }, - ), - ) - ], - ), + }, + ), + ) + ], + ), + ), + ], ), - ], - ), + ); + } ); } } diff --git a/pubspec.yaml b/pubspec.yaml index 184e65c..640e731 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.2.1+1 +version: 1.2.2+1 environment: sdk: '>=3.0.0'