@ -0,0 +1,39 @@ | |||
package Database | |||
import ( | |||
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" | |||
"gorm.io/gorm" | |||
"gorm.io/gorm/clause" | |||
) | |||
func GetMessageDataById(id string) (Models.MessageData, error) { | |||
var ( | |||
messageData Models.MessageData | |||
err error | |||
) | |||
err = DB.Preload(clause.Associations). | |||
First(&messageData, "id = ?", id). | |||
Error | |||
return messageData, err | |||
} | |||
func CreateMessageData(messageData *Models.MessageData) error { | |||
var ( | |||
err error | |||
) | |||
err = DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||
Create(messageData). | |||
Error | |||
return err | |||
} | |||
func DeleteMessageData(messageData *Models.MessageData) error { | |||
return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||
Delete(messageData). | |||
Error | |||
} |
@ -0,0 +1,104 @@ | |||
import '/utils/storage/database.dart'; | |||
import '/models/conversations.dart'; | |||
class ConversationUser{ | |||
String id; | |||
String conversationId; | |||
String username; | |||
String associationKey; | |||
String admin; | |||
ConversationUser({ | |||
required this.id, | |||
required this.conversationId, | |||
required this.username, | |||
required this.associationKey, | |||
required this.admin, | |||
}); | |||
factory ConversationUser.fromJson(Map<String, dynamic> json, String conversationId) { | |||
return ConversationUser( | |||
id: json['id'], | |||
conversationId: conversationId, | |||
username: json['username'], | |||
associationKey: json['association_key'], | |||
admin: json['admin'], | |||
); | |||
} | |||
Map<String, dynamic> toMap() { | |||
return { | |||
'id': id, | |||
'conversation_id': conversationId, | |||
'username': username, | |||
'association_key': associationKey, | |||
'admin': admin, | |||
}; | |||
} | |||
} | |||
// A method that retrieves all the dogs from the dogs table. | |||
Future<List<ConversationUser>> getConversationUsers(Conversation conversation) async { | |||
final db = await getDatabaseConnection(); | |||
final List<Map<String, dynamic>> maps = await db.query( | |||
'conversation_users', | |||
where: 'conversation_id = ?', | |||
whereArgs: [conversation.id], | |||
); | |||
return List.generate(maps.length, (i) { | |||
return ConversationUser( | |||
id: maps[i]['id'], | |||
conversationId: maps[i]['conversation_id'], | |||
username: maps[i]['username'], | |||
associationKey: maps[i]['association_key'], | |||
admin: maps[i]['admin'], | |||
); | |||
}); | |||
} | |||
Future<ConversationUser> getConversationUserById(Conversation conversation, String id) async { | |||
final db = await getDatabaseConnection(); | |||
final List<Map<String, dynamic>> maps = await db.query( | |||
'conversation_users', | |||
where: 'conversation_id = ? AND id = ?', | |||
whereArgs: [conversation.id, id], | |||
); | |||
if (maps.length != 1) { | |||
throw ArgumentError('Invalid conversation_id or id'); | |||
} | |||
return ConversationUser( | |||
id: maps[0]['id'], | |||
conversationId: maps[0]['conversation_id'], | |||
username: maps[0]['username'], | |||
associationKey: maps[0]['association_key'], | |||
admin: maps[0]['admin'], | |||
); | |||
} | |||
Future<ConversationUser> getConversationUserByUsername(Conversation conversation, String username) async { | |||
final db = await getDatabaseConnection(); | |||
final List<Map<String, dynamic>> maps = await db.query( | |||
'conversation_users', | |||
where: 'conversation_id = ? AND username = ?', | |||
whereArgs: [conversation.id, username], | |||
); | |||
if (maps.length != 1) { | |||
throw ArgumentError('Invalid conversation_id or username'); | |||
} | |||
return ConversationUser( | |||
id: maps[0]['id'], | |||
conversationId: maps[0]['conversation_id'], | |||
username: maps[0]['username'], | |||
associationKey: maps[0]['association_key'], | |||
admin: maps[0]['admin'], | |||
); | |||
} |
@ -1,76 +1,112 @@ | |||
import 'dart:convert'; | |||
import 'package:Envelope/models/messages.dart'; | |||
import 'package:uuid/uuid.dart'; | |||
import 'package:Envelope/models/conversation_users.dart'; | |||
import 'package:intl/intl.dart'; | |||
import 'package:http/http.dart' as http; | |||
import 'package:flutter_dotenv/flutter_dotenv.dart'; | |||
import 'package:pointycastle/export.dart'; | |||
import 'package:sqflite/sqflite.dart'; | |||
import 'package:Envelope/models/conversations.dart'; | |||
import 'package:shared_preferences/shared_preferences.dart'; | |||
import '/utils/storage/session_cookie.dart'; | |||
import '/utils/storage/encryption_keys.dart'; | |||
import '/utils/storage/database.dart'; | |||
import '/models/conversations.dart'; | |||
// TODO: Move this to table | |||
Map<String, Map<String, String>> _mapUsers(String users) { | |||
List<dynamic> usersJson = jsonDecode(users); | |||
Map<String, Map<String, String>> mapped = {}; | |||
for (var i = 0; i < usersJson.length; i++) { | |||
mapped[usersJson[i]['id']] = { | |||
'username': usersJson[i]['username'], | |||
'admin': usersJson[i]['admin'], | |||
}; | |||
} | |||
return mapped; | |||
} | |||
import '/models/messages.dart'; | |||
Future<void> updateMessageThread(Conversation conversation, {RSAPrivateKey? privKey}) async { | |||
privKey ??= await getPrivateKey(); | |||
var resp = await http.get( | |||
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/messages/${conversation.messageThreadKey}'), | |||
headers: { | |||
'cookie': await getSessionCookie(), | |||
} | |||
privKey ??= await getPrivateKey(); | |||
final preferences = await SharedPreferences.getInstance(); | |||
String username = preferences.getString('username')!; | |||
ConversationUser currentUser = await getConversationUserByUsername(conversation, username); | |||
var resp = await http.get( | |||
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/messages/${currentUser.associationKey}'), | |||
headers: { | |||
'cookie': await getSessionCookie(), | |||
} | |||
); | |||
if (resp.statusCode != 200) { | |||
throw Exception(resp.body); | |||
} | |||
List<dynamic> messageThreadJson = jsonDecode(resp.body); | |||
final db = await getDatabaseConnection(); | |||
for (var i = 0; i < messageThreadJson.length; i++) { | |||
Message message = Message.fromJson( | |||
messageThreadJson[i] as Map<String, dynamic>, | |||
privKey, | |||
); | |||
if (resp.statusCode != 200) { | |||
throw Exception(resp.body); | |||
} | |||
var mapped = _mapUsers(conversation.users!); | |||
ConversationUser messageUser = await getConversationUserById(conversation, message.senderId); | |||
message.senderUsername = messageUser.username; | |||
List<dynamic> messageThreadJson = jsonDecode(resp.body); | |||
final db = await getDatabaseConnection(); | |||
for (var i = 0; i < messageThreadJson.length; i++) { | |||
Message message = Message.fromJson( | |||
messageThreadJson[i] as Map<String, dynamic>, | |||
privKey, | |||
); | |||
// TODO: Fix this | |||
message.senderUsername = mapped[message.senderId]!['username']!; | |||
await db.insert( | |||
'messages', | |||
message.toMap(), | |||
conflictAlgorithm: ConflictAlgorithm.replace, | |||
); | |||
} | |||
await db.insert( | |||
'messages', | |||
message.toMap(), | |||
conflictAlgorithm: ConflictAlgorithm.replace, | |||
); | |||
} | |||
} | |||
Future<void> updateMessageThreads({List<Conversation>? conversations}) async { | |||
RSAPrivateKey privKey = await getPrivateKey(); | |||
RSAPrivateKey privKey = await getPrivateKey(); | |||
conversations ??= await getConversations(); | |||
conversations ??= await getConversations(); | |||
for (var i = 0; i < conversations.length; i++) { | |||
await updateMessageThread(conversations[i], privKey: privKey); | |||
} | |||
for (var i = 0; i < conversations.length; i++) { | |||
await updateMessageThread(conversations[i], privKey: privKey); | |||
} | |||
} | |||
Future<void> sendMessage(Conversation conversation, String data) async { | |||
final preferences = await SharedPreferences.getInstance(); | |||
final userId = preferences.getString('userId'); | |||
final username = preferences.getString('username'); | |||
if (userId == null || username == null) { | |||
throw Exception('Invalid user id'); | |||
} | |||
var uuid = const Uuid(); | |||
final String messageDataId = uuid.v4(); | |||
ConversationUser currentUser = await getConversationUserByUsername(conversation, username); | |||
Message message = Message( | |||
id: messageDataId, | |||
symmetricKey: '', | |||
userSymmetricKey: '', | |||
senderId: userId, | |||
senderUsername: username, | |||
data: data, | |||
createdAt: DateTime.now().toIso8601String(), | |||
associationKey: currentUser.associationKey, | |||
); | |||
final db = await getDatabaseConnection(); | |||
print(await db.query('messages')); | |||
await db.insert( | |||
'messages', | |||
message.toMap(), | |||
conflictAlgorithm: ConflictAlgorithm.replace, | |||
); | |||
String messageJson = await message.toJson(conversation, messageDataId); | |||
final resp = await http.post( | |||
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/message'), | |||
headers: <String, String>{ | |||
'Content-Type': 'application/json; charset=UTF-8', | |||
'cookie': await getSessionCookie(), | |||
}, | |||
body: messageJson, | |||
); | |||
// TODO: If statusCode not successfull, mark as needing resend | |||
print(resp.statusCode); | |||
} |
@ -0,0 +1,8 @@ | |||
import 'dart:math'; | |||
String generateRandomString(int len) { | |||
var r = Random(); | |||
const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; | |||
return List.generate(len, (index) => _chars[r.nextInt(_chars.length)]).join(); | |||
} | |||
@ -1,110 +1,133 @@ | |||
import 'package:flutter/material.dart'; | |||
import '/models/conversations.dart'; | |||
import '/views/main/conversation_list_item.dart'; | |||
import '/utils/storage/messages.dart'; | |||
class ConversationList extends StatefulWidget { | |||
const ConversationList({Key? key}) : super(key: key); | |||
final List<Conversation> conversations; | |||
const ConversationList({ | |||
Key? key, | |||
required this.conversations, | |||
}) : super(key: key); | |||
@override | |||
State<ConversationList> createState() => _ConversationListState(); | |||
@override | |||
State<ConversationList> createState() => _ConversationListState(); | |||
} | |||
class _ConversationListState extends State<ConversationList> { | |||
List<Conversation> conversations = []; | |||
List<Conversation> conversations = []; | |||
@override | |||
void initState() { | |||
super.initState(); | |||
fetchConversations(); | |||
} | |||
void fetchConversations() async { | |||
conversations = await getConversations(); | |||
setState(() {}); | |||
} | |||
@override | |||
void initState() { | |||
super.initState(); | |||
conversations.addAll(widget.conversations); | |||
setState(() {}); | |||
} | |||
Widget list() { | |||
void filterSearchResults(String query) { | |||
List<Conversation> dummySearchList = []; | |||
dummySearchList.addAll(widget.conversations); | |||
if (conversations.isEmpty) { | |||
return const Center( | |||
child: Text('No Conversations'), | |||
); | |||
if(query.isNotEmpty) { | |||
List<Conversation> dummyListData = []; | |||
dummySearchList.forEach((item) { | |||
if (item.name.toLowerCase().contains(query)) { | |||
dummyListData.add(item); | |||
} | |||
return ListView.builder( | |||
itemCount: conversations.length, | |||
shrinkWrap: true, | |||
padding: const EdgeInsets.only(top: 16), | |||
physics: const NeverScrollableScrollPhysics(), | |||
itemBuilder: (context, i) { | |||
return ConversationListItem( | |||
conversation: conversations[i], | |||
); | |||
}, | |||
); | |||
}); | |||
setState(() { | |||
conversations.clear(); | |||
conversations.addAll(dummyListData); | |||
}); | |||
return; | |||
} | |||
@override | |||
Widget build(BuildContext context) { | |||
return Scaffold( | |||
body: SingleChildScrollView( | |||
physics: const BouncingScrollPhysics(), | |||
child: Column( | |||
crossAxisAlignment: CrossAxisAlignment.start, | |||
children: <Widget>[ | |||
SafeArea( | |||
child: Padding( | |||
padding: const EdgeInsets.only(left: 16,right: 16,top: 10), | |||
child: Row( | |||
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |||
children: <Widget>[ | |||
const Text("Conversations",style: TextStyle(fontSize: 32,fontWeight: FontWeight.bold),), | |||
Container( | |||
padding: const EdgeInsets.only(left: 8,right: 8,top: 2,bottom: 2), | |||
height: 30, | |||
decoration: BoxDecoration( | |||
borderRadius: BorderRadius.circular(30), | |||
color: Colors.pink[50], | |||
), | |||
child: Row( | |||
children: const <Widget>[ | |||
Icon(Icons.add,color: Colors.pink,size: 20,), | |||
SizedBox(width: 2,), | |||
Text("Add",style: TextStyle(fontSize: 14,fontWeight: FontWeight.bold),), | |||
], | |||
), | |||
) | |||
], | |||
), | |||
), | |||
), | |||
Padding( | |||
padding: const EdgeInsets.only(top: 16,left: 16,right: 16), | |||
child: TextField( | |||
decoration: InputDecoration( | |||
hintText: "Search...", | |||
hintStyle: TextStyle(color: Colors.grey.shade600), | |||
prefixIcon: Icon(Icons.search,color: Colors.grey.shade600, size: 20,), | |||
filled: true, | |||
fillColor: Colors.grey.shade100, | |||
contentPadding: const EdgeInsets.all(8), | |||
enabledBorder: OutlineInputBorder( | |||
borderRadius: BorderRadius.circular(20), | |||
borderSide: BorderSide( | |||
color: Colors.grey.shade100 | |||
) | |||
), | |||
), | |||
), | |||
), | |||
Padding( | |||
padding: const EdgeInsets.only(top: 16,left: 16,right: 16), | |||
child: list(), | |||
), | |||
], | |||
), | |||
), | |||
); | |||
setState(() { | |||
conversations.clear(); | |||
conversations.addAll(widget.conversations); | |||
}); | |||
} | |||
Widget list() { | |||
if (conversations.isEmpty) { | |||
return const Center( | |||
child: Text('No Conversations'), | |||
); | |||
} | |||
return ListView.builder( | |||
itemCount: conversations.length, | |||
shrinkWrap: true, | |||
padding: const EdgeInsets.only(top: 16), | |||
physics: const NeverScrollableScrollPhysics(), | |||
itemBuilder: (context, i) { | |||
return ConversationListItem( | |||
conversation: conversations[i], | |||
); | |||
}, | |||
); | |||
} | |||
@override | |||
Widget build(BuildContext context) { | |||
return Scaffold( | |||
body: SingleChildScrollView( | |||
physics: const BouncingScrollPhysics(), | |||
child: Column( | |||
crossAxisAlignment: CrossAxisAlignment.start, | |||
children: <Widget>[ | |||
SafeArea( | |||
child: Padding( | |||
padding: const EdgeInsets.only(left: 16,right: 16,top: 10), | |||
child: Row( | |||
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |||
children: <Widget>[ | |||
const Text("Conversations",style: TextStyle(fontSize: 32,fontWeight: FontWeight.bold),), | |||
Container( | |||
padding: const EdgeInsets.only(left: 8,right: 8,top: 2,bottom: 2), | |||
height: 30, | |||
decoration: BoxDecoration( | |||
borderRadius: BorderRadius.circular(30), | |||
color: Colors.pink[50], | |||
), | |||
child: Row( | |||
children: const <Widget>[ | |||
Icon(Icons.add,color: Colors.pink,size: 20,), | |||
SizedBox(width: 2,), | |||
Text("Add",style: TextStyle(fontSize: 14,fontWeight: FontWeight.bold),), | |||
], | |||
), | |||
) | |||
], | |||
), | |||
), | |||
), | |||
Padding( | |||
padding: const EdgeInsets.only(top: 16,left: 16,right: 16), | |||
child: TextField( | |||
decoration: InputDecoration( | |||
hintText: "Search...", | |||
hintStyle: TextStyle(color: Colors.grey.shade600), | |||
prefixIcon: Icon(Icons.search,color: Colors.grey.shade600, size: 20,), | |||
filled: true, | |||
fillColor: Colors.grey.shade100, | |||
contentPadding: const EdgeInsets.all(8), | |||
enabledBorder: OutlineInputBorder( | |||
borderRadius: BorderRadius.circular(20), | |||
borderSide: BorderSide( | |||
color: Colors.grey.shade100 | |||
) | |||
), | |||
), | |||
onChanged: (value) => filterSearchResults(value.toLowerCase()) | |||
), | |||
), | |||
Padding( | |||
padding: const EdgeInsets.only(top: 16,left: 16,right: 16), | |||
child: list(), | |||
), | |||
], | |||
), | |||
), | |||
); | |||
} | |||
} |