Browse Source

Make local conversation records

pull/1/head
Tovi Jaeschke-Rogers 2 years ago
parent
commit
4ca108dafa
13 changed files with 608 additions and 108 deletions
  1. +144
    -82
      mobile/lib/models/conversations.dart
  2. +2
    -0
      mobile/lib/models/friends.dart
  3. +3
    -2
      mobile/lib/models/my_profile.dart
  4. +0
    -1
      mobile/lib/utils/storage/database.dart
  5. +1
    -1
      mobile/lib/views/authentication/unauthenticated_landing.dart
  6. +170
    -0
      mobile/lib/views/main/conversation_create_add_users.dart
  7. +96
    -0
      mobile/lib/views/main/conversation_create_add_users_list.dart
  8. +146
    -0
      mobile/lib/views/main/conversation_edit_details.dart
  9. +31
    -1
      mobile/lib/views/main/conversation_list.dart
  10. +1
    -0
      mobile/lib/views/main/conversation_list_item.dart
  11. +1
    -2
      mobile/lib/views/main/friend_list.dart
  12. +8
    -17
      mobile/lib/views/main/friend_list_item.dart
  13. +5
    -2
      mobile/lib/views/main/home.dart

+ 144
- 82
mobile/lib/models/conversations.dart View File

@ -1,101 +1,163 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:Envelope/models/conversation_users.dart';
import 'package:Envelope/models/friends.dart';
import 'package:Envelope/models/my_profile.dart';
import 'package:pointycastle/export.dart';
import 'package:sqflite/sqflite.dart';
import 'package:uuid/uuid.dart';
import '/utils/encryption/crypto_utils.dart';
import '/utils/encryption/aes_helper.dart';
import '/utils/storage/database.dart';
import '/utils/strings.dart';
Conversation findConversationByDetailId(List<Conversation> conversations, String id) {
for (var conversation in conversations) {
if (conversation.conversationDetailId == id) {
return conversation;
}
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 userId;
String conversationDetailId;
String symmetricKey;
bool admin;
String name;
Conversation({
required this.id,
required this.userId,
required this.conversationDetailId,
required this.symmetricKey,
required this.admin,
required this.name,
});
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 admin = AesHelper.aesDecrypt(
symmetricKeyDecrypted,
base64.decode(json['admin']),
);
return Conversation(
id: json['id'],
userId: json['user_id'],
conversationDetailId: detailId,
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,
'symmetric_key': symmetricKey,
'admin': admin ? 1 : 0,
'name': name,
};
}
String id;
String userId;
String conversationDetailId;
String symmetricKey;
bool admin;
String name;
Conversation({
required this.id,
required this.userId,
required this.conversationDetailId,
required this.symmetricKey,
required this.admin,
required this.name,
});
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 admin = AesHelper.aesDecrypt(
symmetricKeyDecrypted,
base64.decode(json['admin']),
);
return Conversation(
id: json['id'],
userId: json['user_id'],
conversationDetailId: detailId,
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,
'symmetric_key': symmetricKey,
'admin': admin ? 1 : 0,
'name': name,
};
}
}
Future<Conversation> createConversation(String title, List<Friend> friends) async {
final db = await getDatabaseConnection();
MyProfile profile = await MyProfile.getProfile();
var uuid = const Uuid();
final String conversationId = uuid.v4();
Uint8List symmetricKey = AesHelper.deriveKey(generateRandomString(32));
String associationKey = generateRandomString(32);
Conversation conversation = Conversation(
id: conversationId,
userId: profile.id,
conversationDetailId: '',
symmetricKey: base64.encode(symmetricKey),
admin: true,
name: title,
);
await db.insert(
'conversations',
conversation.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
await db.insert(
'conversation_users',
ConversationUser(
id: uuid.v4(),
conversationId: conversationId,
username: profile.username,
associationKey: associationKey,
admin: false,
).toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
for (Friend friend in friends) {
await db.insert(
'conversation_users',
ConversationUser(
id: uuid.v4(),
conversationId: conversationId,
username: friend.username,
associationKey: associationKey,
admin: false,
).toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
return conversation;
}
// 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'],
symmetricKey: maps[i]['symmetric_key'],
admin: maps[i]['admin'] == 1,
name: maps[i]['name'],
);
});
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'],
symmetricKey: maps[i]['symmetric_key'],
admin: maps[i]['admin'] == 1,
name: maps[i]['name'],
);
});
}

+ 2
- 0
mobile/lib/models/friends.dart View File

