| @ -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 conversationId; | |||
| String userId; | |||
| String conversationDetailId; | |||
| String messageThreadKey; | |||
| String symmetricKey; | |||
| String data; | |||
| String messageType; | |||
| String? decryptedData; | |||
| Message({ | |||
| bool admin; | |||
| String name; | |||
| String? users; | |||
| Conversation({ | |||
| required this.id, | |||
| required this.conversationId, | |||
| required this.userId, | |||
| required this.conversationDetailId, | |||
| required this.messageThreadKey, | |||
| 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 '/views/main/conversation_detail.dart'; | |||
| 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> { | |||
| @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'; | |||
| 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> { | |||
| @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 | |||
| // ), | |||
| // ), | |||
| ], | |||
| ), | |||
| ), | |||
| ), | |||
| ), | |||
| ], | |||
| ), | |||
| ), | |||
| ], | |||
| ), | |||
| ), | |||
| ); | |||
| } | |||
| } | |||
| } | |||