Add chat message view

This commit is contained in:
Nutcake 2023-04-29 22:21:00 +02:00 committed by Nils Rother
parent a9a14c09b7
commit 4f67acec8e
9 changed files with 102 additions and 92 deletions

View file

@ -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<AuthenticationData> tryLogin({
required String username,
@ -90,7 +99,7 @@ class ApiClient {
}
Map<String, String> 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();
}

View file

@ -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<Iterable<Friend>> getFriendsList() async {
final response = await _apiClient.get("/users/${_apiClient.userId}/friends");
class FriendApi extends BaseClient {
static Future<Iterable<Friend>> 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));

View file

@ -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<Iterable<Message>> 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<Iterable<Message>> 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"}"

View file

@ -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<SplashScreen> {
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<AuthenticatedClient>();
}
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<AuthenticatedClient>();
assert(result != null, 'No AuthenticatedClient found in context');
return result!;
}
@override
bool updateShouldNotify(covariant AuthenticatedClient oldWidget) => oldWidget.client != client;
}

View file

@ -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) {

View file

@ -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"],
);

View file

@ -12,21 +12,16 @@ class HomeScreen extends StatefulWidget {
}
class _HomeScreenState extends State<HomeScreen> {
late final FriendApi _friendsApi;
Future<List<Friend>>? _friendsFuture;
@override
void initState() {
super.initState();
_friendsApi = FriendApi(apiClient: AuthenticatedClient
.staticOf(context)
.client);
_refreshFriendsList();
}
void _refreshFriendsList() {
_friendsFuture = _friendsApi.getFriendsList().then((Iterable<Friend> value) =>
_friendsFuture = FriendApi.getFriendsList().then((Iterable<Friend> value) =>
value.toList()
..sort((a, b) {
if (a.userStatus.onlineStatus == b.userStatus.onlineStatus) {

View file

@ -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<Messages> {
Future<Iterable<Message>>? _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<Messages> {
if (snapshot.hasData) {
final data = snapshot.data as Iterable<Message>;
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 = "<Session Invite>";
} else if (message.type == MessageType.sound) {
content = "<Voice Message>";
} else if (message.type == MessageType.object) {
content = "<Asset>";
}
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 = "<Session Invite>";
} else if (message.type == MessageType.sound) {
content = "<Voice Message>";
} else if (message.type == MessageType.object) {
content = "<Asset>";
}
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,
),
),
),
),
],
);

View file

@ -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);