Add basic friends list
This commit is contained in:
parent
8aeab5ba28
commit
a9a14c09b7
12 changed files with 448 additions and 58 deletions
44
.gitignore
vendored
Normal file
44
.gitignore
vendored
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.packages
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
33
.metadata
Normal file
33
.metadata
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
|
||||||
|
channel: stable
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
|
||||||
|
base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
|
||||||
|
- platform: android
|
||||||
|
create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
|
||||||
|
base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
|
||||||
|
- platform: linux
|
||||||
|
create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
|
||||||
|
base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
|
@ -11,12 +11,20 @@ class ApiClient {
|
||||||
static const String userIdKey = "userId";
|
static const String userIdKey = "userId";
|
||||||
static const String machineIdKey = "machineId";
|
static const String machineIdKey = "machineId";
|
||||||
static const String tokenKey = "token";
|
static const String tokenKey = "token";
|
||||||
|
static const String passwordKey = "password";
|
||||||
|
|
||||||
final AuthenticationData _authenticationData;
|
final AuthenticationData _authenticationData;
|
||||||
|
|
||||||
|
String get userId => _authenticationData.userId;
|
||||||
|
|
||||||
const ApiClient({required AuthenticationData authenticationData}) : _authenticationData = authenticationData;
|
const ApiClient({required AuthenticationData authenticationData}) : _authenticationData = authenticationData;
|
||||||
|
|
||||||
static Future<AuthenticationData> tryLogin({required String username, required String password, bool rememberMe=false}) async {
|
static Future<AuthenticationData> tryLogin({
|
||||||
|
required String username,
|
||||||
|
required String password,
|
||||||
|
bool rememberMe=true,
|
||||||
|
bool rememberPass=false
|
||||||
|
}) async {
|
||||||
final body = {
|
final body = {
|
||||||
"username": username,
|
"username": username,
|
||||||
"password": password,
|
"password": password,
|
||||||
|
@ -24,21 +32,21 @@ class ApiClient {
|
||||||
"secretMachineId": const Uuid().v4(),
|
"secretMachineId": const Uuid().v4(),
|
||||||
};
|
};
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
Uri.parse("${Config.apiBaseUrl}/api/UserSessions"),
|
buildFullUri("/UserSessions"),
|
||||||
headers: {"Content-Type": "application/json"},
|
headers: {"Content-Type": "application/json"},
|
||||||
body: jsonEncode(body));
|
body: jsonEncode(body));
|
||||||
if (response.statusCode == 400) {
|
if (response.statusCode == 400) {
|
||||||
throw "Invalid Credentials";
|
throw "Invalid Credentials";
|
||||||
} else if (response.statusCode != 200) {
|
|
||||||
throw "Unknown Error${kDebugMode ? ": ${response.statusCode}|${response.body}" : ""}";
|
|
||||||
}
|
}
|
||||||
|
checkResponse(response);
|
||||||
|
|
||||||
final authData = AuthenticationData.fromJson(jsonDecode(response.body));
|
final authData = AuthenticationData.fromMap(jsonDecode(response.body));
|
||||||
if (authData.isAuthenticated) {
|
if (authData.isAuthenticated) {
|
||||||
const FlutterSecureStorage storage = FlutterSecureStorage();
|
const FlutterSecureStorage storage = FlutterSecureStorage();
|
||||||
await storage.write(key: userIdKey, value: authData.userId);
|
await storage.write(key: userIdKey, value: authData.userId);
|
||||||
await storage.write(key: machineIdKey, value: authData.secretMachineId);
|
await storage.write(key: machineIdKey, value: authData.secretMachineId);
|
||||||
await storage.write(key: tokenKey, value: authData.token);
|
await storage.write(key: tokenKey, value: authData.token);
|
||||||
|
if (rememberPass) await storage.write(key: passwordKey, value: password);
|
||||||
}
|
}
|
||||||
return authData;
|
return authData;
|
||||||
}
|
}
|
||||||
|
@ -48,46 +56,67 @@ class ApiClient {
|
||||||
String? userId = await storage.read(key: userIdKey);
|
String? userId = await storage.read(key: userIdKey);
|
||||||
String? machineId = await storage.read(key: machineIdKey);
|
String? machineId = await storage.read(key: machineIdKey);
|
||||||
String? token = await storage.read(key: tokenKey);
|
String? token = await storage.read(key: tokenKey);
|
||||||
|
String? password = await storage.read(key: passwordKey);
|
||||||
|
|
||||||
if (userId == null || machineId == null || token == null) {
|
if (userId == null || machineId == null) {
|
||||||
return AuthenticationData.unauthenticated();
|
return AuthenticationData.unauthenticated();
|
||||||
}
|
}
|
||||||
|
|
||||||
final response = await http.get(Uri.parse("${Config.apiBaseUrl}/api/users/$userId"), headers: {
|
if (token != null) {
|
||||||
"Authorization": "neos $userId:$token"
|
final response = await http.get(buildFullUri("/users/$userId"), headers: {
|
||||||
});
|
"Authorization": "neos $userId:$token"
|
||||||
if (response.statusCode == 200) {
|
});
|
||||||
return AuthenticationData(userId: userId, token: token, secretMachineId: machineId, isAuthenticated: true);
|
if (response.statusCode == 200) {
|
||||||
|
return AuthenticationData(userId: userId, token: token, secretMachineId: machineId, isAuthenticated: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password != null) {
|
||||||
|
try {
|
||||||
|
userId = userId.startsWith("U-") ? userId.replaceRange(0, 2, "") : userId;
|
||||||
|
final loginResult = await tryLogin(username: userId, password: password, rememberPass: true);
|
||||||
|
if (loginResult.isAuthenticated) return loginResult;
|
||||||
|
} catch (_) {
|
||||||
|
// We don't need to notify the user if the cached login fails behind the scenes, so just ignore any exceptions.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return AuthenticationData.unauthenticated();
|
return AuthenticationData.unauthenticated();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void checkResponse(http.Response response) {
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw "Unknown Error${kDebugMode ? ": ${response.statusCode}|${response.body}" : ""}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, String> get authorizationHeader => {
|
Map<String, String> get authorizationHeader => {
|
||||||
"Authorization": "neos ${_authenticationData.userId}:${_authenticationData.token}"
|
"Authorization": "neos ${_authenticationData.userId}:${_authenticationData.token}"
|
||||||
};
|
};
|
||||||
|
|
||||||
Future<http.Response> get(Uri uri, {Map<String, String>? headers}) {
|
static Uri buildFullUri(String path) => Uri.parse("${Config.apiBaseUrl}/api$path");
|
||||||
|
|
||||||
|
Future<http.Response> get(String path, {Map<String, String>? headers}) {
|
||||||
headers ??= {};
|
headers ??= {};
|
||||||
headers.addAll(authorizationHeader);
|
headers.addAll(authorizationHeader);
|
||||||
return http.get(uri, headers: headers);
|
return http.get(buildFullUri(path), headers: headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<http.Response> post(Uri uri, {Object? body, Map<String, String>? headers}) {
|
Future<http.Response> post(String path, {Object? body, Map<String, String>? headers}) {
|
||||||
headers ??= {};
|
headers ??= {};
|
||||||
headers["Content-Type"] = "application/json";
|
headers["Content-Type"] = "application/json";
|
||||||
headers.addAll(authorizationHeader);
|
headers.addAll(authorizationHeader);
|
||||||
return http.post(uri, headers: headers, body: body);
|
return http.post(buildFullUri(path), headers: headers, body: body);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<http.Response> put(Uri uri, {Object? body, Map<String, String>? headers}) {
|
Future<http.Response> put(String path, {Object? body, Map<String, String>? headers}) {
|
||||||
headers ??= {};
|
headers ??= {};
|
||||||
headers.addAll(authorizationHeader);
|
headers.addAll(authorizationHeader);
|
||||||
return http.put(uri, headers: headers, body: body);
|
return http.put(buildFullUri(path), headers: headers, body: body);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<http.Response> delete(Uri uri, {Map<String, String>? headers}) {
|
Future<http.Response> delete(String path, {Map<String, String>? headers}) {
|
||||||
headers ??= {};
|
headers ??= {};
|
||||||
headers.addAll(authorizationHeader);
|
headers.addAll(authorizationHeader);
|
||||||
return http.delete(uri, headers: headers);
|
return http.delete(buildFullUri(path), headers: headers);
|
||||||
}
|
}
|
||||||
}
|
}
|
19
lib/apis/friend_api.dart
Normal file
19
lib/apis/friend_api.dart
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
|
||||||
|
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");
|
||||||
|
ApiClient.checkResponse(response);
|
||||||
|
final data = jsonDecode(response.body) as List;
|
||||||
|
return data.map((e) => Friend.fromMap(e));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +0,0 @@
|
||||||
|
|
||||||
import 'package:contacts_plus/models/friend.dart';
|
|
||||||
|
|
||||||
class FriendsApi {
|
|
||||||
|
|
||||||
static Future<List<Friend>> getFriendsList() async {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
23
lib/apis/message_api.dart
Normal file
23
lib/apis/message_api.dart
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
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"
|
||||||
|
"?maxItems=$maxItems"
|
||||||
|
"${fromTime == null ? "" : "&fromTime${fromTime.toLocal().toIso8601String()}"}"
|
||||||
|
"${userId.isEmpty ? "" : "&user=$userId"}"
|
||||||
|
"&unread=$unreadOnly"
|
||||||
|
);
|
||||||
|
ApiClient.checkResponse(response);
|
||||||
|
final data = jsonDecode(response.body) as List;
|
||||||
|
return data.map((e) => Message.fromMap(e));
|
||||||
|
}
|
||||||
|
}
|
|
@ -70,6 +70,12 @@ class AuthenticatedClient extends InheritedWidget {
|
||||||
return result!;
|
return result!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static AuthenticatedClient staticOf(BuildContext context) {
|
||||||
|
final result = context.findAncestorWidgetOfExactType<AuthenticatedClient>();
|
||||||
|
assert(result != null, 'No AuthenticatedClient found in context');
|
||||||
|
return result!;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool updateShouldNotify(covariant AuthenticatedClient oldWidget) => oldWidget.client != client;
|
bool updateShouldNotify(covariant AuthenticatedClient oldWidget) => oldWidget.client != client;
|
||||||
}
|
}
|
|
@ -9,10 +9,10 @@ class AuthenticationData {
|
||||||
required this.userId, required this.token, required this.secretMachineId, required this.isAuthenticated
|
required this.userId, required this.token, required this.secretMachineId, required this.isAuthenticated
|
||||||
});
|
});
|
||||||
|
|
||||||
factory AuthenticationData.fromJson(Map json) {
|
factory AuthenticationData.fromMap(Map map) {
|
||||||
final userId = json["userId"];
|
final userId = map["userId"];
|
||||||
final token = json["token"];
|
final token = map["token"];
|
||||||
final machineId = json["secretMachineId"];
|
final machineId = map["secretMachineId"];
|
||||||
if (userId == null || token == null || machineId == null) {
|
if (userId == null || token == null || machineId == null) {
|
||||||
return _unauthenticated;
|
return _unauthenticated;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,37 @@
|
||||||
class Friend {
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
class Friend extends Comparable {
|
||||||
final String id;
|
final String id;
|
||||||
final String username;
|
final String username;
|
||||||
final UserStatus userStatus;
|
final UserStatus userStatus;
|
||||||
|
|
||||||
Friend({required this.id, required this.username, required this.userStatus});
|
Friend({required this.id, required this.username, required this.userStatus});
|
||||||
|
|
||||||
|
factory Friend.fromMap(Map map) {
|
||||||
|
return Friend(id: map["id"], username: map["friendUsername"], userStatus: UserStatus.fromMap(map["userStatus"]));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int compareTo(other) {
|
||||||
|
if (userStatus.onlineStatus == other.userStatus.onlineStatus) {
|
||||||
|
return userStatus.lastStatusChange.compareTo(other.userStatus.lastStatusChange);
|
||||||
|
} else {
|
||||||
|
if (userStatus.onlineStatus == OnlineStatus.online) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum OnlineStatus {
|
enum OnlineStatus {
|
||||||
|
unknown,
|
||||||
offline,
|
offline,
|
||||||
|
away,
|
||||||
|
busy,
|
||||||
online,
|
online,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,4 +40,18 @@ class UserStatus {
|
||||||
final DateTime lastStatusChange;
|
final DateTime lastStatusChange;
|
||||||
|
|
||||||
UserStatus({required this.onlineStatus, required this.lastStatusChange});
|
UserStatus({required this.onlineStatus, required this.lastStatusChange});
|
||||||
|
|
||||||
|
factory UserStatus.fromMap(Map map) {
|
||||||
|
final statusString = map["onlineStatus"] as String?;
|
||||||
|
final status = OnlineStatus.values.firstWhere((element) => element.name == statusString?.toLowerCase(),
|
||||||
|
orElse: () => OnlineStatus.unknown,
|
||||||
|
);
|
||||||
|
if (status == OnlineStatus.unknown && statusString != null) {
|
||||||
|
log("Unknown OnlineStatus '$statusString' in response");
|
||||||
|
}
|
||||||
|
return UserStatus(
|
||||||
|
onlineStatus: status,
|
||||||
|
lastStatusChange: DateTime.parse(map["lastStatusChange"]),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
34
lib/models/message.dart
Normal file
34
lib/models/message.dart
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
|
enum MessageType {
|
||||||
|
unknown,
|
||||||
|
text,
|
||||||
|
sound,
|
||||||
|
}
|
||||||
|
|
||||||
|
class Message {
|
||||||
|
final String id;
|
||||||
|
final String recipientId;
|
||||||
|
final String senderId;
|
||||||
|
final MessageType type;
|
||||||
|
final String content;
|
||||||
|
|
||||||
|
Message({required this.id, required this.recipientId, required this.senderId, required this.type, required this.content});
|
||||||
|
|
||||||
|
factory Message.fromMap(Map map) {
|
||||||
|
final typeString = map["messageType"] as String?;
|
||||||
|
final type = MessageType.values.firstWhere((element) => element.name == typeString?.toLowerCase(),
|
||||||
|
orElse: () => MessageType.unknown,
|
||||||
|
);
|
||||||
|
if (type == MessageType.unknown && typeString != null) {
|
||||||
|
log("Unknown MessageType '$typeString' in response");
|
||||||
|
}
|
||||||
|
return Message(
|
||||||
|
id: map["id"],
|
||||||
|
recipientId: map["recipient_id"],
|
||||||
|
senderId: map["sender_id"],
|
||||||
|
type: type,
|
||||||
|
content: map["content"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,7 @@
|
||||||
|
import 'package:contacts_plus/apis/friend_api.dart';
|
||||||
|
import 'package:contacts_plus/main.dart';
|
||||||
|
import 'package:contacts_plus/models/friend.dart';
|
||||||
|
import 'package:contacts_plus/widgets/messages.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends StatefulWidget {
|
||||||
|
@ -9,6 +13,35 @@ class HomeScreen extends StatefulWidget {
|
||||||
|
|
||||||
class _HomeScreenState extends State<HomeScreen> {
|
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) =>
|
||||||
|
value.toList()
|
||||||
|
..sort((a, b) {
|
||||||
|
if (a.userStatus.onlineStatus == b.userStatus.onlineStatus) {
|
||||||
|
return a.userStatus.lastStatusChange.compareTo(b.userStatus.lastStatusChange);
|
||||||
|
} else {
|
||||||
|
if (a.userStatus.onlineStatus == OnlineStatus.online) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -16,31 +49,48 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text("Contacts+"),
|
title: const Text("Contacts+"),
|
||||||
),
|
),
|
||||||
body: FutureBuilder(
|
body: RefreshIndicator(
|
||||||
builder: (context, snapshot) {
|
onRefresh: () async {
|
||||||
if (snapshot.hasData) {
|
_refreshFriendsList();
|
||||||
return ListView.builder(
|
await _friendsFuture;
|
||||||
itemBuilder: (context, index) {
|
},
|
||||||
|
child: FutureBuilder(
|
||||||
},
|
future: _friendsFuture,
|
||||||
);
|
builder: (context, snapshot) {
|
||||||
} else if (snapshot.hasError) {
|
if (snapshot.hasData) {
|
||||||
return Center(child: Padding(
|
final data = snapshot.data as Iterable<Friend>;
|
||||||
padding: const EdgeInsets.all(64),
|
return ListView.builder(
|
||||||
child: Text(
|
itemCount: data.length,
|
||||||
"Something went wrong: ${snapshot.error}",
|
itemBuilder: (context, index) {
|
||||||
softWrap: true,
|
final entry = data.elementAt(index);
|
||||||
style: Theme
|
return ListTile(
|
||||||
.of(context)
|
title: Text(entry.username),
|
||||||
.textTheme
|
subtitle: Text(entry.userStatus.onlineStatus.name),
|
||||||
.labelMedium,
|
onTap: () {
|
||||||
),
|
Navigator.push(context, MaterialPageRoute(builder: (context) => Messages(friend: entry)));
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
} else {
|
},
|
||||||
return const LinearProgressIndicator();
|
);
|
||||||
|
} else if (snapshot.hasError) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(64),
|
||||||
|
child: Text(
|
||||||
|
"Something went wrong: ${snapshot.error}",
|
||||||
|
softWrap: true,
|
||||||
|
style: Theme
|
||||||
|
.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const LinearProgressIndicator();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
123
lib/widgets/messages.dart
Normal file
123
lib/widgets/messages.dart
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
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';
|
||||||
|
|
||||||
|
class Messages extends StatefulWidget {
|
||||||
|
const Messages({required this.friend, super.key});
|
||||||
|
|
||||||
|
final Friend friend;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _MessagesState();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_messageApi = MessageApi(
|
||||||
|
apiClient: AuthenticatedClient
|
||||||
|
.staticOf(context)
|
||||||
|
.client,
|
||||||
|
);
|
||||||
|
_refreshMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final apiClient = AuthenticatedClient.of(context).client;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(widget.friend.username),
|
||||||
|
),
|
||||||
|
body: FutureBuilder(
|
||||||
|
future: _messagesFuture,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
final data = snapshot.data as Iterable<Message>;
|
||||||
|
return ListView.builder(
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if (snapshot.hasError) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Text("Failed to load messages:\n${snapshot.error}"),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
label: const Text("Retry"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const LinearProgressIndicator();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyMessageBubble extends StatelessWidget {
|
||||||
|
const MyMessageBubble({required this.message, super.key});
|
||||||
|
|
||||||
|
final Message message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
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,),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class OtherMessageBubble extends StatelessWidget {
|
||||||
|
const OtherMessageBubble({required this.message, super.key});
|
||||||
|
|
||||||
|
final Message message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
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,),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue