From a6f54d5ef8a7d798a8bba6ea14224e0fa5803de7 Mon Sep 17 00:00:00 2001 From: Tovi Jaeschke-Rogers Date: Sat, 2 Jul 2022 15:20:09 +0930 Subject: [PATCH] Finish sending messages --- Backend/Api/Auth/Login.go | 16 +- Backend/Api/Messages/CreateMessage.go | 28 ++- Backend/Api/Routes.go | 4 +- Backend/Database/MessageData.go | 39 ++++ Backend/Database/Messages.go | 18 +- Backend/Database/Seeder/MessageSeeder.go | 143 +++++++----- Backend/Database/Seeder/UserSeeder.go | 2 +- Backend/Models/Base.go | 4 + Backend/Models/Messages.go | 21 +- mobile/lib/models/conversation_users.dart | 104 +++++++++ mobile/lib/models/conversations.dart | 14 -- mobile/lib/models/friends.dart | 61 ++--- mobile/lib/models/messages.dart | 117 ++++++++-- mobile/lib/utils/storage/conversations.dart | 26 ++- mobile/lib/utils/storage/database.dart | 18 +- mobile/lib/utils/storage/friends.dart | 2 + mobile/lib/utils/storage/messages.dart | 146 +++++++----- mobile/lib/utils/strings.dart | 8 + mobile/lib/views/authentication/login.dart | 9 +- mobile/lib/views/authentication/signup.dart | 10 +- .../lib/views/main/conversation_detail.dart | 53 +++-- mobile/lib/views/main/conversation_list.dart | 209 ++++++++++-------- mobile/lib/views/main/friend_list.dart | 207 +++++++++-------- mobile/lib/views/main/home.dart | 58 ++++- mobile/pubspec.lock | 21 ++ mobile/pubspec.yaml | 2 + 26 files changed, 907 insertions(+), 433 deletions(-) create mode 100644 Backend/Database/MessageData.go create mode 100644 mobile/lib/models/conversation_users.dart create mode 100644 mobile/lib/utils/strings.dart diff --git a/Backend/Api/Auth/Login.go b/Backend/Api/Auth/Login.go index b91d133..15c42a7 100644 --- a/Backend/Api/Auth/Login.go +++ b/Backend/Api/Auth/Login.go @@ -20,9 +20,10 @@ type loginResponse struct { AsymmetricPublicKey string `json:"asymmetric_public_key"` AsymmetricPrivateKey string `json:"asymmetric_private_key"` UserID string `json:"user_id"` + Username string `json:"username"` } -func makeLoginResponse(w http.ResponseWriter, code int, message, pubKey, privKey string, userId string) { +func makeLoginResponse(w http.ResponseWriter, code int, message, pubKey, privKey string, user Models.User) { var ( status string = "error" returnJson []byte @@ -37,7 +38,8 @@ func makeLoginResponse(w http.ResponseWriter, code int, message, pubKey, privKey Message: message, AsymmetricPublicKey: pubKey, AsymmetricPrivateKey: privKey, - UserID: userId, + UserID: user.ID.String(), + Username: user.Username, }, "", " ") if err != nil { http.Error(w, "Error", http.StatusInternalServerError) @@ -61,18 +63,18 @@ func Login(w http.ResponseWriter, r *http.Request) { err = json.NewDecoder(r.Body).Decode(&creds) if err != nil { - makeLoginResponse(w, http.StatusInternalServerError, "An error occurred", "", "", "") + makeLoginResponse(w, http.StatusInternalServerError, "An error occurred", "", "", userData) return } userData, err = Database.GetUserByUsername(creds.Username) if err != nil { - makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "", "") + makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "", userData) return } if !CheckPasswordHash(creds.Password, userData.Password) { - makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "", "") + makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "", userData) return } @@ -86,7 +88,7 @@ func Login(w http.ResponseWriter, r *http.Request) { err = Database.CreateSession(&session) if err != nil { - makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "", "") + makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "", userData) return } @@ -102,6 +104,6 @@ func Login(w http.ResponseWriter, r *http.Request) { "Successfully logged in", userData.AsymmetricPublicKey, userData.AsymmetricPrivateKey, - userData.ID.String(), + userData, ) } diff --git a/Backend/Api/Messages/CreateMessage.go b/Backend/Api/Messages/CreateMessage.go index d73977c..c233fc8 100644 --- a/Backend/Api/Messages/CreateMessage.go +++ b/Backend/Api/Messages/CreateMessage.go @@ -2,22 +2,40 @@ package Messages import ( "encoding/json" - "fmt" "net/http" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" ) +type RawMessageData struct { + MessageData Models.MessageData `json:"message_data"` + Messages []Models.Message `json:"message"` +} + func CreateMessage(w http.ResponseWriter, r *http.Request) { var ( - message Models.Message - err error + rawMessageData RawMessageData + err error ) - err = json.NewDecoder(r.Body).Decode(&message) + err = json.NewDecoder(r.Body).Decode(&rawMessageData) + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + err = Database.CreateMessageData(&rawMessageData.MessageData) + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + err = Database.CreateMessages(&rawMessageData.Messages) if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) return } - fmt.Println(message) + w.WriteHeader(http.StatusOK) } diff --git a/Backend/Api/Routes.go b/Backend/Api/Routes.go index 651578e..7d528ed 100644 --- a/Backend/Api/Routes.go +++ b/Backend/Api/Routes.go @@ -68,9 +68,7 @@ func InitApiEndpoints(router *mux.Router) { authApi.HandleFunc("/conversations", Messages.EncryptedConversationList).Methods("GET") authApi.HandleFunc("/conversation_details", Messages.EncryptedConversationDetailsList).Methods("GET") - // authApi.HandleFunc("/user/{userID}", Friends.Friend).Methods("GET") - // authApi.HandleFunc("/user/{userID}/request", Friends.FriendRequest).Methods("POST") - // Define routes for messages + authApi.HandleFunc("/message", Messages.CreateMessage).Methods("POST") authApi.HandleFunc("/messages/{threadKey}", Messages.Messages).Methods("GET") } diff --git a/Backend/Database/MessageData.go b/Backend/Database/MessageData.go new file mode 100644 index 0000000..80c6515 --- /dev/null +++ b/Backend/Database/MessageData.go @@ -0,0 +1,39 @@ +package Database + +import ( + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +func GetMessageDataById(id string) (Models.MessageData, error) { + var ( + messageData Models.MessageData + err error + ) + + err = DB.Preload(clause.Associations). + First(&messageData, "id = ?", id). + Error + + return messageData, err +} + +func CreateMessageData(messageData *Models.MessageData) error { + var ( + err error + ) + + err = DB.Session(&gorm.Session{FullSaveAssociations: true}). + Create(messageData). + Error + + return err +} + +func DeleteMessageData(messageData *Models.MessageData) error { + return DB.Session(&gorm.Session{FullSaveAssociations: true}). + Delete(messageData). + Error +} diff --git a/Backend/Database/Messages.go b/Backend/Database/Messages.go index 4c1c352..0affa34 100644 --- a/Backend/Database/Messages.go +++ b/Backend/Database/Messages.go @@ -20,14 +20,14 @@ func GetMessageById(id string) (Models.Message, error) { return message, err } -func GetMessagesByThreadKey(threadKey string) ([]Models.Message, error) { +func GetMessagesByThreadKey(associationKey string) ([]Models.Message, error) { var ( messages []Models.Message err error ) - err = DB.Preload(clause.Associations). - Find(&messages, "message_thread_key = ?", threadKey). + err = DB.Preload("MessageData"). + Find(&messages, "association_key = ?", associationKey). Error return messages, err @@ -45,6 +45,18 @@ func CreateMessage(message *Models.Message) error { return err } +func CreateMessages(messages *[]Models.Message) error { + var ( + err error + ) + + err = DB.Session(&gorm.Session{FullSaveAssociations: true}). + Create(messages). + Error + + return err +} + func DeleteMessage(message *Models.Message) error { return DB.Session(&gorm.Session{FullSaveAssociations: true}). Delete(message). diff --git a/Backend/Database/Seeder/MessageSeeder.go b/Backend/Database/Seeder/MessageSeeder.go index aba744a..2139eb1 100644 --- a/Backend/Database/Seeder/MessageSeeder.go +++ b/Backend/Database/Seeder/MessageSeeder.go @@ -7,60 +7,93 @@ import ( "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Util" + "github.com/gofrs/uuid" ) func seedMessage( primaryUser, secondaryUser Models.User, - primaryUserThreadKey, secondaryUserThreadKey string, - thread Models.ConversationDetail, + primaryUserAssociationKey, secondaryUserAssociationKey string, i int, ) error { var ( message Models.Message messageData Models.MessageData - key aesKey + key, userKey aesKey + keyCiphertext []byte plaintext string dataCiphertext []byte senderIdCiphertext []byte + friendId []byte err error ) - key, err = generateAesKey() + plaintext = "Test Message" + + userKey, err = generateAesKey() if err != nil { panic(err) } - plaintext = "Test Message" + key, err = generateAesKey() + if err != nil { + panic(err) + } dataCiphertext, err = key.aesEncrypt([]byte(plaintext)) if err != nil { panic(err) } - senderIdCiphertext, err = key.aesEncrypt([]byte(primaryUser.ID.String())) + friendId, err = base64.StdEncoding.DecodeString(primaryUser.FriendID) + if err != nil { + panic(err) + } + friendId, err = decryptWithPrivateKey(friendId, decodedPrivateKey) + if err != nil { + panic(err) + } + + senderIdCiphertext, err = key.aesEncrypt(friendId) if err != nil { panic(err) } if i%2 == 0 { - senderIdCiphertext, err = key.aesEncrypt([]byte(secondaryUser.ID.String())) + friendId, err = base64.StdEncoding.DecodeString(secondaryUser.FriendID) + if err != nil { + panic(err) + } + friendId, err = decryptWithPrivateKey(friendId, decodedPrivateKey) + if err != nil { + panic(err) + } + + senderIdCiphertext, err = key.aesEncrypt(friendId) if err != nil { panic(err) } } + keyCiphertext, err = userKey.aesEncrypt( + []byte(base64.StdEncoding.EncodeToString(key.Key)), + ) + if err != nil { + panic(err) + } + messageData = Models.MessageData{ - Data: base64.StdEncoding.EncodeToString(dataCiphertext), - SenderID: base64.StdEncoding.EncodeToString(senderIdCiphertext), + Data: base64.StdEncoding.EncodeToString(dataCiphertext), + SenderID: base64.StdEncoding.EncodeToString(senderIdCiphertext), + SymmetricKey: base64.StdEncoding.EncodeToString(keyCiphertext), } message = Models.Message{ MessageData: messageData, SymmetricKey: base64.StdEncoding.EncodeToString( - encryptWithPublicKey(key.Key, decodedPublicKey), + encryptWithPublicKey(userKey.Key, decodedPublicKey), ), - MessageThreadKey: primaryUserThreadKey, + AssociationKey: primaryUserAssociationKey, } err = Database.CreateMessage(&message) @@ -68,22 +101,15 @@ func seedMessage( return err } - // The symmetric key would be encrypted with secondary users public key in production - // But due to using the same pub/priv key pair for all users, we will just duplicate it message = Models.Message{ - MessageDataID: message.MessageDataID, + MessageData: messageData, SymmetricKey: base64.StdEncoding.EncodeToString( - encryptWithPublicKey(key.Key, decodedPublicKey), + encryptWithPublicKey(userKey.Key, decodedPublicKey), ), - MessageThreadKey: secondaryUserThreadKey, - } - - err = Database.CreateMessage(&message) - if err != nil { - return err + AssociationKey: secondaryUserAssociationKey, } - return err + return Database.CreateMessage(&message) } func seedConversationDetail(key aesKey) (Models.ConversationDetail, error) { @@ -132,13 +158,11 @@ func seedUpdateUserConversation( func seedUserConversation( user Models.User, threadID uuid.UUID, - messageThreadKey string, key aesKey, ) (Models.UserConversation, error) { var ( messageThreadUser Models.UserConversation threadIdCiphertext []byte - keyCiphertext []byte adminCiphertext []byte err error ) @@ -148,11 +172,6 @@ func seedUserConversation( return messageThreadUser, err } - keyCiphertext, err = key.aesEncrypt([]byte(messageThreadKey)) - if err != nil { - return messageThreadUser, err - } - adminCiphertext, err = key.aesEncrypt([]byte("true")) if err != nil { return messageThreadUser, err @@ -161,7 +180,6 @@ func seedUserConversation( messageThreadUser = Models.UserConversation{ UserID: user.ID, ConversationDetailID: base64.StdEncoding.EncodeToString(threadIdCiphertext), - MessageThreadKey: base64.StdEncoding.EncodeToString(keyCiphertext), Admin: base64.StdEncoding.EncodeToString(adminCiphertext), SymmetricKey: base64.StdEncoding.EncodeToString( encryptWithPublicKey(key.Key, decodedPublicKey), @@ -174,24 +192,24 @@ func seedUserConversation( func SeedMessages() { var ( - messageThread Models.ConversationDetail - key aesKey - primaryUser Models.User - primaryUserThreadKey string - secondaryUser Models.User - secondaryUserThreadKey string - - userJson string - - thread Models.ConversationDetail - i int - err error + messageThread Models.ConversationDetail + key aesKey + primaryUser Models.User + primaryUserAssociationKey string + secondaryUser Models.User + secondaryUserAssociationKey string + primaryUserFriendId []byte + secondaryUserFriendId []byte + userJson string + i int + err error ) key, err = generateAesKey() messageThread, err = seedConversationDetail(key) - primaryUserThreadKey = Util.RandomString(32) - secondaryUserThreadKey = Util.RandomString(32) + + primaryUserAssociationKey = Util.RandomString(32) + secondaryUserAssociationKey = Util.RandomString(32) primaryUser, err = Database.GetUserByUsername("testUser") if err != nil { @@ -201,7 +219,6 @@ func SeedMessages() { _, err = seedUserConversation( primaryUser, messageThread.ID, - primaryUserThreadKey, key, ) @@ -213,29 +230,50 @@ func SeedMessages() { _, err = seedUserConversation( secondaryUser, messageThread.ID, - secondaryUserThreadKey, key, ) + primaryUserFriendId, err = base64.StdEncoding.DecodeString(primaryUser.FriendID) + if err != nil { + panic(err) + } + primaryUserFriendId, err = decryptWithPrivateKey(primaryUserFriendId, decodedPrivateKey) + if err != nil { + panic(err) + } + + secondaryUserFriendId, err = base64.StdEncoding.DecodeString(secondaryUser.FriendID) + if err != nil { + panic(err) + } + secondaryUserFriendId, err = decryptWithPrivateKey(secondaryUserFriendId, decodedPrivateKey) + if err != nil { + panic(err) + } + userJson = fmt.Sprintf( ` [ { "id": "%s", "username": "%s", - "admin": "true" + "admin": "true", + "association_key": "%s" }, { "id": "%s", "username": "%s", - "admin": "true" + "admin": "true", + "association_key": "%s" } ] `, - primaryUser.ID.String(), + string(primaryUserFriendId), primaryUser.Username, - secondaryUser.ID.String(), + primaryUserAssociationKey, + string(secondaryUserFriendId), secondaryUser.Username, + secondaryUserAssociationKey, ) messageThread, err = seedUpdateUserConversation( @@ -248,9 +286,8 @@ func SeedMessages() { err = seedMessage( primaryUser, secondaryUser, - primaryUserThreadKey, - secondaryUserThreadKey, - thread, + primaryUserAssociationKey, + secondaryUserAssociationKey, i, ) if err != nil { diff --git a/Backend/Database/Seeder/UserSeeder.go b/Backend/Database/Seeder/UserSeeder.go index b9e12e6..69cd543 100644 --- a/Backend/Database/Seeder/UserSeeder.go +++ b/Backend/Database/Seeder/UserSeeder.go @@ -49,7 +49,7 @@ func createUser(username string) (Models.User, error) { publicUserData = Models.Friend{ Username: base64.StdEncoding.EncodeToString(usernameCiphertext), - AsymmetricPublicKey: base64.StdEncoding.EncodeToString([]byte(publicKey)), + AsymmetricPublicKey: publicKey, } err = Database.CreateFriend(&publicUserData) diff --git a/Backend/Models/Base.go b/Backend/Models/Base.go index 2fdcd07..797bccc 100644 --- a/Backend/Models/Base.go +++ b/Backend/Models/Base.go @@ -17,6 +17,10 @@ func (base *Base) BeforeCreate(tx *gorm.DB) error { err error ) + if !base.ID.IsNil() { + return nil + } + id, err = uuid.NewV4() if err != nil { return err diff --git a/Backend/Models/Messages.go b/Backend/Models/Messages.go index d9f309c..fed8909 100644 --- a/Backend/Models/Messages.go +++ b/Backend/Models/Messages.go @@ -9,24 +9,24 @@ import ( // TODO: Add support for images type MessageData struct { Base - Data string `gorm:"not null" json:"data"` // Stored encrypted - SenderID string `gorm:"not null" json:"sender_id"` + Data string `gorm:"not null" json:"data"` // Stored encrypted + SenderID string `gorm:"not null" json:"sender_id"` // Stored encrypted + SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted } type Message struct { Base - MessageDataID uuid.UUID `json:"-"` - MessageData MessageData `json:"message_data"` - SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted - MessageThreadKey string `gorm:"not null" json:"message_thread_key"` - CreatedAt time.Time `gorm:"not null" json:"created_at"` + MessageDataID uuid.UUID `json:"message_data_id"` + MessageData MessageData `json:"message_data"` + SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted + AssociationKey string `gorm:"not null" json:"association_key"` // TODO: This links all encrypted messages for a user in a thread together. Find a way to fix this + CreatedAt time.Time `gorm:"not null" json:"created_at"` } -// TODO: Rename to ConversationDetails type ConversationDetail struct { Base - Name string `gorm:"not null" json:"name"` - Users string `json:"users"` // Stored as encrypted JSON + Name string `gorm:"not null" json:"name"` // Stored encrypted + Users string `json:"users"` // Stored as encrypted JSON } type UserConversation struct { @@ -34,7 +34,6 @@ type UserConversation struct { UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"` User User `json:"user"` ConversationDetailID string `gorm:"not null" json:"conversation_detail_id"` // Stored encrypted - MessageThreadKey string `gorm:"not null" json:"message_thread_key"` // 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 } diff --git a/mobile/lib/models/conversation_users.dart b/mobile/lib/models/conversation_users.dart new file mode 100644 index 0000000..6c94a0d --- /dev/null +++ b/mobile/lib/models/conversation_users.dart @@ -0,0 +1,104 @@ +import '/utils/storage/database.dart'; +import '/models/conversations.dart'; + +class ConversationUser{ + String id; + String conversationId; + String username; + String associationKey; + String admin; + ConversationUser({ + required this.id, + required this.conversationId, + required this.username, + required this.associationKey, + required this.admin, + }); + + factory ConversationUser.fromJson(Map json, String conversationId) { + return ConversationUser( + id: json['id'], + conversationId: conversationId, + username: json['username'], + associationKey: json['association_key'], + admin: json['admin'], + ); + } + + Map toMap() { + return { + 'id': id, + 'conversation_id': conversationId, + 'username': username, + 'association_key': associationKey, + 'admin': admin, + }; + } +} + +// 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], + ); + + return List.generate(maps.length, (i) { + return ConversationUser( + id: maps[i]['id'], + conversationId: maps[i]['conversation_id'], + username: maps[i]['username'], + associationKey: maps[i]['association_key'], + admin: maps[i]['admin'], + ); + }); +} + +Future getConversationUserById(Conversation conversation, String id) async { + final db = await getDatabaseConnection(); + + final List> maps = await db.query( + 'conversation_users', + where: 'conversation_id = ? AND id = ?', + whereArgs: [conversation.id, id], + ); + + if (maps.length != 1) { + throw ArgumentError('Invalid conversation_id or id'); + } + + return ConversationUser( + id: maps[0]['id'], + conversationId: maps[0]['conversation_id'], + username: maps[0]['username'], + associationKey: maps[0]['association_key'], + admin: maps[0]['admin'], + ); + +} + +Future getConversationUserByUsername(Conversation conversation, String username) async { + final db = await getDatabaseConnection(); + + final List> maps = await db.query( + 'conversation_users', + where: 'conversation_id = ? AND username = ?', + whereArgs: [conversation.id, username], + ); + + if (maps.length != 1) { + throw ArgumentError('Invalid conversation_id or username'); + } + + return ConversationUser( + id: maps[0]['id'], + conversationId: maps[0]['conversation_id'], + username: maps[0]['username'], + associationKey: maps[0]['association_key'], + admin: maps[0]['admin'], + ); + +} diff --git a/mobile/lib/models/conversations.dart b/mobile/lib/models/conversations.dart index 2963b6b..ac18d89 100644 --- a/mobile/lib/models/conversations.dart +++ b/mobile/lib/models/conversations.dart @@ -18,21 +18,17 @@ class Conversation { String id; String userId; String conversationDetailId; - String messageThreadKey; String symmetricKey; bool admin; String name; - String? users; Conversation({ required this.id, required this.userId, required this.conversationDetailId, - required this.messageThreadKey, required this.symmetricKey, required this.admin, required this.name, - this.users, }); @@ -47,11 +43,6 @@ class Conversation { base64.decode(json['conversation_detail_id']), ); - var threadKey = AesHelper.aesDecrypt( - symmetricKeyDecrypted, - base64.decode(json['message_thread_key']), - ); - var admin = AesHelper.aesDecrypt( symmetricKeyDecrypted, base64.decode(json['admin']), @@ -61,7 +52,6 @@ class Conversation { id: json['id'], userId: json['user_id'], conversationDetailId: detailId, - messageThreadKey: threadKey, symmetricKey: base64.encode(symmetricKeyDecrypted), admin: admin == 'true', name: 'Unknown', @@ -84,11 +74,9 @@ admin: $admin'''; 'id': id, 'user_id': userId, 'conversation_detail_id': conversationDetailId, - 'message_thread_key': messageThreadKey, 'symmetric_key': symmetricKey, 'admin': admin ? 1 : 0, 'name': name, - 'users': users, }; } } @@ -105,11 +93,9 @@ Future> getConversations() async { id: maps[i]['id'], userId: maps[i]['user_id'], conversationDetailId: maps[i]['conversation_detail_id'], - messageThreadKey: maps[i]['message_thread_key'], symmetricKey: maps[i]['symmetric_key'], admin: maps[i]['admin'] == 1, name: maps[i]['name'], - users: maps[i]['users'], ); }); } diff --git a/mobile/lib/models/friends.dart b/mobile/lib/models/friends.dart index 3bd1b1e..0b94b4a 100644 --- a/mobile/lib/models/friends.dart +++ b/mobile/lib/models/friends.dart @@ -16,17 +16,19 @@ Friend findFriendByFriendId(List friends, String id) { class Friend{ String id; String userId; - String? username; + String username; String friendId; String friendSymmetricKey; + String asymmetricPublicKey; String acceptedAt; Friend({ required this.id, required this.userId, + required this.username, required this.friendId, required this.friendSymmetricKey, + required this.asymmetricPublicKey, required this.acceptedAt, - this.username }); factory Friend.fromJson(Map json, RSAPrivateKey privKey) { @@ -43,8 +45,10 @@ class Friend{ return Friend( id: json['id'], userId: json['user_id'], + username: '', friendId: String.fromCharCodes(friendIdDecrypted), friendSymmetricKey: base64.encode(friendSymmetricKeyDecrypted), + asymmetricPublicKey: '', acceptedAt: json['accepted_at'], ); } @@ -68,6 +72,7 @@ class Friend{ 'username': username, 'friend_id': friendId, 'symmetric_key': friendSymmetricKey, + 'asymmetric_public_key': asymmetricPublicKey, 'accepted_at': acceptedAt, }; } @@ -86,35 +91,35 @@ Future> getFriends() async { userId: maps[i]['user_id'], friendId: maps[i]['friend_id'], friendSymmetricKey: maps[i]['symmetric_key'], + asymmetricPublicKey: maps[i]['asymmetric_public_key'], acceptedAt: maps[i]['accepted_at'], username: maps[i]['username'], ); }); } -// Future getFriendByUserId(String userId) async { -// final db = await getDatabaseConnection(); -// -// List whereArguments = [userId]; -// -// final List> maps = await db.query( -// 'friends', -// where: 'friend_id = ?', -// whereArgs: whereArguments, -// ); -// -// print(userId); -// -// if (maps.length != 1) { -// throw ArgumentError('Invalid user id'); -// } -// -// return Friend( -// id: maps[0]['id'], -// userId: maps[0]['user_id'], -// friendId: maps[0]['friend_id'], -// friendSymmetricKey: maps[0]['symmetric_key'], -// acceptedAt: maps[0]['accepted_at'], -// username: maps[0]['username'], -// ); -// } +Future getFriendByFriendId(String userId) async { + final db = await getDatabaseConnection(); + + List whereArguments = [userId]; + + final List> maps = await db.query( + 'friends', + where: 'friend_id = ?', + whereArgs: whereArguments, + ); + + if (maps.length != 1) { + throw ArgumentError('Invalid user id'); + } + + return Friend( + id: maps[0]['id'], + userId: maps[0]['user_id'], + friendId: maps[0]['friend_id'], + friendSymmetricKey: maps[0]['symmetric_key'], + asymmetricPublicKey: maps[0]['asymmetric_public_key'], + acceptedAt: maps[0]['accepted_at'], + username: maps[0]['username'], + ); +} diff --git a/mobile/lib/models/messages.dart b/mobile/lib/models/messages.dart index 4fc3728..0cb0bef 100644 --- a/mobile/lib/models/messages.dart +++ b/mobile/lib/models/messages.dart @@ -1,8 +1,14 @@ import 'dart:convert'; +import 'dart:typed_data'; +import 'package:uuid/uuid.dart'; +import 'package:Envelope/models/conversation_users.dart'; +import 'package:Envelope/models/conversations.dart'; import 'package:pointycastle/export.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '/utils/encryption/crypto_utils.dart'; import '/utils/encryption/aes_helper.dart'; import '/utils/storage/database.dart'; +import '/utils/strings.dart'; import '/models/friends.dart'; const messageTypeSender = 'sender'; @@ -11,49 +17,116 @@ const messageTypeReceiver = 'receiver'; class Message { String id; String symmetricKey; - String messageThreadKey; + String userSymmetricKey; String data; String senderId; String senderUsername; + String associationKey; String createdAt; Message({ required this.id, required this.symmetricKey, - required this.messageThreadKey, + required this.userSymmetricKey, required this.data, required this.senderId, required this.senderUsername, + required this.associationKey, required this.createdAt, }); factory Message.fromJson(Map json, RSAPrivateKey privKey) { - var symmetricKey = CryptoUtils.rsaDecrypt( + var userSymmetricKey = CryptoUtils.rsaDecrypt( base64.decode(json['symmetric_key']), privKey, ); - var data = AesHelper.aesDecrypt( - symmetricKey, - base64.decode(json['message_data']['data']), + var symmetricKey = AesHelper.aesDecrypt( + userSymmetricKey, + base64.decode(json['message_data']['symmetric_key']), ); var senderId = AesHelper.aesDecrypt( - symmetricKey, + base64.decode(symmetricKey), base64.decode(json['message_data']['sender_id']), ); + var data = AesHelper.aesDecrypt( + base64.decode(symmetricKey), + base64.decode(json['message_data']['data']), + ); + return Message( id: json['id'], - messageThreadKey: json['message_thread_key'], - symmetricKey: base64.encode(symmetricKey), + symmetricKey: symmetricKey, + userSymmetricKey: base64.encode(userSymmetricKey), data: data, senderId: senderId, - senderUsername: 'Unknown', // TODO + senderUsername: 'Unknown', + associationKey: json['association_key'], createdAt: json['created_at'], ); } + Future toJson(Conversation conversation, String messageDataId) async { + final preferences = await SharedPreferences.getInstance(); + RSAPublicKey publicKey = CryptoUtils.rsaPublicKeyFromPem(preferences.getString('asymmetricPublicKey')!); + + final userSymmetricKey = AesHelper.deriveKey(generateRandomString(32)); + final symmetricKey = AesHelper.deriveKey(generateRandomString(32)); + + List> messages = []; + + String id = ''; + + List conversationUsers = await getConversationUsers(conversation); + + for (var i = 0; i < conversationUsers.length; i++) { + ConversationUser user = conversationUsers[i]; + if (preferences.getString('username') == user.username) { + id = user.id; + + messages.add({ + 'message_data_id': messageDataId, + 'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt( + userSymmetricKey, + publicKey, + )), + 'association_key': user.associationKey, + }); + + continue; + } + + Friend friend = await getFriendByFriendId(user.id); + RSAPublicKey friendPublicKey = CryptoUtils.rsaPublicKeyFromPem(friend.asymmetricPublicKey); + + messages.add({ + 'message_data_id': messageDataId, + 'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt( + userSymmetricKey, + friendPublicKey, + )), + 'association_key': user.associationKey, + }); + } + + Map messageData = { + 'id': messageDataId, + 'data': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(data.codeUnits)), + 'sender_id': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(id.codeUnits)), + 'symmetric_key': AesHelper.aesEncrypt( + userSymmetricKey, + Uint8List.fromList(base64.encode(symmetricKey).codeUnits), + ), + }; + + return jsonEncode({ + 'message_data': messageData, + 'message': messages, + }); + } + @override String toString() { return ''' @@ -63,6 +136,7 @@ class Message { data: $data senderId: $senderId senderUsername: $senderUsername + associationKey: $associationKey createdAt: $createdAt '''; } @@ -70,37 +144,40 @@ class Message { Map toMap() { return { 'id': id, - 'message_thread_key': messageThreadKey, 'symmetric_key': symmetricKey, + 'user_symmetric_key': userSymmetricKey, 'data': data, 'sender_id': senderId, 'sender_username': senderUsername, + 'association_key': associationKey, 'created_at': createdAt, }; } } -Future> getMessagesForThread(String messageThreadKey) async { +Future> getMessagesForThread(Conversation conversation) async { final db = await getDatabaseConnection(); - List whereArguments = [messageThreadKey]; - - final List> maps = await db.query( - 'messages', - where: 'message_thread_key = ?', - whereArgs: whereArguments, - orderBy: 'created_at DESC', + 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; + ''', + [conversation.id] ); return List.generate(maps.length, (i) { return Message( id: maps[i]['id'], - messageThreadKey: maps[i]['message_thread_key'], symmetricKey: maps[i]['symmetric_key'], + userSymmetricKey: maps[i]['user_symmetric_key'], data: maps[i]['data'], senderId: maps[i]['sender_id'], senderUsername: maps[i]['sender_username'], + associationKey: maps[i]['association_key'], createdAt: maps[i]['created_at'], ); }); diff --git a/mobile/lib/utils/storage/conversations.dart b/mobile/lib/utils/storage/conversations.dart index b2c5266..749bd67 100644 --- a/mobile/lib/utils/storage/conversations.dart +++ b/mobile/lib/utils/storage/conversations.dart @@ -4,6 +4,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:pointycastle/export.dart'; import 'package:sqflite/sqflite.dart'; import '/models/conversations.dart'; +import '/models/conversation_users.dart'; import '/utils/storage/database.dart'; import '/utils/storage/session_cookie.dart'; import '/utils/storage/encryption_keys.dart'; @@ -65,15 +66,30 @@ Future updateConversations() async { base64.decode(conversationDetailJson['name']), ); - conversation.users = AesHelper.aesDecrypt( - base64.decode(conversation.symmetricKey), - base64.decode(conversationDetailJson['users']), - ); - await db.insert( 'conversations', conversation.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); + + List usersData = json.decode( + AesHelper.aesDecrypt( + base64.decode(conversation.symmetricKey), + base64.decode(conversationDetailJson['users']), + ) + ); + + for (var i = 0; i < usersData.length; i++) { + ConversationUser conversationUser = ConversationUser.fromJson( + usersData[i] as Map, + conversation.id, + ); + + await db.insert( + 'conversation_users', + conversationUser.toMap(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } } } diff --git a/mobile/lib/utils/storage/database.dart b/mobile/lib/utils/storage/database.dart index 3be45e7..41419e5 100644 --- a/mobile/lib/utils/storage/database.dart +++ b/mobile/lib/utils/storage/database.dart @@ -24,6 +24,7 @@ Future getDatabaseConnection() async { username TEXT, friend_id TEXT, symmetric_key TEXT, + asymmetric_public_key TEXT, accepted_at TEXT ); '''); @@ -35,7 +36,6 @@ Future getDatabaseConnection() async { id TEXT PRIMARY KEY, user_id TEXT, conversation_detail_id TEXT, - message_thread_key TEXT, symmetric_key TEXT, admin INTEGER, name TEXT, @@ -43,18 +43,32 @@ Future getDatabaseConnection() async { ); '''); + await db.execute( + ''' + CREATE TABLE IF NOT EXISTS conversation_users( + id TEXT PRIMARY KEY, + conversation_id TEXT, + username TEXT, + data TEXT, + association_key TEXT, + admin TEXT + ); + '''); + await db.execute( ''' CREATE TABLE IF NOT EXISTS messages( id TEXT PRIMARY KEY, - message_thread_key TEXT, symmetric_key TEXT, + user_symmetric_key TEXT, data TEXT, sender_id TEXT, sender_username TEXT, + association_key TEXT, created_at TEXT ); '''); + }, // Set the version. This executes the onCreate function and provides a // path to perform database upgrades and downgrades. diff --git a/mobile/lib/utils/storage/friends.dart b/mobile/lib/utils/storage/friends.dart index 529bbcf..a4fc827 100644 --- a/mobile/lib/utils/storage/friends.dart +++ b/mobile/lib/utils/storage/friends.dart @@ -67,6 +67,8 @@ Future updateFriends() async { base64.decode(friendJson['username']), ); + friend.asymmetricPublicKey = friendJson['asymmetric_public_key']; + await db.insert( 'friends', friend.toMap(), diff --git a/mobile/lib/utils/storage/messages.dart b/mobile/lib/utils/storage/messages.dart index c2fb659..33c13e6 100644 --- a/mobile/lib/utils/storage/messages.dart +++ b/mobile/lib/utils/storage/messages.dart @@ -1,76 +1,112 @@ import 'dart:convert'; -import 'package:Envelope/models/messages.dart'; +import 'package:uuid/uuid.dart'; +import 'package:Envelope/models/conversation_users.dart'; +import 'package:intl/intl.dart'; import 'package:http/http.dart' as http; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:pointycastle/export.dart'; import 'package:sqflite/sqflite.dart'; -import 'package:Envelope/models/conversations.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '/utils/storage/session_cookie.dart'; import '/utils/storage/encryption_keys.dart'; import '/utils/storage/database.dart'; import '/models/conversations.dart'; - -// TODO: Move this to table -Map> _mapUsers(String users) { - List usersJson = jsonDecode(users); - - Map> mapped = {}; - - for (var i = 0; i < usersJson.length; i++) { - mapped[usersJson[i]['id']] = { - 'username': usersJson[i]['username'], - 'admin': usersJson[i]['admin'], - }; - } - - return mapped; -} +import '/models/messages.dart'; Future updateMessageThread(Conversation conversation, {RSAPrivateKey? privKey}) async { - privKey ??= await getPrivateKey(); - - var resp = await http.get( - Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/messages/${conversation.messageThreadKey}'), - headers: { - 'cookie': await getSessionCookie(), - } + privKey ??= await getPrivateKey(); + final preferences = await SharedPreferences.getInstance(); + String username = preferences.getString('username')!; + ConversationUser currentUser = await getConversationUserByUsername(conversation, username); + + var resp = await http.get( + Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/messages/${currentUser.associationKey}'), + headers: { + 'cookie': await getSessionCookie(), + } + ); + + if (resp.statusCode != 200) { + throw Exception(resp.body); + } + + List messageThreadJson = jsonDecode(resp.body); + + final db = await getDatabaseConnection(); + + for (var i = 0; i < messageThreadJson.length; i++) { + Message message = Message.fromJson( + messageThreadJson[i] as Map, + privKey, ); - if (resp.statusCode != 200) { - throw Exception(resp.body); - } - - var mapped = _mapUsers(conversation.users!); + ConversationUser messageUser = await getConversationUserById(conversation, message.senderId); + message.senderUsername = messageUser.username; - List messageThreadJson = jsonDecode(resp.body); - - final db = await getDatabaseConnection(); - - for (var i = 0; i < messageThreadJson.length; i++) { - Message message = Message.fromJson( - messageThreadJson[i] as Map, - privKey, - ); - - // TODO: Fix this - message.senderUsername = mapped[message.senderId]!['username']!; - - await db.insert( - 'messages', - message.toMap(), - conflictAlgorithm: ConflictAlgorithm.replace, - ); - - } + await db.insert( + 'messages', + message.toMap(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } } Future updateMessageThreads({List? conversations}) async { - RSAPrivateKey privKey = await getPrivateKey(); + RSAPrivateKey privKey = await getPrivateKey(); - conversations ??= await getConversations(); + conversations ??= await getConversations(); - for (var i = 0; i < conversations.length; i++) { - await updateMessageThread(conversations[i], privKey: privKey); - } + for (var i = 0; i < conversations.length; i++) { + await updateMessageThread(conversations[i], privKey: privKey); + } } +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'); + } + + var uuid = const Uuid(); + final String messageDataId = uuid.v4(); + + ConversationUser currentUser = await getConversationUserByUsername(conversation, username); + + + Message message = Message( + id: messageDataId, + symmetricKey: '', + userSymmetricKey: '', + senderId: userId, + senderUsername: username, + data: data, + createdAt: DateTime.now().toIso8601String(), + associationKey: currentUser.associationKey, + ); + + final db = await getDatabaseConnection(); + + print(await db.query('messages')); + + await db.insert( + 'messages', + message.toMap(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + + String messageJson = await message.toJson(conversation, messageDataId); + + final resp = await http.post( + Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/message'), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + 'cookie': await getSessionCookie(), + }, + body: messageJson, + ); + + // TODO: If statusCode not successfull, mark as needing resend + print(resp.statusCode); +} diff --git a/mobile/lib/utils/strings.dart b/mobile/lib/utils/strings.dart new file mode 100644 index 0000000..1c8b117 --- /dev/null +++ b/mobile/lib/utils/strings.dart @@ -0,0 +1,8 @@ +import 'dart:math'; + +String generateRandomString(int len) { + var r = Random(); + const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; + return List.generate(len, (index) => _chars[r.nextInt(_chars.length)]).join(); +} + diff --git a/mobile/lib/views/authentication/login.dart b/mobile/lib/views/authentication/login.dart index b702f11..9802423 100644 --- a/mobile/lib/views/authentication/login.dart +++ b/mobile/lib/views/authentication/login.dart @@ -14,6 +14,7 @@ class LoginResponse { final String asymmetricPublicKey; final String asymmetricPrivateKey; final String userId; + final String username; const LoginResponse({ required this.status, @@ -21,6 +22,7 @@ class LoginResponse { required this.asymmetricPublicKey, required this.asymmetricPrivateKey, required this.userId, + required this.username, }); factory LoginResponse.fromJson(Map json) { @@ -30,6 +32,7 @@ class LoginResponse { asymmetricPublicKey: json['asymmetric_public_key'], asymmetricPrivateKey: json['asymmetric_private_key'], userId: json['user_id'], + username: json['username'], ); } } @@ -60,14 +63,14 @@ Future login(context, String username, String password) async { var rsaPrivPem = AesHelper.aesDecrypt(password, base64.decode(response.asymmetricPrivateKey)); - debugPrint(rsaPrivPem); - var rsaPriv = CryptoUtils.rsaPrivateKeyFromPem(rsaPrivPem); setPrivateKey(rsaPriv); final preferences = await SharedPreferences.getInstance(); preferences.setBool('islogin', true); preferences.setString('userId', response.userId); + preferences.setString('username', response.username); + preferences.setString('asymmetricPublicKey', response.asymmetricPublicKey); return response; } @@ -75,8 +78,6 @@ Future login(context, String username, String password) async { class Login extends StatelessWidget { const Login({Key? key}) : super(key: key); - static const String _title = 'Envelope'; - @override Widget build(BuildContext context) { return Scaffold( diff --git a/mobile/lib/views/authentication/signup.dart b/mobile/lib/views/authentication/signup.dart index d179a21..74425c3 100644 --- a/mobile/lib/views/authentication/signup.dart +++ b/mobile/lib/views/authentication/signup.dart @@ -47,25 +47,19 @@ Future signUp(context, String username, String password, String 'asymmetric_private_key': encRsaPriv, }), ); - + SignupResponse response = SignupResponse.fromJson(jsonDecode(resp.body)); if (resp.statusCode != 201) { throw Exception(response.message); } - debugPrint(rsaPubPem); - debugPrint(rsaPrivPem); - debugPrint(resp.body); - return response; } class Signup extends StatelessWidget { const Signup({Key? key}) : super(key: key); - static const String _title = 'Envelope'; - @override Widget build(BuildContext context) { return Scaffold( @@ -113,7 +107,7 @@ class _SignupWidgetState extends State { minimumSize: const Size.fromHeight(50), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), textStyle: const TextStyle( - fontSize: 20, + fontSize: 20, fontWeight: FontWeight.bold, color: Colors.red, ), diff --git a/mobile/lib/views/main/conversation_detail.dart b/mobile/lib/views/main/conversation_detail.dart index aaa158e..c3f0bb4 100644 --- a/mobile/lib/views/main/conversation_detail.dart +++ b/mobile/lib/views/main/conversation_detail.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '/models/conversations.dart'; import '/models/messages.dart'; +import '/utils/storage/messages.dart'; String convertToAgo(String input){ DateTime time = DateTime.parse(input); @@ -36,7 +37,9 @@ class ConversationDetail extends StatefulWidget{ class _ConversationDetailState extends State { List messages = []; - String userId = ''; + String username = ''; + + TextEditingController msgController = TextEditingController(); @override void initState() { @@ -44,10 +47,10 @@ class _ConversationDetailState extends State { fetchMessages(); } - void fetchMessages() async { + Future fetchMessages() async { final preferences = await SharedPreferences.getInstance(); - userId = preferences.getString('userId')!; - messages = await getMessagesForThread(widget.conversation.messageThreadKey); + username = preferences.getString('username')!; + messages = await getMessagesForThread(widget.conversation); setState(() {}); } @@ -84,7 +87,7 @@ class _ConversationDetailState extends State { ], ), ), - const Icon(Icons.settings,color: Colors.black54,), + const Icon(Icons.settings,color: Colors.black54), ], ), ), @@ -95,35 +98,35 @@ class _ConversationDetailState extends State { ListView.builder( itemCount: messages.length, shrinkWrap: true, - padding: const EdgeInsets.only(top: 10,bottom: 10), + padding: const EdgeInsets.only(top: 10,bottom: 90), reverse: true, itemBuilder: (context, index) { return Container( padding: const EdgeInsets.only(left: 14,right: 14,top: 0,bottom: 0), child: Align( alignment: ( - messages[index].senderId == userId ? - Alignment.topLeft: - Alignment.topRight + messages[index].senderUsername == username ? + Alignment.topRight : + Alignment.topLeft ), child: Column( - crossAxisAlignment: messages[index].senderId == userId ? - CrossAxisAlignment.start : - CrossAxisAlignment.end, + crossAxisAlignment: messages[index].senderUsername == username ? + CrossAxisAlignment.end : + CrossAxisAlignment.start, children: [ Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), color: ( - messages[index].senderId == userId ? - Colors.grey.shade200 : - Colors.blue[200] + messages[index].senderUsername == username ? + Colors.blue[200] : + Colors.grey.shade200 ), ), padding: const EdgeInsets.all(12), child: Text(messages[index].data, style: const TextStyle(fontSize: 15)), ), - messages[index].senderId != userId ? + messages[index].senderUsername != username ? Text(messages[index].senderUsername) : const SizedBox.shrink(), Text( @@ -167,24 +170,32 @@ class _ConversationDetailState extends State { ), ), const SizedBox(width: 15,), - const Expanded( + Expanded( child: TextField( - decoration: InputDecoration( + decoration: const InputDecoration( hintText: "Write message...", hintStyle: TextStyle(color: Colors.black54), border: InputBorder.none, ), maxLines: null, + controller: msgController, ), ), - const SizedBox(width: 15,), + const SizedBox(width: 15), FloatingActionButton( - onPressed: () { + onPressed: () async { + if (msgController.text == '') { + return; + } + await sendMessage(widget.conversation, msgController.text); + messages = await getMessagesForThread(widget.conversation); + setState(() {}); + msgController.text = ''; }, child: const Icon(Icons.send,color: Colors.white,size: 18,), backgroundColor: Colors.blue, - elevation: 0, ), + const SizedBox(width: 10), ], ), ), diff --git a/mobile/lib/views/main/conversation_list.dart b/mobile/lib/views/main/conversation_list.dart index 7ff74b6..1636f4f 100644 --- a/mobile/lib/views/main/conversation_list.dart +++ b/mobile/lib/views/main/conversation_list.dart @@ -1,110 +1,133 @@ import 'package:flutter/material.dart'; import '/models/conversations.dart'; import '/views/main/conversation_list_item.dart'; -import '/utils/storage/messages.dart'; class ConversationList extends StatefulWidget { - const ConversationList({Key? key}) : super(key: key); + final List conversations; + const ConversationList({ + Key? key, + required this.conversations, + }) : super(key: key); - @override - State createState() => _ConversationListState(); + @override + State createState() => _ConversationListState(); } class _ConversationListState extends State { - List conversations = []; + List conversations = []; - @override - void initState() { - super.initState(); - fetchConversations(); - } - - void fetchConversations() async { - conversations = await getConversations(); - setState(() {}); - } + @override + void initState() { + super.initState(); + conversations.addAll(widget.conversations); + setState(() {}); + } - Widget list() { + void filterSearchResults(String query) { + List dummySearchList = []; + dummySearchList.addAll(widget.conversations); - if (conversations.isEmpty) { - return const Center( - child: Text('No Conversations'), - ); + if(query.isNotEmpty) { + List dummyListData = []; + dummySearchList.forEach((item) { + if (item.name.toLowerCase().contains(query)) { + dummyListData.add(item); } - - return ListView.builder( - itemCount: conversations.length, - shrinkWrap: true, - padding: const EdgeInsets.only(top: 16), - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, i) { - return ConversationListItem( - conversation: conversations[i], - ); - }, - ); + }); + setState(() { + conversations.clear(); + conversations.addAll(dummyListData); + }); + return; } - @override - Widget build(BuildContext context) { - return Scaffold( - body: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SafeArea( - child: Padding( - padding: const EdgeInsets.only(left: 16,right: 16,top: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text("Conversations",style: TextStyle(fontSize: 32,fontWeight: FontWeight.bold),), - Container( - padding: const EdgeInsets.only(left: 8,right: 8,top: 2,bottom: 2), - height: 30, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - color: Colors.pink[50], - ), - child: Row( - children: const [ - Icon(Icons.add,color: Colors.pink,size: 20,), - SizedBox(width: 2,), - Text("Add",style: TextStyle(fontSize: 14,fontWeight: FontWeight.bold),), - ], - ), - ) - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 16,left: 16,right: 16), - child: TextField( - decoration: InputDecoration( - hintText: "Search...", - hintStyle: TextStyle(color: Colors.grey.shade600), - prefixIcon: Icon(Icons.search,color: Colors.grey.shade600, size: 20,), - filled: true, - fillColor: Colors.grey.shade100, - contentPadding: const EdgeInsets.all(8), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(20), - borderSide: BorderSide( - color: Colors.grey.shade100 - ) - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 16,left: 16,right: 16), - child: list(), - ), - ], - ), - ), - ); + setState(() { + conversations.clear(); + conversations.addAll(widget.conversations); + }); + } + + Widget list() { + if (conversations.isEmpty) { + return const Center( + child: Text('No Conversations'), + ); } + + return ListView.builder( + itemCount: conversations.length, + shrinkWrap: true, + padding: const EdgeInsets.only(top: 16), + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, i) { + return ConversationListItem( + conversation: conversations[i], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SafeArea( + child: Padding( + padding: const EdgeInsets.only(left: 16,right: 16,top: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text("Conversations",style: TextStyle(fontSize: 32,fontWeight: FontWeight.bold),), + Container( + padding: const EdgeInsets.only(left: 8,right: 8,top: 2,bottom: 2), + height: 30, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: Colors.pink[50], + ), + child: Row( + children: const [ + Icon(Icons.add,color: Colors.pink,size: 20,), + SizedBox(width: 2,), + Text("Add",style: TextStyle(fontSize: 14,fontWeight: FontWeight.bold),), + ], + ), + ) + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16,left: 16,right: 16), + child: TextField( + decoration: InputDecoration( + hintText: "Search...", + hintStyle: TextStyle(color: Colors.grey.shade600), + prefixIcon: Icon(Icons.search,color: Colors.grey.shade600, size: 20,), + filled: true, + fillColor: Colors.grey.shade100, + contentPadding: const EdgeInsets.all(8), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(20), + borderSide: BorderSide( + color: Colors.grey.shade100 + ) + ), + ), + onChanged: (value) => filterSearchResults(value.toLowerCase()) + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16,left: 16,right: 16), + child: list(), + ), + ], + ), + ), + ); + } } diff --git a/mobile/lib/views/main/friend_list.dart b/mobile/lib/views/main/friend_list.dart index 2437672..719c57c 100644 --- a/mobile/lib/views/main/friend_list.dart +++ b/mobile/lib/views/main/friend_list.dart @@ -3,108 +3,133 @@ import '/models/friends.dart'; import '/views/main/friend_list_item.dart'; class FriendList extends StatefulWidget { - const FriendList({Key? key}) : super(key: key); + final List friends; + const FriendList({ + Key? key, + required this.friends, + }) : super(key: key); - @override - State createState() => _FriendListState(); + @override + State createState() => _FriendListState(); } class _FriendListState extends State { - List friends = []; + List friends = []; + List friendsDuplicate = []; - @override - void initState() { - super.initState(); - fetchFriends(); - } - - void fetchFriends() async { - friends = await getFriends(); - setState(() {}); - } + @override + void initState() { + super.initState(); + friends.addAll(widget.friends); + setState(() {}); + } - Widget list() { + void filterSearchResults(String query) { + List dummySearchList = []; + dummySearchList.addAll(widget.friends); - if (friends.isEmpty) { - return const Center( - child: Text('No Friends'), - ); + if(query.isNotEmpty) { + List dummyListData = []; + dummySearchList.forEach((item) { + if(item.username.toLowerCase().contains(query)) { + dummyListData.add(item); } + }); + setState(() { + friends.clear(); + friends.addAll(dummyListData); + }); + return; + } - return ListView.builder( - itemCount: friends.length, - shrinkWrap: true, - padding: const EdgeInsets.only(top: 16), - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, i) { - return FriendListItem( - id: friends[i].id, - username: friends[i].username!, - ); - }, - ); + setState(() { + friends.clear(); + friends.addAll(widget.friends); + }); + } + + Widget list() { + if (friends.isEmpty) { + return const Center( + child: Text('No Friends'), + ); } - @override - Widget build(BuildContext context) { - return Scaffold( - body: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SafeArea( - child: Padding( - padding: const EdgeInsets.only(left: 16,right: 16,top: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text("Friends",style: TextStyle(fontSize: 32,fontWeight: FontWeight.bold),), - Container( - padding: const EdgeInsets.only(left: 8,right: 8,top: 2,bottom: 2), - height: 30, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - color: Colors.pink[50], - ), - child: Row( - children: const [ - Icon(Icons.add,color: Colors.pink,size: 20,), - SizedBox(width: 2,), - Text("Add",style: TextStyle(fontSize: 14,fontWeight: FontWeight.bold),), - ], - ), - ) - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 16,left: 16,right: 16), - child: TextField( - decoration: InputDecoration( - hintText: "Search...", - hintStyle: TextStyle(color: Colors.grey.shade600), - prefixIcon: Icon(Icons.search,color: Colors.grey.shade600, size: 20,), - filled: true, - fillColor: Colors.grey.shade100, - contentPadding: const EdgeInsets.all(8), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(20), - borderSide: BorderSide( - color: Colors.grey.shade100 - ) - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 16,left: 16,right: 16), - child: list(), - ), - ], + return ListView.builder( + itemCount: friends.length, + shrinkWrap: true, + padding: const EdgeInsets.only(top: 16), + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, i) { + return FriendListItem( + id: friends[i].id, + username: friends[i].username, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SafeArea( + child: Padding( + padding: const EdgeInsets.only(left: 16,right: 16,top: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text("Friends",style: TextStyle(fontSize: 32,fontWeight: FontWeight.bold),), + Container( + padding: const EdgeInsets.only(left: 8,right: 8,top: 2,bottom: 2), + height: 30, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: Colors.pink[50], + ), + child: Row( + children: const [ + Icon(Icons.add,color: Colors.pink,size: 20,), + SizedBox(width: 2,), + Text("Add",style: TextStyle(fontSize: 14,fontWeight: FontWeight.bold),), + ], + ), + ) + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16,left: 16,right: 16), + child: TextField( + decoration: InputDecoration( + hintText: "Search...", + hintStyle: TextStyle(color: Colors.grey.shade600), + prefixIcon: Icon(Icons.search,color: Colors.grey.shade600, size: 20,), + filled: true, + fillColor: Colors.grey.shade100, + contentPadding: const EdgeInsets.all(8), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(20), + borderSide: BorderSide( + color: Colors.grey.shade100 + ) + ), ), + onChanged: (value) => filterSearchResults(value.toLowerCase()) ), - ); - } + ), + Padding( + padding: const EdgeInsets.only(top: 16,left: 16,right: 16), + child: list(), + ), + ], + ), + ), + ); + } } diff --git a/mobile/lib/views/main/home.dart b/mobile/lib/views/main/home.dart index 7d4e6e9..06c6295 100644 --- a/mobile/lib/views/main/home.dart +++ b/mobile/lib/views/main/home.dart @@ -6,6 +6,8 @@ import '/views/main/profile.dart'; import '/utils/storage/friends.dart'; import '/utils/storage/conversations.dart'; import '/utils/storage/messages.dart'; +import '/models/conversations.dart'; +import '/models/friends.dart'; class Home extends StatefulWidget { const Home({Key? key}) : super(key: key); @@ -15,6 +17,17 @@ class Home extends StatefulWidget { } class _HomeState extends State { + List conversations = []; + List friends = []; + + bool isLoading = true; + int _selectedIndex = 0; + List _widgetOptions = [ + const ConversationList(conversations: []), + const FriendList(friends: []), + const Profile(), + ]; + @override void initState() { super.initState(); @@ -26,6 +39,18 @@ class _HomeState extends State { await updateFriends(); await updateConversations(); await updateMessageThreads(); + + conversations = await getConversations(); + friends = await getFriends(); + + setState(() { + _widgetOptions = [ + ConversationList(conversations: conversations), + FriendList(friends: friends), + const Profile(), + ]; + isLoading = false; + }); } // TODO: Do server GET check here @@ -36,26 +61,41 @@ class _HomeState extends State { } } - int _selectedIndex = 0; - static const List _widgetOptions = [ - ConversationList(), - FriendList(), - Profile(), - ]; - void _onItemTapped(int index) { setState(() { _selectedIndex = index; }); } + Widget loading() { + return Stack( + children: [ + const Opacity( + opacity: 0.1, + child: ModalBarrier(dismissible: false, color: Colors.black), + ), + Center( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + CircularProgressIndicator(), + SizedBox(height: 25), + Text("Loading..."), + ], + ) + ), + ] + ); + } + @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async => false, child: Scaffold( - body: _widgetOptions.elementAt(_selectedIndex), - bottomNavigationBar: BottomNavigationBar( + body: isLoading ? loading() : _widgetOptions.elementAt(_selectedIndex), + bottomNavigationBar: isLoading ? const SizedBox.shrink() : BottomNavigationBar( currentIndex: _selectedIndex, onTap: _onItemTapped, selectedItemColor: Colors.red, diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index c7d9286..0f77a70 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -57,6 +57,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" cupertino_icons: dependency: "direct main" description: @@ -135,6 +142,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.0.1" + intl: + dependency: "direct main" + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" js: dependency: transitive description: @@ -357,6 +371,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" + uuid: + dependency: "direct main" + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.6" vector_math: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 9dffd90..ded0ef6 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -20,6 +20,8 @@ dependencies: sqflite: ^2.0.2 path: 1.8.1 flutter_dotenv: ^5.0.2 + intl: ^0.17.0 + uuid: ^3.0.6 dev_dependencies: flutter_test: