Add user avatars
This commit is contained in:
parent
7b776db632
commit
8a3ff70523
11 changed files with 415 additions and 85 deletions
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
import 'package:contacts_plus/models/message.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
@ -7,7 +8,11 @@ import 'package:contacts_plus/models/authentication_data.dart';
|
||||||
import 'package:signalr_netcore/http_connection_options.dart';
|
import 'package:signalr_netcore/http_connection_options.dart';
|
||||||
import 'package:signalr_netcore/hub_connection.dart';
|
import 'package:signalr_netcore/hub_connection.dart';
|
||||||
import 'package:signalr_netcore/hub_connection_builder.dart';
|
import 'package:signalr_netcore/hub_connection_builder.dart';
|
||||||
|
import 'package:signalr_netcore/ihub_protocol.dart';
|
||||||
|
import 'package:signalr_netcore/msgpack_hub_protocol.dart';
|
||||||
|
import 'package:signalr_netcore/web_supporting_http_client.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
import 'config.dart';
|
import 'config.dart';
|
||||||
|
|
||||||
|
@ -17,21 +22,17 @@ class ApiClient {
|
||||||
static const String tokenKey = "token";
|
static const String tokenKey = "token";
|
||||||
static const String passwordKey = "password";
|
static const String passwordKey = "password";
|
||||||
|
|
||||||
static final ApiClient _singleton = ApiClient._internal();
|
ApiClient({required AuthenticationData authenticationData}) : _authenticationData = authenticationData {
|
||||||
|
if (_authenticationData.isAuthenticated) {
|
||||||
factory ApiClient() {
|
hub.start();
|
||||||
return _singleton;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiClient._internal();
|
late final NeosHub hub = NeosHub(token: authorizationHeader.values.first);
|
||||||
|
final AuthenticationData _authenticationData;
|
||||||
|
|
||||||
final NeosHub _hub = NeosHub();
|
String get userId => _authenticationData.userId;
|
||||||
AuthenticationData? _authenticationData;
|
bool get isAuthenticated => _authenticationData.isAuthenticated;
|
||||||
|
|
||||||
set authenticationData(value) => _authenticationData = value;
|
|
||||||
|
|
||||||
String get userId => _authenticationData!.userId;
|
|
||||||
bool get isAuthenticated => _authenticationData?.isAuthenticated ?? false;
|
|
||||||
|
|
||||||
static Future<AuthenticationData> tryLogin({
|
static Future<AuthenticationData> tryLogin({
|
||||||
required String username,
|
required String username,
|
||||||
|
@ -104,7 +105,7 @@ class ApiClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String> get authorizationHeader => {
|
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");
|
static Uri buildFullUri(String path) => Uri.parse("${Config.apiBaseUrl}/api$path");
|
||||||
|
@ -136,19 +137,38 @@ class ApiClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
class NeosHub {
|
class NeosHub {
|
||||||
final HubConnection hubConnection;
|
late final HubConnection hubConnection;
|
||||||
late final Future<void>? _hubConnectedFuture;
|
final Logger _logger = Logger("NeosHub");
|
||||||
|
|
||||||
NeosHub() : hubConnection = HubConnectionBuilder()
|
NeosHub({required String token}) {
|
||||||
.withUrl(Config.neosHubUrl, options: HttpConnectionOptions())
|
hubConnection = HubConnectionBuilder()
|
||||||
.withAutomaticReconnect()
|
.withUrl(
|
||||||
.build() {
|
Config.neosHubUrl,
|
||||||
_hubConnectedFuture = hubConnection.start()?.whenComplete(() {
|
options: HttpConnectionOptions(
|
||||||
|
headers: MessageHeaders()
|
||||||
|
..setHeaderValue("Authorization", token),
|
||||||
|
httpClient: WebSupportingHttpClient(
|
||||||
|
_logger,
|
||||||
|
),
|
||||||
|
logger: _logger,
|
||||||
|
logMessageContent: true
|
||||||
|
),
|
||||||
|
).withAutomaticReconnect().build();
|
||||||
|
hubConnection.onreconnecting(({error}) {
|
||||||
|
log("onreconnecting called with error $error");
|
||||||
|
});
|
||||||
|
hubConnection.onreconnected(({connectionId}) {
|
||||||
|
log("onreconnected called");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void start() {
|
||||||
|
hubConnection.start()?.onError((error, stackTrace) => log(error.toString())).whenComplete(() {
|
||||||
log("Hub connection established");
|
log("Hub connection established");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class BaseClient {
|
Future<void> sendMessage(Message message) async {
|
||||||
static final client = ApiClient();
|
await hubConnection.send("SendMessage", args: [message.toMap()]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,9 +4,9 @@ import 'dart:convert';
|
||||||
import 'package:contacts_plus/api_client.dart';
|
import 'package:contacts_plus/api_client.dart';
|
||||||
import 'package:contacts_plus/models/friend.dart';
|
import 'package:contacts_plus/models/friend.dart';
|
||||||
|
|
||||||
class FriendApi extends BaseClient {
|
class FriendApi {
|
||||||
static Future<Iterable<Friend>> getFriendsList() async {
|
static Future<Iterable<Friend>> getFriendsList(ApiClient client) async {
|
||||||
final response = await BaseClient.client.get("/users/${BaseClient.client.userId}/friends");
|
final response = await client.get("/users/${client.userId}/friends");
|
||||||
ApiClient.checkResponse(response);
|
ApiClient.checkResponse(response);
|
||||||
final data = jsonDecode(response.body) as List;
|
final data = jsonDecode(response.body) as List;
|
||||||
return data.map((e) => Friend.fromMap(e));
|
return data.map((e) => Friend.fromMap(e));
|
||||||
|
|
|
@ -3,9 +3,9 @@ import 'dart:convert';
|
||||||
import 'package:contacts_plus/api_client.dart';
|
import 'package:contacts_plus/api_client.dart';
|
||||||
import 'package:contacts_plus/models/message.dart';
|
import 'package:contacts_plus/models/message.dart';
|
||||||
|
|
||||||
class MessageApi extends BaseClient {
|
class MessageApi {
|
||||||
static Future<Iterable<Message>> getUserMessages({String userId="", DateTime? fromTime, int maxItems=50, bool unreadOnly=false}) async {
|
static Future<Iterable<Message>> getUserMessages(ApiClient client, {String userId="", DateTime? fromTime, int maxItems=50, bool unreadOnly=false}) async {
|
||||||
final response = await BaseClient.client.get("/users/${BaseClient.client.userId}/messages"
|
final response = await client.get("/users/${client.userId}/messages"
|
||||||
"?maxItems=$maxItems"
|
"?maxItems=$maxItems"
|
||||||
"${fromTime == null ? "" : "&fromTime${fromTime.toLocal().toIso8601String()}"}"
|
"${fromTime == null ? "" : "&fromTime${fromTime.toLocal().toIso8601String()}"}"
|
||||||
"${userId.isEmpty ? "" : "&user=$userId"}"
|
"${userId.isEmpty ? "" : "&user=$userId"}"
|
||||||
|
|
|
@ -5,50 +5,62 @@ import 'api_client.dart';
|
||||||
import 'models/authentication_data.dart';
|
import 'models/authentication_data.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(ContactsPlus());
|
runApp(const ContactsPlus());
|
||||||
}
|
}
|
||||||
|
|
||||||
class ContactsPlus extends StatelessWidget {
|
class ContactsPlus extends StatefulWidget {
|
||||||
ContactsPlus({super.key});
|
const ContactsPlus({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ContactsPlus> createState() => _ContactsPlusState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ContactsPlusState extends State<ContactsPlus> {
|
||||||
final Typography _typography = Typography.material2021(platform: TargetPlatform.android);
|
final Typography _typography = Typography.material2021(platform: TargetPlatform.android);
|
||||||
|
AuthenticationData _authData = AuthenticationData.unauthenticated();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return ClientHolder(
|
||||||
title: 'Contacts+',
|
authenticationData: _authData,
|
||||||
theme: ThemeData(
|
child: MaterialApp(
|
||||||
textTheme: _typography.white,
|
title: 'Contacts+',
|
||||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark)
|
theme: ThemeData(
|
||||||
|
textTheme: _typography.white,
|
||||||
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark)
|
||||||
|
),
|
||||||
|
home: _authData.isAuthenticated ?
|
||||||
|
const HomeScreen() :
|
||||||
|
LoginScreen(
|
||||||
|
onLoginSuccessful: (AuthenticationData authData) {
|
||||||
|
if (authData.isAuthenticated) {
|
||||||
|
setState(() {
|
||||||
|
_authData = authData;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
home: const SplashScreen(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SplashScreen extends StatefulWidget {
|
class ClientHolder extends InheritedWidget {
|
||||||
const SplashScreen({super.key});
|
final ApiClient client;
|
||||||
|
|
||||||
@override
|
ClientHolder({super.key, required AuthenticationData authenticationData, required super.child})
|
||||||
State<SplashScreen> createState() => _SplashScreenState();
|
: client = ApiClient(authenticationData: authenticationData);
|
||||||
}
|
|
||||||
|
|
||||||
class _SplashScreenState extends State<SplashScreen> {
|
static ClientHolder? maybeOf(BuildContext context) {
|
||||||
final ApiClient _apiClient = ApiClient();
|
return context.dependOnInheritedWidgetOfExactType<ClientHolder>();
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (_apiClient.isAuthenticated) {
|
|
||||||
return const HomeScreen();
|
|
||||||
} else {
|
|
||||||
return LoginScreen(
|
|
||||||
onLoginSuccessful: (AuthenticationData authData) {
|
|
||||||
if (authData.isAuthenticated) {
|
|
||||||
setState(() {
|
|
||||||
_apiClient.authenticationData = authData;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static ClientHolder of(BuildContext context) {
|
||||||
|
final ClientHolder? result = maybeOf(context);
|
||||||
|
assert(result != null, 'No AuthenticatedClient found in context');
|
||||||
|
return result!;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(covariant ClientHolder oldWidget) => oldWidget.client != client;
|
||||||
}
|
}
|
|
@ -1,16 +1,22 @@
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:contacts_plus/models/user_profile.dart';
|
||||||
|
|
||||||
class Friend extends Comparable {
|
class Friend extends Comparable {
|
||||||
final String id;
|
final String id;
|
||||||
final String username;
|
final String username;
|
||||||
final UserStatus userStatus;
|
final UserStatus userStatus;
|
||||||
|
final UserProfile userProfile;
|
||||||
|
|
||||||
Friend({required this.id, required this.username, required this.userStatus});
|
Friend({required this.id, required this.username, required this.userStatus, required this.userProfile});
|
||||||
|
|
||||||
factory Friend.fromMap(Map map) {
|
factory Friend.fromMap(Map map) {
|
||||||
return Friend(id: map["id"], username: map["friendUsername"], userStatus: UserStatus.fromMap(map["userStatus"]));
|
return Friend(
|
||||||
|
id: map["id"],
|
||||||
|
username: map["friendUsername"],
|
||||||
|
userStatus: UserStatus.fromMap(map["userStatus"]),
|
||||||
|
userProfile: UserProfile.fromMap(map["profile"] ?? {"iconUrl": ""}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -1,11 +1,30 @@
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
enum MessageType {
|
enum MessageType {
|
||||||
unknown,
|
unknown,
|
||||||
text,
|
text,
|
||||||
sound,
|
sound,
|
||||||
sessionInvite,
|
sessionInvite,
|
||||||
object,
|
object;
|
||||||
|
|
||||||
|
static const Map<MessageType, String> _mapper = {
|
||||||
|
MessageType.text: "Text",
|
||||||
|
MessageType.sound: "Sound",
|
||||||
|
MessageType.sessionInvite: "SessionInvite",
|
||||||
|
MessageType.object: "Object",
|
||||||
|
};
|
||||||
|
|
||||||
|
factory MessageType.fromName(String name) {
|
||||||
|
return MessageType.values.firstWhere((element) => element.name.toLowerCase() == name.toLowerCase(),
|
||||||
|
orElse: () => MessageType.unknown,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? toName() {
|
||||||
|
return _mapper[this];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Message {
|
class Message {
|
||||||
|
@ -20,11 +39,9 @@ class Message {
|
||||||
required this.content, required this.sendTime});
|
required this.content, required this.sendTime});
|
||||||
|
|
||||||
factory Message.fromMap(Map map) {
|
factory Message.fromMap(Map map) {
|
||||||
final typeString = map["messageType"] as String?;
|
final typeString = (map["messageType"] as String?) ?? "";
|
||||||
final type = MessageType.values.firstWhere((element) => element.name.toLowerCase() == typeString?.toLowerCase(),
|
final type = MessageType.fromName(typeString);
|
||||||
orElse: () => MessageType.unknown,
|
if (type == MessageType.unknown && typeString.isNotEmpty) {
|
||||||
);
|
|
||||||
if (type == MessageType.unknown && typeString != null) {
|
|
||||||
log("Unknown MessageType '$typeString' in response");
|
log("Unknown MessageType '$typeString' in response");
|
||||||
}
|
}
|
||||||
return Message(
|
return Message(
|
||||||
|
@ -36,4 +53,18 @@ class Message {
|
||||||
sendTime: DateTime.parse(map["sendTime"]),
|
sendTime: DateTime.parse(map["sendTime"]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map toMap() => {
|
||||||
|
"id": id,
|
||||||
|
"recipientId": recipientId,
|
||||||
|
"senderId": senderId,
|
||||||
|
"ownerId": senderId,
|
||||||
|
"messageType": type.toName(),
|
||||||
|
"content": content,
|
||||||
|
"sendTime": sendTime.toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
static String generateId() {
|
||||||
|
return "MSG-${const Uuid().v4()}";
|
||||||
|
}
|
||||||
}
|
}
|
20
lib/models/user_profile.dart
Normal file
20
lib/models/user_profile.dart
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import 'package:contacts_plus/config.dart';
|
||||||
|
|
||||||
|
class UserProfile {
|
||||||
|
final String iconUrl;
|
||||||
|
|
||||||
|
UserProfile({required this.iconUrl});
|
||||||
|
|
||||||
|
factory UserProfile.fromMap(Map map) {
|
||||||
|
return UserProfile(iconUrl: map["iconUrl"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri get httpIconUri {
|
||||||
|
final fullUri = iconUrl.replaceFirst("neosdb:///", Config.neosCdnUrl);
|
||||||
|
final lastPeriodIndex = fullUri.lastIndexOf(".");
|
||||||
|
if (lastPeriodIndex != -1 && fullUri.length - lastPeriodIndex < 8) {
|
||||||
|
return Uri.parse(fullUri.substring(0, lastPeriodIndex));
|
||||||
|
}
|
||||||
|
return Uri.parse(fullUri);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:contacts_plus/apis/friend_api.dart';
|
import 'package:contacts_plus/apis/friend_api.dart';
|
||||||
|
import 'package:contacts_plus/aux.dart';
|
||||||
import 'package:contacts_plus/main.dart';
|
import 'package:contacts_plus/main.dart';
|
||||||
import 'package:contacts_plus/models/friend.dart';
|
import 'package:contacts_plus/models/friend.dart';
|
||||||
import 'package:contacts_plus/widgets/messages.dart';
|
import 'package:contacts_plus/widgets/messages.dart';
|
||||||
|
@ -13,15 +15,20 @@ class HomeScreen extends StatefulWidget {
|
||||||
|
|
||||||
class _HomeScreenState extends State<HomeScreen> {
|
class _HomeScreenState extends State<HomeScreen> {
|
||||||
Future<List<Friend>>? _friendsFuture;
|
Future<List<Friend>>? _friendsFuture;
|
||||||
|
ClientHolder? _clientHolder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void didChangeDependencies() {
|
||||||
super.initState();
|
super.didChangeDependencies();
|
||||||
_refreshFriendsList();
|
final clientHolder = ClientHolder.of(context);
|
||||||
|
if (_clientHolder != clientHolder) {
|
||||||
|
_clientHolder = clientHolder;
|
||||||
|
_refreshFriendsList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _refreshFriendsList() {
|
void _refreshFriendsList() {
|
||||||
_friendsFuture = FriendApi.getFriendsList().then((Iterable<Friend> value) =>
|
_friendsFuture = FriendApi.getFriendsList(_clientHolder!.client).then((Iterable<Friend> value) =>
|
||||||
value.toList()
|
value.toList()
|
||||||
..sort((a, b) {
|
..sort((a, b) {
|
||||||
if (a.userStatus.onlineStatus == b.userStatus.onlineStatus) {
|
if (a.userStatus.onlineStatus == b.userStatus.onlineStatus) {
|
||||||
|
@ -40,6 +47,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final apiClient = ClientHolder.of(context).client;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text("Contacts+"),
|
title: const Text("Contacts+"),
|
||||||
|
@ -58,11 +66,27 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||||
itemCount: data.length,
|
itemCount: data.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final entry = data.elementAt(index);
|
final entry = data.elementAt(index);
|
||||||
|
final iconUri = entry.userProfile.httpIconUri.toString();
|
||||||
return ListTile(
|
return ListTile(
|
||||||
|
leading: CachedNetworkImage(
|
||||||
|
imageBuilder: (context, imageProvider) {
|
||||||
|
return CircleAvatar(
|
||||||
|
foregroundImage: imageProvider,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
imageUrl: iconUri,
|
||||||
|
placeholder: (context, url) {
|
||||||
|
return const CircleAvatar(backgroundColor: Colors.white54,);
|
||||||
|
},
|
||||||
|
errorWidget: (context, error, what) => const CircleAvatar(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
child: Icon(Icons.person),
|
||||||
|
),
|
||||||
|
),
|
||||||
title: Text(entry.username),
|
title: Text(entry.username),
|
||||||
subtitle: Text(entry.userStatus.onlineStatus.name),
|
subtitle: Text(entry.userStatus.onlineStatus.name),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (context) => Messages(friend: entry)));
|
Navigator.of(context).push(MaterialPageRoute(builder: (context) => Messages(friend: entry)));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:contacts_plus/api_client.dart';
|
|
||||||
import 'package:contacts_plus/apis/message_api.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/friend.dart';
|
||||||
import 'package:contacts_plus/models/message.dart';
|
import 'package:contacts_plus/models/message.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -18,22 +18,27 @@ class Messages extends StatefulWidget {
|
||||||
class _MessagesState extends State<Messages> {
|
class _MessagesState extends State<Messages> {
|
||||||
Future<Iterable<Message>>? _messagesFuture;
|
Future<Iterable<Message>>? _messagesFuture;
|
||||||
final TextEditingController _messageTextController = TextEditingController();
|
final TextEditingController _messageTextController = TextEditingController();
|
||||||
|
ClientHolder? _clientHolder;
|
||||||
|
|
||||||
bool _isSendable = false;
|
bool _isSendable = false;
|
||||||
|
|
||||||
void _refreshMessages() {
|
void _refreshMessages() {
|
||||||
_messagesFuture = MessageApi.getUserMessages(userId: widget.friend.id)..then((value) => value.toList());
|
_messagesFuture = MessageApi.getUserMessages(_clientHolder!.client, userId: widget.friend.id)..then((value) => value.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void didChangeDependencies() {
|
||||||
super.initState();
|
super.didChangeDependencies();
|
||||||
_refreshMessages();
|
final clientHolder = ClientHolder.of(context);
|
||||||
|
if (_clientHolder != clientHolder) {
|
||||||
|
_clientHolder = clientHolder;
|
||||||
|
_refreshMessages();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final apiClient = ApiClient();
|
final apiClient = ClientHolder.of(context).client;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(widget.friend.username),
|
title: Text(widget.friend.username),
|
||||||
|
@ -110,7 +115,33 @@ class _MessagesState extends State<Messages> {
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
splashRadius: 24,
|
splashRadius: 24,
|
||||||
onPressed: _isSendable ? () {} : null,
|
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 {
|
||||||
|
await apiClient.hub.sendMessage(message);
|
||||||
|
_messageTextController.clear();
|
||||||
|
} catch (e) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text("Failed to send message\n$e",
|
||||||
|
maxLines: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_isSendable = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} : null,
|
||||||
iconSize: 28,
|
iconSize: 28,
|
||||||
icon: const Icon(Icons.send),
|
icon: const Icon(Icons.send),
|
||||||
),
|
),
|
||||||
|
|
188
pubspec.lock
188
pubspec.lock
|
@ -17,6 +17,30 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.1"
|
||||||
|
cached_network_image:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cached_network_image
|
||||||
|
sha256: fd3d0dc1d451f9a252b32d95d3f0c3c487bc41a75eba2e6097cb0b9c71491b15
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.3"
|
||||||
|
cached_network_image_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_platform_interface
|
||||||
|
sha256: bb2b8403b4ccdc60ef5f25c70dead1f3d32d24b9d6117cfc087f496b178594a7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
|
cached_network_image_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_web
|
||||||
|
sha256: b8eb814ebfcb4dea049680f8c1ffb2df399e4d03bf7a352c775e26fa06e02fa0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -65,11 +89,43 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.1"
|
||||||
|
ffi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi
|
||||||
|
sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.1"
|
||||||
|
file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.4"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_blurhash:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_blurhash
|
||||||
|
sha256: "05001537bd3fac7644fa6558b09ec8c0a3f2eba78c0765f88912882b1331a5c6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.0"
|
||||||
|
flutter_cache_manager:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_cache_manager
|
||||||
|
sha256: "32cd900555219333326a2d0653aaaf8671264c29befa65bbd9856d204a4c9fb3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.3.0"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
|
@ -177,7 +233,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.1"
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: logging
|
name: logging
|
||||||
sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d"
|
sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d"
|
||||||
|
@ -216,6 +272,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.0"
|
version: "1.8.0"
|
||||||
|
octo_image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: octo_image
|
||||||
|
sha256: "107f3ed1330006a3bea63615e81cf637433f5135a52466c7caa0e7152bca9143"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
path:
|
path:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -224,6 +288,70 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.2"
|
version: "1.8.2"
|
||||||
|
path_provider:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider
|
||||||
|
sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.14"
|
||||||
|
path_provider_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_android
|
||||||
|
sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.27"
|
||||||
|
path_provider_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_foundation
|
||||||
|
sha256: ad4c4d011830462633f03eb34445a45345673dfd4faf1ab0b4735fbd93b19183
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.2"
|
||||||
|
path_provider_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_linux
|
||||||
|
sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.10"
|
||||||
|
path_provider_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_platform_interface
|
||||||
|
sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.6"
|
||||||
|
path_provider_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_windows
|
||||||
|
sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.6"
|
||||||
|
pedantic:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pedantic
|
||||||
|
sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.11.1"
|
||||||
|
platform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: platform
|
||||||
|
sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -232,6 +360,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
|
process:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: process
|
||||||
|
sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.2.4"
|
||||||
|
rxdart:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: rxdart
|
||||||
|
sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.27.7"
|
||||||
signalr_netcore:
|
signalr_netcore:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -253,6 +397,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.9.1"
|
||||||
|
sqflite:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite
|
||||||
|
sha256: "8453780d1f703ead201a39673deb93decf85d543f359f750e2afc4908b55533f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.8"
|
||||||
|
sqflite_common:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_common
|
||||||
|
sha256: e77abf6ff961d69dfef41daccbb66b51e9983cdd5cb35bf30733598057401555
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.5"
|
||||||
sse_client:
|
sse_client:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -285,6 +445,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.2.0"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -341,6 +509,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.0"
|
version: "2.4.0"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
sha256: dd8f9344bc305ae2923e3d11a2a911d9a4e2c7dd6fe0ed10626d63211a69676e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.3"
|
||||||
|
xdg_directories:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xdg_directories
|
||||||
|
sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.19.6 <3.0.0"
|
dart: ">=2.19.6 <3.0.0"
|
||||||
flutter: ">=2.0.0"
|
flutter: ">=3.3.0"
|
||||||
|
|
|
@ -41,6 +41,8 @@ dependencies:
|
||||||
intl: ^0.18.1
|
intl: ^0.18.1
|
||||||
path: ^1.8.2
|
path: ^1.8.2
|
||||||
signalr_netcore: ^1.3.3
|
signalr_netcore: ^1.3.3
|
||||||
|
logging: ^1.1.1
|
||||||
|
cached_network_image: ^3.2.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
Loading…
Reference in a new issue