Browse Source

Fix messages doubling up due to mismatched ids

pull/1/head
Tovi Jaeschke-Rogers 2 years ago
parent
commit
c89dcf10ec
24 changed files with 932 additions and 760 deletions
  1. +4
    -4
      Backend/Api/Messages/CreateConversation.go
  2. +8
    -8
      Backend/Api/Messages/MessageThread.go
  3. +4
    -4
      Backend/Api/Messages/UpdateConversation.go
  4. +41
    -0
      Backend/Database/ConversationDetailUsers.go
  5. +1
    -0
      Backend/Database/Init.go
  6. +3
    -7
      Backend/Database/Messages.go
  7. +90
    -68
      Backend/Database/Seeder/MessageSeeder.go
  8. +33
    -0
      Backend/Models/Conversations.go
  9. +0
    -16
      Backend/Models/Messages.go
  10. +2
    -2
      Backend/main.go
  11. +12
    -11
      README.md
  12. +94
    -8
      mobile/lib/models/conversation_users.dart
  13. +15
    -20
      mobile/lib/models/conversations.dart
  14. +26
    -20
      mobile/lib/models/friends.dart
  15. +8
    -6
      mobile/lib/models/messages.dart
  16. +3
    -8
      mobile/lib/utils/storage/conversations.dart
  17. +2
    -3
      mobile/lib/utils/storage/database.dart
  18. +3
    -3
      mobile/lib/utils/storage/messages.dart
  19. +10
    -10
      mobile/lib/views/authentication/login.dart
  20. +1
    -1
      mobile/lib/views/authentication/signup.dart
  21. +91
    -94
      mobile/lib/views/main/conversation/create_add_users.dart
  22. +227
    -217
      mobile/lib/views/main/conversation/detail.dart
  23. +188
    -184
      mobile/lib/views/main/conversation/settings.dart
  24. +66
    -66
      mobile/lib/views/main/friend/list.dart

+ 4
- 4
Backend/Api/Messages/CreateConversation.go View File

@ -11,10 +11,10 @@ import (
) )
type RawCreateConversationData struct { type RawCreateConversationData struct {
ID string `json:"id"`
Name string `json:"name"`
Users string `json:"users"`
UserConversations []Models.UserConversation `json:"user_conversations"`
ID string `json:"id"`
Name string `json:"name"`
Users []Models.ConversationDetailUser `json:"users"`
UserConversations []Models.UserConversation `json:"user_conversations"`
} }
func CreateConversation(w http.ResponseWriter, r *http.Request) { func CreateConversation(w http.ResponseWriter, r *http.Request) {


+ 8
- 8
Backend/Api/Messages/MessageThread.go View File

@ -12,22 +12,22 @@ import (
func Messages(w http.ResponseWriter, r *http.Request) { func Messages(w http.ResponseWriter, r *http.Request) {
var ( var (
messages []Models.Message
urlVars map[string]string
threadKey string
returnJson []byte
ok bool
err error
messages []Models.Message
urlVars map[string]string
associationKey string
returnJson []byte
ok bool
err error
) )
urlVars = mux.Vars(r) urlVars = mux.Vars(r)
threadKey, ok = urlVars["threadKey"]
associationKey, ok = urlVars["threadKey"]
if !ok { if !ok {
http.Error(w, "Not Found", http.StatusNotFound) http.Error(w, "Not Found", http.StatusNotFound)
return return
} }
messages, err = Database.GetMessagesByThreadKey(threadKey)
messages, err = Database.GetMessagesByAssociationKey(associationKey)
if !ok { if !ok {
http.Error(w, "Not Found", http.StatusNotFound) http.Error(w, "Not Found", http.StatusNotFound)
return return


+ 4
- 4
Backend/Api/Messages/UpdateConversation.go View File

@ -11,10 +11,10 @@ import (
) )
type RawUpdateConversationData struct { type RawUpdateConversationData struct {
ID string `json:"id"`
Name string `json:"name"`
Users string `json:"users"`
UserConversations []Models.UserConversation `json:"user_conversations"`
ID string `json:"id"`
Name string `json:"name"`
Users []Models.ConversationDetailUser `json:"users"`
UserConversations []Models.UserConversation `json:"user_conversations"`
} }
func UpdateConversation(w http.ResponseWriter, r *http.Request) { func UpdateConversation(w http.ResponseWriter, r *http.Request) {


+ 41
- 0
Backend/Database/ConversationDetailUsers.go View File

@ -0,0 +1,41 @@
package Database
import (
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
func GetConversationDetailUserById(id string) (Models.ConversationDetailUser, error) {
var (
messageThread Models.ConversationDetailUser
err error
)
err = DB.Preload(clause.Associations).
Where("id = ?", id).
First(&messageThread).
Error
return messageThread, err
}
func CreateConversationDetailUser(messageThread *Models.ConversationDetailUser) error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(messageThread).
Error
}
func UpdateConversationDetailUser(messageThread *Models.ConversationDetailUser) error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Where("id = ?", messageThread.ID).
Updates(messageThread).
Error
}
func DeleteConversationDetailUser(messageThread *Models.ConversationDetailUser) error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Delete(messageThread).
Error
}

+ 1
- 0
Backend/Database/Init.go View File

@ -24,6 +24,7 @@ func GetModels() []interface{} {
&Models.MessageData{}, &Models.MessageData{},
&Models.Message{}, &Models.Message{},
&Models.ConversationDetail{}, &Models.ConversationDetail{},
&Models.ConversationDetailUser{},
&Models.UserConversation{}, &Models.UserConversation{},
} }
} }


+ 3
- 7
Backend/Database/Messages.go View File

@ -20,7 +20,7 @@ func GetMessageById(id string) (Models.Message, error) {
return message, err return message, err
} }
func GetMessagesByThreadKey(associationKey string) ([]Models.Message, error) {
func GetMessagesByAssociationKey(associationKey string) ([]Models.Message, error) {
var ( var (
messages []Models.Message messages []Models.Message
err error err error
@ -34,9 +34,7 @@ func GetMessagesByThreadKey(associationKey string) ([]Models.Message, error) {
} }
func CreateMessage(message *Models.Message) error { func CreateMessage(message *Models.Message) error {
var (
err error
)
var err error
err = DB.Session(&gorm.Session{FullSaveAssociations: true}). err = DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(message). Create(message).
@ -46,9 +44,7 @@ func CreateMessage(message *Models.Message) error {
} }
func CreateMessages(messages *[]Models.Message) error { func CreateMessages(messages *[]Models.Message) error {
var (
err error
)
var err error
err = DB.Session(&gorm.Session{FullSaveAssociations: true}). err = DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(messages). Create(messages).


+ 90
- 68
Backend/Database/Seeder/MessageSeeder.go View File

@ -2,7 +2,6 @@ package Seeder
import ( import (
"encoding/base64" "encoding/base64"
"fmt"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models"
@ -115,39 +114,19 @@ func seedConversationDetail(key aesKey) (Models.ConversationDetail, error) {
return messageThread, err return messageThread, err
} }
func seedUpdateUserConversation(
userJson string,
key aesKey,
messageThread Models.ConversationDetail,
) (Models.ConversationDetail, error) {
var (
usersCiphertext []byte
err error
)
usersCiphertext, err = key.aesEncrypt([]byte(userJson))
if err != nil {
return messageThread, err
}
messageThread.Users = base64.StdEncoding.EncodeToString(usersCiphertext)
err = Database.UpdateConversationDetail(&messageThread)
return messageThread, err
}
func seedUserConversation( func seedUserConversation(
user Models.User, user Models.User,
threadID uuid.UUID, threadID uuid.UUID,
key aesKey, key aesKey,
) (Models.UserConversation, error) { ) (Models.UserConversation, error) {
var ( var (
messageThreadUser Models.UserConversation
threadIdCiphertext []byte
adminCiphertext []byte
err error
messageThreadUser Models.UserConversation
conversationDetailIDCiphertext []byte
adminCiphertext []byte
err error
) )
threadIdCiphertext, err = key.aesEncrypt([]byte(threadID.String()))
conversationDetailIDCiphertext, err = key.aesEncrypt([]byte(threadID.String()))
if err != nil { if err != nil {
return messageThreadUser, err return messageThreadUser, err
} }
@ -159,7 +138,7 @@ func seedUserConversation(
messageThreadUser = Models.UserConversation{ messageThreadUser = Models.UserConversation{
UserID: user.ID, UserID: user.ID,
ConversationDetailID: base64.StdEncoding.EncodeToString(threadIdCiphertext),
ConversationDetailID: base64.StdEncoding.EncodeToString(conversationDetailIDCiphertext),
Admin: base64.StdEncoding.EncodeToString(adminCiphertext), Admin: base64.StdEncoding.EncodeToString(adminCiphertext),
SymmetricKey: base64.StdEncoding.EncodeToString( SymmetricKey: base64.StdEncoding.EncodeToString(
encryptWithPublicKey(key.Key, decodedPublicKey), encryptWithPublicKey(key.Key, decodedPublicKey),
@ -170,16 +149,78 @@ func seedUserConversation(
return messageThreadUser, err return messageThreadUser, err
} }
func seedConversationDetailUser(
user Models.User,
conversationDetail Models.ConversationDetail,
associationKey uuid.UUID,
admin bool,
key aesKey,
) (Models.ConversationDetailUser, error) {
var (
conversationDetailUser Models.ConversationDetailUser
adminString string = "false"
userIdCiphertext []byte
usernameCiphertext []byte
adminCiphertext []byte
associationKeyCiphertext []byte
publicKeyCiphertext []byte
err error
)
if admin {
adminString = "true"
}
userIdCiphertext, err = key.aesEncrypt([]byte(user.ID.String()))
if err != nil {
return conversationDetailUser, err
}
usernameCiphertext, err = key.aesEncrypt([]byte(user.Username))
if err != nil {
return conversationDetailUser, err
}
adminCiphertext, err = key.aesEncrypt([]byte(adminString))
if err != nil {
return conversationDetailUser, err
}
associationKeyCiphertext, err = key.aesEncrypt([]byte(associationKey.String()))
if err != nil {
return conversationDetailUser, err
}
publicKeyCiphertext, err = key.aesEncrypt([]byte(user.AsymmetricPublicKey))
if err != nil {
return conversationDetailUser, err
}
conversationDetailUser = Models.ConversationDetailUser{
ConversationDetailID: conversationDetail.ID,
UserID: base64.StdEncoding.EncodeToString(userIdCiphertext),
Username: base64.StdEncoding.EncodeToString(usernameCiphertext),
Admin: base64.StdEncoding.EncodeToString(adminCiphertext),
AssociationKey: base64.StdEncoding.EncodeToString(associationKeyCiphertext),
PublicKey: base64.StdEncoding.EncodeToString(publicKeyCiphertext),
}
err = Database.CreateConversationDetailUser(&conversationDetailUser)
return conversationDetailUser, err
}
func SeedMessages() { func SeedMessages() {
var ( var (
messageThread Models.ConversationDetail
conversationDetail Models.ConversationDetail
key aesKey key aesKey
primaryUser Models.User primaryUser Models.User
primaryUserAssociationKey uuid.UUID primaryUserAssociationKey uuid.UUID
secondaryUser Models.User secondaryUser Models.User
secondaryUserAssociationKey uuid.UUID secondaryUserAssociationKey uuid.UUID
userJson string
id1, id2 uuid.UUID
i int i int
err error err error
) )
@ -188,7 +229,7 @@ func SeedMessages() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
messageThread, err = seedConversationDetail(key)
conversationDetail, err = seedConversationDetail(key)
primaryUserAssociationKey, err = uuid.NewV4() primaryUserAssociationKey, err = uuid.NewV4()
if err != nil { if err != nil {
@ -206,7 +247,7 @@ func SeedMessages() {
_, err = seedUserConversation( _, err = seedUserConversation(
primaryUser, primaryUser,
messageThread.ID,
conversationDetail.ID,
key, key,
) )
if err != nil { if err != nil {
@ -220,53 +261,34 @@ func SeedMessages() {
_, err = seedUserConversation( _, err = seedUserConversation(
secondaryUser, secondaryUser,
messageThread.ID,
conversationDetail.ID,
key, key,
) )
id1, err = uuid.NewV4()
if err != nil { if err != nil {
panic(err) panic(err)
} }
id2, err = uuid.NewV4()
_, err = seedConversationDetailUser(
primaryUser,
conversationDetail,
primaryUserAssociationKey,
true,
key,
)
if err != nil { if err != nil {
panic(err) panic(err)
} }
userJson = fmt.Sprintf(
`
[
{
"id": "%s",
"user_id": "%s",
"username": "%s",
"admin": "true",
"association_key": "%s"
},
{
"id": "%s",
"user_id": "%s",
"username": "%s",
"admin": "false",
"association_key": "%s"
}
]
`,
id1.String(),
primaryUser.ID.String(),
primaryUser.Username,
primaryUserAssociationKey.String(),
id2.String(),
secondaryUser.ID.String(),
secondaryUser.Username,
secondaryUserAssociationKey.String(),
)
messageThread, err = seedUpdateUserConversation(
userJson,
_, err = seedConversationDetailUser(
secondaryUser,
conversationDetail,
secondaryUserAssociationKey,
false,
key, key,
messageThread,
) )
if err != nil {
panic(err)
}
for i = 0; i <= 20; i++ { for i = 0; i <= 20; i++ {
err = seedMessage( err = seedMessage(


+ 33
- 0
Backend/Models/Conversations.go View File

@ -0,0 +1,33 @@
package Models
import (
"github.com/gofrs/uuid"
)
type ConversationDetail struct {
Base
Name string `gorm:"not null" json:"name"` // Stored encrypted
Users []ConversationDetailUser ` json:"users"`
}
type ConversationDetailUser struct {
Base
ConversationDetailID uuid.UUID `gorm:"not null" json:"conversation_detail_id"`
ConversationDetail ConversationDetail `gorm:"not null" json:"conversation"`
UserID string `gorm:"not null" json:"user_id"` // Stored encrypted
Username string `gorm:"not null" json:"username"` // Stored encrypted
Admin string `gorm:"not null" json:"admin"` // Stored encrypted
AssociationKey string `gorm:"not null" json:"association_key"` // Stored encrypted
PublicKey string `gorm:"not null" json:"public_key"` // Stored encrypted
}
// 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"`
User User ` json:"user"`
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
}

+ 0
- 16
Backend/Models/Messages.go View File

@ -22,19 +22,3 @@ type Message struct {
AssociationKey string `json:"association_key" gorm:"not null"` // TODO: This links all encrypted messages for a user in a thread together. Find a way to fix this AssociationKey string `json:"association_key" gorm:"not null"` // TODO: This links all encrypted messages for a user in a thread together. Find a way to fix this
CreatedAt time.Time `json:"created_at" gorm:"not null"` CreatedAt time.Time `json:"created_at" gorm:"not null"`
} }
type ConversationDetail struct {
Base
Name string `gorm:"not null" json:"name"` // Stored encrypted
Users string ` json:"users"` // Stored as encrypted JSON
}
type UserConversation struct {
Base
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
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
}

+ 2
- 2
Backend/main.go View File

@ -2,7 +2,7 @@ package main
import ( import (
"flag" "flag"
"fmt"
"log"
"net/http" "net/http"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Api" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Api"
@ -37,7 +37,7 @@ func main() {
Api.InitApiEndpoints(router) Api.InitApiEndpoints(router)
fmt.Println("Listening on port :8080")
log.Println("Listening on port :8080")
err = http.ListenAndServe(":8080", router) err = http.ListenAndServe(":8080", router)
if err != nil { if err != nil {
panic(err) panic(err)


+ 12
- 11
README.md View File

@ -4,14 +4,15 @@ Encrypted messaging app
## TODO ## TODO
- Fix adding users to conversations
- Fix users recieving messages
- Fix the admin checks on conversation settings page
- Add admin checks to conversation settings page
- Add admin checks on backend
- Add errors to login / signup page
- Add errors when updating conversations
- Refactor the update conversations function
- Finish the friends list page
- Allow adding friends
- Finish the disappearing messages functionality
[x] Fix adding users to conversations
[x] Fix users recieving messages
[x] Fix the admin checks on conversation settings page
[x] Fix sending messages in a conversation that includes users that are not the current users friend
[x] Add admin checks to conversation settings page
[ ] Add admin checks on backend
[ ] Add errors to login / signup page
[ ] Add errors when updating conversations
[ ] Refactor the update conversations function
[ ] Finish the friends list page
[ ] Allow adding friends
[ ] Finish the disappearing messages functionality

+ 94
- 8
mobile/lib/models/conversation_users.dart View File

@ -1,3 +1,10 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:Envelope/utils/encryption/aes_helper.dart';
import 'package:Envelope/utils/encryption/crypto_utils.dart';
import 'package:pointycastle/impl.dart';
import '/models/conversations.dart'; import '/models/conversations.dart';
import '/utils/storage/database.dart'; import '/utils/storage/database.dart';
@ -20,6 +27,7 @@ Future<ConversationUser> getConversationUser(Conversation conversation, String u
conversationId: maps[0]['conversation_id'], conversationId: maps[0]['conversation_id'],
username: maps[0]['username'], username: maps[0]['username'],
associationKey: maps[0]['association_key'], associationKey: maps[0]['association_key'],
publicKey: CryptoUtils.rsaPublicKeyFromPem(maps[0]['asymmetric_public_key']),
admin: maps[0]['admin'] == 1, admin: maps[0]['admin'] == 1,
); );
@ -33,19 +41,60 @@ Future<List<ConversationUser>> getConversationUsers(Conversation conversation) a
'conversation_users', 'conversation_users',
where: 'conversation_id = ?', where: 'conversation_id = ?',
whereArgs: [conversation.id], whereArgs: [conversation.id],
orderBy: 'admin',
orderBy: 'username',
); );
return List.generate(maps.length, (i) {
List<ConversationUser> conversationUsers = List.generate(maps.length, (i) {
return ConversationUser( return ConversationUser(
id: maps[i]['id'], id: maps[i]['id'],
userId: maps[i]['user_id'], userId: maps[i]['user_id'],
conversationId: maps[i]['conversation_id'], conversationId: maps[i]['conversation_id'],
username: maps[i]['username'], username: maps[i]['username'],
associationKey: maps[i]['association_key'], associationKey: maps[i]['association_key'],
publicKey: CryptoUtils.rsaPublicKeyFromPem(maps[i]['asymmetric_public_key']),
admin: maps[i]['admin'] == 1, admin: maps[i]['admin'] == 1,
); );
}); });
int index = 0;
List<ConversationUser> finalConversationUsers = [];
for (ConversationUser conversationUser in conversationUsers) {
if (!conversationUser.admin) {
finalConversationUsers.add(conversationUser);
continue;
}
finalConversationUsers.insert(index, conversationUser);
index++;
}
return finalConversationUsers;
}
Future<List<Map<String, dynamic>>> getEncryptedConversationUsers(Conversation conversation, Uint8List symKey) async {
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> maps = await db.query(
'conversation_users',
where: 'conversation_id = ?',
whereArgs: [conversation.id],
orderBy: 'username',
);
List<Map<String, dynamic>> conversationUsers = List.generate(maps.length, (i) {
return {
'id': maps[i]['id'],
'conversation_id': maps[i]['conversation_id'],
'user_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(maps[i]['user_id'].codeUnits)),
'username': AesHelper.aesEncrypt(symKey, Uint8List.fromList(maps[i]['username'].codeUnits)),
'association_key': AesHelper.aesEncrypt(symKey, Uint8List.fromList(maps[i]['association_key'].codeUnits)),
'public_key': AesHelper.aesEncrypt(symKey, Uint8List.fromList(maps[i]['asymmetric_public_key'].codeUnits)),
'admin': AesHelper.aesEncrypt(symKey, Uint8List.fromList((maps[i]['admin'] == 1 ? 'true' : 'false').codeUnits)),
};
});
return conversationUsers;
} }
class ConversationUser{ class ConversationUser{
@ -54,6 +103,7 @@ class ConversationUser{
String conversationId; String conversationId;
String username; String username;
String associationKey; String associationKey;
RSAPublicKey publicKey;
bool admin; bool admin;
ConversationUser({ ConversationUser({
required this.id, required this.id,
@ -61,17 +111,47 @@ class ConversationUser{
required this.conversationId, required this.conversationId,
required this.username, required this.username,
required this.associationKey, required this.associationKey,
required this.publicKey,
required this.admin, required this.admin,
}); });
factory ConversationUser.fromJson(Map<String, dynamic> json, String conversationId) {
factory ConversationUser.fromJson(Map<String, dynamic> json, Uint8List symmetricKey) {
String userId = AesHelper.aesDecrypt(
symmetricKey,
base64.decode(json['user_id']),
);
String username = AesHelper.aesDecrypt(
symmetricKey,
base64.decode(json['username']),
);
String associationKey = AesHelper.aesDecrypt(
symmetricKey,
base64.decode(json['association_key']),
);
String admin = AesHelper.aesDecrypt(
symmetricKey,
base64.decode(json['admin']),
);
String publicKeyString = AesHelper.aesDecrypt(
symmetricKey,
base64.decode(json['public_key']),
);
RSAPublicKey publicKey = CryptoUtils.rsaPublicKeyFromPem(publicKeyString);
return ConversationUser( return ConversationUser(
id: json['id'], id: json['id'],
userId: json['user_id'],
conversationId: conversationId,
username: json['username'],
associationKey: json['association_key'],
admin: json['admin'] == 'true',
conversationId: json['conversation_detail_id'],
userId: userId,
username: username,
associationKey: associationKey,
publicKey: publicKey,
admin: admin == 'true',
); );
} }
@ -81,6 +161,7 @@ class ConversationUser{
'user_id': userId, 'user_id': userId,
'username': username, 'username': username,
'association_key': associationKey, 'association_key': associationKey,
'asymmetric_public_key': publicKeyPem(),
'admin': admin ? 'true' : 'false', 'admin': admin ? 'true' : 'false',
}; };
} }
@ -92,7 +173,12 @@ class ConversationUser{
'conversation_id': conversationId, 'conversation_id': conversationId,
'username': username, 'username': username,
'association_key': associationKey, 'association_key': associationKey,
'asymmetric_public_key': publicKeyPem(),
'admin': admin ? 1 : 0, 'admin': admin ? 1 : 0,
}; };
} }
String publicKeyPem() {
return CryptoUtils.encodeRSAPublicKeyToPem(publicKey);
}
} }

+ 15
- 20
mobile/lib/models/conversations.dart View File

@ -21,14 +21,12 @@ Future<Conversation> createConversation(String title, List<Friend> friends) asyn
var uuid = const Uuid(); var uuid = const Uuid();
final String conversationId = uuid.v4(); final String conversationId = uuid.v4();
final String conversationDetailId = uuid.v4();
Uint8List symmetricKey = AesHelper.deriveKey(generateRandomString(32)); Uint8List symmetricKey = AesHelper.deriveKey(generateRandomString(32));
Conversation conversation = Conversation( Conversation conversation = Conversation(
id: conversationId, id: conversationId,
userId: profile.id, userId: profile.id,
conversationDetailId: conversationDetailId,
symmetricKey: base64.encode(symmetricKey), symmetricKey: base64.encode(symmetricKey),
admin: true, admin: true,
name: title, name: title,
@ -50,6 +48,7 @@ Future<Conversation> createConversation(String title, List<Friend> friends) asyn
conversationId: conversationId, conversationId: conversationId,
username: profile.username, username: profile.username,
associationKey: uuid.v4(), associationKey: uuid.v4(),
publicKey: profile.publicKey!,
admin: true, admin: true,
).toMap(), ).toMap(),
conflictAlgorithm: ConflictAlgorithm.fail, conflictAlgorithm: ConflictAlgorithm.fail,
@ -64,6 +63,7 @@ Future<Conversation> createConversation(String title, List<Friend> friends) asyn
conversationId: conversationId, conversationId: conversationId,
username: friend.username, username: friend.username,
associationKey: uuid.v4(), associationKey: uuid.v4(),
publicKey: friend.publicKey,
admin: false, admin: false,
).toMap(), ).toMap(),
conflictAlgorithm: ConflictAlgorithm.replace, conflictAlgorithm: ConflictAlgorithm.replace,
@ -88,6 +88,7 @@ Future<Conversation> addUsersToConversation(Conversation conversation, List<Frie
conversationId: conversation.id, conversationId: conversation.id,
username: friend.username, username: friend.username,
associationKey: uuid.v4(), associationKey: uuid.v4(),
publicKey: friend.publicKey,
admin: false, admin: false,
).toMap(), ).toMap(),
conflictAlgorithm: ConflictAlgorithm.replace, conflictAlgorithm: ConflictAlgorithm.replace,
@ -99,7 +100,7 @@ Future<Conversation> addUsersToConversation(Conversation conversation, List<Frie
Conversation findConversationByDetailId(List<Conversation> conversations, String id) { Conversation findConversationByDetailId(List<Conversation> conversations, String id) {
for (var conversation in conversations) { for (var conversation in conversations) {
if (conversation.conversationDetailId == id) {
if (conversation.id == id) {
return conversation; return conversation;
} }
} }
@ -123,7 +124,6 @@ Future<Conversation> getConversationById(String id) async {
return Conversation( return Conversation(
id: maps[0]['id'], id: maps[0]['id'],
userId: maps[0]['user_id'], userId: maps[0]['user_id'],
conversationDetailId: maps[0]['conversation_detail_id'],
symmetricKey: maps[0]['symmetric_key'], symmetricKey: maps[0]['symmetric_key'],
admin: maps[0]['admin'] == 1, admin: maps[0]['admin'] == 1,
name: maps[0]['name'], name: maps[0]['name'],
@ -142,7 +142,6 @@ Future<List<Conversation>> getConversations() async {
return Conversation( return Conversation(
id: maps[i]['id'], id: maps[i]['id'],
userId: maps[i]['user_id'], userId: maps[i]['user_id'],
conversationDetailId: maps[i]['conversation_detail_id'],
symmetricKey: maps[i]['symmetric_key'], symmetricKey: maps[i]['symmetric_key'],
admin: maps[i]['admin'] == 1, admin: maps[i]['admin'] == 1,
name: maps[i]['name'], name: maps[i]['name'],
@ -156,7 +155,6 @@ Future<List<Conversation>> getConversations() async {
class Conversation { class Conversation {
String id; String id;
String userId; String userId;
String conversationDetailId;
String symmetricKey; String symmetricKey;
bool admin; bool admin;
String name; String name;
@ -166,7 +164,6 @@ class Conversation {
Conversation({ Conversation({
required this.id, required this.id,
required this.userId, required this.userId,
required this.conversationDetailId,
required this.symmetricKey, required this.symmetricKey,
required this.admin, required this.admin,
required this.name, required this.name,
@ -181,7 +178,7 @@ class Conversation {
privKey, privKey,
); );
var detailId = AesHelper.aesDecrypt(
var id = AesHelper.aesDecrypt(
symmetricKeyDecrypted, symmetricKeyDecrypted,
base64.decode(json['conversation_detail_id']), base64.decode(json['conversation_detail_id']),
); );
@ -192,9 +189,8 @@ class Conversation {
); );
return Conversation( return Conversation(
id: json['id'],
id: id,
userId: json['user_id'], userId: json['user_id'],
conversationDetailId: detailId,
symmetricKey: base64.encode(symmetricKeyDecrypted), symmetricKey: base64.encode(symmetricKeyDecrypted),
admin: admin == 'true', admin: admin == 'true',
name: 'Unknown', name: 'Unknown',
@ -208,16 +204,16 @@ class Conversation {
var symKey = base64.decode(symmetricKey); var symKey = base64.decode(symmetricKey);
List<ConversationUser> users = await getConversationUsers(this);
if (!includeUsers) { if (!includeUsers) {
return { return {
'id': conversationDetailId,
'id': id,
'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)), 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)),
'users': AesHelper.aesEncrypt(symKey, Uint8List.fromList(jsonEncode(users).codeUnits)),
'users': await getEncryptedConversationUsers(this, symKey),
}; };
} }
List<ConversationUser> users = await getConversationUsers(this);
List<Object> userConversations = []; List<Object> userConversations = [];
for (ConversationUser user in users) { for (ConversationUser user in users) {
@ -227,23 +223,23 @@ class Conversation {
if (profile.id != user.userId) { if (profile.id != user.userId) {
Friend friend = await getFriendByFriendId(user.userId); Friend friend = await getFriendByFriendId(user.userId);
pubKey = CryptoUtils.rsaPublicKeyFromPem(friend.asymmetricPublicKey);
pubKey = friend.publicKey;
newId = (const Uuid()).v4(); newId = (const Uuid()).v4();
} }
userConversations.add({ userConversations.add({
'id': newId, 'id': newId,
'user_id': user.userId, 'user_id': user.userId,
'conversation_detail_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(conversationDetailId.codeUnits)),
'admin': AesHelper.aesEncrypt(symKey, Uint8List.fromList((admin ? 'true' : 'false').codeUnits)),
'conversation_detail_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(id.codeUnits)),
'admin': AesHelper.aesEncrypt(symKey, Uint8List.fromList((user.admin ? 'true' : 'false').codeUnits)),
'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(symKey, pubKey)), 'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(symKey, pubKey)),
}); });
} }
return { return {
'id': conversationDetailId,
'id': id,
'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)), 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)),
'users': AesHelper.aesEncrypt(symKey, Uint8List.fromList(jsonEncode(users).codeUnits)),
'users': await getEncryptedConversationUsers(this, symKey),
'user_conversations': userConversations, 'user_conversations': userConversations,
}; };
} }
@ -252,7 +248,6 @@ class Conversation {
return { return {
'id': id, 'id': id,
'user_id': userId, 'user_id': userId,
'conversation_detail_id': conversationDetailId,
'symmetric_key': symmetricKey, 'symmetric_key': symmetricKey,
'admin': admin ? 1 : 0, 'admin': admin ? 1 : 0,
'name': name, 'name': name,


+ 26
- 20
mobile/lib/models/friends.dart View File

@ -21,7 +21,7 @@ class Friend{
String username; String username;
String friendId; String friendId;
String friendSymmetricKey; String friendSymmetricKey;
String asymmetricPublicKey;
RSAPublicKey publicKey;
String acceptedAt; String acceptedAt;
bool? selected; bool? selected;
Friend({ Friend({
@ -30,39 +30,41 @@ class Friend{
required this.username, required this.username,
required this.friendId, required this.friendId,
required this.friendSymmetricKey, required this.friendSymmetricKey,
required this.asymmetricPublicKey,
required this.publicKey,
required this.acceptedAt, required this.acceptedAt,
this.selected, this.selected,
}); });
factory Friend.fromJson(Map<String, dynamic> json, RSAPrivateKey privKey) { factory Friend.fromJson(Map<String, dynamic> json, RSAPrivateKey privKey) {
Uint8List friendIdDecrypted = CryptoUtils.rsaDecrypt(
Uint8List idDecrypted = CryptoUtils.rsaDecrypt(
base64.decode(json['friend_id']), base64.decode(json['friend_id']),
privKey, privKey,
); );
Uint8List friendUsername = CryptoUtils.rsaDecrypt(
Uint8List username = CryptoUtils.rsaDecrypt(
base64.decode(json['friend_username']), base64.decode(json['friend_username']),
privKey, privKey,
); );
Uint8List friendSymmetricKeyDecrypted = CryptoUtils.rsaDecrypt(
Uint8List symmetricKeyDecrypted = CryptoUtils.rsaDecrypt(
base64.decode(json['symmetric_key']), base64.decode(json['symmetric_key']),
privKey, privKey,
); );
String asymmetricPublicKey = AesHelper.aesDecrypt(
friendSymmetricKeyDecrypted,
String publicKeyString = AesHelper.aesDecrypt(
symmetricKeyDecrypted,
base64.decode(json['asymmetric_public_key']) base64.decode(json['asymmetric_public_key'])
); );
RSAPublicKey publicKey = CryptoUtils.rsaPublicKeyFromPem(publicKeyString);
return Friend( return Friend(
id: json['id'], id: json['id'],
userId: json['user_id'], userId: json['user_id'],
username: String.fromCharCodes(friendUsername),
friendId: String.fromCharCodes(friendIdDecrypted),
friendSymmetricKey: base64.encode(friendSymmetricKeyDecrypted),
asymmetricPublicKey: asymmetricPublicKey,
username: String.fromCharCodes(username),
friendId: String.fromCharCodes(idDecrypted),
friendSymmetricKey: base64.encode(symmetricKeyDecrypted),
publicKey: publicKey,
acceptedAt: json['accepted_at'], acceptedAt: json['accepted_at'],
); );
} }
@ -86,10 +88,14 @@ class Friend{
'username': username, 'username': username,
'friend_id': friendId, 'friend_id': friendId,
'symmetric_key': friendSymmetricKey, 'symmetric_key': friendSymmetricKey,
'asymmetric_public_key': asymmetricPublicKey,
'asymmetric_public_key': publicKeyPem(),
'accepted_at': acceptedAt, 'accepted_at': acceptedAt,
}; };
} }
String publicKeyPem() {
return CryptoUtils.encodeRSAPublicKeyToPem(publicKey);
}
} }
@ -105,7 +111,7 @@ Future<List<Friend>> getFriends() async {
userId: maps[i]['user_id'], userId: maps[i]['user_id'],
friendId: maps[i]['friend_id'], friendId: maps[i]['friend_id'],
friendSymmetricKey: maps[i]['symmetric_key'], friendSymmetricKey: maps[i]['symmetric_key'],
asymmetricPublicKey: maps[i]['asymmetric_public_key'],
publicKey: CryptoUtils.rsaPublicKeyFromPem(maps[i]['asymmetric_public_key']),
acceptedAt: maps[i]['accepted_at'], acceptedAt: maps[i]['accepted_at'],
username: maps[i]['username'], username: maps[i]['username'],
); );
@ -126,12 +132,12 @@ Future<Friend> getFriendByFriendId(String userId) async {
} }
return Friend( 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'],
id: maps[0]['id'],
userId: maps[0]['user_id'],
friendId: maps[0]['friend_id'],
friendSymmetricKey: maps[0]['symmetric_key'],
publicKey: CryptoUtils.rsaPublicKeyFromPem(maps[0]['asymmetric_public_key']),
acceptedAt: maps[0]['accepted_at'],
username: maps[0]['username'],
); );
} }

+ 8
- 6
mobile/lib/models/messages.dart View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:pointycastle/export.dart'; import 'package:pointycastle/export.dart';
import 'package:uuid/uuid.dart';
import '/models/conversation_users.dart'; import '/models/conversation_users.dart';
import '/models/conversations.dart'; import '/models/conversations.dart';
@ -101,20 +102,20 @@ class Message {
); );
} }
Future<String> toJson(Conversation conversation, String messageDataId) async {
Future<String> payloadJson(Conversation conversation, String messageId) async {
MyProfile profile = await MyProfile.getProfile(); MyProfile profile = await MyProfile.getProfile();
if (profile.publicKey == null) { if (profile.publicKey == null) {
throw Exception('Could not get profile.publicKey'); throw Exception('Could not get profile.publicKey');
} }
RSAPublicKey publicKey = profile.publicKey!; RSAPublicKey publicKey = profile.publicKey!;
final String messageDataId = (const Uuid()).v4();
final userSymmetricKey = AesHelper.deriveKey(generateRandomString(32)); final userSymmetricKey = AesHelper.deriveKey(generateRandomString(32));
final symmetricKey = AesHelper.deriveKey(generateRandomString(32)); final symmetricKey = AesHelper.deriveKey(generateRandomString(32));
List<Map<String, String>> messages = []; List<Map<String, String>> messages = [];
String id = '';
List<ConversationUser> conversationUsers = await getConversationUsers(conversation); List<ConversationUser> conversationUsers = await getConversationUsers(conversation);
for (var i = 0; i < conversationUsers.length; i++) { for (var i = 0; i < conversationUsers.length; i++) {
@ -124,6 +125,7 @@ class Message {
id = user.id; id = user.id;
messages.add({ messages.add({
'id': messageId,
'message_data_id': messageDataId, 'message_data_id': messageDataId,
'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt( 'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(
userSymmetricKey, userSymmetricKey,
@ -135,8 +137,8 @@ class Message {
continue; continue;
} }
Friend friend = await getFriendByFriendId(user.userId);
RSAPublicKey friendPublicKey = CryptoUtils.rsaPublicKeyFromPem(friend.asymmetricPublicKey);
ConversationUser conversationUser = await getConversationUser(conversation, user.userId);
RSAPublicKey friendPublicKey = conversationUser.publicKey;
messages.add({ messages.add({
'message_data_id': messageDataId, 'message_data_id': messageDataId,


+ 3
- 8
mobile/lib/utils/storage/conversations.dart View File

@ -41,7 +41,7 @@ Future<void> updateConversations() async {
privKey, privKey,
); );
conversations.add(conversation); conversations.add(conversation);
conversationsDetailIds.add(conversation.conversationDetailId);
conversationsDetailIds.add(conversation.id);
} }
Map<String, String> params = {}; Map<String, String> params = {};
@ -78,17 +78,12 @@ Future<void> updateConversations() async {
conflictAlgorithm: ConflictAlgorithm.replace, conflictAlgorithm: ConflictAlgorithm.replace,
); );
List<dynamic> usersData = json.decode(
AesHelper.aesDecrypt(
base64.decode(conversation.symmetricKey),
base64.decode(conversationDetailJson['users']),
)
);
List<dynamic> usersData = conversationDetailJson['users'];
for (var i = 0; i < usersData.length; i++) { for (var i = 0; i < usersData.length; i++) {
ConversationUser conversationUser = ConversationUser.fromJson( ConversationUser conversationUser = ConversationUser.fromJson(
usersData[i] as Map<String, dynamic>, usersData[i] as Map<String, dynamic>,
conversation.id,
base64.decode(conversation.symmetricKey),
); );
await db.insert( await db.insert(


+ 2
- 3
mobile/lib/utils/storage/database.dart View File

@ -35,7 +35,6 @@ Future<Database> getDatabaseConnection() async {
CREATE TABLE IF NOT EXISTS conversations( CREATE TABLE IF NOT EXISTS conversations(
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
user_id TEXT, user_id TEXT,
conversation_detail_id TEXT,
symmetric_key TEXT, symmetric_key TEXT,
admin INTEGER, admin INTEGER,
name TEXT, name TEXT,
@ -51,9 +50,9 @@ Future<Database> getDatabaseConnection() async {
user_id TEXT, user_id TEXT,
conversation_id TEXT, conversation_id TEXT,
username TEXT, username TEXT,
data TEXT,
association_key TEXT, association_key TEXT,
admin INTEGER
admin INTEGER,
asymmetric_public_key TEXT
); );
'''); ''');


+ 3
- 3
mobile/lib/utils/storage/messages.dart View File

@ -16,12 +16,12 @@ Future<void> sendMessage(Conversation conversation, String data) async {
MyProfile profile = await MyProfile.getProfile(); MyProfile profile = await MyProfile.getProfile();
var uuid = const Uuid(); var uuid = const Uuid();
final String messageDataId = uuid.v4();
final String messageId = uuid.v4();
ConversationUser currentUser = await getConversationUser(conversation, profile.id); ConversationUser currentUser = await getConversationUser(conversation, profile.id);
Message message = Message( Message message = Message(
id: messageDataId,
id: messageId,
symmetricKey: '', symmetricKey: '',
userSymmetricKey: '', userSymmetricKey: '',
senderId: currentUser.userId, senderId: currentUser.userId,
@ -42,7 +42,7 @@ Future<void> sendMessage(Conversation conversation, String data) async {
String sessionCookie = await getSessionCookie(); String sessionCookie = await getSessionCookie();
message.toJson(conversation, messageDataId)
message.payloadJson(conversation, messageId)
.then((messageJson) { .then((messageJson) {
return http.post( return http.post(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/message'), Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/message'),


+ 10
- 10
mobile/lib/views/authentication/login.dart View File

@ -8,28 +8,28 @@ import '/utils/storage/session_cookie.dart';
class LoginResponse { class LoginResponse {
final String status; final String status;
final String message; final String message;
final String asymmetricPublicKey;
final String asymmetricPrivateKey;
final String publicKey;
final String privateKey;
final String userId; final String userId;
final String username; final String username;
const LoginResponse({ const LoginResponse({
required this.status, required this.status,
required this.message, required this.message,
required this.asymmetricPublicKey,
required this.asymmetricPrivateKey,
required this.publicKey,
required this.privateKey,
required this.userId, required this.userId,
required this.username, required this.username,
}); });
factory LoginResponse.fromJson(Map<String, dynamic> json) { factory LoginResponse.fromJson(Map<String, dynamic> json) {
return LoginResponse( return LoginResponse(
status: json['status'],
message: json['message'],
asymmetricPublicKey: json['asymmetric_public_key'],
asymmetricPrivateKey: json['asymmetric_private_key'],
userId: json['user_id'],
username: json['username'],
status: json['status'],
message: json['message'],
publicKey: json['asymmetric_public_key'],
privateKey: json['asymmetric_private_key'],
userId: json['user_id'],
username: json['username'],
); );
} }
} }


+ 1
- 1
mobile/lib/views/authentication/signup.dart View File

@ -28,7 +28,7 @@ Future<SignupResponse> signUp(context, String username, String password, String
var rsaPubPem = CryptoUtils.encodeRSAPublicKeyToPem(keyPair.publicKey); var rsaPubPem = CryptoUtils.encodeRSAPublicKeyToPem(keyPair.publicKey);
var rsaPrivPem = CryptoUtils.encodeRSAPrivateKeyToPem(keyPair.privateKey); var rsaPrivPem = CryptoUtils.encodeRSAPrivateKeyToPem(keyPair.privateKey);
var encRsaPriv = AesHelper.aesEncrypt(password, Uint8List.fromList(rsaPrivPem.codeUnits));
String encRsaPriv = AesHelper.aesEncrypt(password, Uint8List.fromList(rsaPrivPem.codeUnits));
// TODO: Check for timeout here // TODO: Check for timeout here
final resp = await http.post( final resp = await http.post(


+ 91
- 94
mobile/lib/views/main/conversation/create_add_users.dart View File

@ -1,10 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '/models/conversations.dart';
import '/models/friends.dart'; import '/models/friends.dart';
import '/utils/storage/conversations.dart';
import '/views/main/conversation/create_add_users_list.dart'; import '/views/main/conversation/create_add_users_list.dart';
import '/views/main/conversation/detail.dart';
class ConversationAddFriendsList extends StatefulWidget { class ConversationAddFriendsList extends StatefulWidget {
final List<Friend> friends; final List<Friend> friends;
@ -26,81 +23,81 @@ class _ConversationAddFriendsListState extends State<ConversationAddFriendsList>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(
elevation: 0,
automaticallyImplyLeading: false,
flexibleSpace: SafeArea(
child: Container(
padding: const EdgeInsets.only(right: 16),
child: Row(
children: <Widget>[
IconButton(
onPressed: (){
Navigator.pop(context);
},
icon: const Icon(Icons.arrow_back),
),
const SizedBox(width: 2,),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
friendsSelected.isEmpty ?
'Select Friends' :
'${friendsSelected.length} Friends Selected',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600
),
),
],
),
),
],
),
appBar: AppBar(
elevation: 0,
automaticallyImplyLeading: false,
flexibleSpace: SafeArea(
child: Container(
padding: const EdgeInsets.only(right: 16),
child: Row(
children: <Widget>[
IconButton(
onPressed: (){
Navigator.pop(context);
},
icon: const Icon(Icons.arrow_back),
), ),
),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 20,left: 16,right: 16),
child: TextField(
decoration: const InputDecoration(
hintText: "Search...",
prefixIcon: Icon(
Icons.search,
size: 20
),
const SizedBox(width: 2,),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
friendsSelected.isEmpty ?
'Select Friends' :
'${friendsSelected.length} Friends Selected',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600
), ),
onChanged: (value) => filterSearchResults(value.toLowerCase())
),
),
Padding(
padding: const EdgeInsets.only(top: 0,left: 16,right: 16),
child: list(),
),
],
),
), ),
],
],
),
),
), ),
floatingActionButton: Padding(
padding: const EdgeInsets.only(right: 10, bottom: 10),
child: FloatingActionButton(
onPressed: () {
widget.saveCallback(friendsSelected);
setState(() {
friendsSelected = [];
});
},
backgroundColor: Theme.of(context).colorScheme.primary,
child: friendsSelected.isEmpty ?
const Text('Skip') :
const Icon(Icons.add, size: 30),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 20,left: 16,right: 16),
child: TextField(
decoration: const InputDecoration(
hintText: "Search...",
prefixIcon: Icon(
Icons.search,
size: 20
),
),
onChanged: (value) => filterSearchResults(value.toLowerCase())
), ),
),
Padding(
padding: const EdgeInsets.only(top: 0,left: 16,right: 16),
child: list(),
),
],
),
floatingActionButton: Padding(
padding: const EdgeInsets.only(right: 10, bottom: 10),
child: FloatingActionButton(
onPressed: () {
widget.saveCallback(friendsSelected);
setState(() {
friendsSelected = [];
});
},
backgroundColor: Theme.of(context).colorScheme.primary,
child: friendsSelected.isEmpty ?
const Text('Skip') :
const Icon(Icons.add, size: 30),
), ),
),
); );
} }
@ -111,10 +108,10 @@ class _ConversationAddFriendsListState extends State<ConversationAddFriendsList>
if(query.isNotEmpty) { if(query.isNotEmpty) {
List<Friend> dummyListData = []; List<Friend> dummyListData = [];
for (Friend friend in dummySearchList) { for (Friend friend in dummySearchList) {
if (friend.username.toLowerCase().contains(query)) {
dummyListData.add(friend);
}
if (friend.username.toLowerCase().contains(query)) {
dummyListData.add(friend);
} }
}
setState(() { setState(() {
friends.clear(); friends.clear();
friends.addAll(dummyListData); friends.addAll(dummyListData);
@ -138,30 +135,30 @@ class _ConversationAddFriendsListState extends State<ConversationAddFriendsList>
Widget list() { Widget list() {
if (friends.isEmpty) { if (friends.isEmpty) {
return const Center( return const Center(
child: Text('No Friends'),
child: Text('No Friends'),
); );
} }
return ListView.builder( return ListView.builder(
itemCount: friends.length,
shrinkWrap: true,
padding: const EdgeInsets.only(top: 16),
physics: const BouncingScrollPhysics(),
itemBuilder: (context, i) {
return ConversationAddFriendItem(
friend: friends[i],
isSelected: (bool value) {
setState(() {
widget.friends[i].selected = value;
if (value) {
friendsSelected.add(friends[i]);
return;
}
friendsSelected.remove(friends[i]);
});
itemCount: friends.length,
shrinkWrap: true,
padding: const EdgeInsets.only(top: 16),
physics: const BouncingScrollPhysics(),
itemBuilder: (context, i) {
return ConversationAddFriendItem(
friend: friends[i],
isSelected: (bool value) {
setState(() {
widget.friends[i].selected = value;
if (value) {
friendsSelected.add(friends[i]);
return;
} }
);
},
friendsSelected.remove(friends[i]);
});
}
);
},
); );
} }
} }

+ 227
- 217
mobile/lib/views/main/conversation/detail.dart View File

@ -28,212 +28,136 @@ class _ConversationDetailState extends State<ConversationDetail> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(
elevation: 0,
automaticallyImplyLeading: false,
flexibleSpace: SafeArea(
child: Container(
padding: const EdgeInsets.only(right: 16),
child: Row(
children: <Widget>[
IconButton(
onPressed: (){
Navigator.pop(context);
},
icon: Icon(
Icons.arrow_back,
color: Theme.of(context).appBarTheme.iconTheme?.color,
),
),
const SizedBox(width: 2,),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
widget.conversation.name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).appBarTheme.toolbarTextStyle?.color
),
),
],
),
),
IconButton(
onPressed: (){
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ConversationSettings(conversation: widget.conversation)),
);
},
icon: Icon(
Icons.settings,
color: Theme.of(context).appBarTheme.iconTheme?.color,
),
),
],
),
),
appBar: AppBar(
elevation: 0,
automaticallyImplyLeading: false,
flexibleSpace: SafeArea(
child: Container(
padding: const EdgeInsets.only(right: 16),
child: Row(
children: <Widget>[
IconButton(
onPressed: (){
Navigator.pop(context);
},
icon: Icon(
Icons.arrow_back,
color: Theme.of(context).appBarTheme.iconTheme?.color,
), ),
), ),
body: Stack(
const SizedBox(width: 2,),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
ListView.builder(
itemCount: messages.length,
shrinkWrap: true,
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].senderUsername == profile.username ?
Alignment.topRight :
Alignment.topLeft
),
child: Column(
crossAxisAlignment: messages[index].senderUsername == profile.username ?
CrossAxisAlignment.end :
CrossAxisAlignment.start,
children: <Widget>[
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: (
messages[index].senderUsername == profile.username ?
Theme.of(context).colorScheme.primary :
Theme.of(context).colorScheme.tertiary
),
),
padding: const EdgeInsets.all(12),
child: Text(
messages[index].data,
style: TextStyle(
fontSize: 15,
color: messages[index].senderUsername == profile.username ?
Theme.of(context).colorScheme.onPrimary :
Theme.of(context).colorScheme.onTertiary,
)
),
),
const SizedBox(height: 1.5),
Row(
mainAxisAlignment: messages[index].senderUsername == profile.username ?
MainAxisAlignment.end :
MainAxisAlignment.start,
children: <Widget>[
const SizedBox(width: 10),
usernameOrFailedToSend(index),
],
),
const SizedBox(height: 1.5),
Row(
mainAxisAlignment: messages[index].senderUsername == profile.username ?
MainAxisAlignment.end :
MainAxisAlignment.start,
children: <Widget>[
const SizedBox(width: 10),
Text(
convertToAgo(messages[index].createdAt),
textAlign: messages[index].senderUsername == profile.username ?
TextAlign.left :
TextAlign.right,
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
),
),
],
),
index != 0 ?
const SizedBox(height: 20) :
const SizedBox.shrink(),
],
)
),
);
Text(
widget.conversation.name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).appBarTheme.toolbarTextStyle?.color
),
),
],
),
),
IconButton(
onPressed: (){
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ConversationSettings(conversation: widget.conversation)),
);
},
icon: Icon(
Icons.settings,
color: Theme.of(context).appBarTheme.iconTheme?.color,
),
),
],
),
),
),
),
body: Stack(
children: <Widget>[
messagesView(),
Align(
alignment: Alignment.bottomLeft,
child: ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 200.0,
),
child: Container(
padding: const EdgeInsets.only(left: 10,bottom: 10,top: 10),
// height: 60,
width: double.infinity,
color: Theme.of(context).backgroundColor,
child: Row(
children: <Widget>[
GestureDetector(
onTap: (){
},
child: Container(
height: 30,
width: 30,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(30),
),
child: Icon(
Icons.add,
color: Theme.of(context).colorScheme.onPrimary,
size: 20
),
),
),
const SizedBox(width: 15,),
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: "Write message...",
hintStyle: TextStyle(
color: Theme.of(context).hintColor,
),
border: InputBorder.none,
),
maxLines: null,
controller: msgController,
),
),
const SizedBox(width: 15),
Container(
width: 45,
height: 45,
child: FittedBox(
child: FloatingActionButton(
onPressed: () async {
if (msgController.text == '') {
return;
}
await sendMessage(widget.conversation, msgController.text);
messages = await getMessagesForThread(widget.conversation);
setState(() {});
msgController.text = '';
}, },
child: Icon(
Icons.send,
color: Theme.of(context).colorScheme.onPrimary,
size: 22
), ),
Align(
alignment: Alignment.bottomLeft,
child: ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 200.0,
),
child: Container(
padding: const EdgeInsets.only(left: 10,bottom: 10,top: 10),
// height: 60,
width: double.infinity,
color: Theme.of(context).backgroundColor,
child: Row(
children: <Widget>[
GestureDetector(
onTap: (){
},
child: Container(
height: 30,
width: 30,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(30),
),
child: Icon(
Icons.add,
color: Theme.of(context).colorScheme.onPrimary,
size: 20
),
),
),
const SizedBox(width: 15,),
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: "Write message...",
hintStyle: TextStyle(
color: Theme.of(context).hintColor,
),
border: InputBorder.none,
),
maxLines: null,
controller: msgController,
),
),
const SizedBox(width: 15),
Container(
width: 45,
height: 45,
child: FittedBox(
child: FloatingActionButton(
onPressed: () async {
if (msgController.text == '') {
return;
}
await sendMessage(widget.conversation, msgController.text);
messages = await getMessagesForThread(widget.conversation);
setState(() {});
msgController.text = '';
},
child: Icon(
Icons.send,
color: Theme.of(context).colorScheme.onPrimary,
size: 22
),
backgroundColor: Theme.of(context).primaryColor,
),
),
),
const SizedBox(width: 10),
],
),
),
),
backgroundColor: Theme.of(context).primaryColor,
),
), ),
),
const SizedBox(width: 10),
], ],
),
),
), ),
);
),
],
),
);
} }
Future<void> fetchMessages() async { Future<void> fetchMessages() async {
@ -251,32 +175,118 @@ class _ConversationDetailState extends State<ConversationDetail> {
Widget usernameOrFailedToSend(int index) { Widget usernameOrFailedToSend(int index) {
if (messages[index].senderUsername != profile.username) { if (messages[index].senderUsername != profile.username) {
return Text( return Text(
messages[index].senderUsername,
style: TextStyle(
fontSize: 12,
color: Colors.grey[300],
),
);
messages[index].senderUsername,
style: TextStyle(
fontSize: 12,
color: Colors.grey[300],
),
);
} }
if (messages[index].failedToSend) { if (messages[index].failedToSend) {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: const <Widget>[
Icon(
Icons.warning_rounded,
color: Colors.red,
size: 20,
),
Text(
'Failed to send',
style: TextStyle(color: Colors.red, fontSize: 12),
textAlign: TextAlign.right,
),
],
mainAxisAlignment: MainAxisAlignment.end,
children: const <Widget>[
Icon(
Icons.warning_rounded,
color: Colors.red,
size: 20,
),
Text(
'Failed to send',
style: TextStyle(color: Colors.red, fontSize: 12),
textAlign: TextAlign.right,
),
],
); );
} }
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
Widget messagesView() {
if (messages.isEmpty) {
return const Center(
child: Text('No Messages'),
);
}
return ListView.builder(
itemCount: messages.length,
shrinkWrap: true,
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].senderUsername == profile.username ?
Alignment.topRight :
Alignment.topLeft
),
child: Column(
crossAxisAlignment: messages[index].senderUsername == profile.username ?
CrossAxisAlignment.end :
CrossAxisAlignment.start,
children: <Widget>[
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: (
messages[index].senderUsername == profile.username ?
Theme.of(context).colorScheme.primary :
Theme.of(context).colorScheme.tertiary
),
),
padding: const EdgeInsets.all(12),
child: Text(
messages[index].data,
style: TextStyle(
fontSize: 15,
color: messages[index].senderUsername == profile.username ?
Theme.of(context).colorScheme.onPrimary :
Theme.of(context).colorScheme.onTertiary,
)
),
),
const SizedBox(height: 1.5),
Row(
mainAxisAlignment: messages[index].senderUsername == profile.username ?
MainAxisAlignment.end :
MainAxisAlignment.start,
children: <Widget>[
const SizedBox(width: 10),
usernameOrFailedToSend(index),
],
),
const SizedBox(height: 1.5),
Row(
mainAxisAlignment: messages[index].senderUsername == profile.username ?
MainAxisAlignment.end :
MainAxisAlignment.start,
children: <Widget>[
const SizedBox(width: 10),
Text(
convertToAgo(messages[index].createdAt),
textAlign: messages[index].senderUsername == profile.username ?
TextAlign.left :
TextAlign.right,
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
),
),
],
),
index != 0 ?
const SizedBox(height: 20) :
const SizedBox.shrink(),
],
)
),
);
},
);
}
} }