@ -21,6 +21,7 @@ class Friend{
String friendSymmetricKey;
String asymmetricPublicKey;
String acceptedAt;
bool? selected;
Friend({
required this.id,
required this.userId,
@ -29,6 +30,7 @@ class Friend{
required this.friendSymmetricKey,
required this.asymmetricPublicKey,
required this.acceptedAt,
this.selected,
});
factory Friend.fromJson(Map<String, dynamic> json, RSAPrivateKey privKey) {


+ 3
- 2
mobile/lib/models/my_profile.dart View File

@ -89,8 +89,9 @@ class MyProfile {
if (profile.loggedInAt == null) {
return false;
}
return profile.loggedInAt!.isBefore(
(DateTime.now()).add(const Duration(hours: 12))
return profile.loggedInAt!.add(const Duration(hours: 12)).isAfter(
(DateTime.now())
);
}


+ 0
- 1
mobile/lib/utils/storage/database.dart View File

@ -39,7 +39,6 @@ Future<Database> getDatabaseConnection() async {
symmetric_key TEXT,
admin INTEGER,
name TEXT,
users TEXT
);
''');


+ 1
- 1
mobile/lib/views/authentication/unauthenticated_landing.dart View File

@ -18,7 +18,7 @@ class _UnauthenticatedLandingWidgetState extends State<UnauthenticatedLandingWid
primary: Theme.of(context).colorScheme.surface,
onPrimary: Theme.of(context).colorScheme.onSurface,
minimumSize: const Size.fromHeight(50),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 20),
textStyle: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,


+ 170
- 0
mobile/lib/views/main/conversation_create_add_users.dart View File

@ -0,0 +1,170 @@
import 'package:Envelope/models/conversations.dart';
import 'package:Envelope/views/main/conversation_create_add_users_list.dart';
import 'package:Envelope/views/main/conversation_detail.dart';
import 'package:flutter/material.dart';
import '/models/friends.dart';
import '/views/main/friend_list_item.dart';
class ConversationAddFriendsList extends StatefulWidget {
final List<Friend> friends;
final String title;
const ConversationAddFriendsList({
Key? key,
required this.friends,
required this.title,
}) : super(key: key);
@override
State<ConversationAddFriendsList> createState() => _ConversationAddFriendsListState();
}
class _ConversationAddFriendsListState extends State<ConversationAddFriendsList> {
List<Friend> friends = [];
List<Friend> friendsSelected = [];
@override
void initState() {
super.initState();
friends.addAll(widget.friends);
setState(() {});
}
void filterSearchResults(String query) {
List<Friend> dummySearchList = [];
dummySearchList.addAll(widget.friends);
if(query.isNotEmpty) {
List<Friend> dummyListData = [];
for (Friend friend in dummySearchList) {
if (friend.username.toLowerCase().contains(query)) {
dummyListData.add(friend);
}
}
setState(() {
friends.clear();
friends.addAll(dummyListData);
});
return;
}
setState(() {
friends.clear();
friends.addAll(widget.friends);
});
}
Widget list() {
if (friends.isEmpty) {
return const Center(
child: Text('No Friends'),
);
}
return ListView.builder(
itemCount: friends.length,
shrinkWrap: true,
padding: const EdgeInsets.only(top: 16),
physics: const BouncingScrollPhysics(),
itemBuilder: (context, i) {
return ConversationAddFriendItem(
friend: friends[i],
isSelected: (bool value) {
setState(() {
widget.friends[i].selected = value;
if (value) {
friendsSelected.add(friends[i]);
return;
}
friendsSelected.remove(friends[i]);
});
}
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0,
automaticallyImplyLeading: false,
flexibleSpace: SafeArea(
child: Container(
padding: const EdgeInsets.only(right: 16),
child: Row(
children: <Widget>[
IconButton(
onPressed: (){
Navigator.pop(context);
},
icon: const Icon(Icons.arrow_back),
),
const SizedBox(width: 2,),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
friendsSelected.isEmpty ?
'Select Friends' :
'${friendsSelected.length} Friends Selected',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600
),
),
],
),
),
],
),
),
),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 20,left: 16,right: 16),
child: TextField(
decoration: const InputDecoration(
hintText: "Search...",
prefixIcon: Icon(
Icons.search,
size: 20
),
),
onChanged: (value) => filterSearchResults(value.toLowerCase())
),
),
Padding(
padding: const EdgeInsets.only(top: 0,left: 16,right: 16),
child: list(),
),
],
),
floatingActionButton: Padding(
padding: const EdgeInsets.only(right: 10, bottom: 10),
child: FloatingActionButton(
onPressed: () async {
Conversation conversation = await createConversation(widget.title, friendsSelected);
friendsSelected = [];
Navigator.of(context).popUntil((route) => route.isFirst);
Navigator.push(context, MaterialPageRoute(builder: (context){
return ConversationDetail(
conversation: conversation,
);
}));
},
backgroundColor: Theme.of(context).colorScheme.primary,
child: friendsSelected.isEmpty ?
const Text('Skip') :
const Icon(Icons.add, size: 30),
),
),
);
}
}

+ 96
- 0
mobile/lib/views/main/conversation_create_add_users_list.dart View File

@ -0,0 +1,96 @@
import 'package:Envelope/components/custom_circle_avatar.dart';
import 'package:Envelope/models/friends.dart';
import 'package:flutter/material.dart';
class ConversationAddFriendItem extends StatefulWidget{
final Friend friend;
final ValueChanged<bool> isSelected;
const ConversationAddFriendItem({
Key? key,
required this.friend,
required this.isSelected,
}) : super(key: key);
@override
_ConversationAddFriendItemState createState() => _ConversationAddFriendItemState();
}
class _ConversationAddFriendItemState extends State<ConversationAddFriendItem> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () async {
setState(() {
widget.friend.selected = !(widget.friend.selected ?? false);
widget.isSelected(widget.friend.selected ?? false);
});
},
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.friend.username[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.friend.username,
style: const TextStyle(
fontSize: 16
)
),
],
),
),
),
),
(widget.friend.selected ?? false)
? Align(
alignment: Alignment.centerRight,
child: Icon(
Icons.check_circle,
color: Theme.of(context).colorScheme.primary,
size: 36,
),
)
: Padding(
padding: const EdgeInsets.only(right: 3),
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.primary,
width: 2,
),
borderRadius: const BorderRadius.all(Radius.circular(100))
),
)
),
],
),
),
],
),
),
);
}
}

+ 146
- 0
mobile/lib/views/main/conversation_edit_details.dart View File

@ -0,0 +1,146 @@
import 'package:flutter/material.dart';
import '/components/custom_circle_avatar.dart';
import '/views/main/conversation_create_add_users.dart';
import '/models/friends.dart';
import '/models/conversations.dart';
class ConversationEditDetails extends StatefulWidget {
final Conversation? conversation;
final List<Friend>? friends;
const ConversationEditDetails({
Key? key,
this.conversation,
this.friends,
}) : super(key: key);
@override
State<ConversationEditDetails> createState() => _ConversationEditDetails();
}
class _ConversationEditDetails extends State<ConversationEditDetails> {
final _formKey = GlobalKey<FormState>();
List<Conversation> conversations = [];
TextEditingController conversationNameController = TextEditingController();
@override
Widget build(BuildContext context) {
const TextStyle inputTextStyle = TextStyle(
fontSize: 25,
);
final OutlineInputBorder inputBorderStyle = OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(
color: Colors.transparent,
)
);
final ButtonStyle buttonStyle = ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10),
textStyle: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.error,
),
);
return Scaffold(
appBar: AppBar(
elevation: 0,
automaticallyImplyLeading: false,
flexibleSpace: SafeArea(
child: Container(
padding: const EdgeInsets.only(right: 16),
child: Row(
children: <Widget>[
IconButton(
onPressed: (){
Navigator.pop(context);
},
icon: const Icon(Icons.arrow_back),
),
const SizedBox(width: 2,),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
widget.conversation != null ?
widget.conversation!.name + " Settings" :
'Add Conversation',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600
),
),
],
),
),
],
),
),
),
),
body: Center(
child: Padding(
padding: const EdgeInsets.only(
top: 50,
left: 25,
right: 25,
),
child: Form(
key: _formKey,
child: Column(
children: [
CustomCircleAvatar(
icon: widget.conversation != null ?
null : // TODO: Add icon here
const Icon(Icons.people, size: 60),
imagePath: null,
radius: 50,
),
const SizedBox(height: 30),
TextFormField(
controller: conversationNameController,
textAlign: TextAlign.center,
decoration: InputDecoration(
hintText: 'Title',
enabledBorder: inputBorderStyle,
focusedBorder: inputBorderStyle,
),
style: inputTextStyle,
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return 'Add a title';
}
return null;
},
),
const SizedBox(height: 30),
ElevatedButton(
style: buttonStyle,
onPressed: () {
if (_formKey.currentState!.validate()) {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ConversationAddFriendsList(
friends: widget.friends!,
title: conversationNameController.text,
)
)
);
}
},
child: const Text('Save'),
),
],
),
),
),
),
);
}
}

+ 31
- 1
mobile/lib/views/main/conversation_list.dart View File

@ -1,12 +1,16 @@
import 'package:Envelope/models/friends.dart';
import 'package:Envelope/views/main/conversation_edit_details.dart';
import 'package:flutter/material.dart';
import '/models/conversations.dart';
import '/views/main/conversation_list_item.dart';
class ConversationList extends StatefulWidget {
final List<Conversation> conversations;
final List<Friend> friends;
const ConversationList({
Key? key,
required this.conversations,
required this.friends,
}) : super(key: key);
@override
@ -15,11 +19,13 @@ class ConversationList extends StatefulWidget {
class _ConversationListState extends State<ConversationList> {
List<Conversation> conversations = [];
List<Friend> friends = [];
@override
void initState() {
super.initState();
conversations.addAll(widget.conversations);
friends.addAll(widget.friends);
setState(() {});
}
@ -81,7 +87,13 @@ class _ConversationListState extends State<ConversationList> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const <Widget>[
Text("Conversations",style: TextStyle(fontSize: 32,fontWeight: FontWeight.bold),),
Text(
'Conversations',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold
)
),
],
),
),
@ -106,6 +118,24 @@ class _ConversationListState extends State<ConversationList> {
],
),
),
floatingActionButton: Padding(
padding: const EdgeInsets.only(right: 10, bottom: 10),
child: FloatingActionButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ConversationEditDetails(
friends: friends,
)),
).then(onGoBack);
},
backgroundColor: Theme.of(context).colorScheme.primary,
child: const Icon(Icons.add, size: 30),
),
),
);
}
onGoBack(dynamic value) {
setState(() {});
}
}

+ 1
- 0
mobile/lib/views/main/conversation_list_item.dart View File

@ -19,6 +19,7 @@ class _ConversationListItemState extends State<ConversationListItem> {
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context){
return ConversationDetail(


+ 1
- 2
mobile/lib/views/main/friend_list.dart View File

@ -62,8 +62,7 @@ class _FriendListState extends State<FriendList> {
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, i) {
return FriendListItem(
id: friends[i].id,
username: friends[i].username,
friend: friends[i],
);
},
);


+ 8
- 17
mobile/lib/views/main/friend_list_item.dart View File

@ -1,15 +1,12 @@
import 'package:Envelope/components/custom_circle_avatar.dart';
import 'package:Envelope/models/friends.dart';
import 'package:flutter/material.dart';
class FriendListItem extends StatefulWidget{
final String id;
final String username;
final String? imagePath;
final Friend friend;
const FriendListItem({
Key? key,
required this.id,
required this.username,
this.imagePath,
required this.friend,
}) : super(key: key);
@override
@ -21,7 +18,8 @@ class _FriendListItemState extends State<FriendListItem> {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: (){
behavior: HitTestBehavior.opaque,
onTap: () async {
},
child: Container(
padding: const EdgeInsets.only(left: 16,right: 16,top: 10,bottom: 10),
@ -31,8 +29,8 @@ class _FriendListItemState extends State<FriendListItem> {
child: Row(
children: <Widget>[
CustomCircleAvatar(
initials: widget.username[0].toUpperCase(),
imagePath: widget.imagePath,
initials: widget.friend.username[0].toUpperCase(),
imagePath: null,
),
const SizedBox(width: 16),
Expanded(
@ -43,14 +41,7 @@ class _FriendListItemState extends State<FriendListItem> {
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
// ),
// ),
Text(widget.friend.username, style: const TextStyle(fontSize: 16)),
],
),
),


+ 5
- 2
mobile/lib/views/main/home.dart View File

@ -31,7 +31,7 @@ class _HomeState extends State<Home> {
bool isLoading = true;
int _selectedIndex = 0;
List<Widget> _widgetOptions = <Widget>[
const ConversationList(conversations: []),
const ConversationList(conversations: [], friends: []),
const FriendList(friends: []),
Profile(
profile: MyProfile(
@ -61,7 +61,10 @@ class _HomeState extends State<Home> {
setState(() {
_widgetOptions = <Widget>[
ConversationList(conversations: conversations),
ConversationList(
conversations: conversations,
friends: friends,
),
FriendList(friends: friends),
Profile(profile: profile),
];


Loading…
Cancel
Save