Browse Source

Fix conversation creation & slight polish on converstation pages

pull/1/head
Tovi Jaeschke-Rogers 2 years ago
parent
commit
b409ddaac8
15 changed files with 225 additions and 152 deletions
  1. +0
    -1
      Backend/Api/Messages/Conversations.go
  2. +7
    -4
      Backend/Api/Messages/CreateConversation.go
  3. +1
    -1
      Backend/Database/ConversationDetails.go
  4. +3
    -8
      Backend/Database/UserConversations.go
  5. +49
    -50
      mobile/lib/models/conversation_users.dart
  6. +54
    -27
      mobile/lib/models/conversations.dart
  7. +13
    -15
      mobile/lib/utils/storage/conversations.dart
  8. +3
    -4
      mobile/lib/utils/storage/database.dart
  9. +4
    -10
      mobile/lib/utils/storage/messages.dart
  10. +19
    -0
      mobile/lib/utils/time.dart
  11. +1
    -19
      mobile/lib/views/main/conversation/detail.dart
  12. +13
    -4
      mobile/lib/views/main/conversation/edit_details.dart
  13. +39
    -4
      mobile/lib/views/main/conversation/list_item.dart
  14. +15
    -1
      mobile/lib/views/main/conversation/settings.dart
  15. +4
    -4
      mobile/lib/views/main/conversation/settings_user_list_item.dart

+ 0
- 1
Backend/Api/Messages/Conversations.go View File

@ -79,5 +79,4 @@ func EncryptedConversationDetailsList(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write(returnJson)
}

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

@ -2,7 +2,9 @@ package Messages
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gofrs/uuid"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
@ -30,9 +32,9 @@ func CreateConversation(w http.ResponseWriter, r *http.Request) {
}
messageThread = Models.ConversationDetail{
Base: Models.Base{
ID: uuid.FromStringOrNil(rawConversationData.ID),
},
Base: Models.Base{
ID: uuid.FromStringOrNil(rawConversationData.ID),
},
Name: rawConversationData.Name,
Users: rawConversationData.Users,
}
@ -43,9 +45,10 @@ func CreateConversation(w http.ResponseWriter, r *http.Request) {
return
}
fmt.Println(rawConversationData.UserConversations[0])
err = Database.CreateUserConversations(&rawConversationData.UserConversations)
if err != nil {
panic(err)
http.Error(w, "Error", http.StatusInternalServerError)
return
}


+ 1
- 1
Backend/Database/ConversationDetails.go View File

@ -29,7 +29,7 @@ func GetConversationDetailsByIds(id []string) ([]Models.ConversationDetail, erro
err = DB.Preload(clause.Associations).
Where("id IN ?", id).
First(&messageThread).
Find(&messageThread).
Error
return messageThread, err


+ 3
- 8
Backend/Database/UserConversations.go View File

@ -31,9 +31,7 @@ func GetUserConversationsByUserId(id string) ([]Models.UserConversation, error)
}
func CreateUserConversation(userConversation *Models.UserConversation) error {
var (
err error
)
var err error
err = DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(userConversation).
@ -43,12 +41,9 @@ func CreateUserConversation(userConversation *Models.UserConversation) error {
}
func CreateUserConversations(userConversations *[]Models.UserConversation) error {
var (
err error
)
var err error
err = DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(userConversations).
err = DB.Create(userConversations).
Error
return err


+ 49
- 50
mobile/lib/models/conversation_users.dart View File

@ -1,5 +1,52 @@
import '/utils/storage/database.dart';
import '/models/conversations.dart';
import '/utils/storage/database.dart';
Future<ConversationUser> getConversationUser(Conversation conversation, String userId) async {
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],
);
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,
);
}
// 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],
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,
);
});
}
class ConversationUser{
String id;
@ -33,7 +80,7 @@ class ConversationUser{
'id': id,
'user_id': userId,
'username': username,
'associationKey': associationKey,
'association_key': associationKey,
'admin': admin ? 'true' : 'false',
};
}
@ -49,51 +96,3 @@ class ConversationUser{
};
}
}
// 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],
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,
);
});
}
Future<ConversationUser> getConversationUser(Conversation conversation, String userId) async {
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],
);
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,
);
}

+ 54
- 27
mobile/lib/models/conversations.dart View File

@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:Envelope/models/messages.dart';
import 'package:pointycastle/export.dart';
import 'package:sqflite/sqflite.dart';
import 'package:uuid/uuid.dart';
@ -20,6 +21,7 @@ Future<Conversation> createConversation(String title, List<Friend> friends) asyn
var uuid = const Uuid();
final String conversationId = uuid.v4();
final String conversationDetailId = uuid.v4();
Uint8List symmetricKey = AesHelper.deriveKey(generateRandomString(32));
@ -28,11 +30,12 @@ Future<Conversation> createConversation(String title, List<Friend> friends) asyn
Conversation conversation = Conversation(
id: conversationId,
userId: profile.id,
conversationDetailId: '',
conversationDetailId: conversationDetailId,
symmetricKey: base64.encode(symmetricKey),
admin: true,
name: title,
status: ConversationStatus.pending,
isRead: true,
);
await db.insert(
@ -51,7 +54,7 @@ Future<Conversation> createConversation(String title, List<Friend> friends) asyn
associationKey: associationKey,
admin: true,
).toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
conflictAlgorithm: ConflictAlgorithm.fail,
);
for (Friend friend in friends) {
@ -103,6 +106,7 @@ Future<Conversation> getConversationById(String id) async {
admin: maps[0]['admin'] == 1,
name: maps[0]['name'],
status: ConversationStatus.values[maps[0]['status']],
isRead: maps[0]['is_read'] == 1,
);
}
@ -121,6 +125,7 @@ Future<List<Conversation>> getConversations() async {
admin: maps[i]['admin'] == 1,
name: maps[i]['name'],
status: ConversationStatus.values[maps[i]['status']],
isRead: maps[i]['is_read'] == 1,
);
});
}
@ -134,6 +139,7 @@ class Conversation {
bool admin;
String name;
ConversationStatus status;
bool isRead;
Conversation({
required this.id,
@ -143,6 +149,7 @@ class Conversation {
required this.admin,
required this.name,
required this.status,
required this.isRead,
});
@ -170,6 +177,7 @@ class Conversation {
admin: admin == 'true',
name: 'Unknown',
status: ConversationStatus.complete,
isRead: true,
);
}
@ -181,41 +189,28 @@ class Conversation {
List<ConversationUser> users = await getConversationUsers(this);
List<Object> userConversations = [];
for (var x in users) {
print(x.toMap());
}
for (ConversationUser user in users) {
if (profile.id == user.userId) {
userConversations.add({
'id': user.id,
'user_id': profile.id,
'conversation_detail_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(id.codeUnits)),
'admin': AesHelper.aesEncrypt(symKey, Uint8List.fromList((admin ? 'true' : 'false').codeUnits)),
'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(symKey, profile.publicKey!)),
});
continue;
}
Friend friend = await getFriendByFriendId(user.userId);
RSAPublicKey pubKey = CryptoUtils.rsaPublicKeyFromPem(friend.asymmetricPublicKey);
RSAPublicKey pubKey = profile.publicKey!;
String newId = id;
if (profile.id != user.userId) {
Friend friend = await getFriendByFriendId(user.userId);
pubKey = CryptoUtils.rsaPublicKeyFromPem(friend.asymmetricPublicKey);
newId = (const Uuid()).v4();
}
userConversations.add({
'id': user.id,
'user_id': friend.userId,
'conversation_detail_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(id.codeUnits)),
'id': newId,
'user_id': user.userId,
'conversation_detail_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(conversationDetailId.codeUnits)),
'admin': AesHelper.aesEncrypt(symKey, Uint8List.fromList((admin ? 'true' : 'false').codeUnits)),
'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(symKey, pubKey)),
});
}
for (var x in userConversations) {
print(x);
}
return {
'id': id,
'id': conversationDetailId,
'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)),
'users': AesHelper.aesEncrypt(symKey, Uint8List.fromList(jsonEncode(users).codeUnits)),
'user_conversations': userConversations,
@ -231,6 +226,7 @@ class Conversation {
'admin': admin ? 1 : 0,
'name': name,
'status': status.index,
'is_read': isRead ? 1 : 0,
};
}
@ -244,6 +240,37 @@ class Conversation {
name: $name
admin: $admin''';
}
Future<Message?> getRecentMessage() 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
LIMIT 1;
''',
[id],
);
if (maps.isEmpty) {
return null;
}
return Message(
id: maps[0]['id'],
symmetricKey: maps[0]['symmetric_key'],
userSymmetricKey: maps[0]['user_symmetric_key'],
data: maps[0]['data'],
senderId: maps[0]['sender_id'],
senderUsername: maps[0]['sender_username'],
associationKey: maps[0]['association_key'],
createdAt: maps[0]['created_at'],
failedToSend: maps[0]['failed_to_send'] == 1,
);
}
}


+ 13
- 15
mobile/lib/utils/storage/conversations.dart View File

@ -14,7 +14,7 @@ import '/utils/encryption/aes_helper.dart';
Future<void> updateConversations() async {
RSAPrivateKey privKey = await MyProfile.getPrivateKey();
try {
// try {
var resp = await http.get(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'),
headers: {
@ -94,9 +94,9 @@ Future<void> updateConversations() async {
);
}
}
} catch (SocketException) {
return;
}
// } catch (SocketException) {
// return;
// }
}
Future<void> uploadConversation(Conversation conversation) async {
@ -104,16 +104,14 @@ Future<void> uploadConversation(Conversation conversation) async {
Map<String, dynamic> conversationJson = await conversation.toJson();
print(conversationJson);
var x = await http.post(
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),
);
// var x = await http.post(
// 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),
// );
// print(x.statusCode);
print(x.statusCode);
}

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

@ -39,7 +39,8 @@ Future<Database> getDatabaseConnection() async {
symmetric_key TEXT,
admin INTEGER,
name TEXT,
status INTEGER
status INTEGER,
is_read INTEGER
);
''');
@ -72,9 +73,7 @@ Future<Database> getDatabaseConnection() async {
''');
},
// Set the version. This executes the onCreate function and provides a
// path to perform database upgrades and downgrades.
version: 2,
version: 2,
);
return database;


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

@ -2,7 +2,6 @@ import 'dart:convert';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sqflite/sqflite.dart';
import 'package:uuid/uuid.dart';
@ -14,24 +13,19 @@ import '/utils/storage/database.dart';
import '/utils/storage/session_cookie.dart';
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');
}
MyProfile profile = await MyProfile.getProfile();
var uuid = const Uuid();
final String messageDataId = uuid.v4();
ConversationUser currentUser = await getConversationUser(conversation, username);
ConversationUser currentUser = await getConversationUser(conversation, profile.id);
Message message = Message(
id: messageDataId,
symmetricKey: '',
userSymmetricKey: '',
senderId: userId,
senderUsername: username,
senderId: profile.id,
senderUsername: profile.username,
data: data,
associationKey: currentUser.associationKey,
createdAt: DateTime.now().toIso8601String(),


+ 19
- 0
mobile/lib/utils/time.dart View File

@ -0,0 +1,19 @@
String convertToAgo(String input, { bool short = false }) {
DateTime time = DateTime.parse(input);
Duration diff = DateTime.now().difference(time);
if(diff.inDays >= 1){
return '${diff.inDays} day${diff.inDays == 1 ? "" : "s"} ${short ? '': 'ago'}';
}
if(diff.inHours >= 1){
return '${diff.inHours} hour${diff.inHours == 1 ? "" : "s"} ${short ? '' : 'ago'}';
}
if(diff.inMinutes >= 1){
return '${diff.inMinutes} ${short ? 'min' : 'minute'}${diff.inMinutes == 1 ? "" : "s"} ${short ? '' : 'ago'}';
}
if (diff.inSeconds >= 1){
return '${diff.inSeconds} ${short ? '' : 'second'}${diff.inSeconds == 1 ? "" : "s"} ${short ? '' : 'ago'}';
}
return 'just now';
}

+ 1
- 19
mobile/lib/views/main/conversation/detail.dart View File

@ -4,27 +4,9 @@ import '/models/conversations.dart';
import '/models/messages.dart';
import '/models/my_profile.dart';
import '/utils/storage/messages.dart';
import '/utils/time.dart';
import '/views/main/conversation/settings.dart';
String convertToAgo(String input){
DateTime time = DateTime.parse(input);
Duration diff = DateTime.now().difference(time);
if(diff.inDays >= 1){
return '${diff.inDays} day${diff.inDays == 1 ? "" : "s"} ago';
}
if(diff.inHours >= 1){
return '${diff.inHours} hour${diff.inHours == 1 ? "" : "s"} ago';
}
if(diff.inMinutes >= 1){
return '${diff.inMinutes} minute${diff.inMinutes == 1 ? "" : "s"} ago';
}
if (diff.inSeconds >= 1){
return '${diff.inSeconds} second${diff.inSeconds == 1 ? "" : "s"} ago';
}
return 'just now';
}
class ConversationDetail extends StatefulWidget{
final Conversation conversation;
const ConversationDetail({


+ 13
- 4
mobile/lib/views/main/conversation/edit_details.dart View File

@ -1,3 +1,4 @@
import 'package:Envelope/models/conversation_users.dart';
import 'package:flutter/material.dart';
import '/components/custom_circle_avatar.dart';
@ -8,10 +9,12 @@ import '/views/main/conversation/create_add_users.dart';
class ConversationEditDetails extends StatefulWidget {
final Conversation? conversation;
final List<Friend>? friends;
final List<ConversationUser>? users;
const ConversationEditDetails({
Key? key,
this.conversation,
this.friends,
this.users,
}) : super(key: key);
@override
@ -25,6 +28,14 @@ class _ConversationEditDetails extends State<ConversationEditDetails> {
TextEditingController conversationNameController = TextEditingController();
@override
void initState() {
if (widget.conversation != null) {
conversationNameController.text = widget.conversation!.name;
}
super.initState();
}
@override
Widget build(BuildContext context) {
const TextStyle inputTextStyle = TextStyle(
@ -96,10 +107,8 @@ class _ConversationEditDetails extends State<ConversationEditDetails> {
key: _formKey,
child: Column(
children: [
CustomCircleAvatar(
icon: widget.conversation != null ?
null : // TODO: Add icon here
const Icon(Icons.people, size: 60),
const CustomCircleAvatar(
icon: const Icon(Icons.people, size: 60),
imagePath: null,
radius: 50,
),


+ 39
- 4
mobile/lib/views/main/conversation/list_item.dart View File

@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import '/models/messages.dart';
import '/components/custom_circle_avatar.dart';
import '/models/conversations.dart';
import '/views/main/conversation/detail.dart';
import '/utils/time.dart';
class ConversationListItem extends StatefulWidget{
final Conversation conversation;
@ -17,6 +19,7 @@ class ConversationListItem extends StatefulWidget{
class _ConversationListItemState extends State<ConversationListItem> {
late Conversation conversation;
late Message? recentMessage;
bool loaded = false;
@override
@ -31,7 +34,7 @@ class _ConversationListItemState extends State<ConversationListItem> {
})).then(onGoBack) : null;
},
child: Container(
padding: const EdgeInsets.only(left: 16,right: 16,top: 10,bottom: 10),
padding: const EdgeInsets.only(left: 16,right: 0,top: 10,bottom: 10),
child: !loaded ? null : Row(
children: <Widget>[
Expanded(
@ -54,13 +57,40 @@ class _ConversationListItemState extends State<ConversationListItem> {
conversation.name,
style: const TextStyle(fontSize: 16)
),
//Text(widget.messageText,style: TextStyle(fontSize: 13,color: Colors.grey.shade600, fontWeight: widget.isMessageRead?FontWeight.bold:FontWeight.normal),),
],
recentMessage != null ?
const SizedBox(height: 2) :
const SizedBox.shrink()
,
recentMessage != null ?
Text(
recentMessage!.data,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(
fontSize: 13,
color: Colors.grey.shade600,
fontWeight: conversation.isRead ? FontWeight.normal : FontWeight.bold,
),
) :
const SizedBox.shrink(),
],
),
),
),
),
],
recentMessage != null ?
Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
convertToAgo(recentMessage!.createdAt, short: true),
style: TextStyle(
fontSize: 13,
color: Colors.grey.shade600,
),
)
):
const SizedBox.shrink(),
],
),
),
],
@ -72,7 +102,12 @@ class _ConversationListItemState extends State<ConversationListItem> {
@override
void initState() {
super.initState();
getConversationData();
}
Future<void> getConversationData() async {
conversation = widget.conversation;
recentMessage = await conversation.getRecentMessage();
loaded = true;
setState(() {});
}


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

@ -1,4 +1,5 @@
import 'package:Envelope/components/custom_circle_avatar.dart';
import 'package:Envelope/views/main/conversation/edit_details.dart';
import 'package:flutter/material.dart';
import '/models/conversation_users.dart';
@ -109,7 +110,13 @@ class _ConversationSettingsState extends State<ConversationSettings> {
padding: const EdgeInsets.all(5.0),
splashRadius: 25,
onPressed: () {
// TODO: Redirect to edit screen
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ConversationEditDetails(
users: users,
conversation: widget.conversation,
friends: null,
)),
).then(onGoBack);
},
) : const SizedBox.shrink(),
],
@ -228,5 +235,12 @@ class _ConversationSettingsState extends State<ConversationSettings> {
}
);
}
onGoBack(dynamic value) async {
nameController.text = widget.conversation.name;
super.initState();
getUsers();
setState(() {});
}
}

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

