diff --git a/lib/api_client.dart b/lib/api_client.dart index fb62e01..043bd73 100644 --- a/lib/api_client.dart +++ b/lib/api_client.dart @@ -13,11 +13,20 @@ class ApiClient { static const String tokenKey = "token"; static const String passwordKey = "password"; - final AuthenticationData _authenticationData; + static final ApiClient _singleton = ApiClient._internal(); - String get userId => _authenticationData.userId; + factory ApiClient() { + return _singleton; + } - const ApiClient({required AuthenticationData authenticationData}) : _authenticationData = authenticationData; + ApiClient._internal(); + + AuthenticationData? _authenticationData; + + set authenticationData(value) => _authenticationData = value; + + String get userId => _authenticationData!.userId; + bool get isAuthenticated => _authenticationData?.isAuthenticated ?? false; static Future tryLogin({ required String username, @@ -90,7 +99,7 @@ class ApiClient { } Map get authorizationHeader => { - "Authorization": "neos ${_authenticationData.userId}:${_authenticationData.token}" + "Authorization": "neos ${_authenticationData!.userId}:${_authenticationData!.token}" }; static Uri buildFullUri(String path) => Uri.parse("${Config.apiBaseUrl}/api$path"); @@ -119,4 +128,8 @@ class ApiClient { headers.addAll(authorizationHeader); return http.delete(buildFullUri(path), headers: headers); } +} + +class BaseClient { + static final client = ApiClient(); } \ No newline at end of file diff --git a/lib/apis/friend_api.dart b/lib/apis/friend_api.dart index f523dea..eca53b0 100644 --- a/lib/apis/friend_api.dart +++ b/lib/apis/friend_api.dart @@ -4,14 +4,9 @@ import 'dart:convert'; import 'package:contacts_plus/api_client.dart'; import 'package:contacts_plus/models/friend.dart'; -class FriendApi { - - const FriendApi({required apiClient}) : _apiClient = apiClient; - - final ApiClient _apiClient; - - Future> getFriendsList() async { - final response = await _apiClient.get("/users/${_apiClient.userId}/friends"); +class FriendApi extends BaseClient { + static Future> getFriendsList() async { + final response = await BaseClient.client.get("/users/${BaseClient.client.userId}/friends"); ApiClient.checkResponse(response); final data = jsonDecode(response.body) as List; return data.map((e) => Friend.fromMap(e)); diff --git a/lib/apis/message_api.dart b/lib/apis/message_api.dart index 8106517..762309b 100644 --- a/lib/apis/message_api.dart +++ b/lib/apis/message_api.dart @@ -3,14 +3,9 @@ import 'dart:convert'; import 'package:contacts_plus/api_client.dart'; import 'package:contacts_plus/models/message.dart'; -class MessageApi { - - const MessageApi({required ApiClient apiClient}) : _apiClient = apiClient; - - final ApiClient _apiClient; - - Future> getUserMessages({String userId="", DateTime? fromTime, int maxItems=50, bool unreadOnly=false}) async { - final response = await _apiClient.get("/users/${_apiClient.userId}/messages" +class MessageApi extends BaseClient { + static Future> getUserMessages({String userId="", DateTime? fromTime, int maxItems=50, bool unreadOnly=false}) async { + final response = await BaseClient.client.get("/users/${BaseClient.client.userId}/messages" "?maxItems=$maxItems" "${fromTime == null ? "" : "&fromTime${fromTime.toLocal().toIso8601String()}"}" "${userId.isEmpty ? "" : "&user=$userId"}" diff --git a/lib/main.dart b/lib/main.dart index 18c49f3..b1217d9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,18 +5,20 @@ import 'api_client.dart'; import 'models/authentication_data.dart'; void main() { - runApp(const ContactsPlus()); + runApp(ContactsPlus()); } class ContactsPlus extends StatelessWidget { - const ContactsPlus({super.key}); + ContactsPlus({super.key}); + final Typography _typography = Typography.material2021(platform: TargetPlatform.android); @override Widget build(BuildContext context) { return MaterialApp( title: 'Contacts+', theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange, brightness: Brightness.dark) + textTheme: _typography.white, + colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark) ), home: const SplashScreen(), ); @@ -31,51 +33,22 @@ class SplashScreen extends StatefulWidget { } class _SplashScreenState extends State { - AuthenticationData _authenticationData = AuthenticationData.unauthenticated(); + final ApiClient _apiClient = ApiClient(); @override Widget build(BuildContext context) { - if (_authenticationData.isAuthenticated) { - return AuthenticatedClient( - authenticationData: _authenticationData, - child: const HomeScreen(), - ); + if (_apiClient.isAuthenticated) { + return const HomeScreen(); } else { return LoginScreen( onLoginSuccessful: (AuthenticationData authData) { if (authData.isAuthenticated) { setState(() { - _authenticationData = authData; + _apiClient.authenticationData = authData; }); } }, ); } } -} - -class AuthenticatedClient extends InheritedWidget { - final ApiClient client; - - AuthenticatedClient({super.key, required AuthenticationData authenticationData, required super.child}) - : client = ApiClient(authenticationData: authenticationData); - - static AuthenticatedClient? maybeOf(BuildContext context) { - return context.dependOnInheritedWidgetOfExactType(); - } - - static AuthenticatedClient of(BuildContext context) { - final AuthenticatedClient? result = maybeOf(context); - assert(result != null, 'No AuthenticatedClient found in context'); - return result!; - } - - static AuthenticatedClient staticOf(BuildContext context) { - final result = context.findAncestorWidgetOfExactType(); - assert(result != null, 'No AuthenticatedClient found in context'); - return result!; - } - - @override - bool updateShouldNotify(covariant AuthenticatedClient oldWidget) => oldWidget.client != client; } \ No newline at end of file diff --git a/lib/models/friend.dart b/lib/models/friend.dart index 8535c13..6ec49b1 100644 --- a/lib/models/friend.dart +++ b/lib/models/friend.dart @@ -43,7 +43,7 @@ class UserStatus { factory UserStatus.fromMap(Map map) { final statusString = map["onlineStatus"] as String?; - final status = OnlineStatus.values.firstWhere((element) => element.name == statusString?.toLowerCase(), + final status = OnlineStatus.values.firstWhere((element) => element.name.toLowerCase() == statusString?.toLowerCase(), orElse: () => OnlineStatus.unknown, ); if (status == OnlineStatus.unknown && statusString != null) { diff --git a/lib/models/message.dart b/lib/models/message.dart index 7adea25..9153028 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -4,6 +4,8 @@ enum MessageType { unknown, text, sound, + sessionInvite, + object, } class Message { @@ -17,7 +19,7 @@ class Message { factory Message.fromMap(Map map) { final typeString = map["messageType"] as String?; - final type = MessageType.values.firstWhere((element) => element.name == typeString?.toLowerCase(), + final type = MessageType.values.firstWhere((element) => element.name.toLowerCase() == typeString?.toLowerCase(), orElse: () => MessageType.unknown, ); if (type == MessageType.unknown && typeString != null) { @@ -25,8 +27,8 @@ class Message { } return Message( id: map["id"], - recipientId: map["recipient_id"], - senderId: map["sender_id"], + recipientId: map["recipientId"], + senderId: map["senderId"], type: type, content: map["content"], ); diff --git a/lib/widgets/home_screen.dart b/lib/widgets/home_screen.dart index 1cf6fe3..24dec17 100644 --- a/lib/widgets/home_screen.dart +++ b/lib/widgets/home_screen.dart @@ -12,21 +12,16 @@ class HomeScreen extends StatefulWidget { } class _HomeScreenState extends State { - - late final FriendApi _friendsApi; Future>? _friendsFuture; @override void initState() { super.initState(); - _friendsApi = FriendApi(apiClient: AuthenticatedClient - .staticOf(context) - .client); _refreshFriendsList(); } void _refreshFriendsList() { - _friendsFuture = _friendsApi.getFriendsList().then((Iterable value) => + _friendsFuture = FriendApi.getFriendsList().then((Iterable value) => value.toList() ..sort((a, b) { if (a.userStatus.onlineStatus == b.userStatus.onlineStatus) { diff --git a/lib/widgets/messages.dart b/lib/widgets/messages.dart index ee27847..522c1ef 100644 --- a/lib/widgets/messages.dart +++ b/lib/widgets/messages.dart @@ -1,5 +1,5 @@ +import 'package:contacts_plus/api_client.dart'; import 'package:contacts_plus/apis/message_api.dart'; -import 'package:contacts_plus/main.dart'; import 'package:contacts_plus/models/friend.dart'; import 'package:contacts_plus/models/message.dart'; import 'package:flutter/material.dart'; @@ -16,26 +16,20 @@ class Messages extends StatefulWidget { class _MessagesState extends State { Future>? _messagesFuture; - late final MessageApi _messageApi; void _refreshMessages() { - _messagesFuture = _messageApi.getUserMessages(userId: widget.friend.id)..then((value) => value.toList()); + _messagesFuture = MessageApi.getUserMessages(userId: widget.friend.id)..then((value) => value.toList()); } @override void initState() { super.initState(); - _messageApi = MessageApi( - apiClient: AuthenticatedClient - .staticOf(context) - .client, - ); _refreshMessages(); } @override Widget build(BuildContext context) { - final apiClient = AuthenticatedClient.of(context).client; + final apiClient = ApiClient(); return Scaffold( appBar: AppBar( title: Text(widget.friend.username), @@ -46,14 +40,13 @@ class _MessagesState extends State { if (snapshot.hasData) { final data = snapshot.data as Iterable; return ListView.builder( + reverse: true, itemCount: data.length, itemBuilder: (context, index) { final entry = data.elementAt(index); - if (entry.senderId == apiClient.userId) { - return MyMessageBubble(message: entry); - } else { - return OtherMessageBubble(message: entry); - } + return entry.senderId == apiClient.userId + ? MyMessageBubble(message: entry) + : OtherMessageBubble(message: entry); }, ); } else if (snapshot.hasError) { @@ -85,15 +78,35 @@ class MyMessageBubble extends StatelessWidget { @override Widget build(BuildContext context) { + var content = message.content; + if (message.type == MessageType.sessionInvite) { + content = ""; + } else if (message.type == MessageType.sound) { + content = ""; + } else if (message.type == MessageType.object) { + content = ""; + } return Row( mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.max, + mainAxisSize: MainAxisSize.min, children: [ - Container( - color: Theme.of(context).colorScheme.primaryContainer, - margin: const EdgeInsets.only(left:16), - padding: const EdgeInsets.all(12), - child: Text(message.content, softWrap: true,), + Flexible( + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + color: Theme.of(context).colorScheme.primaryContainer, + margin: const EdgeInsets.only(left: 32, bottom: 16, right: 8), + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + content, + softWrap: true, + maxLines: null, + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + ), ), ], ); @@ -108,14 +121,38 @@ class OtherMessageBubble extends StatelessWidget { @override Widget build(BuildContext context) { + var content = message.content; + if (message.type == MessageType.sessionInvite) { + content = ""; + } else if (message.type == MessageType.sound) { + content = ""; + } else if (message.type == MessageType.object) { + content = ""; + } return Row( + mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, children: [ - Container( - color: Theme.of(context).colorScheme.secondaryContainer, - margin: const EdgeInsets.only(right: 16), - padding: const EdgeInsets.all(12), - child: Text(message.content, softWrap: true,), + Flexible( + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + color: Theme + .of(context) + .colorScheme + .secondaryContainer, + margin: const EdgeInsets.only(right: 32, bottom: 16, left: 8), + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + content, + softWrap: true, + maxLines: null, + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + ), ), ], ); diff --git a/test/widget_test.dart b/test/widget_test.dart index 035c014..60edc3f 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -13,7 +13,7 @@ import 'package:contacts_plus/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(const ContactsPlus()); + await tester.pumpWidget(ContactsPlus()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget);