@ -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 | |||
// ), | |||
// ), | |||
], | |||
), | |||
), | |||
), | |||
), | |||
], | |||
), | |||
), | |||
], | |||
), | |||
), | |||
); | |||
} | |||
} | |||
} |