From b409ddaac8c0a1646cc55e2dc178b711a3c46168 Mon Sep 17 00:00:00 2001 From: Tovi Jaeschke-Rogers Date: Mon, 25 Jul 2022 19:39:27 +0930 Subject: [PATCH] Fix conversation creation & slight polish on converstation pages --- Backend/Api/Messages/Conversations.go | 1 - Backend/Api/Messages/CreateConversation.go | 11 ++- Backend/Database/ConversationDetails.go | 2 +- Backend/Database/UserConversations.go | 11 +-- mobile/lib/models/conversation_users.dart | 99 +++++++++---------- mobile/lib/models/conversations.dart | 81 ++++++++++----- mobile/lib/utils/storage/conversations.dart | 28 +++--- mobile/lib/utils/storage/database.dart | 7 +- mobile/lib/utils/storage/messages.dart | 14 +-- mobile/lib/utils/time.dart | 19 ++++ .../lib/views/main/conversation/detail.dart | 20 +--- .../views/main/conversation/edit_details.dart | 17 +++- .../views/main/conversation/list_item.dart | 43 +++++++- .../lib/views/main/conversation/settings.dart | 16 ++- .../conversation/settings_user_list_item.dart | 8 +- 15 files changed, 225 insertions(+), 152 deletions(-) create mode 100644 mobile/lib/utils/time.dart diff --git a/Backend/Api/Messages/Conversations.go b/Backend/Api/Messages/Conversations.go index c0beeee..4475790 100644 --- a/Backend/Api/Messages/Conversations.go +++ b/Backend/Api/Messages/Conversations.go @@ -79,5 +79,4 @@ func EncryptedConversationDetailsList(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write(returnJson) - } diff --git a/Backend/Api/Messages/CreateConversation.go b/Backend/Api/Messages/CreateConversation.go index b2dccd9..a806473 100644 --- a/Backend/Api/Messages/CreateConversation.go +++ b/Backend/Api/Messages/CreateConversation.go @@ -2,7 +2,9 @@ package Messages import ( "encoding/json" + "fmt" "net/http" + "github.com/gofrs/uuid" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" @@ -30,9 +32,9 @@ func CreateConversation(w http.ResponseWriter, r *http.Request) { } messageThread = Models.ConversationDetail{ - Base: Models.Base{ - ID: uuid.FromStringOrNil(rawConversationData.ID), - }, + Base: Models.Base{ + ID: uuid.FromStringOrNil(rawConversationData.ID), + }, Name: rawConversationData.Name, Users: rawConversationData.Users, } @@ -43,9 +45,10 @@ func CreateConversation(w http.ResponseWriter, r *http.Request) { return } + fmt.Println(rawConversationData.UserConversations[0]) + err = Database.CreateUserConversations(&rawConversationData.UserConversations) if err != nil { - panic(err) http.Error(w, "Error", http.StatusInternalServerError) return } diff --git a/Backend/Database/ConversationDetails.go b/Backend/Database/ConversationDetails.go index cd1140d..9893022 100644 --- a/Backend/Database/ConversationDetails.go +++ b/Backend/Database/ConversationDetails.go @@ -29,7 +29,7 @@ func GetConversationDetailsByIds(id []string) ([]Models.ConversationDetail, erro err = DB.Preload(clause.Associations). Where("id IN ?", id). - First(&messageThread). + Find(&messageThread). Error return messageThread, err diff --git a/Backend/Database/UserConversations.go b/Backend/Database/UserConversations.go index 3623fda..02fb62d 100644 --- a/Backend/Database/UserConversations.go +++ b/Backend/Database/UserConversations.go @@ -31,9 +31,7 @@ func GetUserConversationsByUserId(id string) ([]Models.UserConversation, error) } func CreateUserConversation(userConversation *Models.UserConversation) error { - var ( - err error - ) + var err error err = DB.Session(&gorm.Session{FullSaveAssociations: true}). Create(userConversation). @@ -43,12 +41,9 @@ func CreateUserConversation(userConversation *Models.UserConversation) error { } func CreateUserConversations(userConversations *[]Models.UserConversation) error { - var ( - err error - ) + var err error - err = DB.Session(&gorm.Session{FullSaveAssociations: true}). - Create(userConversations). + err = DB.Create(userConversations). Error return err diff --git a/mobile/lib/models/conversation_users.dart b/mobile/lib/models/conversation_users.dart index 34e6e40..3417792 100644 --- a/mobile/lib/models/conversation_users.dart +++ b/mobile/lib/models/conversation_users.dart @@ -1,5 +1,52 @@ -import '/utils/storage/database.dart'; import '/models/conversations.dart'; +import '/utils/storage/database.dart'; + +Future getConversationUser(Conversation conversation, String userId) async { + final db = await getDatabaseConnection(); + + final List> maps = await db.query( + 'conversation_users', + where: 'conversation_id = ? AND user_id = ?', + whereArgs: [conversation.id, userId], + ); + + if (maps.length != 1) { + throw ArgumentError('Invalid conversation_id or username'); + } + + return ConversationUser( + id: maps[0]['id'], + userId: maps[0]['user_id'], + conversationId: maps[0]['conversation_id'], + username: maps[0]['username'], + associationKey: maps[0]['association_key'], + admin: maps[0]['admin'] == 1, + ); + +} + +// A method that retrieves all the dogs from the dogs table. +Future> getConversationUsers(Conversation conversation) async { + final db = await getDatabaseConnection(); + + final List> maps = await db.query( + 'conversation_users', + where: 'conversation_id = ?', + whereArgs: [conversation.id], + orderBy: 'admin', + ); + + return List.generate(maps.length, (i) { + return ConversationUser( + id: maps[i]['id'], + userId: maps[i]['user_id'], + conversationId: maps[i]['conversation_id'], + username: maps[i]['username'], + associationKey: maps[i]['association_key'], + admin: maps[i]['admin'] == 1, + ); + }); +} class ConversationUser{ String id; @@ -33,7 +80,7 @@ class ConversationUser{ 'id': id, 'user_id': userId, 'username': username, - 'associationKey': associationKey, + 'association_key': associationKey, 'admin': admin ? 'true' : 'false', }; } @@ -49,51 +96,3 @@ class ConversationUser{ }; } } - -// A method that retrieves all the dogs from the dogs table. -Future> getConversationUsers(Conversation conversation) async { - final db = await getDatabaseConnection(); - - final List> maps = await db.query( - 'conversation_users', - where: 'conversation_id = ?', - whereArgs: [conversation.id], - orderBy: 'admin', - ); - - return List.generate(maps.length, (i) { - return ConversationUser( - id: maps[i]['id'], - userId: maps[i]['user_id'], - conversationId: maps[i]['conversation_id'], - username: maps[i]['username'], - associationKey: maps[i]['association_key'], - admin: maps[i]['admin'] == 1, - ); - }); -} - -Future getConversationUser(Conversation conversation, String userId) async { - final db = await getDatabaseConnection(); - - - final List> maps = await db.query( - 'conversation_users', - where: 'conversation_id = ? AND user_id = ?', - whereArgs: [conversation.id, userId], - ); - - if (maps.length != 1) { - throw ArgumentError('Invalid conversation_id or username'); - } - - return ConversationUser( - id: maps[0]['id'], - userId: maps[0]['user_id'], - conversationId: maps[0]['conversation_id'], - username: maps[0]['username'], - associationKey: maps[0]['association_key'], - admin: maps[0]['admin'] == 1, - ); - -} diff --git a/mobile/lib/models/conversations.dart b/mobile/lib/models/conversations.dart index 3494e48..88adea3 100644 --- a/mobile/lib/models/conversations.dart +++ b/mobile/lib/models/conversations.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:Envelope/models/messages.dart'; import 'package:pointycastle/export.dart'; import 'package:sqflite/sqflite.dart'; import 'package:uuid/uuid.dart'; @@ -20,6 +21,7 @@ Future createConversation(String title, List friends) asyn var uuid = const Uuid(); final String conversationId = uuid.v4(); + final String conversationDetailId = uuid.v4(); Uint8List symmetricKey = AesHelper.deriveKey(generateRandomString(32)); @@ -28,11 +30,12 @@ Future createConversation(String title, List friends) asyn Conversation conversation = Conversation( id: conversationId, userId: profile.id, - conversationDetailId: '', + conversationDetailId: conversationDetailId, symmetricKey: base64.encode(symmetricKey), admin: true, name: title, status: ConversationStatus.pending, + isRead: true, ); await db.insert( @@ -51,7 +54,7 @@ Future createConversation(String title, List friends) asyn associationKey: associationKey, admin: true, ).toMap(), - conflictAlgorithm: ConflictAlgorithm.replace, + conflictAlgorithm: ConflictAlgorithm.fail, ); for (Friend friend in friends) { @@ -103,6 +106,7 @@ Future getConversationById(String id) async { admin: maps[0]['admin'] == 1, name: maps[0]['name'], status: ConversationStatus.values[maps[0]['status']], + isRead: maps[0]['is_read'] == 1, ); } @@ -121,6 +125,7 @@ Future> getConversations() async { admin: maps[i]['admin'] == 1, name: maps[i]['name'], status: ConversationStatus.values[maps[i]['status']], + isRead: maps[i]['is_read'] == 1, ); }); } @@ -134,6 +139,7 @@ class Conversation { bool admin; String name; ConversationStatus status; + bool isRead; Conversation({ required this.id, @@ -143,6 +149,7 @@ class Conversation { required this.admin, required this.name, required this.status, + required this.isRead, }); @@ -170,6 +177,7 @@ class Conversation { admin: admin == 'true', name: 'Unknown', status: ConversationStatus.complete, + isRead: true, ); } @@ -181,41 +189,28 @@ class Conversation { List users = await getConversationUsers(this); List userConversations = []; - for (var x in users) { - print(x.toMap()); - } - for (ConversationUser user in users) { - if (profile.id == user.userId) { - userConversations.add({ - 'id': user.id, - 'user_id': profile.id, - 'conversation_detail_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(id.codeUnits)), - 'admin': AesHelper.aesEncrypt(symKey, Uint8List.fromList((admin ? 'true' : 'false').codeUnits)), - 'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(symKey, profile.publicKey!)), - }); - - continue; - } - Friend friend = await getFriendByFriendId(user.userId); - RSAPublicKey pubKey = CryptoUtils.rsaPublicKeyFromPem(friend.asymmetricPublicKey); + RSAPublicKey pubKey = profile.publicKey!; + String newId = id; + + if (profile.id != user.userId) { + Friend friend = await getFriendByFriendId(user.userId); + pubKey = CryptoUtils.rsaPublicKeyFromPem(friend.asymmetricPublicKey); + newId = (const Uuid()).v4(); + } userConversations.add({ - 'id': user.id, - 'user_id': friend.userId, - 'conversation_detail_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(id.codeUnits)), + 'id': newId, + 'user_id': user.userId, + 'conversation_detail_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(conversationDetailId.codeUnits)), 'admin': AesHelper.aesEncrypt(symKey, Uint8List.fromList((admin ? 'true' : 'false').codeUnits)), 'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(symKey, pubKey)), }); } - for (var x in userConversations) { - print(x); - } - return { - 'id': id, + 'id': conversationDetailId, 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)), 'users': AesHelper.aesEncrypt(symKey, Uint8List.fromList(jsonEncode(users).codeUnits)), 'user_conversations': userConversations, @@ -231,6 +226,7 @@ class Conversation { 'admin': admin ? 1 : 0, 'name': name, 'status': status.index, + 'is_read': isRead ? 1 : 0, }; } @@ -244,6 +240,37 @@ class Conversation { name: $name admin: $admin'''; } + + Future getRecentMessage() async { + final db = await getDatabaseConnection(); + + final List> maps = await db.rawQuery( + ''' + SELECT * FROM messages WHERE association_key IN ( + SELECT association_key FROM conversation_users WHERE conversation_id = ? + ) + ORDER BY created_at DESC + LIMIT 1; + ''', + [id], + ); + + if (maps.isEmpty) { + return null; + } + + return Message( + id: maps[0]['id'], + symmetricKey: maps[0]['symmetric_key'], + userSymmetricKey: maps[0]['user_symmetric_key'], + data: maps[0]['data'], + senderId: maps[0]['sender_id'], + senderUsername: maps[0]['sender_username'], + associationKey: maps[0]['association_key'], + createdAt: maps[0]['created_at'], + failedToSend: maps[0]['failed_to_send'] == 1, + ); + } } diff --git a/mobile/lib/utils/storage/conversations.dart b/mobile/lib/utils/storage/conversations.dart index 7fd199a..69d5f5c 100644 --- a/mobile/lib/utils/storage/conversations.dart +++ b/mobile/lib/utils/storage/conversations.dart @@ -14,7 +14,7 @@ import '/utils/encryption/aes_helper.dart'; Future updateConversations() async { RSAPrivateKey privKey = await MyProfile.getPrivateKey(); - try { + // try { var resp = await http.get( Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'), headers: { @@ -94,9 +94,9 @@ Future updateConversations() async { ); } } - } catch (SocketException) { - return; - } + // } catch (SocketException) { + // return; + // } } Future uploadConversation(Conversation conversation) async { @@ -104,16 +104,14 @@ Future uploadConversation(Conversation conversation) async { Map conversationJson = await conversation.toJson(); - print(conversationJson); + var x = await http.post( + Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + 'cookie': sessionCookie, + }, + body: jsonEncode(conversationJson), + ); - // var x = await http.post( - // Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'), - // headers: { - // 'Content-Type': 'application/json; charset=UTF-8', - // 'cookie': sessionCookie, - // }, - // body: jsonEncode(conversationJson), - // ); - - // print(x.statusCode); + print(x.statusCode); } diff --git a/mobile/lib/utils/storage/database.dart b/mobile/lib/utils/storage/database.dart index d2aa0f3..4466f6d 100644 --- a/mobile/lib/utils/storage/database.dart +++ b/mobile/lib/utils/storage/database.dart @@ -39,7 +39,8 @@ Future getDatabaseConnection() async { symmetric_key TEXT, admin INTEGER, name TEXT, - status INTEGER + status INTEGER, + is_read INTEGER ); '''); @@ -72,9 +73,7 @@ Future getDatabaseConnection() async { '''); }, - // Set the version. This executes the onCreate function and provides a - // path to perform database upgrades and downgrades. - version: 2, + version: 2, ); return database; diff --git a/mobile/lib/utils/storage/messages.dart b/mobile/lib/utils/storage/messages.dart index 4342e43..128c84d 100644 --- a/mobile/lib/utils/storage/messages.dart +++ b/mobile/lib/utils/storage/messages.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:http/http.dart' as http; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:sqflite/sqflite.dart'; import 'package:uuid/uuid.dart'; @@ -14,24 +13,19 @@ import '/utils/storage/database.dart'; import '/utils/storage/session_cookie.dart'; Future sendMessage(Conversation conversation, String data) async { - final preferences = await SharedPreferences.getInstance(); - final userId = preferences.getString('userId'); - final username = preferences.getString('username'); - if (userId == null || username == null) { - throw Exception('Invalid user id'); - } + MyProfile profile = await MyProfile.getProfile(); var uuid = const Uuid(); final String messageDataId = uuid.v4(); - ConversationUser currentUser = await getConversationUser(conversation, username); + ConversationUser currentUser = await getConversationUser(conversation, profile.id); Message message = Message( id: messageDataId, symmetricKey: '', userSymmetricKey: '', - senderId: userId, - senderUsername: username, + senderId: profile.id, + senderUsername: profile.username, data: data, associationKey: currentUser.associationKey, createdAt: DateTime.now().toIso8601String(), diff --git a/mobile/lib/utils/time.dart b/mobile/lib/utils/time.dart new file mode 100644 index 0000000..fb08e74 --- /dev/null +++ b/mobile/lib/utils/time.dart @@ -0,0 +1,19 @@ + +String convertToAgo(String input, { bool short = false }) { + DateTime time = DateTime.parse(input); + Duration diff = DateTime.now().difference(time); + + if(diff.inDays >= 1){ + return '${diff.inDays} day${diff.inDays == 1 ? "" : "s"} ${short ? '': 'ago'}'; + } + if(diff.inHours >= 1){ + return '${diff.inHours} hour${diff.inHours == 1 ? "" : "s"} ${short ? '' : 'ago'}'; + } + if(diff.inMinutes >= 1){ + return '${diff.inMinutes} ${short ? 'min' : 'minute'}${diff.inMinutes == 1 ? "" : "s"} ${short ? '' : 'ago'}'; + } + if (diff.inSeconds >= 1){ + return '${diff.inSeconds} ${short ? '' : 'second'}${diff.inSeconds == 1 ? "" : "s"} ${short ? '' : 'ago'}'; + } + return 'just now'; +} diff --git a/mobile/lib/views/main/conversation/detail.dart b/mobile/lib/views/main/conversation/detail.dart index 511b538..cdee4ac 100644 --- a/mobile/lib/views/main/conversation/detail.dart +++ b/mobile/lib/views/main/conversation/detail.dart @@ -4,27 +4,9 @@ import '/models/conversations.dart'; import '/models/messages.dart'; import '/models/my_profile.dart'; import '/utils/storage/messages.dart'; +import '/utils/time.dart'; import '/views/main/conversation/settings.dart'; -String convertToAgo(String input){ - DateTime time = DateTime.parse(input); - Duration diff = DateTime.now().difference(time); - - if(diff.inDays >= 1){ - return '${diff.inDays} day${diff.inDays == 1 ? "" : "s"} ago'; - } - if(diff.inHours >= 1){ - return '${diff.inHours} hour${diff.inHours == 1 ? "" : "s"} ago'; - } - if(diff.inMinutes >= 1){ - return '${diff.inMinutes} minute${diff.inMinutes == 1 ? "" : "s"} ago'; - } - if (diff.inSeconds >= 1){ - return '${diff.inSeconds} second${diff.inSeconds == 1 ? "" : "s"} ago'; - } - return 'just now'; -} - class ConversationDetail extends StatefulWidget{ final Conversation conversation; const ConversationDetail({ diff --git a/mobile/lib/views/main/conversation/edit_details.dart b/mobile/lib/views/main/conversation/edit_details.dart index be48588..5309579 100644 --- a/mobile/lib/views/main/conversation/edit_details.dart +++ b/mobile/lib/views/main/conversation/edit_details.dart @@ -1,3 +1,4 @@ +import 'package:Envelope/models/conversation_users.dart'; import 'package:flutter/material.dart'; import '/components/custom_circle_avatar.dart'; @@ -8,10 +9,12 @@ import '/views/main/conversation/create_add_users.dart'; class ConversationEditDetails extends StatefulWidget { final Conversation? conversation; final List? friends; + final List? users; const ConversationEditDetails({ Key? key, this.conversation, this.friends, + this.users, }) : super(key: key); @override @@ -25,6 +28,14 @@ class _ConversationEditDetails extends State { TextEditingController conversationNameController = TextEditingController(); + @override + void initState() { + if (widget.conversation != null) { + conversationNameController.text = widget.conversation!.name; + } + super.initState(); + } + @override Widget build(BuildContext context) { const TextStyle inputTextStyle = TextStyle( @@ -96,10 +107,8 @@ class _ConversationEditDetails extends State { key: _formKey, child: Column( children: [ - CustomCircleAvatar( - icon: widget.conversation != null ? - null : // TODO: Add icon here - const Icon(Icons.people, size: 60), + const CustomCircleAvatar( + icon: const Icon(Icons.people, size: 60), imagePath: null, radius: 50, ), diff --git a/mobile/lib/views/main/conversation/list_item.dart b/mobile/lib/views/main/conversation/list_item.dart index 45015e9..defc0da 100644 --- a/mobile/lib/views/main/conversation/list_item.dart +++ b/mobile/lib/views/main/conversation/list_item.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; +import '/models/messages.dart'; import '/components/custom_circle_avatar.dart'; import '/models/conversations.dart'; import '/views/main/conversation/detail.dart'; +import '/utils/time.dart'; class ConversationListItem extends StatefulWidget{ final Conversation conversation; @@ -17,6 +19,7 @@ class ConversationListItem extends StatefulWidget{ class _ConversationListItemState extends State { late Conversation conversation; + late Message? recentMessage; bool loaded = false; @override @@ -31,7 +34,7 @@ class _ConversationListItemState extends State { })).then(onGoBack) : null; }, child: Container( - padding: const EdgeInsets.only(left: 16,right: 16,top: 10,bottom: 10), + padding: const EdgeInsets.only(left: 16,right: 0,top: 10,bottom: 10), child: !loaded ? null : Row( children: [ Expanded( @@ -54,13 +57,40 @@ class _ConversationListItemState extends State { conversation.name, style: const TextStyle(fontSize: 16) ), - //Text(widget.messageText,style: TextStyle(fontSize: 13,color: Colors.grey.shade600, fontWeight: widget.isMessageRead?FontWeight.bold:FontWeight.normal),), - ], + recentMessage != null ? + const SizedBox(height: 2) : + const SizedBox.shrink() + , + recentMessage != null ? + Text( + recentMessage!.data, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade600, + fontWeight: conversation.isRead ? FontWeight.normal : FontWeight.bold, + ), + ) : + const SizedBox.shrink(), + ], ), ), ), ), - ], + recentMessage != null ? + Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + convertToAgo(recentMessage!.createdAt, short: true), + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade600, + ), + ) + ): + const SizedBox.shrink(), + ], ), ), ], @@ -72,7 +102,12 @@ class _ConversationListItemState extends State { @override void initState() { super.initState(); + getConversationData(); + } + + Future getConversationData() async { conversation = widget.conversation; + recentMessage = await conversation.getRecentMessage(); loaded = true; setState(() {}); } diff --git a/mobile/lib/views/main/conversation/settings.dart b/mobile/lib/views/main/conversation/settings.dart index 85dbc7e..2e8673c 100644 --- a/mobile/lib/views/main/conversation/settings.dart +++ b/mobile/lib/views/main/conversation/settings.dart @@ -1,4 +1,5 @@ import 'package:Envelope/components/custom_circle_avatar.dart'; +import 'package:Envelope/views/main/conversation/edit_details.dart'; import 'package:flutter/material.dart'; import '/models/conversation_users.dart'; @@ -109,7 +110,13 @@ class _ConversationSettingsState extends State { padding: const EdgeInsets.all(5.0), splashRadius: 25, onPressed: () { - // TODO: Redirect to edit screen + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => ConversationEditDetails( + users: users, + conversation: widget.conversation, + friends: null, + )), + ).then(onGoBack); }, ) : const SizedBox.shrink(), ], @@ -228,5 +235,12 @@ class _ConversationSettingsState extends State { } ); } + + onGoBack(dynamic value) async { + nameController.text = widget.conversation.name; + super.initState(); + getUsers(); + setState(() {}); + } } diff --git a/mobile/lib/views/main/conversation/settings_user_list_item.dart b/mobile/lib/views/main/conversation/settings_user_list_item.dart index 7c676a0..9657bab 100644 --- a/mobile/lib/views/main/conversation/settings_user_list_item.dart +++ b/mobile/lib/views/main/conversation/settings_user_list_item.dart @@ -43,7 +43,7 @@ class _ConversationSettingsUserListItemState extends State( @@ -75,7 +75,7 @@ class _ConversationSettingsUserListItemState extends State[ Expanded( @@ -104,7 +104,7 @@ class _ConversationSettingsUserListItemState extends State[ CustomCircleAvatar( initials: widget.user.username[0].toUpperCase(), - imagePath: null, // TODO: Add image here + imagePath: null, radius: 15, ), const SizedBox(width: 16),