Add chat message view
This commit is contained in:
parent
a9a14c09b7
commit
4f67acec8e
9 changed files with 102 additions and 92 deletions
|
@ -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");
|
||||
|
@ -120,3 +129,7 @@ class ApiClient {
|
|||
return http.delete(buildFullUri(path), headers: headers);
|
||||
}
|
||||
}
|
||||
|
||||
class BaseClient {
|
||||
static final client = ApiClient();
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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"}"
|
||||
|
|
|
@ -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,21 +33,18 @@ 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;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -53,29 +52,3 @@ class _SplashScreenState extends State<SplashScreen> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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"],
|
||||
);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(
|
||||
Flexible(
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
margin: const EdgeInsets.only(left:16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(message.content, softWrap: true,),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue