Browse Source

Working conversations and messages

pull/1/head
Tovi Jaeschke-Rogers 2 years ago
parent
commit
d7af0a9ac9
17 changed files with 537 additions and 270 deletions
  1. +2
    -5
      Backend/Api/Messages/CreateConversation.go
  2. +56
    -0
      Backend/Api/Messages/UpdateConversation.go
  3. +1
    -0
      Backend/Api/Routes.go
  4. +22
    -11
      Backend/Database/Seeder/MessageSeeder.go
  5. +35
    -0
      Backend/Database/UserConversations.go
  6. +10
    -9
      Backend/Models/Messages.go
  7. +8
    -4
      Backend/main.go
  8. +15
    -1
      README.md
  9. +77
    -77
      mobile/lib/models/conversation_users.dart
  10. +36
    -5
      mobile/lib/models/conversations.dart
  11. +119
    -113
      mobile/lib/models/messages.dart
  12. +26
    -1
      mobile/lib/utils/storage/conversations.dart
  13. +2
    -1
      mobile/lib/utils/storage/messages.dart
  14. +4
    -12
      mobile/lib/views/main/conversation/create_add_users.dart
  15. +7
    -15
      mobile/lib/views/main/conversation/edit_details.dart
  16. +25
    -1
      mobile/lib/views/main/conversation/list.dart
  17. +92
    -15
      mobile/lib/views/main/conversation/settings.dart

+ 2
- 5
Backend/Api/Messages/CreateConversation.go View File

@ -2,7 +2,6 @@ package Messages
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gofrs/uuid"
@ -11,7 +10,7 @@ import (
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models"
)
type RawConversationData struct {
type RawCreateConversationData struct {
ID string `json:"id"`
Name string `json:"name"`
Users string `json:"users"`
@ -20,7 +19,7 @@ type RawConversationData struct {
func CreateConversation(w http.ResponseWriter, r *http.Request) {
var (
rawConversationData RawConversationData
rawConversationData RawCreateConversationData
messageThread Models.ConversationDetail
err error
)
@ -45,8 +44,6 @@ func CreateConversation(w http.ResponseWriter, r *http.Request) {
return
}
fmt.Println(rawConversationData.UserConversations[0])
err = Database.CreateUserConversations(&rawConversationData.UserConversations)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)


+ 56
- 0
Backend/Api/Messages/UpdateConversation.go View File

@ -0,0 +1,56 @@
package Messages
import (
"encoding/json"
"net/http"
"github.com/gofrs/uuid"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models"
)
type RawUpdateConversationData struct {
ID string `json:"id"`
Name string `json:"name"`
Users string `json:"users"`
UserConversations []Models.UserConversation `json:"user_conversations"`
}
func UpdateConversation(w http.ResponseWriter, r *http.Request) {
var (
rawConversationData RawCreateConversationData
messageThread Models.ConversationDetail
err error
)
err = json.NewDecoder(r.Body).Decode(&rawConversationData)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
messageThread = Models.ConversationDetail{
Base: Models.Base{
ID: uuid.FromStringOrNil(rawConversationData.ID),
},
Name: rawConversationData.Name,
Users: rawConversationData.Users,
}
err = Database.UpdateConversationDetail(&messageThread)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
if len(rawConversationData.UserConversations) > 0 {
err = Database.UpdateOrCreateUserConversations(&rawConversationData.UserConversations)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
}
w.WriteHeader(http.StatusOK)
}

+ 1
- 0
Backend/Api/Routes.go View File

@ -67,6 +67,7 @@ func InitApiEndpoints(router *mux.Router) {
authApi.HandleFunc("/conversation_details", Messages.EncryptedConversationDetailsList).Methods("GET")
authApi.HandleFunc("/conversations", Messages.CreateConversation).Methods("POST")
authApi.HandleFunc("/conversations", Messages.UpdateConversation).Methods("PUT")
// Define routes for messages
authApi.HandleFunc("/message", Messages.CreateMessage).Methods("POST")


+ 22
- 11
Backend/Database/Seeder/MessageSeeder.go View File

@ -6,7 +6,6 @@ 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"
)
@ -176,9 +175,9 @@ func SeedMessages() {
messageThread Models.ConversationDetail
key aesKey
primaryUser Models.User
primaryUserAssociationKey string
primaryUserAssociationKey uuid.UUID
secondaryUser Models.User
secondaryUserAssociationKey string
secondaryUserAssociationKey uuid.UUID
userJson string
id1, id2 uuid.UUID
i int
@ -186,10 +185,19 @@ func SeedMessages() {
)
key, err = generateAesKey()
if err != nil {
panic(err)
}
messageThread, err = seedConversationDetail(key)
primaryUserAssociationKey = Util.RandomString(32)
secondaryUserAssociationKey = Util.RandomString(32)
primaryUserAssociationKey, err = uuid.NewV4()
if err != nil {
panic(err)
}
secondaryUserAssociationKey, err = uuid.NewV4()
if err != nil {
panic(err)
}
primaryUser, err = Database.GetUserByUsername("testUser")
if err != nil {
@ -201,6 +209,9 @@ func SeedMessages() {
messageThread.ID,
key,
)
if err != nil {
panic(err)
}
secondaryUser, err = Database.GetUserByUsername("ATestUser2")
if err != nil {
@ -227,14 +238,14 @@ func SeedMessages() {
[
{
"id": "%s",
"user_id": "%s",
"user_id": "%s",
"username": "%s",
"admin": "true",
"association_key": "%s"
},
{
"id": "%s",
"user_id": "%s",
"user_id": "%s",
"username": "%s",
"admin": "false",
"association_key": "%s"
@ -244,11 +255,11 @@ func SeedMessages() {
id1.String(),
primaryUser.ID.String(),
primaryUser.Username,
primaryUserAssociationKey,
primaryUserAssociationKey.String(),
id2.String(),
secondaryUser.ID.String(),
secondaryUser.Username,
secondaryUserAssociationKey,
secondaryUserAssociationKey.String(),
)
messageThread, err = seedUpdateUserConversation(
@ -261,8 +272,8 @@ func SeedMessages() {
err = seedMessage(
primaryUser,
secondaryUser,
primaryUserAssociationKey,
secondaryUserAssociationKey,
primaryUserAssociationKey.String(),
secondaryUserAssociationKey.String(),
i,
)
if err != nil {


+ 35
- 0
Backend/Database/UserConversations.go View File

@ -4,6 +4,7 @@ import (
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
func GetUserConversationById(id string) (Models.UserConversation, error) {
@ -49,6 +50,40 @@ func CreateUserConversations(userConversations *[]Models.UserConversation) error
return err
}
func UpdateUserConversation(userConversation *Models.UserConversation) error {
var err error
err = DB.Model(Models.UserConversation{}).
Updates(userConversation).
Error
return err
}
func UpdateUserConversations(userConversations *[]Models.UserConversation) error {
var err error
err = DB.Model(Models.UserConversation{}).
Updates(userConversations).
Error
return err
}
func UpdateOrCreateUserConversations(userConversations *[]Models.UserConversation) error {
var err error
err = DB.Model(Models.UserConversation{}).
Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.AssignmentColumns([]string{"admin"}),
}).
Create(userConversations).
Error
return err
}
func DeleteUserConversation(userConversation *Models.UserConversation) error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Delete(userConversation).


+ 10
- 9
Backend/Models/Messages.go View File

@ -18,22 +18,23 @@ type Message struct {
Base
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"`
SymmetricKey string `json:"symmetric_key" gorm:"not null"` // Stored encrypted
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"`
}
type ConversationDetail struct {
Base
Name string `gorm:"not null" json:"name"` // Stored encrypted
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 {
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
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
}

+ 8
- 4
Backend/main.go View File

@ -2,6 +2,7 @@ package main
import (
"flag"
"fmt"
"net/http"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Api"
@ -11,9 +12,7 @@ import (
"github.com/gorilla/mux"
)
var (
seed bool
)
var seed bool
func init() {
Database.Init()
@ -26,6 +25,7 @@ func init() {
func main() {
var (
router *mux.Router
err error
)
if seed {
@ -37,5 +37,9 @@ func main() {
Api.InitApiEndpoints(router)
http.ListenAndServe(":8080", router)
fmt.Println("Listening on port :8080")
err = http.ListenAndServe(":8080", router)
if err != nil {
panic(err)
}
}

+ 15
- 1
README.md View File

@ -1,3 +1,17 @@
# Envelope
Encrypted messaging app
Encrypted messaging app
## 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

+ 77
- 77
mobile/lib/models/conversation_users.dart View File

@ -2,97 +2,97 @@ import '/models/conversations.dart';
import '/utils/storage/database.dart';
Future<ConversationUser> getConversationUser(Conversation conversation, String userId) async {
final db = await getDatabaseConnection();
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> maps = await db.query(
'conversation_users',
where: 'conversation_id = ? AND user_id = ?',
whereArgs: [conversation.id, userId],
);
final List<Map<String, dynamic>> maps = await db.query(
'conversation_users',
where: 'conversation_id = ? AND user_id = ?',
whereArgs: [conversation.id, userId],
);
if (maps.length != 1) {
throw ArgumentError('Invalid conversation_id or username');
}
if (maps.length != 1) {
throw ArgumentError('Invalid conversation_id or username');
}
return ConversationUser(
id: maps[0]['id'],
userId: maps[0]['user_id'],
conversationId: maps[0]['conversation_id'],
username: maps[0]['username'],
associationKey: maps[0]['association_key'],
admin: maps[0]['admin'] == 1,
);
return ConversationUser(
id: maps[0]['id'],
userId: maps[0]['user_id'],
conversationId: maps[0]['conversation_id'],
username: maps[0]['username'],
associationKey: maps[0]['association_key'],
admin: maps[0]['admin'] == 1,
);
}
// A method that retrieves all the dogs from the dogs table.
Future<List<ConversationUser>> getConversationUsers(Conversation conversation) async {
final db = await getDatabaseConnection();
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> maps = await db.query(
'conversation_users',
where: 'conversation_id = ?',
whereArgs: [conversation.id],
orderBy: 'admin',
);
final List<Map<String, dynamic>> maps = await db.query(
'conversation_users',
where: 'conversation_id = ?',
whereArgs: [conversation.id],
orderBy: 'admin',
);
return List.generate(maps.length, (i) {
return ConversationUser(
id: maps[i]['id'],
userId: maps[i]['user_id'],
conversationId: maps[i]['conversation_id'],
username: maps[i]['username'],
associationKey: maps[i]['association_key'],
admin: maps[i]['admin'] == 1,
);
});
return List.generate(maps.length, (i) {
return ConversationUser(
id: maps[i]['id'],
userId: maps[i]['user_id'],
conversationId: maps[i]['conversation_id'],
username: maps[i]['username'],
associationKey: maps[i]['association_key'],
admin: maps[i]['admin'] == 1,
);
});
}
class ConversationUser{
String id;
String userId;
String conversationId;
String username;
String associationKey;
bool admin;
ConversationUser({
required this.id,
required this.userId,
required this.conversationId,
required this.username,
required this.associationKey,
required this.admin,
});
String id;
String userId;
String conversationId;
String username;
String associationKey;
bool admin;
ConversationUser({
required this.id,
required this.userId,
required this.conversationId,
required this.username,
required this.associationKey,
required this.admin,
});
factory ConversationUser.fromJson(Map<String, dynamic> json, String conversationId) {
return ConversationUser(
id: json['id'],
userId: json['user_id'],
conversationId: conversationId,
username: json['username'],
associationKey: json['association_key'],
admin: json['admin'] == 'true',
);
}
factory ConversationUser.fromJson(Map<String, dynamic> json, String conversationId) {
return ConversationUser(
id: json['id'],
userId: json['user_id'],
conversationId: conversationId,
username: json['username'],
associationKey: json['association_key'],
admin: json['admin'] == 'true',
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'user_id': userId,
'username': username,
'association_key': associationKey,
'admin': admin ? 'true' : 'false',
};
}
Map<String, dynamic> toJson() {
return {
'id': id,
'user_id': userId,
'username': username,
'association_key': associationKey,
'admin': admin ? 'true' : 'false',
};
}
Map<String, dynamic> toMap() {
return {
'id': id,
'user_id': userId,
'conversation_id': conversationId,
'username': username,
'association_key': associationKey,
'admin': admin ? 1 : 0,
};
}
Map<String, dynamic> toMap() {
return {
'id': id,
'user_id': userId,
'conversation_id': conversationId,
'username': username,
'association_key': associationKey,
'admin': admin ? 1 : 0,
};
}
}

+ 36
- 5
mobile/lib/models/conversations.dart View File

@ -25,8 +25,6 @@ Future<Conversation> createConversation(String title, List<Friend> friends) asyn
Uint8List symmetricKey = AesHelper.deriveKey(generateRandomString(32));
String associationKey = generateRandomString(32);
Conversation conversation = Conversation(
id: conversationId,
userId: profile.id,
@ -51,7 +49,7 @@ Future<Conversation> createConversation(String title, List<Friend> friends) asyn
userId: profile.id,
conversationId: conversationId,
username: profile.username,
associationKey: associationKey,
associationKey: uuid.v4(),
admin: true,
).toMap(),
conflictAlgorithm: ConflictAlgorithm.fail,
@ -65,7 +63,31 @@ Future<Conversation> createConversation(String title, List<Friend> friends) asyn
userId: friend.friendId,
conversationId: conversationId,
username: friend.username,
associationKey: associationKey,
associationKey: uuid.v4(),
admin: false,
).toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
return conversation;
}
Future<Conversation> addUsersToConversation(Conversation conversation, List<Friend> friends) async {
final db = await getDatabaseConnection();
var uuid = const Uuid();
for (Friend friend in friends) {
await db.insert(
'conversation_users',
ConversationUser(
id: uuid.v4(),
userId: friend.friendId,
conversationId: conversation.id,
username: friend.username,
associationKey: uuid.v4(),
admin: false,
).toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
@ -181,12 +203,21 @@ class Conversation {
);
}
Future<Map<String, dynamic>> toJson() async {
Future<Map<String, dynamic>> payloadJson({ bool includeUsers = true }) async {
MyProfile profile = await MyProfile.getProfile();
var symKey = base64.decode(symmetricKey);
List<ConversationUser> users = await getConversationUsers(this);
if (!includeUsers) {
return {
'id': conversationDetailId,
'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)),
'users': AesHelper.aesEncrypt(symKey, Uint8List.fromList(jsonEncode(users).codeUnits)),
};
}
List<Object> userConversations = [];
for (ConversationUser user in users) {


+ 119
- 113
mobile/lib/models/messages.dart View File

@ -1,17 +1,48 @@
import 'dart:convert';
import 'dart:typed_data';
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 '/models/conversation_users.dart';
import '/models/conversations.dart';
import '/models/my_profile.dart';
import '/models/friends.dart';
import '/utils/encryption/aes_helper.dart';
import '/utils/encryption/crypto_utils.dart';
import '/utils/storage/database.dart';
import '/utils/strings.dart';
import '/models/friends.dart';
const messageTypeSender = 'sender';
const messageTypeReceiver = 'receiver';
const messageTypeSender = 'sender';
Future<List<Message>> getMessagesForThread(Conversation conversation) async {
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> 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'],
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'],
failedToSend: maps[i]['failed_to_send'] == 1,
);
});
}
class Message {
String id;
@ -38,109 +69,99 @@ class Message {
factory Message.fromJson(Map<String, dynamic> json, RSAPrivateKey privKey) {
var userSymmetricKey = CryptoUtils.rsaDecrypt(
base64.decode(json['symmetric_key']),
privKey,
base64.decode(json['symmetric_key']),
privKey,
);
var symmetricKey = AesHelper.aesDecrypt(
userSymmetricKey,
base64.decode(json['message_data']['symmetric_key']),
userSymmetricKey,
base64.decode(json['message_data']['symmetric_key']),
);
var senderId = AesHelper.aesDecrypt(
base64.decode(symmetricKey),
base64.decode(json['message_data']['sender_id']),
base64.decode(symmetricKey),
base64.decode(json['message_data']['sender_id']),
);
var data = AesHelper.aesDecrypt(
base64.decode(symmetricKey),
base64.decode(json['message_data']['data']),
base64.decode(symmetricKey),
base64.decode(json['message_data']['data']),
);
return Message(
id: json['id'],
symmetricKey: symmetricKey,
userSymmetricKey: base64.encode(userSymmetricKey),
data: data,
senderId: senderId,
senderUsername: 'Unknown',
associationKey: json['association_key'],
createdAt: json['created_at'],
failedToSend: false,
id: json['id'],
symmetricKey: symmetricKey,
userSymmetricKey: base64.encode(userSymmetricKey),
data: data,
senderId: senderId,
senderUsername: 'Unknown',
associationKey: json['association_key'],
createdAt: json['created_at'],
failedToSend: false,
);
}
Future<String> 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<Map<String, String>> messages = [];
String id = '';
List<ConversationUser> 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,
});
}
MyProfile profile = await MyProfile.getProfile();
if (profile.publicKey == null) {
throw Exception('Could not get profile.publicKey');
}
RSAPublicKey publicKey = profile.publicKey!;
final userSymmetricKey = AesHelper.deriveKey(generateRandomString(32));
final symmetricKey = AesHelper.deriveKey(generateRandomString(32));
List<Map<String, String>> messages = [];
String id = '';
List<ConversationUser> conversationUsers = await getConversationUsers(conversation);
Map<String, String> messageData = {
'id': messageDataId,
'data': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(data.codeUnits)),
'sender_id': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(id.codeUnits)),
'symmetric_key': AesHelper.aesEncrypt(
for (var i = 0; i < conversationUsers.length; i++) {
ConversationUser user = conversationUsers[i];
if (profile.id == user.userId) {
id = user.id;
messages.add({
'message_data_id': messageDataId,
'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(
userSymmetricKey,
Uint8List.fromList(base64.encode(symmetricKey).codeUnits),
),
};
publicKey,
)),
'association_key': user.associationKey,
});
return jsonEncode(<String, dynamic>{
'message_data': messageData,
'message': messages,
});
}
continue;
}
@override
String toString() {
return '''
Friend friend = await getFriendByFriendId(user.userId);
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<String, String> messageData = {
'id': messageDataId,
'data': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(data.codeUnits)),
'sender_id': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(senderId.codeUnits)),
'symmetric_key': AesHelper.aesEncrypt(
userSymmetricKey,
Uint8List.fromList(base64.encode(symmetricKey).codeUnits),
),
};
id: $id
data: $data
senderId: $senderId
senderUsername: $senderUsername
associationKey: $associationKey
createdAt: $createdAt
''';
return jsonEncode(<String, dynamic>{
'message_data': messageData,
'message': messages,
});
}
Map<String, dynamic> toMap() {
@ -157,33 +178,18 @@ class Message {
};
}
}
Future<List<Message>> getMessagesForThread(Conversation conversation) async {
final db = await getDatabaseConnection();
@override
String toString() {
return '''
final List<Map<String, dynamic>> 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'],
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'],
failedToSend: maps[i]['failed_to_send'] == 1,
);
});
id: $id
data: $data
senderId: $senderId
senderUsername: $senderUsername
associationKey: $associationKey
createdAt: $createdAt
''';
}
}

+ 26
- 1
mobile/lib/utils/storage/conversations.dart View File

@ -31,6 +31,10 @@ Future<void> updateConversations() async {
List<dynamic> conversationsJson = jsonDecode(resp.body);
if (conversationsJson.isEmpty) {
return;
}
for (var i = 0; i < conversationsJson.length; i++) {
Conversation conversation = Conversation.fromJson(
conversationsJson[i] as Map<String, dynamic>,
@ -102,7 +106,7 @@ Future<void> updateConversations() async {
Future<void> uploadConversation(Conversation conversation) async {
String sessionCookie = await getSessionCookie();
Map<String, dynamic> conversationJson = await conversation.toJson();
Map<String, dynamic> conversationJson = await conversation.payloadJson();
var x = await http.post(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'),
@ -113,5 +117,26 @@ Future<void> uploadConversation(Conversation conversation) async {
body: jsonEncode(conversationJson),
);
// TODO: Handle errors here
print(x.statusCode);
}
Future<void> updateConversation(Conversation conversation, { includeUsers = true } ) async {
String sessionCookie = await getSessionCookie();
Map<String, dynamic> conversationJson = await conversation.payloadJson(includeUsers: includeUsers);
var x = await http.put(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
'cookie': sessionCookie,
},
body: jsonEncode(conversationJson),
);
// TODO: Handle errors here
print(x.statusCode);
}

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

@ -24,7 +24,7 @@ Future<void> sendMessage(Conversation conversation, String data) async {
id: messageDataId,
symmetricKey: '',
userSymmetricKey: '',
senderId: profile.id,
senderId: currentUser.userId,
senderUsername: profile.username,
data: data,
associationKey: currentUser.associationKey,
@ -66,6 +66,7 @@ Future<void> sendMessage(Conversation conversation, String data) async {
where: 'id = ?',
whereArgs: [message.id],
);
throw exception;
});
}


+ 4
- 12
mobile/lib/views/main/conversation/create_add_users.dart View File

@ -8,11 +8,11 @@ import '/views/main/conversation/detail.dart';
class ConversationAddFriendsList extends StatefulWidget {
final List<Friend> friends;
final String title;
final Function(List<Friend> friendsSelected) saveCallback;
const ConversationAddFriendsList({
Key? key,
required this.friends,
required this.title,
required this.saveCallback,
}) : super(key: key);
@override
@ -88,20 +88,12 @@ class _ConversationAddFriendsListState extends State<ConversationAddFriendsList>
floatingActionButton: Padding(
padding: const EdgeInsets.only(right: 10, bottom: 10),
child: FloatingActionButton(
onPressed: () async {
Conversation conversation = await createConversation(widget.title, friendsSelected);
uploadConversation(conversation);
onPressed: () {
widget.saveCallback(friendsSelected);
setState(() {
friendsSelected = [];
});
Navigator.of(context).popUntil((route) => route.isFirst);
Navigator.push(context, MaterialPageRoute(builder: (context){
return ConversationDetail(
conversation: conversation,
);
}));
},
backgroundColor: Theme.of(context).colorScheme.primary,
child: friendsSelected.isEmpty ?


+ 7
- 15
mobile/lib/views/main/conversation/edit_details.dart View File

@ -1,20 +1,15 @@
import 'package:Envelope/models/conversation_users.dart';
import 'package:flutter/material.dart';
import '/components/custom_circle_avatar.dart';
import '/models/conversations.dart';
import '/models/friends.dart';
import '/views/main/conversation/create_add_users.dart';
class ConversationEditDetails extends StatefulWidget {
final Function(String conversationName) saveCallback;
final Conversation? conversation;
final List<Friend>? friends;
final List<ConversationUser>? users;
const ConversationEditDetails({
Key? key,
required this.saveCallback,
this.conversation,
this.friends,
this.users,
}) : super(key: key);
@override
@ -134,15 +129,12 @@ class _ConversationEditDetails extends State<ConversationEditDetails> {
ElevatedButton(
style: buttonStyle,
onPressed: () {
if (_formKey.currentState!.validate()) {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ConversationAddFriendsList(
friends: widget.friends!,
title: conversationNameController.text,
)
)
);
if (!_formKey.currentState!.validate()) {
// TODO: Show error here
return;
}
widget.saveCallback(conversationNameController.text);
},
child: const Text('Save'),
),


+ 25
- 1
mobile/lib/views/main/conversation/list.dart View File

@ -1,9 +1,12 @@
import 'package:Envelope/models/friends.dart';
import 'package:Envelope/utils/storage/conversations.dart';
import 'package:flutter/material.dart';
import '/models/conversations.dart';
import '/views/main/conversation/edit_details.dart';
import '/views/main/conversation/list_item.dart';
import 'create_add_users.dart';
import 'detail.dart';
class ConversationList extends StatefulWidget {
final List<Conversation> conversations;
@ -73,7 +76,28 @@ class _ConversationListState extends State<ConversationList> {
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ConversationEditDetails(
friends: friends,
saveCallback: (String conversationName) {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ConversationAddFriendsList(
friends: friends,
saveCallback: (List<Friend> friendsSelected) async {
Conversation conversation = await createConversation(
conversationName,
friendsSelected
);
uploadConversation(conversation);
Navigator.of(context).popUntil((route) => route.isFirst);
Navigator.push(context, MaterialPageRoute(builder: (context){
return ConversationDetail(
conversation: conversation,
);
}));
},
))
);
},
)),
).then(onGoBack);
},


+ 92
- 15
mobile/lib/views/main/conversation/settings.dart View File

@ -1,11 +1,15 @@
import 'package:Envelope/components/custom_circle_avatar.dart';
import 'package:Envelope/views/main/conversation/edit_details.dart';
import 'package:Envelope/models/friends.dart';
import 'package:Envelope/views/main/conversation/create_add_users.dart';
import 'package:flutter/material.dart';
import '/models/conversation_users.dart';
import '/models/conversations.dart';
import '/models/my_profile.dart';
import '/views/main/conversation/settings_user_list_item.dart';
import '/views/main/conversation/edit_details.dart';
import '/components/custom_circle_avatar.dart';
import '/utils/storage/database.dart';
import '/utils/storage/conversations.dart';
class ConversationSettings extends StatefulWidget {
final Conversation conversation;
@ -78,7 +82,7 @@ class _ConversationSettingsState extends State<ConversationSettings> {
widget.conversation.admin ?
const SizedBox(height: 25) :
const SizedBox.shrink(),
sectionTitle('Members'),
sectionTitle('Members', showUsersAdd: true),
usersList(),
const SizedBox(height: 25),
myAccess(),
@ -112,9 +116,22 @@ class _ConversationSettingsState extends State<ConversationSettings> {
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ConversationEditDetails(
users: users,
conversation: widget.conversation,
friends: null,
saveCallback: (String conversationName) async {
widget.conversation.name = conversationName;
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);
},
@ -160,16 +177,49 @@ class _ConversationSettingsState extends State<ConversationSettings> {
);
}
Widget sectionTitle(String title) {
Widget sectionTitle(String title, { bool showUsersAdd = false}) {
return Align(
alignment: Alignment.centerLeft,
child: Container(
padding: const EdgeInsets.only(left: 12),
child: Text(
title,
style: const TextStyle(fontSize: 20),
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(right: 6),
child: Row(
children: [
Expanded(
child: Container(
padding: const EdgeInsets.only(left: 12),
child: Text(
title,
style: const TextStyle(fontSize: 20),
),
),
),
),
!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,
);
await updateConversation(widget.conversation, includeUsers: true);
await getUsers();
Navigator.pop(context);
},
))
);
},
),
],
)
)
);
}
@ -236,9 +286,36 @@ class _ConversationSettingsState extends State<ConversationSettings> {
);
}
Future<List<Friend>> unselectedFriends() async {
final db = await getDatabaseConnection();
List<String> notInArgs = [];
for (var user in users) {
notInArgs.add(user.userId);
}
final List<Map<String, dynamic>> maps = await db.query(
'friends',
where: 'friend_id not in (${List.filled(notInArgs.length, '?').join(',')})',
whereArgs: notInArgs,
orderBy: 'username',
);
return List.generate(maps.length, (i) {
return Friend(
id: maps[i]['id'],
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'],
);
});
}
onGoBack(dynamic value) async {
nameController.text = widget.conversation.name;
super.initState();
getUsers();
setState(() {});
}


Loading…
Cancel
Save