diff --git a/lib/enums/load_error_level.dart b/lib/enums/load_error_level.dart new file mode 100644 index 0000000..420134c --- /dev/null +++ b/lib/enums/load_error_level.dart @@ -0,0 +1,5 @@ +/// Load error level +/// +/// @author Pierre HUBERT + +enum LoadErrorLevel {MINOR, MAJOR, NONE} \ No newline at end of file diff --git a/lib/helpers/api_helper.dart b/lib/helpers/api_helper.dart index 2f486b2..c45814c 100644 --- a/lib/helpers/api_helper.dart +++ b/lib/helpers/api_helper.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:comunic/helpers/account_credentials_helper.dart'; import 'package:comunic/models/api_request.dart'; import 'package:comunic/models/api_response.dart'; import 'package:comunic/models/config.dart'; @@ -23,7 +24,12 @@ class APIHelper { request.addString("serviceToken", config().serviceToken); //Add user tokens (if required) - if (request.needLogin) throw "Can add user tokens right now !"; + if (request.needLogin) { + final tokens = await AccountCredentialsHelper().get(); + assert(tokens != null); + request.addString("userToken1", tokens.tokenOne); + request.addString("userToken2", tokens.tokenTwo); + } // Prepare request body String requestBody = ""; diff --git a/lib/helpers/conversations_helper.dart b/lib/helpers/conversations_helper.dart new file mode 100644 index 0000000..15b4916 --- /dev/null +++ b/lib/helpers/conversations_helper.dart @@ -0,0 +1,36 @@ +import 'package:comunic/models/api_request.dart'; +import 'package:comunic/models/conversation.dart'; + +/// Conversation helper +/// +/// @author Pierre HUBERT + +class ConversationsHelper { + /// Download the list of conversations from the server + Future> downloadList() async { + final response = + await APIRequest(uri: "conversations/getList", needLogin: true).exec(); + + if (response.code != 200) return null; + + try { + List list = List(); + response.getArray().forEach((f) => + list.add(Conversation( + id: f["ID"], + ownerID: f["ID_owner"], + lastActive: f["last_active"], + name: f["name"] == false ? null : f["name"], + following: f["following"] == 1, + sawLastMessage: f["saw_last_message"] == 1, + members: f["members"].map((f) => int.parse(f)).toList(), + ))); + + return list; + + } on Exception catch(e){ + print(e.toString()); + return null; + } + } +} diff --git a/lib/models/api_request.dart b/lib/models/api_request.dart index 9ae2e74..3704574 100644 --- a/lib/models/api_request.dart +++ b/lib/models/api_request.dart @@ -1,3 +1,5 @@ +import 'package:comunic/helpers/api_helper.dart'; +import 'package:comunic/models/api_response.dart'; import 'package:meta/meta.dart'; /// API Request model @@ -11,12 +13,11 @@ class APIRequest { final bool needLogin; Map args; - APIRequest({ - @required this.uri, - this.needLogin = false, - }) : assert(uri != null), - assert(needLogin != null), - args = Map(); + APIRequest({@required this.uri, this.needLogin = false, this.args}) + : assert(uri != null), + assert(needLogin != null) { + if (this.args == null) this.args = Map(); + } void addString(String name, String value) => args[name] = value; @@ -24,4 +25,7 @@ class APIRequest { void addBool(String name, bool value) => args[name] = value ? "true" : "false"; + + /// Execute the request + Future exec() async => APIHelper().exec(this); } diff --git a/lib/models/conversation.dart b/lib/models/conversation.dart new file mode 100644 index 0000000..245393f --- /dev/null +++ b/lib/models/conversation.dart @@ -0,0 +1,30 @@ +import 'package:meta/meta.dart'; + +/// Conversation model +/// +/// @author Pierre HUBERT + +class Conversation { + final int id; + final int ownerID; + final int lastActive; + final String name; + final bool following; + final bool sawLastMessage; + final List members; + + const Conversation({ + @required this.id, + @required this.ownerID, + @required this.lastActive, + @required this.name, + @required this.following, + @required this.sawLastMessage, + @required this.members, + }) : assert(id != null), + assert(ownerID != null), + assert(lastActive != null), + assert(following != null), + assert(sawLastMessage != null), + assert(members != null); +} diff --git a/lib/ui/routes/home_route.dart b/lib/ui/routes/home_route.dart index 77c205f..f7b2ccf 100644 --- a/lib/ui/routes/home_route.dart +++ b/lib/ui/routes/home_route.dart @@ -1,3 +1,4 @@ +import 'package:comunic/ui/screens/conversations_screen.dart'; import 'package:comunic/ui/screens/menus_screen.dart'; import 'package:comunic/ui/tiles/CustomBottomNavigationBarItem.dart'; import 'package:flutter/material.dart'; @@ -48,7 +49,7 @@ class _HomeRouteState extends State { Widget _buildBody(BuildContext context) { switch (_currTab) { case 0: - return Text("Conversations"); + return ConversationsScreen(); case 1: return MenuScreen(); diff --git a/lib/ui/screens/conversations_screen.dart b/lib/ui/screens/conversations_screen.dart new file mode 100644 index 0000000..056fe59 --- /dev/null +++ b/lib/ui/screens/conversations_screen.dart @@ -0,0 +1,87 @@ +import 'package:comunic/enums/load_error_level.dart'; +import 'package:comunic/helpers/conversations_helper.dart'; +import 'package:comunic/models/conversation.dart'; +import 'package:comunic/ui/tiles/conversation_tile.dart'; +import 'package:comunic/utils/intl_utils.dart'; +import 'package:comunic/utils/ui_utils.dart'; +import 'package:flutter/material.dart'; + +/// Conversations screen +/// +/// @author Pierre HUBERT + +class ConversationsScreen extends StatefulWidget { + @override + State createState() => _ConversationScreenState(); +} + +class _ConversationScreenState extends State { + final ConversationsHelper _conversationsHelper = ConversationsHelper(); + List _list; + LoadErrorLevel _error = LoadErrorLevel.NONE; + bool _loading = true; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _loadConversations(); + } + + void setError(LoadErrorLevel err) => setState(() => _error = err); + + void setLoading(bool loading) => setState(() => _loading = loading); + + void gotLoadingError() { + setLoading(false); + setError(_list == null ? LoadErrorLevel.MAJOR : LoadErrorLevel.MINOR); + } + + /// Load the list of conversations + Future _loadConversations() async { + setError(LoadErrorLevel.NONE); + setLoading(true); + + final list = await _conversationsHelper.downloadList(); + + if (list == null) return gotLoadingError(); + + //Save list + _list = list; + + setLoading(false); + } + + /// Build an error card + Widget _buildErrorCard() { + return buildErrorCard( + tr("Could not retrieve the list of conversations!"), + actions: [ + FlatButton( + onPressed: _loadConversations, + child: Text( + tr("Retry").toUpperCase(), + style: TextStyle( + color: Colors.white, + ), + ), + ) + ], + ); + } + + @override + Widget build(BuildContext context) { + if (_error == LoadErrorLevel.MAJOR) return _buildErrorCard(); + if (_list == null) return buildCenteredProgressBar(); + + // Show the list of conversations + return ListView.builder( + itemBuilder: (context, index) { + return ConversationTile( + conversation: _list.elementAt(index), + ); + }, + itemCount: _list.length, + ); + } +} diff --git a/lib/ui/tiles/conversation_tile.dart b/lib/ui/tiles/conversation_tile.dart new file mode 100644 index 0000000..7975d55 --- /dev/null +++ b/lib/ui/tiles/conversation_tile.dart @@ -0,0 +1,55 @@ +import 'package:comunic/models/conversation.dart'; +import 'package:comunic/utils/intl_utils.dart'; +import 'package:flutter/material.dart'; + +/// Single conversation tile +/// +/// @author Pierre HUBERT + +class ConversationTile extends StatelessWidget { + final Conversation conversation; + + const ConversationTile({Key key, this.conversation}) : super(key: key); + + _buildSubInformation(IconData icon, String content) { + return Row( + children: [ + Icon( + icon, + size: 15.0, + color: Colors.grey, + ), + Text(" " + content), + ], + ); + } + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(conversation.name == null ? "Unknown" : conversation.name), + leading: Icon( + conversation.sawLastMessage ? Icons.check_circle : Icons.lens, + color: conversation.sawLastMessage ? null : Colors.blue, + ), + isThreeLine: true, + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildSubInformation(Icons.access_time, "time"), //TODO : improve the way the time is shown + _buildSubInformation( + Icons.group, + conversation.members.length == 1 + ? tr("1 member") + : tr( + "%num% members", + args: { + "num": conversation.members.length.toString(), + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/utils/intl_utils.dart b/lib/utils/intl_utils.dart index 57f2b79..bdae768 100644 --- a/lib/utils/intl_utils.dart +++ b/lib/utils/intl_utils.dart @@ -5,7 +5,15 @@ /// Translate a string /// /// Translate a given [string] into the current language, if available -String tr(String string) { +/// +/// Then apply the list of [args] to the string, each argument name is +/// surrounded by '%' +String tr(String string, {Map args}) { //TODO : create translation system + + //Apply arguments + if(args != null) + args.forEach((key, value) => string = string.replaceAll("%$key%", value)); + return string; -} \ No newline at end of file +} diff --git a/lib/utils/ui_utils.dart b/lib/utils/ui_utils.dart index be5b919..0c9dd12 100644 --- a/lib/utils/ui_utils.dart +++ b/lib/utils/ui_utils.dart @@ -18,7 +18,7 @@ Widget buildLoadingPage() { } /// Build and return an error card -Widget buildErrorCard(String message) { +Widget buildErrorCard(String message, {List actions}) { return Card( elevation: 2.0, color: Colors.red, @@ -39,6 +39,9 @@ Widget buildErrorCard(String message) { style: TextStyle(color: Colors.white), maxLines: null, ), + ), + Row( + children: actions == null ? [] : actions, ) ], ),