diff --git a/Backend/Database/Seeder/MessageSeeder.go b/Backend/Database/Seeder/MessageSeeder.go index dc9a221..0480131 100644 --- a/Backend/Database/Seeder/MessageSeeder.go +++ b/Backend/Database/Seeder/MessageSeeder.go @@ -21,7 +21,7 @@ func seedMessage( keyCiphertext []byte plaintext string dataCiphertext []byte - senderIdCiphertext []byte + senderIDCiphertext []byte err error ) @@ -42,13 +42,13 @@ func seedMessage( panic(err) } - senderIdCiphertext, err = key.aesEncrypt([]byte(primaryUser.ID.String())) + senderIDCiphertext, err = key.aesEncrypt([]byte(primaryUser.ID.String())) if err != nil { panic(err) } if i%2 == 0 { - senderIdCiphertext, err = key.aesEncrypt([]byte(secondaryUser.ID.String())) + senderIDCiphertext, err = key.aesEncrypt([]byte(secondaryUser.ID.String())) if err != nil { panic(err) } @@ -63,7 +63,7 @@ func seedMessage( messageData = Models.MessageData{ Data: base64.StdEncoding.EncodeToString(dataCiphertext), - SenderID: base64.StdEncoding.EncodeToString(senderIdCiphertext), + SenderID: base64.StdEncoding.EncodeToString(senderIDCiphertext), SymmetricKey: base64.StdEncoding.EncodeToString(keyCiphertext), } @@ -93,10 +93,11 @@ func seedMessage( func seedConversationDetail(key aesKey) (Models.ConversationDetail, error) { var ( - messageThread Models.ConversationDetail - name string - nameCiphertext []byte - err error + messageThread Models.ConversationDetail + name string + nameCiphertext []byte + twoUserCiphertext []byte + err error ) name = "Test Conversation" @@ -106,8 +107,14 @@ func seedConversationDetail(key aesKey) (Models.ConversationDetail, error) { panic(err) } + twoUserCiphertext, err = key.aesEncrypt([]byte("false")) + if err != nil { + panic(err) + } + messageThread = Models.ConversationDetail{ - Name: base64.StdEncoding.EncodeToString(nameCiphertext), + Name: base64.StdEncoding.EncodeToString(nameCiphertext), + TwoUser: base64.StdEncoding.EncodeToString(twoUserCiphertext), } err = Database.CreateConversationDetail(&messageThread) @@ -159,14 +166,14 @@ func seedConversationDetailUser( var ( conversationDetailUser Models.ConversationDetailUser - adminString string = "false" - - userIdCiphertext []byte + userIDCiphertext []byte usernameCiphertext []byte adminCiphertext []byte associationKeyCiphertext []byte publicKeyCiphertext []byte + adminString = "false" + err error ) @@ -174,7 +181,7 @@ func seedConversationDetailUser( adminString = "true" } - userIdCiphertext, err = key.aesEncrypt([]byte(user.ID.String())) + userIDCiphertext, err = key.aesEncrypt([]byte(user.ID.String())) if err != nil { return conversationDetailUser, err } @@ -201,7 +208,7 @@ func seedConversationDetailUser( conversationDetailUser = Models.ConversationDetailUser{ ConversationDetailID: conversationDetail.ID, - UserID: base64.StdEncoding.EncodeToString(userIdCiphertext), + UserID: base64.StdEncoding.EncodeToString(userIDCiphertext), Username: base64.StdEncoding.EncodeToString(usernameCiphertext), Admin: base64.StdEncoding.EncodeToString(adminCiphertext), AssociationKey: base64.StdEncoding.EncodeToString(associationKeyCiphertext), @@ -213,6 +220,7 @@ func seedConversationDetailUser( return conversationDetailUser, err } +// SeedMessages seeds messages & conversations for testing func SeedMessages() { var ( conversationDetail Models.ConversationDetail diff --git a/Backend/Models/Conversations.go b/Backend/Models/Conversations.go index 586ff98..fa88987 100644 --- a/Backend/Models/Conversations.go +++ b/Backend/Models/Conversations.go @@ -4,12 +4,15 @@ import ( "github.com/gofrs/uuid" ) +// ConversationDetail stores the name for the conversation type ConversationDetail struct { Base - Name string `gorm:"not null" json:"name"` // Stored encrypted - Users []ConversationDetailUser ` json:"users"` + Name string `gorm:"not null" json:"name"` // Stored encrypted + Users []ConversationDetailUser ` json:"users"` + TwoUser string `gorm:"not null" json:"two_user"` } +// ConversationDetailUser all users associated with a customer type ConversationDetailUser struct { Base ConversationDetailID uuid.UUID `gorm:"not null" json:"conversation_detail_id"` @@ -21,7 +24,7 @@ type ConversationDetailUser struct { PublicKey string `gorm:"not null" json:"public_key"` // Stored encrypted } -// Used to link the current user to their conversations +// UserConversation Used to link the current user to their conversations type UserConversation struct { Base UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"` @@ -29,5 +32,4 @@ type UserConversation struct { ConversationDetailID string `gorm:"not null" json:"conversation_detail_id"` // Stored encrypted Admin string `gorm:"not null" json:"admin"` // Bool if user is admin of thread, stored encrypted SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted - // TODO: Add association_key here } diff --git a/mobile/lib/models/conversation_users.dart b/mobile/lib/models/conversation_users.dart index 73ab7ac..04ca747 100644 --- a/mobile/lib/models/conversation_users.dart +++ b/mobile/lib/models/conversation_users.dart @@ -14,7 +14,7 @@ Future getConversationUser(Conversation conversation, String u final List> maps = await db.query( 'conversation_users', where: 'conversation_id = ? AND user_id = ?', - whereArgs: [conversation.id, userId], + whereArgs: [ conversation.id, userId ], ); if (maps.length != 1) { @@ -40,7 +40,7 @@ Future> getConversationUsers(Conversation conversation) a final List> maps = await db.query( 'conversation_users', where: 'conversation_id = ?', - whereArgs: [conversation.id], + whereArgs: [ conversation.id ], orderBy: 'username', ); diff --git a/mobile/lib/models/conversations.dart b/mobile/lib/models/conversations.dart index 6b10460..69bc423 100644 --- a/mobile/lib/models/conversations.dart +++ b/mobile/lib/models/conversations.dart @@ -14,7 +14,7 @@ import '/utils/encryption/crypto_utils.dart'; import '/utils/storage/database.dart'; import '/utils/strings.dart'; -Future createConversation(String title, List friends) async { +Future createConversation(String title, List friends, bool twoUser) async { final db = await getDatabaseConnection(); MyProfile profile = await MyProfile.getProfile(); @@ -30,6 +30,7 @@ Future createConversation(String title, List friends) asyn symmetricKey: base64.encode(symmetricKey), admin: true, name: title, + twoUser: twoUser, status: ConversationStatus.pending, isRead: true, ); @@ -105,7 +106,7 @@ Conversation findConversationByDetailId(List conversations, String } } // Or return `null`. - throw ArgumentError.value(id, "id", "No element with that id"); + throw ArgumentError.value(id, 'id', 'No element with that id'); } Future getConversationById(String id) async { @@ -127,6 +128,7 @@ Future getConversationById(String id) async { symmetricKey: maps[0]['symmetric_key'], admin: maps[0]['admin'] == 1, name: maps[0]['name'], + twoUser: maps[0]['two_user'] == 1, status: ConversationStatus.values[maps[0]['status']], isRead: maps[0]['is_read'] == 1, ); @@ -145,12 +147,48 @@ Future> getConversations() async { symmetricKey: maps[i]['symmetric_key'], admin: maps[i]['admin'] == 1, name: maps[i]['name'], + twoUser: maps[i]['two_user'] == 1, status: ConversationStatus.values[maps[i]['status']], isRead: maps[i]['is_read'] == 1, ); }); } +Future getTwoUserConversation(String userId) async { + final db = await getDatabaseConnection(); + + MyProfile profile = await MyProfile.getProfile(); + + print(userId); + print(profile.id); + + final List> maps = await db.rawQuery( + ''' + SELECT conversations.* FROM conversations + LEFT JOIN conversation_users ON conversation_users.conversation_id = conversations.id + WHERE conversation_users.user_id = ? + AND conversation_users.user_id != ? + AND conversations.two_user = 1 + ''', + [ userId, profile.id ], + ); + + if (maps.length != 1) { + return null; + } + + return Conversation( + id: maps[0]['id'], + userId: maps[0]['user_id'], + symmetricKey: maps[0]['symmetric_key'], + admin: maps[0]['admin'] == 1, + name: maps[0]['name'], + twoUser: maps[0]['two_user'] == 1, + status: ConversationStatus.values[maps[0]['status']], + isRead: maps[0]['is_read'] == 1, + ); + +} class Conversation { String id; @@ -158,6 +196,7 @@ class Conversation { String symmetricKey; bool admin; String name; + bool twoUser; ConversationStatus status; bool isRead; @@ -167,6 +206,7 @@ class Conversation { required this.symmetricKey, required this.admin, required this.name, + required this.twoUser, required this.status, required this.isRead, }); @@ -194,6 +234,7 @@ class Conversation { symmetricKey: base64.encode(symmetricKeyDecrypted), admin: admin == 'true', name: 'Unknown', + twoUser: false, status: ConversationStatus.complete, isRead: true, ); @@ -251,6 +292,7 @@ class Conversation { 'symmetric_key': symmetricKey, 'admin': admin ? 1 : 0, 'name': name, + 'two_user': twoUser ? 1 : 0, 'status': status.index, 'is_read': isRead ? 1 : 0, }; @@ -297,6 +339,28 @@ class Conversation { failedToSend: maps[0]['failed_to_send'] == 1, ); } + + Future getName() async { + if (!twoUser) { + return name; + } + + MyProfile profile = await MyProfile.getProfile(); + + final db = await getDatabaseConnection(); + + List> maps = await db.query( + 'conversation_users', + where: 'conversation_id = ? AND user_id != ?', + whereArgs: [ id, profile.id ], + ); + + if (maps.length != 1) { + throw ArgumentError('Invalid user id'); + } + + return maps[0]['username']; + } } diff --git a/mobile/lib/utils/storage/conversations.dart b/mobile/lib/utils/storage/conversations.dart index d5068d4..f4722a3 100644 --- a/mobile/lib/utils/storage/conversations.dart +++ b/mobile/lib/utils/storage/conversations.dart @@ -92,6 +92,11 @@ Future updateConversations() async { base64.decode(conversationDetailJson['name']), ); + conversation.twoUser = AesHelper.aesDecrypt( + base64.decode(conversation.symmetricKey), + base64.decode(conversationDetailJson['two_user']), + ) == 'true'; + await db.insert( 'conversations', conversation.toMap(), diff --git a/mobile/lib/utils/storage/database.dart b/mobile/lib/utils/storage/database.dart index 9814bf8..e643f53 100644 --- a/mobile/lib/utils/storage/database.dart +++ b/mobile/lib/utils/storage/database.dart @@ -29,7 +29,6 @@ Future getDatabaseConnection() async { ); '''); - // TODO: Change users to use its own table, as it is a json blob await db.execute( ''' CREATE TABLE IF NOT EXISTS conversations( @@ -38,6 +37,7 @@ Future getDatabaseConnection() async { symmetric_key TEXT, admin INTEGER, name TEXT, + two_user INTEGER, status INTEGER, is_read INTEGER ); diff --git a/mobile/lib/views/main/conversation/detail.dart b/mobile/lib/views/main/conversation/detail.dart index 667077f..039ab16 100644 --- a/mobile/lib/views/main/conversation/detail.dart +++ b/mobile/lib/views/main/conversation/detail.dart @@ -21,6 +21,7 @@ class ConversationDetail extends StatefulWidget{ } class _ConversationDetailState extends State { + String conversationName = ''; List messages = []; MyProfile profile = MyProfile(id: '', username: ''); @@ -31,7 +32,7 @@ class _ConversationDetailState extends State { return Scaffold( appBar: CustomTitleBar( title: Text( - widget.conversation.name, + conversationName, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -42,7 +43,9 @@ class _ConversationDetailState extends State { rightHandButton: IconButton( onPressed: (){ Navigator.of(context).push( - MaterialPageRoute(builder: (context) => ConversationSettings(conversation: widget.conversation)), + MaterialPageRoute(builder: (context) => ConversationSettings( + conversation: widget.conversation + )), ); }, icon: Icon( @@ -51,6 +54,7 @@ class _ConversationDetailState extends State { ), ), ), + body: Stack( children: [ messagesView(), @@ -88,7 +92,7 @@ class _ConversationDetailState extends State { Expanded( child: TextField( decoration: InputDecoration( - hintText: "Write message...", + hintText: 'Write message...', hintStyle: TextStyle( color: Theme.of(context).hintColor, ), @@ -134,6 +138,7 @@ class _ConversationDetailState extends State { } Future fetchMessages() async { + conversationName = await widget.conversation.getName(); profile = await MyProfile.getProfile(); messages = await getMessagesForThread(widget.conversation); setState(() {}); diff --git a/mobile/lib/views/main/conversation/list.dart b/mobile/lib/views/main/conversation/list.dart index 155ae2a..a19887b 100644 --- a/mobile/lib/views/main/conversation/list.dart +++ b/mobile/lib/views/main/conversation/list.dart @@ -73,7 +73,8 @@ class _ConversationListState extends State { saveCallback: (List friendsSelected) async { Conversation conversation = await createConversation( conversationName, - friendsSelected + friendsSelected, + false, ); uploadConversation(conversation); diff --git a/mobile/lib/views/main/conversation/list_item.dart b/mobile/lib/views/main/conversation/list_item.dart index defc0da..5523204 100644 --- a/mobile/lib/views/main/conversation/list_item.dart +++ b/mobile/lib/views/main/conversation/list_item.dart @@ -19,83 +19,84 @@ class ConversationListItem extends StatefulWidget{ class _ConversationListItemState extends State { late Conversation conversation; + late String conversationName; late Message? recentMessage; bool loaded = false; @override Widget build(BuildContext context) { return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - loaded ? Navigator.push(context, MaterialPageRoute(builder: (context){ - return ConversationDetail( - conversation: conversation, - ); - })).then(onGoBack) : null; - }, - child: Container( - padding: const EdgeInsets.only(left: 16,right: 0,top: 10,bottom: 10), - child: !loaded ? null : Row( + behavior: HitTestBehavior.opaque, + onTap: () { + loaded ? Navigator.push(context, MaterialPageRoute(builder: (context){ + return ConversationDetail( + conversation: conversation, + ); + })).then(onGoBack) : null; + }, + child: Container( + padding: const EdgeInsets.only(left: 16,right: 0,top: 10,bottom: 10), + child: !loaded ? null : Row( + children: [ + Expanded( + child: Row( children: [ + CustomCircleAvatar( + initials: conversationName[0].toUpperCase(), + imagePath: null, + ), + const SizedBox(width: 16), Expanded( - child: Row( + child: Align( + alignment: Alignment.centerLeft, + child: Container( + color: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomCircleAvatar( - initials: conversation.name[0].toUpperCase(), - imagePath: null, - ), - const SizedBox(width: 16), - Expanded( - child: Align( - alignment: Alignment.centerLeft, - child: Container( - color: Colors.transparent, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - conversation.name, - style: const TextStyle(fontSize: 16) - ), - 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(), - ], - ), - ), - ), + Text( + conversationName, + style: const TextStyle(fontSize: 16) ), 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(), - ], + 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(), ], + ), ), + ], ), + ), ); } @@ -107,6 +108,7 @@ class _ConversationListItemState extends State { Future getConversationData() async { conversation = widget.conversation; + conversationName = await widget.conversation.getName(); recentMessage = await conversation.getRecentMessage(); loaded = true; setState(() {}); diff --git a/mobile/lib/views/main/friend/list_item.dart b/mobile/lib/views/main/friend/list_item.dart index 3610ff1..5f6a4d3 100644 --- a/mobile/lib/views/main/friend/list_item.dart +++ b/mobile/lib/views/main/friend/list_item.dart @@ -1,5 +1,8 @@ import 'package:Envelope/components/custom_circle_avatar.dart'; +import 'package:Envelope/models/conversations.dart'; import 'package:Envelope/models/friends.dart'; +import 'package:Envelope/utils/strings.dart'; +import 'package:Envelope/views/main/conversation/detail.dart'; import 'package:flutter/material.dart'; class FriendListItem extends StatefulWidget{ @@ -19,8 +22,7 @@ class _FriendListItemState extends State { Widget build(BuildContext context) { return GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () async { - }, + onTap: findOrCreateConversation, child: Container( padding: const EdgeInsets.only(left: 16,right: 16,top: 0,bottom: 20), child: Row( @@ -55,4 +57,20 @@ class _FriendListItemState extends State { ), ); } + + Future findOrCreateConversation() async { + Conversation? conversation = await getTwoUserConversation(widget.friend.friendId); + + conversation ??= await createConversation( + generateRandomString(32), + [ widget.friend ], + true, + ); + + Navigator.push(context, MaterialPageRoute(builder: (context){ + return ConversationDetail( + conversation: conversation!, + ); + })); + } }