+ 188
- 184
mobile/lib/views/main/conversation/settings.dart View File

@ -1,4 +1,5 @@
import 'package:Envelope/models/friends.dart'; import 'package:Envelope/models/friends.dart';
import 'package:Envelope/utils/encryption/crypto_utils.dart';
import 'package:Envelope/views/main/conversation/create_add_users.dart'; import 'package:Envelope/views/main/conversation/create_add_users.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -31,111 +32,113 @@ class _ConversationSettingsState extends State<ConversationSettings> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(
elevation: 0,
automaticallyImplyLeading: false,
flexibleSpace: SafeArea(
child: Container(
padding: const EdgeInsets.only(right: 16),
child: Row(
children: <Widget>[
IconButton(
onPressed: (){
Navigator.pop(context);
},
icon: const Icon(Icons.arrow_back),
),
const SizedBox(width: 2,),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
widget.conversation.name + " Settings",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600
),
),
],
),
),
],
),
appBar: AppBar(
elevation: 0,
automaticallyImplyLeading: false,
flexibleSpace: SafeArea(
child: Container(
padding: const EdgeInsets.only(right: 16),
child: Row(
children: <Widget>[
IconButton(
onPressed: (){
Navigator.pop(context);
},
icon: const Icon(Icons.arrow_back),
), ),
const SizedBox(width: 2,),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
widget.conversation.name + " Settings",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600
),
),
],
),
),
],
),
), ),
), ),
body: Padding(
padding: const EdgeInsets.all(15),
child: Column(
children: <Widget> [
const SizedBox(height: 30),
conversationName(),
const SizedBox(height: 25),
widget.conversation.admin ?
sectionTitle('Settings') :
const SizedBox.shrink(),
widget.conversation.admin ?
settings() :
const SizedBox.shrink(),
widget.conversation.admin ?
const SizedBox(height: 25) :
const SizedBox.shrink(),
sectionTitle('Members', showUsersAdd: true),
usersList(),
const SizedBox(height: 25),
myAccess(),
],
),
body: Padding(
padding: const EdgeInsets.all(15),
child: SingleChildScrollView(
child: Column(
children: <Widget> [
const SizedBox(height: 30),
conversationName(),
const SizedBox(height: 25),
widget.conversation.admin ?
sectionTitle('Settings') :
const SizedBox.shrink(),
widget.conversation.admin ?
settings() :
const SizedBox.shrink(),
widget.conversation.admin ?
const SizedBox(height: 25) :
const SizedBox.shrink(),
sectionTitle('Members', showUsersAdd: true),
usersList(),
const SizedBox(height: 25),
myAccess(),
],
), ),
), ),
),
); );
} }
Widget conversationName() { Widget conversationName() {
return Row( return Row(
children: <Widget> [
const CustomCircleAvatar(
icon: Icon(Icons.people, size: 40),
imagePath: null, // TODO: Add image here
radius: 30,
),
const SizedBox(width: 10),
Text(
widget.conversation.name,
style: const TextStyle(
fontSize: 25,
fontWeight: FontWeight.w500,
),
children: <Widget> [
const CustomCircleAvatar(
icon: Icon(Icons.people, size: 40),
imagePath: null, // TODO: Add image here
radius: 30,
),
const SizedBox(width: 10),
Text(
widget.conversation.name,
style: const TextStyle(
fontSize: 25,
fontWeight: FontWeight.w500,
), ),
widget.conversation.admin ? IconButton(
iconSize: 20,
icon: const Icon(Icons.edit),
padding: const EdgeInsets.all(5.0),
splashRadius: 25,
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ConversationEditDetails(
saveCallback: (String conversationName) async {
widget.conversation.name = conversationName;
),
widget.conversation.admin ? IconButton(
iconSize: 20,
icon: const Icon(Icons.edit),
padding: const EdgeInsets.all(5.0),
splashRadius: 25,
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ConversationEditDetails(
saveCallback: (String conversationName) async {
widget.conversation.name = conversationName;
final db = await getDatabaseConnection();
db.update(
'conversations',
widget.conversation.toMap(),
where: 'id = ?',
whereArgs: [widget.conversation.id],
);
final db = await getDatabaseConnection();
db.update(
'conversations',
widget.conversation.toMap(),
where: 'id = ?',
whereArgs: [widget.conversation.id],
);
await updateConversation(widget.conversation, includeUsers: true);
setState(() {});
Navigator.pop(context);
},
conversation: widget.conversation,
)),
).then(onGoBack);
},
) : const SizedBox.shrink(),
await updateConversation(widget.conversation, includeUsers: true);
setState(() {});
Navigator.pop(context);
},
conversation: widget.conversation,
)),
).then(onGoBack);
},
) : const SizedBox.shrink(),
], ],
); );
} }
@ -155,24 +158,24 @@ class _ConversationSettingsState extends State<ConversationSettings> {
Widget myAccess() { Widget myAccess() {
return Align( return Align(
alignment: Alignment.centerLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextButton.icon(
label: const Text(
'Leave Conversation',
style: TextStyle(fontSize: 16)
),
icon: const Icon(Icons.exit_to_app),
style: const ButtonStyle(
alignment: Alignment.centerLeft,
),
onPressed: () {
print('Leave Group');
}
alignment: Alignment.centerLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextButton.icon(
label: const Text(
'Leave Conversation',
style: TextStyle(fontSize: 16)
),
icon: const Icon(Icons.exit_to_app),
style: const ButtonStyle(
alignment: Alignment.centerLeft,
), ),
],
onPressed: () {
print('Leave Group');
}
),
],
), ),
); );
} }
@ -194,29 +197,29 @@ class _ConversationSettingsState extends State<ConversationSettings> {
), ),
), ),
!showUsersAdd ? !showUsersAdd ?
const SizedBox.shrink() :
IconButton(
icon: const Icon(Icons.add),
padding: const EdgeInsets.all(0),
onPressed: () async {
List<Friend> friends = await unselectedFriends();
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ConversationAddFriendsList(
friends: friends,
saveCallback: (List<Friend> selectedFriends) async {
addUsersToConversation(
widget.conversation,
selectedFriends,
);
const SizedBox.shrink() :
IconButton(
icon: const Icon(Icons.add),
padding: const EdgeInsets.all(0),
onPressed: () async {
List<Friend> friends = await unselectedFriends();
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ConversationAddFriendsList(
friends: friends,
saveCallback: (List<Friend> selectedFriends) async {
addUsersToConversation(
widget.conversation,
selectedFriends,
);
await updateConversation(widget.conversation, includeUsers: true);
await getUsers();
Navigator.pop(context);
},
))
);
},
),
await updateConversation(widget.conversation, includeUsers: true);
await getUsers();
Navigator.pop(context);
},
))
);
},
),
], ],
) )
) )
@ -225,64 +228,65 @@ class _ConversationSettingsState extends State<ConversationSettings> {
Widget settings() { Widget settings() {
return Align( return Align(
alignment: Alignment.centerLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 5),
TextButton.icon(
label: const Text(
'Disappearing Messages',
style: TextStyle(fontSize: 16)
),
icon: const Icon(Icons.timer),
style: ButtonStyle(
alignment: Alignment.centerLeft,
foregroundColor: MaterialStateProperty.resolveWith<Color>(
(Set<MaterialState> states) {
return Theme.of(context).colorScheme.onBackground;
},
)
),
onPressed: () {
print('Disappearing Messages');
}
alignment: Alignment.centerLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 5),
TextButton.icon(
label: const Text(
'Disappearing Messages',
style: TextStyle(fontSize: 16)
), ),
TextButton.icon(
label: const Text(
'Permissions',
style: TextStyle(fontSize: 16)
),
icon: const Icon(Icons.lock),
style: ButtonStyle(
alignment: Alignment.centerLeft,
foregroundColor: MaterialStateProperty.resolveWith<Color>(
(Set<MaterialState> states) {
return Theme.of(context).colorScheme.onBackground;
},
)
),
onPressed: () {
print('Permissions');
}
icon: const Icon(Icons.timer),
style: ButtonStyle(
alignment: Alignment.centerLeft,
foregroundColor: MaterialStateProperty.resolveWith<Color>(
(Set<MaterialState> states) {
return Theme.of(context).colorScheme.onBackground;
},
)
), ),
],
onPressed: () {
print('Disappearing Messages');
}
),
TextButton.icon(
label: const Text(
'Permissions',
style: TextStyle(fontSize: 16)
),
icon: const Icon(Icons.lock),
style: ButtonStyle(
alignment: Alignment.centerLeft,
foregroundColor: MaterialStateProperty.resolveWith<Color>(
(Set<MaterialState> states) {
return Theme.of(context).colorScheme.onBackground;
},
)
),
onPressed: () {
print('Permissions');
}
),
],
), ),
); );
} }
Widget usersList() { Widget usersList() {
return ListView.builder( return ListView.builder(
itemCount: users.length,
shrinkWrap: true,
padding: const EdgeInsets.only(top: 5, bottom: 0),
itemBuilder: (context, i) {
return ConversationSettingsUserListItem(
user: users[i],
isAdmin: widget.conversation.admin,
profile: profile!, // TODO: Fix this
);
}
itemCount: users.length,
shrinkWrap: true,
padding: const EdgeInsets.only(top: 5, bottom: 0),
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, i) {
return ConversationSettingsUserListItem(
user: users[i],
isAdmin: widget.conversation.admin,
profile: profile!, // TODO: Fix this
);
}
); );
} }
@ -291,8 +295,8 @@ class _ConversationSettingsState extends State<ConversationSettings> {
List<String> notInArgs = []; List<String> notInArgs = [];
for (var user in users) { for (var user in users) {
notInArgs.add(user.userId);
}
notInArgs.add(user.userId);
}
final List<Map<String, dynamic>> maps = await db.query( final List<Map<String, dynamic>> maps = await db.query(
'friends', 'friends',
@ -307,7 +311,7 @@ class _ConversationSettingsState extends State<ConversationSettings> {
userId: maps[i]['user_id'], userId: maps[i]['user_id'],
friendId: maps[i]['friend_id'], friendId: maps[i]['friend_id'],
friendSymmetricKey: maps[i]['symmetric_key'], friendSymmetricKey: maps[i]['symmetric_key'],
asymmetricPublicKey: maps[i]['asymmetric_public_key'],
publicKey: CryptoUtils.rsaPublicKeyFromPem(maps[i]['asymmetric_public_key']),
acceptedAt: maps[i]['accepted_at'], acceptedAt: maps[i]['accepted_at'],
username: maps[i]['username'], username: maps[i]['username'],
); );


+ 66
- 66
mobile/lib/views/main/friend/list.dart View File

@ -21,65 +21,65 @@ class _FriendListState extends State<FriendList> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SafeArea(
child: Padding(
padding: const EdgeInsets.only(left: 16,right: 16,top: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
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(20),
color: Theme.of(context).colorScheme.tertiary
),
child: Row(
children: <Widget>[
Icon(
Icons.add,
color: Theme.of(context).primaryColor,
size: 20
),
const SizedBox(width: 2,),
const Text(
"Add",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold
)
),
],
),
)
],
body: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SafeArea(
child: Padding(
padding: const EdgeInsets.only(left: 16,right: 16,top: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
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(20),
color: Theme.of(context).colorScheme.tertiary
),
child: Row(
children: <Widget>[
Icon(
Icons.add,
color: Theme.of(context).primaryColor,
size: 20
), ),
),
),
Padding(
padding: const EdgeInsets.only(top: 16,left: 16,right: 16),
child: TextField(
decoration: const InputDecoration(
hintText: "Search...",
prefixIcon: Icon(
Icons.search,
size: 20
),
),
onChanged: (value) => filterSearchResults(value.toLowerCase())
),
const SizedBox(width: 2,),
const Text(
"Add",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold
)
),
],
),
)
],
), ),
Padding(
padding: const EdgeInsets.only(top: 16,left: 16,right: 16),
child: list(),
),
),
Padding(
padding: const EdgeInsets.only(top: 16,left: 16,right: 16),
child: TextField(
decoration: const InputDecoration(
hintText: "Search...",
prefixIcon: Icon(
Icons.search,
size: 20
),
), ),
],
onChanged: (value) => filterSearchResults(value.toLowerCase())
),
),
Padding(
padding: const EdgeInsets.only(top: 16,left: 16,right: 16),
child: list(),
),
],
), ),
), ),
); );
@ -119,20 +119,20 @@ class _FriendListState extends State<FriendList> {
Widget list() { Widget list() {
if (friends.isEmpty) { if (friends.isEmpty) {
return const Center( return const Center(
child: Text('No Friends'),
child: Text('No Friends'),
); );
} }
return ListView.builder( return ListView.builder(
itemCount: friends.length,
shrinkWrap: true,
padding: const EdgeInsets.only(top: 16),
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, i) {
return FriendListItem(
friend: friends[i],
);
},
itemCount: friends.length,
shrinkWrap: true,
padding: const EdgeInsets.only(top: 16),
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, i) {
return FriendListItem(
friend: friends[i],
);
},
); );
} }
} }

Loading…
Cancel
Save