@ -43,7 +43,7 @@ class _ConversationSettingsUserListItemState extends State<ConversationSettingsU
Widget adminUserActions() {
if (!widget.isAdmin || widget.user.username == widget.profile.username) {
return const SizedBox.shrink();
return const SizedBox(height: 50);
}
return PopupMenuButton<String>(
@ -75,7 +75,7 @@ class _ConversationSettingsUserListItemState extends State<ConversationSettingsU
),
),
],
offset: const Offset(0, 50),
offset: const Offset(0, 0),
elevation: 2,
// on selected we show the dialog box
onSelected: (String value) {
@ -95,7 +95,7 @@ class _ConversationSettingsUserListItemState extends State<ConversationSettingsU
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.only(left: 12,right: 5,top: 0,bottom: 0),
padding: const EdgeInsets.only(left: 12,right: 5,top: 0,bottom: 5),
child: Row(
children: <Widget>[
Expanded(
@ -104,7 +104,7 @@ class _ConversationSettingsUserListItemState extends State<ConversationSettingsU
children: <Widget>[
CustomCircleAvatar(
initials: widget.user.username[0].toUpperCase(),
imagePath: null, // TODO: Add image here
imagePath: null,
radius: 15,
),
const SizedBox(width: 16),


Loading…
Cancel
Save