@ -1 +1 @@ | |||||
/mobile/nsconfig.json | |||||
/mobile/.env |
@ -0,0 +1,83 @@ | |||||
package Messages | |||||
import ( | |||||
"encoding/json" | |||||
"net/http" | |||||
"net/url" | |||||
"strings" | |||||
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Auth" | |||||
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" | |||||
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" | |||||
) | |||||
func EncryptedConversationList(w http.ResponseWriter, r *http.Request) { | |||||
var ( | |||||
userConversations []Models.UserConversation | |||||
userSession Models.Session | |||||
returnJson []byte | |||||
err error | |||||
) | |||||
userSession, err = Auth.CheckCookie(r) | |||||
if err != nil { | |||||
http.Error(w, "Forbidden", http.StatusUnauthorized) | |||||
return | |||||
} | |||||
userConversations, err = Database.GetUserConversationsByUserId( | |||||
userSession.UserID.String(), | |||||
) | |||||
if err != nil { | |||||
http.Error(w, "Error", http.StatusInternalServerError) | |||||
return | |||||
} | |||||
returnJson, err = json.MarshalIndent(userConversations, "", " ") | |||||
if err != nil { | |||||
http.Error(w, "Error", http.StatusInternalServerError) | |||||
return | |||||
} | |||||
w.WriteHeader(http.StatusOK) | |||||
w.Write(returnJson) | |||||
} | |||||
func EncryptedConversationDetailsList(w http.ResponseWriter, r *http.Request) { | |||||
var ( | |||||
userConversations []Models.ConversationDetail | |||||
query url.Values | |||||
conversationIds []string | |||||
returnJson []byte | |||||
ok bool | |||||
err error | |||||
) | |||||
query = r.URL.Query() | |||||
conversationIds, ok = query["conversation_detail_ids"] | |||||
if !ok { | |||||
http.Error(w, "Invalid Data", http.StatusBadGateway) | |||||
return | |||||
} | |||||
// TODO: Fix error handling here | |||||
conversationIds = strings.Split(conversationIds[0], ",") | |||||
userConversations, err = Database.GetConversationDetailsByIds( | |||||
conversationIds, | |||||
) | |||||
if err != nil { | |||||
http.Error(w, "Error", http.StatusInternalServerError) | |||||
return | |||||
} | |||||
returnJson, err = json.MarshalIndent(userConversations, "", " ") | |||||
if err != nil { | |||||
http.Error(w, "Error", http.StatusInternalServerError) | |||||
return | |||||
} | |||||
w.WriteHeader(http.StatusOK) | |||||
w.Write(returnJson) | |||||
} |
@ -0,0 +1,55 @@ | |||||
package Database | |||||
import ( | |||||
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" | |||||
"gorm.io/gorm" | |||||
"gorm.io/gorm/clause" | |||||
) | |||||
func GetConversationDetailById(id string) (Models.ConversationDetail, error) { | |||||
var ( | |||||
messageThread Models.ConversationDetail | |||||
err error | |||||
) | |||||
err = DB.Preload(clause.Associations). | |||||
Where("id = ?", id). | |||||
First(&messageThread). | |||||
Error | |||||
return messageThread, err | |||||
} | |||||
func GetConversationDetailsByIds(id []string) ([]Models.ConversationDetail, error) { | |||||
var ( | |||||
messageThread []Models.ConversationDetail | |||||
err error | |||||
) | |||||
err = DB.Preload(clause.Associations). | |||||
Where("id = ?", id). | |||||
First(&messageThread). | |||||
Error | |||||
return messageThread, err | |||||
} | |||||
func CreateConversationDetail(messageThread *Models.ConversationDetail) error { | |||||
return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
Create(messageThread). | |||||
Error | |||||
} | |||||
func UpdateConversationDetail(messageThread *Models.ConversationDetail) error { | |||||
return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
Where("id = ?", messageThread.ID). | |||||
Updates(messageThread). | |||||
Error | |||||
} | |||||
func DeleteConversationDetail(messageThread *Models.ConversationDetail) error { | |||||
return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
Delete(messageThread). | |||||
Error | |||||
} |
@ -0,0 +1,47 @@ | |||||
package Database | |||||
import ( | |||||
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" | |||||
"gorm.io/gorm" | |||||
"gorm.io/gorm/clause" | |||||
) | |||||
func GetFriendRequestById(id string) (Models.FriendRequest, error) { | |||||
var ( | |||||
friendRequest Models.FriendRequest | |||||
err error | |||||
) | |||||
err = DB.Preload(clause.Associations). | |||||
First(&friendRequest, "id = ?", id). | |||||
Error | |||||
return friendRequest, err | |||||
} | |||||
func GetFriendRequestsByUserId(userID string) ([]Models.FriendRequest, error) { | |||||
var ( | |||||
friends []Models.FriendRequest | |||||
err error | |||||
) | |||||
err = DB.Model(Models.FriendRequest{}). | |||||
Where("user_id = ?", userID). | |||||
Find(&friends). | |||||
Error | |||||
return friends, err | |||||
} | |||||
func CreateFriendRequest(FriendRequest *Models.FriendRequest) error { | |||||
return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
Create(FriendRequest). | |||||
Error | |||||
} | |||||
func DeleteFriendRequest(FriendRequest *Models.FriendRequest) error { | |||||
return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
Delete(FriendRequest). | |||||
Error | |||||
} |
@ -1,39 +0,0 @@ | |||||
package Database | |||||
import ( | |||||
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" | |||||
"gorm.io/gorm" | |||||
"gorm.io/gorm/clause" | |||||
) | |||||
func GetMessageThreadUserById(id string) (Models.MessageThreadUser, error) { | |||||
var ( | |||||
message Models.MessageThreadUser | |||||
err error | |||||
) | |||||
err = DB.Preload(clause.Associations). | |||||
First(&message, "id = ?", id). | |||||
Error | |||||
return message, err | |||||
} | |||||
func CreateMessageThreadUser(messageThreadUser *Models.MessageThreadUser) error { | |||||
var ( | |||||
err error | |||||
) | |||||
err = DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
Create(messageThreadUser). | |||||
Error | |||||
return err | |||||
} | |||||
func DeleteMessageThreadUser(messageThreadUser *Models.MessageThreadUser) error { | |||||
return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
Delete(messageThreadUser). | |||||
Error | |||||
} |
@ -1,42 +0,0 @@ | |||||
package Database | |||||
import ( | |||||
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" | |||||
"gorm.io/gorm" | |||||
"gorm.io/gorm/clause" | |||||
) | |||||
func GetMessageThreadById(id string, user Models.User) (Models.MessageThread, error) { | |||||
var ( | |||||
messageThread Models.MessageThread | |||||
err error | |||||
) | |||||
err = DB.Preload(clause.Associations). | |||||
Where("id = ?", id). | |||||
Where("user_id = ?", user.ID). | |||||
First(&messageThread). | |||||
Error | |||||
return messageThread, err | |||||
} | |||||
func CreateMessageThread(messageThread *Models.MessageThread) error { | |||||
return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
Create(messageThread). | |||||
Error | |||||
} | |||||
func UpdateMessageThread(messageThread *Models.MessageThread) error { | |||||
return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
Where("id = ?", messageThread.ID). | |||||
Updates(messageThread). | |||||
Error | |||||
} | |||||
func DeleteMessageThread(messageThread *Models.MessageThread) error { | |||||
return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
Delete(messageThread). | |||||
Error | |||||
} |
@ -0,0 +1,188 @@ | |||||
package Seeder | |||||
// THIS FILE IS ONLY USED FOR SEEDING DATA DURING DEVELOPMENT | |||||
import ( | |||||
"bytes" | |||||
"crypto/aes" | |||||
"crypto/cipher" | |||||
"crypto/hmac" | |||||
"crypto/rand" | |||||
"crypto/rsa" | |||||
"crypto/sha256" | |||||
"encoding/base64" | |||||
"fmt" | |||||
"hash" | |||||
"golang.org/x/crypto/pbkdf2" | |||||
) | |||||
type aesKey struct { | |||||
Key []byte | |||||
Iv []byte | |||||
} | |||||
func (key aesKey) encode() string { | |||||
return base64.StdEncoding.EncodeToString(key.Key) | |||||
} | |||||
// Appends padding. | |||||
func pkcs7Padding(data []byte, blocklen int) ([]byte, error) { | |||||
var ( | |||||
padlen int = 1 | |||||
pad []byte | |||||
) | |||||
if blocklen <= 0 { | |||||
return nil, fmt.Errorf("invalid blocklen %d", blocklen) | |||||
} | |||||
for ((len(data) + padlen) % blocklen) != 0 { | |||||
padlen = padlen + 1 | |||||
} | |||||
pad = bytes.Repeat([]byte{byte(padlen)}, padlen) | |||||
return append(data, pad...), nil | |||||
} | |||||
// pkcs7strip remove pkcs7 padding | |||||
func pkcs7strip(data []byte, blockSize int) ([]byte, error) { | |||||
var ( | |||||
length int | |||||
padLen int | |||||
ref []byte | |||||
) | |||||
length = len(data) | |||||
if length == 0 { | |||||
return nil, fmt.Errorf("pkcs7: Data is empty") | |||||
} | |||||
if (length % blockSize) != 0 { | |||||
return nil, fmt.Errorf("pkcs7: Data is not block-aligned") | |||||
} | |||||
padLen = int(data[length-1]) | |||||
ref = bytes.Repeat([]byte{byte(padLen)}, padLen) | |||||
if padLen > blockSize || padLen == 0 || !bytes.HasSuffix(data, ref) { | |||||
return nil, fmt.Errorf("pkcs7: Invalid padding") | |||||
} | |||||
return data[:length-padLen], nil | |||||
} | |||||
func generateAesKey() (aesKey, error) { | |||||
var ( | |||||
saltBytes []byte = []byte{} | |||||
password []byte | |||||
seed []byte | |||||
iv []byte | |||||
err error | |||||
) | |||||
password = make([]byte, 64) | |||||
_, err = rand.Read(password) | |||||
if err != nil { | |||||
return aesKey{}, err | |||||
} | |||||
seed = make([]byte, 64) | |||||
_, err = rand.Read(seed) | |||||
if err != nil { | |||||
return aesKey{}, err | |||||
} | |||||
iv = make([]byte, 16) | |||||
_, err = rand.Read(iv) | |||||
if err != nil { | |||||
return aesKey{}, err | |||||
} | |||||
return aesKey{ | |||||
Key: pbkdf2.Key( | |||||
password, | |||||
saltBytes, | |||||
1000, | |||||
32, | |||||
func() hash.Hash { return hmac.New(sha256.New, seed) }, | |||||
), | |||||
Iv: iv, | |||||
}, nil | |||||
} | |||||
func (key aesKey) aesEncrypt(plaintext []byte) ([]byte, error) { | |||||
var ( | |||||
bPlaintext []byte | |||||
ciphertext []byte | |||||
block cipher.Block | |||||
err error | |||||
) | |||||
bPlaintext, err = pkcs7Padding(plaintext, 16) | |||||
block, err = aes.NewCipher(key.Key) | |||||
if err != nil { | |||||
return []byte{}, err | |||||
} | |||||
ciphertext = make([]byte, len(bPlaintext)) | |||||
mode := cipher.NewCBCEncrypter(block, key.Iv) | |||||
mode.CryptBlocks(ciphertext, bPlaintext) | |||||
ciphertext = append(key.Iv, ciphertext...) | |||||
return ciphertext, nil | |||||
} | |||||
func (key aesKey) aesDecrypt(ciphertext []byte) ([]byte, error) { | |||||
var ( | |||||
plaintext []byte | |||||
iv []byte | |||||
block cipher.Block | |||||
err error | |||||
) | |||||
iv = ciphertext[:aes.BlockSize] | |||||
plaintext = ciphertext[aes.BlockSize:] | |||||
block, err = aes.NewCipher(key.Key) | |||||
if err != nil { | |||||
return []byte{}, err | |||||
} | |||||
decMode := cipher.NewCBCDecrypter(block, iv) | |||||
decMode.CryptBlocks(plaintext, plaintext) | |||||
return plaintext, nil | |||||
} | |||||
// EncryptWithPublicKey encrypts data with public key | |||||
func encryptWithPublicKey(msg []byte, pub *rsa.PublicKey) []byte { | |||||
var ( | |||||
hash hash.Hash | |||||
) | |||||
hash = sha256.New() | |||||
ciphertext, err := rsa.EncryptOAEP(hash, rand.Reader, pub, msg, nil) | |||||
if err != nil { | |||||
panic(err) | |||||
} | |||||
return ciphertext | |||||
} | |||||
// DecryptWithPrivateKey decrypts data with private key | |||||
func decryptWithPrivateKey(ciphertext []byte, priv *rsa.PrivateKey) ([]byte, error) { | |||||
var ( | |||||
hash hash.Hash | |||||
plaintext []byte | |||||
err error | |||||
) | |||||
hash = sha256.New() | |||||
plaintext, err = rsa.DecryptOAEP(hash, rand.Reader, priv, ciphertext, nil) | |||||
if err != nil { | |||||
return plaintext, err | |||||
} | |||||
return plaintext, nil | |||||
} |
@ -0,0 +1,38 @@ | |||||
package Database | |||||
import ( | |||||
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" | |||||
"gorm.io/gorm/clause" | |||||
) | |||||
func GetSessionById(id string) (Models.Session, error) { | |||||
var ( | |||||
session Models.Session | |||||
err error | |||||
) | |||||
err = DB.Preload(clause.Associations). | |||||
First(&session, "id = ?", id). | |||||
Error | |||||
return session, err | |||||
} | |||||
func CreateSession(session *Models.Session) error { | |||||
var ( | |||||
err error | |||||
) | |||||
err = DB.Create(session).Error | |||||
return err | |||||
} | |||||
func DeleteSession(session *Models.Session) error { | |||||
return DB.Delete(session).Error | |||||
} | |||||
func DeleteSessionById(id string) error { | |||||
return DB.Delete(&Models.Session{}, id).Error | |||||
} |
@ -0,0 +1,49 @@ | |||||
package Database | |||||
import ( | |||||
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" | |||||
"gorm.io/gorm" | |||||
) | |||||
func GetUserConversationById(id string) (Models.UserConversation, error) { | |||||
var ( | |||||
message Models.UserConversation | |||||
err error | |||||
) | |||||
err = DB.First(&message, "id = ?", id). | |||||
Error | |||||
return message, err | |||||
} | |||||
func GetUserConversationsByUserId(id string) ([]Models.UserConversation, error) { | |||||
var ( | |||||
conversations []Models.UserConversation | |||||
err error | |||||
) | |||||
err = DB.Find(&conversations, "user_id = ?", id). | |||||
Error | |||||
return conversations, err | |||||
} | |||||
func CreateUserConversation(messageThreadUser *Models.UserConversation) error { | |||||
var ( | |||||
err error | |||||
) | |||||
err = DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
Create(messageThreadUser). | |||||
Error | |||||
return err | |||||
} | |||||
func DeleteUserConversation(messageThreadUser *Models.UserConversation) error { | |||||
return DB.Session(&gorm.Session{FullSaveAssociations: true}). | |||||
Delete(messageThreadUser). | |||||
Error | |||||
} |
@ -0,0 +1,18 @@ | |||||
package Models | |||||
import ( | |||||
"time" | |||||
"github.com/gofrs/uuid" | |||||
) | |||||
func (s Session) IsExpired() bool { | |||||
return s.Expiry.Before(time.Now()) | |||||
} | |||||
type Session struct { | |||||
Base | |||||
UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;"` | |||||
User User | |||||
Expiry time.Time | |||||
} |
@ -0,0 +1,39 @@ | |||||
import 'package:flutter/material.dart'; | |||||
class CustomCircleAvatar extends StatefulWidget { | |||||
final String initials; | |||||
final String? imagePath; | |||||
const CustomCircleAvatar({ | |||||
Key? key, | |||||
required this.initials, | |||||
this.imagePath, | |||||
}) : super(key: key); | |||||
@override | |||||
_CustomCircleAvatarState createState() => _CustomCircleAvatarState(); | |||||
} | |||||
class _CustomCircleAvatarState extends State<CustomCircleAvatar>{ | |||||
bool _checkLoading = true; | |||||
@override | |||||
void initState() { | |||||
super.initState(); | |||||
if (widget.imagePath != null) { | |||||
_checkLoading = false; | |||||
} | |||||
} | |||||
@override | |||||
Widget build(BuildContext context) { | |||||
return _checkLoading == true ? | |||||
CircleAvatar( | |||||
backgroundColor: Colors.grey[300], | |||||
child: Text(widget.initials) | |||||
) : CircleAvatar( | |||||
backgroundImage: AssetImage(widget.imagePath!) | |||||
); | |||||
} | |||||
} |
@ -1,30 +1,116 @@ | |||||
const messageTypeSender = 'sender'; | |||||
const messageTypeReceiver = 'receiver'; | |||||
import 'dart:convert'; | |||||
import 'package:pointycastle/export.dart'; | |||||
import '/utils/encryption/crypto_utils.dart'; | |||||
import '/utils/encryption/aes_helper.dart'; | |||||
import '/utils/storage/database.dart'; | |||||
class Message { | |||||
Conversation findConversationByDetailId(List<Conversation> conversations, String id) { | |||||
for (var conversation in conversations) { | |||||
if (conversation.conversationDetailId == id) { | |||||
return conversation; | |||||
} | |||||
} | |||||
// Or return `null`. | |||||
throw ArgumentError.value(id, "id", "No element with that id"); | |||||
} | |||||
class Conversation { | |||||
String id; | String id; | ||||
String conversationId; | |||||
String userId; | |||||
String conversationDetailId; | |||||
String messageThreadKey; | |||||
String symmetricKey; | String symmetricKey; | ||||
String data; | |||||
String messageType; | |||||
String? decryptedData; | |||||
Message({ | |||||
bool admin; | |||||
String name; | |||||
String? users; | |||||
Conversation({ | |||||
required this.id, | required this.id, | ||||
required this.conversationId, | |||||
required this.userId, | |||||
required this.conversationDetailId, | |||||
required this.messageThreadKey, | |||||
required this.symmetricKey, | required this.symmetricKey, | ||||
required this.data, | |||||
required this.messageType, | |||||
this.decryptedData, | |||||
required this.admin, | |||||
required this.name, | |||||
this.users, | |||||
}); | }); | ||||
factory Conversation.fromJson(Map<String, dynamic> json, RSAPrivateKey privKey) { | |||||
var symmetricKeyDecrypted = CryptoUtils.rsaDecrypt( | |||||
base64.decode(json['symmetric_key']), | |||||
privKey, | |||||
); | |||||
var detailId = AesHelper.aesDecrypt( | |||||
symmetricKeyDecrypted, | |||||
base64.decode(json['conversation_detail_id']), | |||||
); | |||||
var threadKey = AesHelper.aesDecrypt( | |||||
symmetricKeyDecrypted, | |||||
base64.decode(json['message_thread_key']), | |||||
); | |||||
var admin = AesHelper.aesDecrypt( | |||||
symmetricKeyDecrypted, | |||||
base64.decode(json['admin']), | |||||
); | |||||
return Conversation( | |||||
id: json['id'], | |||||
userId: json['user_id'], | |||||
conversationDetailId: detailId, | |||||
messageThreadKey: threadKey, | |||||
symmetricKey: base64.encode(symmetricKeyDecrypted), | |||||
admin: admin == 'true', | |||||
name: 'Unknown', | |||||
); | |||||
} | |||||
@override | |||||
String toString() { | |||||
return ''' | |||||
id: $id | |||||
userId: $userId | |||||
name: $name | |||||
admin: $admin'''; | |||||
} | |||||
Map<String, dynamic> toMap() { | |||||
return { | |||||
'id': id, | |||||
'user_id': userId, | |||||
'conversation_detail_id': conversationDetailId, | |||||
'message_thread_key': messageThreadKey, | |||||
'symmetric_key': symmetricKey, | |||||
'admin': admin ? 1 : 0, | |||||
'name': name, | |||||
'users': users, | |||||
}; | |||||
} | |||||
} | } | ||||
class Conversation { | |||||
String id; | |||||
String friendId; | |||||
String recentMessageId; | |||||
Conversation({ | |||||
required this.id, | |||||
required this.friendId, | |||||
required this.recentMessageId, | |||||
// A method that retrieves all the dogs from the dogs table. | |||||
Future<List<Conversation>> getConversations() async { | |||||
final db = await getDatabaseConnection(); | |||||
final List<Map<String, dynamic>> maps = await db.query('conversations'); | |||||
return List.generate(maps.length, (i) { | |||||
return Conversation( | |||||
id: maps[i]['id'], | |||||
userId: maps[i]['user_id'], | |||||
conversationDetailId: maps[i]['conversation_detail_id'], | |||||
messageThreadKey: maps[i]['message_thread_key'], | |||||
symmetricKey: maps[i]['symmetric_key'], | |||||
admin: maps[i]['admin'] == 1, | |||||
name: maps[i]['name'], | |||||
users: maps[i]['users'], | |||||
); | |||||
}); | }); | ||||
} | } |
@ -0,0 +1,20 @@ | |||||
const messageTypeSender = 'sender'; | |||||
const messageTypeReceiver = 'receiver'; | |||||
class Message { | |||||
String id; | |||||
String symmetricKey; | |||||
String messageThreadKey; | |||||
String data; | |||||
String senderId; | |||||
String senderUsername; | |||||
Message({ | |||||
required this.id, | |||||
required this.symmetricKey, | |||||
required this.messageThreadKey, | |||||
required this.data, | |||||
required this.senderId, | |||||
required this.senderUsername, | |||||
}); | |||||
} |
@ -0,0 +1,79 @@ | |||||
import 'dart:convert'; | |||||
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 '/models/conversations.dart'; | |||||
import '/utils/storage/database.dart'; | |||||
import '/utils/storage/session_cookie.dart'; | |||||
import '/utils/storage/encryption_keys.dart'; | |||||
import '/utils/encryption/aes_helper.dart'; | |||||
Future<void> updateConversations() async { | |||||
RSAPrivateKey privKey = await getPrivateKey(); | |||||
var resp = await http.get( | |||||
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'), | |||||
headers: { | |||||
'cookie': await getSessionCookie(), | |||||
} | |||||
); | |||||
if (resp.statusCode != 200) { | |||||
throw Exception(resp.body); | |||||
} | |||||
List<Conversation> conversations = []; | |||||
List<String> conversationsDetailIds = []; | |||||
List<dynamic> conversationsJson = jsonDecode(resp.body); | |||||
for (var i = 0; i < conversationsJson.length; i++) { | |||||
Conversation conversation = Conversation.fromJson( | |||||
conversationsJson[i] as Map<String, dynamic>, | |||||
privKey, | |||||
); | |||||
conversations.add(conversation); | |||||
conversationsDetailIds.add(conversation.conversationDetailId); | |||||
} | |||||
Map<String, String> params = {}; | |||||
params['conversation_detail_ids'] = conversationsDetailIds.join(','); | |||||
var uri = Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversation_details'); | |||||
uri = uri.replace(queryParameters: params); | |||||
resp = await http.get( | |||||
uri, | |||||
headers: { | |||||
'cookie': await getSessionCookie(), | |||||
} | |||||
); | |||||
if (resp.statusCode != 200) { | |||||
throw Exception(resp.body); | |||||
} | |||||
final db = await getDatabaseConnection(); | |||||
List<dynamic> conversationsDetailsJson = jsonDecode(resp.body); | |||||
for (var i = 0; i < conversationsDetailsJson.length; i++) { | |||||
var conversationDetailJson = conversationsDetailsJson[i] as Map<String, dynamic>; | |||||
var conversation = findConversationByDetailId(conversations, conversationDetailJson['id']); | |||||
conversation.name = AesHelper.aesDecrypt( | |||||
base64.decode(conversation.symmetricKey), | |||||
base64.decode(conversationDetailJson['name']), | |||||
); | |||||
conversation.users = AesHelper.aesDecrypt( | |||||
base64.decode(conversation.symmetricKey), | |||||
base64.decode(conversationDetailJson['users']), | |||||
); | |||||
await db.insert( | |||||
'conversations', | |||||
conversation.toMap(), | |||||
conflictAlgorithm: ConflictAlgorithm.replace, | |||||
); | |||||
} | |||||
} |
@ -1,60 +1,66 @@ | |||||
import 'package:Envelope/components/custom_circle_avatar.dart'; | |||||
import 'package:Envelope/models/conversations.dart'; | |||||
import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||
import '/views/main/conversation_detail.dart'; | import '/views/main/conversation_detail.dart'; | ||||
class ConversationListItem extends StatefulWidget{ | class ConversationListItem extends StatefulWidget{ | ||||
final String id; | |||||
final String username; | |||||
const ConversationListItem({ | |||||
Key? key, | |||||
required this.id, | |||||
required this.username, | |||||
}) : super(key: key); | |||||
final Conversation conversation; | |||||
const ConversationListItem({ | |||||
Key? key, | |||||
required this.conversation, | |||||
}) : super(key: key); | |||||
@override | |||||
_ConversationListItemState createState() => _ConversationListItemState(); | |||||
@override | |||||
_ConversationListItemState createState() => _ConversationListItemState(); | |||||
} | } | ||||
class _ConversationListItemState extends State<ConversationListItem> { | class _ConversationListItemState extends State<ConversationListItem> { | ||||
@override | |||||
Widget build(BuildContext context) { | |||||
return GestureDetector( | |||||
onTap: () { | |||||
Navigator.push(context, MaterialPageRoute(builder: (context){ | |||||
return ConversationDetail(); | |||||
})); | |||||
}, | |||||
child: Container( | |||||
padding: const EdgeInsets.only(left: 16,right: 16,top: 10,bottom: 10), | |||||
child: Row( | |||||
children: <Widget>[ | |||||
Expanded( | |||||
child: Row( | |||||
children: <Widget>[ | |||||
// CircleAvatar( | |||||
// backgroundImage: NetworkImage(widget.imageUrl), | |||||
// maxRadius: 30, | |||||
// ), | |||||
//const SizedBox(width: 16), | |||||
Expanded( | |||||
child: Container( | |||||
color: Colors.transparent, | |||||
child: Column( | |||||
crossAxisAlignment: CrossAxisAlignment.start, | |||||
children: <Widget>[ | |||||
Text(widget.username, style: const TextStyle(fontSize: 16)), | |||||
const SizedBox(height: 6), | |||||
//Text(widget.messageText,style: TextStyle(fontSize: 13,color: Colors.grey.shade600, fontWeight: widget.isMessageRead?FontWeight.bold:FontWeight.normal),), | |||||
const Divider(), | |||||
], | |||||
), | |||||
), | |||||
), | |||||
], | |||||
), | |||||
), | |||||
], | |||||
), | |||||
), | |||||
@override | |||||
Widget build(BuildContext context) { | |||||
return GestureDetector( | |||||
onTap: () { | |||||
Navigator.push(context, MaterialPageRoute(builder: (context){ | |||||
return ConversationDetail( | |||||
conversation: widget.conversation, | |||||
); | |||||
})); | |||||
}, | |||||
child: Container( | |||||
padding: const EdgeInsets.only(left: 16,right: 16,top: 10,bottom: 10), | |||||
child: Row( | |||||
children: <Widget>[ | |||||
Expanded( | |||||
child: Row( | |||||
children: <Widget>[ | |||||
CustomCircleAvatar( | |||||
initials: widget.conversation.name[0].toUpperCase(), | |||||
imagePath: null, | |||||
), | |||||
const SizedBox(width: 16), | |||||
Expanded( | |||||
child: Align( | |||||
alignment: Alignment.centerLeft, | |||||
child: Container( | |||||
color: Colors.transparent, | |||||
child: Column( | |||||
crossAxisAlignment: CrossAxisAlignment.start, | |||||
children: <Widget>[ | |||||
Text( | |||||
widget.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),), | |||||
], | |||||
), | |||||
), | |||||
), | |||||
), | |||||
], | |||||
), | |||||
), | |||||
], | |||||
), | |||||
), | |||||
); | ); | ||||
} | |||||
} | |||||
} | } |
@ -1,56 +1,67 @@ | |||||
import 'package:Envelope/components/custom_circle_avatar.dart'; | |||||
import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||
class FriendListItem extends StatefulWidget{ | class FriendListItem extends StatefulWidget{ | ||||
final String id; | |||||
final String username; | |||||
const FriendListItem({ | |||||
Key? key, | |||||
required this.id, | |||||
required this.username, | |||||
}) : super(key: key); | |||||
final String id; | |||||
final String username; | |||||
final String? imagePath; | |||||
const FriendListItem({ | |||||
Key? key, | |||||
required this.id, | |||||
required this.username, | |||||
this.imagePath, | |||||
}) : super(key: key); | |||||
@override | |||||
_FriendListItemState createState() => _FriendListItemState(); | |||||
@override | |||||
_FriendListItemState createState() => _FriendListItemState(); | |||||
} | } | ||||
class _FriendListItemState extends State<FriendListItem> { | class _FriendListItemState extends State<FriendListItem> { | ||||
@override | |||||
Widget build(BuildContext context) { | |||||
return GestureDetector( | |||||
onTap: (){ | |||||
}, | |||||
child: Container( | |||||
padding: const EdgeInsets.only(left: 16,right: 16,top: 10,bottom: 10), | |||||
child: Row( | |||||
children: <Widget>[ | |||||
Expanded( | |||||
child: Row( | |||||
children: <Widget>[ | |||||
// CircleAvatar( | |||||
// backgroundImage: NetworkImage(widget.imageUrl), | |||||
// maxRadius: 30, | |||||
// ), | |||||
//const SizedBox(width: 16), | |||||
Expanded( | |||||
child: Container( | |||||
color: Colors.transparent, | |||||
child: Column( | |||||
crossAxisAlignment: CrossAxisAlignment.start, | |||||
children: <Widget>[ | |||||
Text(widget.username, style: const TextStyle(fontSize: 16)), | |||||
const SizedBox(height: 6), | |||||
//Text(widget.messageText,style: TextStyle(fontSize: 13,color: Colors.grey.shade600, fontWeight: widget.isMessageRead?FontWeight.bold:FontWeight.normal),), | |||||
const Divider(), | |||||
], | |||||
), | |||||
), | |||||
), | |||||
], | |||||
), | |||||
), | |||||
], | |||||
), | |||||
), | |||||
@override | |||||
Widget build(BuildContext context) { | |||||
return GestureDetector( | |||||
onTap: (){ | |||||
}, | |||||
child: Container( | |||||
padding: const EdgeInsets.only(left: 16,right: 16,top: 10,bottom: 10), | |||||
child: Row( | |||||
children: <Widget>[ | |||||
Expanded( | |||||
child: Row( | |||||
children: <Widget>[ | |||||
CustomCircleAvatar( | |||||
initials: widget.username[0].toUpperCase(), | |||||
imagePath: widget.imagePath, | |||||
), | |||||
const SizedBox(width: 16), | |||||
Expanded( | |||||
child: Align( | |||||
alignment: Alignment.centerLeft, | |||||
child: Container( | |||||
color: Colors.transparent, | |||||
child: Column( | |||||
crossAxisAlignment: CrossAxisAlignment.start, | |||||
children: <Widget>[ | |||||
Text(widget.username, style: const TextStyle(fontSize: 16)), | |||||
// Text( | |||||
// widget.messageText, | |||||
// style: TextStyle(fontSize: 13, | |||||
// color: Colors.grey.shade600, | |||||
// fontWeight: widget.isMessageRead?FontWeight.bold:FontWeight.normal | |||||
// ), | |||||
// ), | |||||
], | |||||
), | |||||
), | |||||
), | |||||
), | |||||
], | |||||
), | |||||
), | |||||
], | |||||
), | |||||
), | |||||
); | ); | ||||
} | |||||
} | |||||
} | } |