Browse Source

Code clean up and organisation of colors and profile storage

pull/1/head
Tovi Jaeschke-Rogers 2 years ago
parent
commit
0dac8e86ae
19 changed files with 1016 additions and 503 deletions
  1. +1
    -1
      Backend/Database/Seeder/FriendSeeder.go
  2. +2
    -2
      Backend/Database/Seeder/MessageSeeder.go
  3. +1
    -1
      Backend/Database/Seeder/UserSeeder.go
  4. +52
    -12
      mobile/lib/components/custom_circle_avatar.dart
  5. +86
    -38
      mobile/lib/main.dart
  6. +7
    -6
      mobile/lib/models/conversation_users.dart
  7. +94
    -0
      mobile/lib/models/my_profile.dart
  8. +1
    -1
      mobile/lib/utils/storage/database.dart
  9. +47
    -40
      mobile/lib/views/authentication/login.dart
  10. +212
    -190
      mobile/lib/views/authentication/signup.dart
  11. +23
    -10
      mobile/lib/views/authentication/unauthenticated_landing.dart
  12. +104
    -36
      mobile/lib/views/main/conversation_detail.dart
  13. +7
    -29
      mobile/lib/views/main/conversation_list.dart
  14. +124
    -0
      mobile/lib/views/main/conversation_settings.dart
  15. +105
    -0
      mobile/lib/views/main/conversation_settings_user_list_item.dart
  16. +20
    -17
      mobile/lib/views/main/friend_list.dart
  17. +36
    -36
      mobile/lib/views/main/friend_list_item.dart
  18. +27
    -26
      mobile/lib/views/main/home.dart
  19. +67
    -58
      mobile/lib/views/main/profile.dart

+ 1
- 1
Backend/Database/Seeder/FriendSeeder.go View File

@ -60,7 +60,7 @@ func SeedFriends() {
panic(err) panic(err)
} }
secondaryUser, err = Database.GetUserByUsername("testUser2")
secondaryUser, err = Database.GetUserByUsername("ATestUser2")
if err != nil { if err != nil {
panic(err) panic(err)
} }


+ 2
- 2
Backend/Database/Seeder/MessageSeeder.go View File

@ -222,7 +222,7 @@ func SeedMessages() {
key, key,
) )
secondaryUser, err = Database.GetUserByUsername("testUser2")
secondaryUser, err = Database.GetUserByUsername("ATestUser2")
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -263,7 +263,7 @@ func SeedMessages() {
{ {
"id": "%s", "id": "%s",
"username": "%s", "username": "%s",
"admin": "true",
"admin": "false",
"association_key": "%s" "association_key": "%s"
} }
] ]


+ 1
- 1
Backend/Database/Seeder/UserSeeder.go View File

@ -88,7 +88,7 @@ func SeedUsers() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
_, err = createUser("testUser2")
_, err = createUser("ATestUser2")
if err != nil { if err != nil {
panic(err) panic(err)
} }


+ 52
- 12
mobile/lib/components/custom_circle_avatar.dart View File

@ -1,13 +1,23 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
enum AvatarTypes {
initials,
icon,
image,
}
class CustomCircleAvatar extends StatefulWidget { class CustomCircleAvatar extends StatefulWidget {
final String initials;
final String? initials;
final Icon? icon;
final String? imagePath; final String? imagePath;
final double radius;
const CustomCircleAvatar({ const CustomCircleAvatar({
Key? key, Key? key,
required this.initials,
this.initials,
this.icon,
this.imagePath, this.imagePath,
this.radius = 20,
}) : super(key: key); }) : super(key: key);
@override @override
@ -15,25 +25,55 @@ class CustomCircleAvatar extends StatefulWidget {
} }
class _CustomCircleAvatarState extends State<CustomCircleAvatar>{ class _CustomCircleAvatarState extends State<CustomCircleAvatar>{
bool _checkLoading = true;
AvatarTypes type = AvatarTypes.image;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (widget.imagePath != null) { if (widget.imagePath != null) {
_checkLoading = false;
type = AvatarTypes.image;
return;
} }
if (widget.icon != null) {
type = AvatarTypes.icon;
return;
}
if (widget.initials != null) {
type = AvatarTypes.initials;
return;
}
throw ArgumentError('Invalid arguments passed to CustomCircleAvatar');
}
Widget avatar() {
if (type == AvatarTypes.initials) {
return CircleAvatar(
backgroundColor: Colors.grey[300],
child: Text(widget.initials!),
radius: widget.radius,
);
}
if (type == AvatarTypes.icon) {
return CircleAvatar(
backgroundColor: Colors.grey[300],
child: widget.icon,
radius: widget.radius,
);
}
return CircleAvatar(
backgroundImage: AssetImage(widget.imagePath!),
radius: widget.radius,
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _checkLoading == true ?
CircleAvatar(
backgroundColor: Colors.grey[300],
child: Text(widget.initials)
) : CircleAvatar(
backgroundImage: AssetImage(widget.imagePath!)
);
return avatar();
} }
} }

+ 86
- 38
mobile/lib/main.dart View File

@ -6,49 +6,97 @@ import '/views/authentication/login.dart';
import '/views/authentication/signup.dart'; import '/views/authentication/signup.dart';
void main() async { void main() async {
await dotenv.load(fileName: ".env");
runApp(const MyApp());
await dotenv.load(fileName: ".env");
runApp(const MyApp());
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Envelope';
static const String _title = 'Envelope';
@override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
routes: {
'/home': (context) => const Home(),
'/landing': (context) => const UnauthenticatedLandingWidget(),
'/login': (context) => const Login(),
'/signup': (context) => const Signup(),
},
home: const Scaffold(
backgroundColor: Colors.cyan,
body: SafeArea(
child: Home(),
)
@override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
routes: {
'/home': (context) => const Home(),
'/landing': (context) => const UnauthenticatedLandingWidget(),
'/login': (context) => const Login(),
'/signup': (context) => const Signup(),
},
home: const Scaffold(
body: SafeArea(
child: Home(),
)
),
theme: ThemeData(
brightness: Brightness.light,
primaryColor: Colors.red,
appBarTheme: const AppBarTheme(
backgroundColor: Colors.cyan,
elevation: 0,
),
inputDecorationTheme: const InputDecorationTheme(
labelStyle: TextStyle(
color: Colors.white,
fontSize: 30,
), ),
theme: ThemeData(
appBarTheme: const AppBarTheme(
backgroundColor: Colors.cyan,
elevation: 0,
),
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
focusedBorder: OutlineInputBorder(),
labelStyle: TextStyle(
color: Colors.white,
fontSize: 30,
),
filled: true,
fillColor: Colors.white,
),
filled: false,
),
),
darkTheme: ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.orange.shade900,
backgroundColor: Colors.grey.shade900,
colorScheme: ColorScheme(
brightness: Brightness.dark,
primary: Colors.orange.shade900,
onPrimary: Colors.white,
secondary: Colors.blue.shade400,
onSecondary: Colors.white,
tertiary: Colors.grey.shade600,
onTertiary: Colors.black,
error: Colors.red,
onError: Colors.yellow,
background: Colors.grey.shade900,
onBackground: Colors.white,
surface: Colors.grey.shade700,
onSurface: Colors.white,
),
hintColor: Colors.grey.shade500,
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.grey.shade800,
hintStyle: TextStyle(
color: Colors.grey.shade500,
),
iconColor: Colors.grey.shade500,
contentPadding: const EdgeInsets.all(8),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(
color: Colors.transparent,
)
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(
color: Colors.transparent,
)
),
),
);
}
),
appBarTheme: AppBarTheme(
color: Colors.grey.shade800,
iconTheme: IconThemeData(
color: Colors.grey.shade400
),
toolbarTextStyle: TextStyle(
color: Colors.grey.shade400
),
),
),
);
}
} }

+ 7
- 6
mobile/lib/models/conversation_users.dart View File

@ -6,7 +6,7 @@ class ConversationUser{
String conversationId; String conversationId;
String username; String username;
String associationKey; String associationKey;
String admin;
bool admin;
ConversationUser({ ConversationUser({
required this.id, required this.id,
required this.conversationId, required this.conversationId,
@ -21,7 +21,7 @@ class ConversationUser{
conversationId: conversationId, conversationId: conversationId,
username: json['username'], username: json['username'],
associationKey: json['association_key'], associationKey: json['association_key'],
admin: json['admin'],
admin: json['admin'] == 'true',
); );
} }
@ -31,7 +31,7 @@ class ConversationUser{
'conversation_id': conversationId, 'conversation_id': conversationId,
'username': username, 'username': username,
'association_key': associationKey, 'association_key': associationKey,
'admin': admin,
'admin': admin ? 1 : 0,
}; };
} }
} }
@ -44,6 +44,7 @@ Future<List<ConversationUser>> getConversationUsers(Conversation conversation) a
'conversation_users', 'conversation_users',
where: 'conversation_id = ?', where: 'conversation_id = ?',
whereArgs: [conversation.id], whereArgs: [conversation.id],
orderBy: 'admin',
); );
return List.generate(maps.length, (i) { return List.generate(maps.length, (i) {
@ -52,7 +53,7 @@ Future<List<ConversationUser>> getConversationUsers(Conversation conversation) a
conversationId: maps[i]['conversation_id'], conversationId: maps[i]['conversation_id'],
username: maps[i]['username'], username: maps[i]['username'],
associationKey: maps[i]['association_key'], associationKey: maps[i]['association_key'],
admin: maps[i]['admin'],
admin: maps[i]['admin'] == 1,
); );
}); });
} }
@ -75,7 +76,7 @@ Future<ConversationUser> getConversationUserById(Conversation conversation, Stri
conversationId: maps[0]['conversation_id'], conversationId: maps[0]['conversation_id'],
username: maps[0]['username'], username: maps[0]['username'],
associationKey: maps[0]['association_key'], associationKey: maps[0]['association_key'],
admin: maps[0]['admin'],
admin: maps[0]['admin'] == 1,
); );
} }
@ -98,7 +99,7 @@ Future<ConversationUser> getConversationUserByUsername(Conversation conversation
conversationId: maps[0]['conversation_id'], conversationId: maps[0]['conversation_id'],
username: maps[0]['username'], username: maps[0]['username'],
associationKey: maps[0]['association_key'], associationKey: maps[0]['association_key'],
admin: maps[0]['admin'],
admin: maps[0]['admin'] == 1,
); );
} }

+ 94
- 0
mobile/lib/models/my_profile.dart View File

@ -0,0 +1,94 @@
import 'dart:convert';
import 'package:Envelope/utils/encryption/aes_helper.dart';
import 'package:Envelope/utils/encryption/crypto_utils.dart';
import 'package:pointycastle/impl.dart';
import 'package:shared_preferences/shared_preferences.dart';
class MyProfile {
String id;
String username;
RSAPrivateKey privateKey;
RSAPublicKey publicKey;
DateTime loggedInAt;
MyProfile({
required this.id,
required this.username,
required this.privateKey,
required this.publicKey,
required this.loggedInAt,
});
factory MyProfile._fromJson(Map<String, dynamic> json) {
DateTime loggedInAt = DateTime.now();
if (json.containsKey('logged_in_at')) {
loggedInAt = DateTime.parse(json['logged_in_at']);
}
return MyProfile(
id: json['user_id'],
username: json['username'],
privateKey: CryptoUtils.rsaPrivateKeyFromPem(json['asymmetric_private_key']),
publicKey: CryptoUtils.rsaPublicKeyFromPem(json['asymmetric_public_key']),
loggedInAt: loggedInAt,
);
}
@override
String toString() {
return '''
user_id: $id
username: $username
logged_in_at: $loggedInAt
public_key: $publicKey
private_key: $privateKey
''';
}
String toJson() {
return jsonEncode(<String, dynamic>{
'user_id': id,
'username': username,
'asymmetric_private_key': CryptoUtils.encodeRSAPrivateKeyToPem(privateKey),
'asymmetric_public_key': CryptoUtils.encodeRSAPublicKeyToPem(publicKey),
'logged_in_at': loggedInAt.toIso8601String(),
});
}
static Future<MyProfile> login(Map<String, dynamic> json, String password) async {
json['asymmetric_private_key'] = AesHelper.aesDecrypt(
password,
base64.decode(json['asymmetric_private_key'])
);
MyProfile profile = MyProfile._fromJson(json);
final preferences = await SharedPreferences.getInstance();
print(profile.toJson());
preferences.setString('profile', profile.toJson());
return profile;
}
static Future<void> logout() async {
final preferences = await SharedPreferences.getInstance();
preferences.remove('profile');
}
static Future<MyProfile> getProfile() async {
final preferences = await SharedPreferences.getInstance();
String? profileJson = preferences.getString('profile');
if (profileJson == null) {
throw Exception('No profile');
}
return MyProfile._fromJson(json.decode(profileJson));
}
static Future<bool> isLoggedIn() async {
MyProfile profile = await MyProfile.getProfile();
return profile.loggedInAt.isBefore((DateTime.now()).add(const Duration(hours: 12)));
}
Future<RSAPrivateKey> getPrivateKey() async {
MyProfile profile = await MyProfile.getProfile();
return profile.privateKey;
}
}

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

@ -51,7 +51,7 @@ Future<Database> getDatabaseConnection() async {
username TEXT, username TEXT,
data TEXT, data TEXT,
association_key TEXT, association_key TEXT,
admin TEXT
admin INTEGER
); );
'''); ''');


+ 47
- 40
mobile/lib/views/authentication/login.dart View File

@ -1,11 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import '/utils/encryption/crypto_utils.dart';
import '/utils/encryption/aes_helper.dart';
import '/utils/storage/encryption_keys.dart';
import '/models/my_profile.dart';
import '/utils/storage/session_cookie.dart'; import '/utils/storage/session_cookie.dart';
class LoginResponse { class LoginResponse {
@ -37,7 +34,7 @@ class LoginResponse {
} }
} }
Future<LoginResponse> login(context, String username, String password) async {
Future<dynamic> login(context, String username, String password) async {
final resp = await http.post( final resp = await http.post(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/login'), Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/login'),
headers: <String, String>{ headers: <String, String>{
@ -59,20 +56,7 @@ Future<LoginResponse> login(context, String username, String password) async {
setSessionCookie((index == -1) ? rawCookie : rawCookie.substring(0, index)); setSessionCookie((index == -1) ? rawCookie : rawCookie.substring(0, index));
} }
LoginResponse response = LoginResponse.fromJson(jsonDecode(resp.body));
var rsaPrivPem = AesHelper.aesDecrypt(password, base64.decode(response.asymmetricPrivateKey));
var rsaPriv = CryptoUtils.rsaPrivateKeyFromPem(rsaPrivPem);
setPrivateKey(rsaPriv);
final preferences = await SharedPreferences.getInstance();
preferences.setString('logged_in_at', (DateTime.now()).toIso8601String());
preferences.setString('userId', response.userId);
preferences.setString('username', response.username);
preferences.setString('asymmetricPublicKey', response.asymmetricPublicKey);
return response;
return await MyProfile.login(json.decode(resp.body), password);
} }
class Login extends StatelessWidget { class Login extends StatelessWidget {
@ -81,17 +65,16 @@ class Login extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Colors.cyan,
appBar: AppBar( appBar: AppBar(
title: null, title: null,
automaticallyImplyLeading: true, automaticallyImplyLeading: true,
//`true` if you want Flutter to automatically add Back Button when needed,
//or `false` if you want to force your own back button every where
leading: IconButton(icon: const Icon(Icons.arrow_back), leading: IconButton(icon: const Icon(Icons.arrow_back),
onPressed:() => { onPressed:() => {
Navigator.pop(context) Navigator.pop(context)
} }
)
),
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
), ),
body: const SafeArea( body: const SafeArea(
child: LoginWidget(), child: LoginWidget(),
@ -115,17 +98,26 @@ class _LoginWidgetState extends State<LoginWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const TextStyle _inputTextStyle = TextStyle(fontSize: 18, color: Colors.black);
const TextStyle inputTextStyle = TextStyle(
fontSize: 18,
);
final ButtonStyle _buttonStyle = ElevatedButton.styleFrom(
primary: Colors.white,
onPrimary: Colors.cyan,
final OutlineInputBorder inputBorderStyle = OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(
color: Colors.transparent,
)
);
final ButtonStyle buttonStyle = ElevatedButton.styleFrom(
primary: Theme.of(context).colorScheme.surface,
onPrimary: Theme.of(context).colorScheme.onSurface,
minimumSize: const Size.fromHeight(50), minimumSize: const Size.fromHeight(50),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
textStyle: const TextStyle(
textStyle: TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.red,
color: Theme.of(context).colorScheme.error,
), ),
); );
@ -134,37 +126,52 @@ class _LoginWidgetState extends State<LoginWidget> {
key: _formKey, key: _formKey,
child: Center( child: Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(15),
padding: const EdgeInsets.only(
left: 20,
right: 20,
top: 0,
bottom: 80,
),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
const Text('Login', style: TextStyle(fontSize: 35, color: Colors.white),),
Text(
'Login',
style: TextStyle(
fontSize: 35,
color: Theme.of(context).colorScheme.onBackground,
),
),
const SizedBox(height: 30), const SizedBox(height: 30),
TextFormField( TextFormField(
controller: usernameController, controller: usernameController,
decoration: const InputDecoration(
decoration: InputDecoration(
hintText: 'Username', hintText: 'Username',
enabledBorder: inputBorderStyle,
focusedBorder: inputBorderStyle,
), ),
style: _inputTextStyle,
style: inputTextStyle,
// The validator receives the text that the user has entered. // The validator receives the text that the user has entered.
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
return 'Create a username';
return 'Enter a username';
} }
return null; return null;
}, },
), ),
const SizedBox(height: 5),
const SizedBox(height: 10),
TextFormField( TextFormField(
controller: passwordController, controller: passwordController,
obscureText: true, obscureText: true,
enableSuggestions: false, enableSuggestions: false,
autocorrect: false, autocorrect: false,
decoration: const InputDecoration(
decoration: InputDecoration(
hintText: 'Password', hintText: 'Password',
enabledBorder: inputBorderStyle,
focusedBorder: inputBorderStyle,
), ),
style: _inputTextStyle,
style: inputTextStyle,
// The validator receives the text that the user has entered. // The validator receives the text that the user has entered.
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
@ -173,9 +180,9 @@ class _LoginWidgetState extends State<LoginWidget> {
return null; return null;
}, },
), ),
const SizedBox(height: 5),
const SizedBox(height: 15),
ElevatedButton( ElevatedButton(
style: _buttonStyle,
style: buttonStyle,
onPressed: () { onPressed: () {
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -186,7 +193,7 @@ class _LoginWidgetState extends State<LoginWidget> {
context, context,
usernameController.text, usernameController.text,
passwordController.text, passwordController.text,
).then((value) {
).then((val) {
Navigator. Navigator.
pushNamedAndRemoveUntil( pushNamedAndRemoveUntil(
context, context,


+ 212
- 190
mobile/lib/views/authentication/signup.dart View File

@ -2,211 +2,233 @@ import 'dart:typed_data';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '/utils/encryption/aes_helper.dart'; import '/utils/encryption/aes_helper.dart';
import '/utils/storage/encryption_keys.dart';
import '/utils/encryption/crypto_utils.dart'; import '/utils/encryption/crypto_utils.dart';
class SignupResponse { class SignupResponse {
final String status;
final String message;
const SignupResponse({
required this.status,
required this.message,
});
factory SignupResponse.fromJson(Map<String, dynamic> json) {
return SignupResponse(
status: json['status'],
message: json['message'],
);
}
final String status;
final String message;
const SignupResponse({
required this.status,
required this.message,
});
factory SignupResponse.fromJson(Map<String, dynamic> json) {
return SignupResponse(
status: json['status'],
message: json['message'],
);
}
} }
Future<SignupResponse> signUp(context, String username, String password, String confirmPassword) async { Future<SignupResponse> signUp(context, String username, String password, String confirmPassword) async {
var keyPair = CryptoUtils.generateRSAKeyPair();
var rsaPubPem = CryptoUtils.encodeRSAPublicKeyToPem(keyPair.publicKey);
var rsaPrivPem = CryptoUtils.encodeRSAPrivateKeyToPem(keyPair.privateKey);
var encRsaPriv = AesHelper.aesEncrypt(password, Uint8List.fromList(rsaPrivPem.codeUnits));
// TODO: Check for timeout here
final resp = await http.post(
Uri.parse('http://192.168.1.5:8080/api/v1/signup'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{
'username': username,
'password': password,
'confirm_password': confirmPassword,
'asymmetric_public_key': rsaPubPem,
'asymmetric_private_key': encRsaPriv,
}),
);
SignupResponse response = SignupResponse.fromJson(jsonDecode(resp.body));
if (resp.statusCode != 201) {
throw Exception(response.message);
}
return response;
var keyPair = CryptoUtils.generateRSAKeyPair();
var rsaPubPem = CryptoUtils.encodeRSAPublicKeyToPem(keyPair.publicKey);
var rsaPrivPem = CryptoUtils.encodeRSAPrivateKeyToPem(keyPair.privateKey);
var encRsaPriv = AesHelper.aesEncrypt(password, Uint8List.fromList(rsaPrivPem.codeUnits));
// TODO: Check for timeout here
final resp = await http.post(
Uri.parse('http://192.168.1.5:8080/api/v1/signup'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{
'username': username,
'password': password,
'confirm_password': confirmPassword,
'asymmetric_public_key': rsaPubPem,
'asymmetric_private_key': encRsaPriv,
}),
);
SignupResponse response = SignupResponse.fromJson(jsonDecode(resp.body));
if (resp.statusCode != 201) {
throw Exception(response.message);
}
return response;
} }
class Signup extends StatelessWidget { class Signup extends StatelessWidget {
const Signup({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.cyan,
appBar: AppBar(
title: null,
automaticallyImplyLeading: true,
//`true` if you want Flutter to automatically add Back Button when needed,
//or `false` if you want to force your own back button every where
leading: IconButton(icon: const Icon(Icons.arrow_back),
//onPressed:() => Navigator.pop(context, false),
onPressed:() => {
Navigator.pop(context)
}
)
),
body: const SafeArea(
child: SignupWidget(),
)
);
}
const Signup({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: null,
automaticallyImplyLeading: true,
//`true` if you want Flutter to automatically add Back Button when needed,
//or `false` if you want to force your own back button every where
leading: IconButton(icon: const Icon(Icons.arrow_back),
onPressed:() => {
Navigator.pop(context)
}
),
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
body: const SafeArea(
child: SignupWidget(),
)
);
}
} }
class SignupWidget extends StatefulWidget { class SignupWidget extends StatefulWidget {
const SignupWidget({Key? key}) : super(key: key);
const SignupWidget({Key? key}) : super(key: key);
@override
State<SignupWidget> createState() => _SignupWidgetState();
@override
State<SignupWidget> createState() => _SignupWidgetState();
} }
class _SignupWidgetState extends State<SignupWidget> { class _SignupWidgetState extends State<SignupWidget> {
final _formKey = GlobalKey<FormState>();
TextEditingController usernameController = TextEditingController();
TextEditingController passwordController = TextEditingController();
TextEditingController passwordConfirmController = TextEditingController();
@override
Widget build(BuildContext context) {
const TextStyle _inputTextStyle = TextStyle(fontSize: 18, color: Colors.black);
final ButtonStyle _buttonStyle = ElevatedButton.styleFrom(
primary: Colors.white,
onPrimary: Colors.cyan,
minimumSize: const Size.fromHeight(50),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
textStyle: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.red,
),
);
return Center(
child: Form(
key: _formKey,
child: Center(
child: Padding(
padding: const EdgeInsets.all(15),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text('Sign Up', style: TextStyle(fontSize: 35, color: Colors.white),),
const SizedBox(height: 30),
TextFormField(
controller: usernameController,
decoration: const InputDecoration(
hintText: 'Username',
),
style: _inputTextStyle,
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return 'Create a username';
}
return null;
},
),
const SizedBox(height: 5),
TextFormField(
controller: passwordController,
obscureText: true,
enableSuggestions: false,
autocorrect: false,
decoration: const InputDecoration(
hintText: 'Password',
),
style: _inputTextStyle,
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return 'Enter a password';
}
return null;
},
),
const SizedBox(height: 5),
TextFormField(
controller: passwordConfirmController,
obscureText: true,
enableSuggestions: false,
autocorrect: false,
decoration: const InputDecoration(
hintText: 'Password',
),
style: _inputTextStyle,
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return 'Confirm your password';
}
if (value != passwordController.text) {
return 'Passwords do not match';
}
return null;
},
),
const SizedBox(height: 5),
ElevatedButton(
style: _buttonStyle,
onPressed: () {
if (_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Processing Data')),
);
signUp(
context,
usernameController.text,
passwordController.text,
passwordConfirmController.text
).then((value) {
Navigator.of(context).popUntil((route) => route.isFirst);
}).catchError((error) {
print(error); // TODO: Show error on interface
});
}
},
child: const Text('Submit'),
),
],
)
)
)
)
);
}
final _formKey = GlobalKey<FormState>();
TextEditingController usernameController = TextEditingController();
TextEditingController passwordController = TextEditingController();
TextEditingController passwordConfirmController = TextEditingController();
@override
Widget build(BuildContext context) {
const TextStyle inputTextStyle = TextStyle(
fontSize: 18,
);
final OutlineInputBorder inputBorderStyle = OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(
color: Colors.transparent,
)
);
final ButtonStyle buttonStyle = ElevatedButton.styleFrom(
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),
textStyle: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.error,
),
);
return Center(
child: Form(
key: _formKey,
child: Center(
child: Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
top: 0,
bottom: 100,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Sign Up',
style: TextStyle(
fontSize: 35,
color: Theme.of(context).colorScheme.onBackground,
),
),
const SizedBox(height: 30),
TextFormField(
controller: usernameController,
decoration: InputDecoration(
hintText: 'Username',
enabledBorder: inputBorderStyle,
focusedBorder: inputBorderStyle,
),
style: inputTextStyle,
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return 'Create a username';
}
return null;
},
),
const SizedBox(height: 10),
TextFormField(
controller: passwordController,
obscureText: true,
enableSuggestions: false,
autocorrect: false,
decoration: InputDecoration(
hintText: 'Password',
enabledBorder: inputBorderStyle,
focusedBorder: inputBorderStyle,
),
style: inputTextStyle,
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return 'Enter a password';
}
return null;
},
),
const SizedBox(height: 10),
TextFormField(
controller: passwordConfirmController,
obscureText: true,
enableSuggestions: false,
autocorrect: false,
decoration: InputDecoration(
hintText: 'Password',
enabledBorder: inputBorderStyle,
focusedBorder: inputBorderStyle,
),
style: inputTextStyle,
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return 'Confirm your password';
}
if (value != passwordController.text) {
return 'Passwords do not match';
}
return null;
},
),
const SizedBox(height: 15),
ElevatedButton(
style: buttonStyle,
onPressed: () {
if (_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Processing Data')),
);
signUp(
context,
usernameController.text,
passwordController.text,
passwordConfirmController.text
).then((value) {
Navigator.of(context).popUntil((route) => route.isFirst);
}).catchError((error) {
print(error); // TODO: Show error on interface
});
}
},
child: const Text('Submit'),
),
],
)
)
)
)
);
}
} }

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

@ -15,21 +15,19 @@ class _UnauthenticatedLandingWidgetState extends State<UnauthenticatedLandingWid
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ButtonStyle buttonStyle = ElevatedButton.styleFrom( final ButtonStyle buttonStyle = ElevatedButton.styleFrom(
primary: Colors.white,
onPrimary: Colors.cyan,
primary: Theme.of(context).colorScheme.surface,
onPrimary: Theme.of(context).colorScheme.onSurface,
minimumSize: const Size.fromHeight(50), minimumSize: const Size.fromHeight(50),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
textStyle: const TextStyle( textStyle: const TextStyle(
fontSize: 20,
fontSize: 22,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.red,
), ),
); );
return WillPopScope( return WillPopScope(
onWillPop: () async => false, onWillPop: () async => false,
child: Scaffold( child: Scaffold(
backgroundColor: Colors.cyan,
body: SafeArea( body: SafeArea(
child: Center( child: Center(
child: Column( child: Column(
@ -40,16 +38,31 @@ class _UnauthenticatedLandingWidgetState extends State<UnauthenticatedLandingWid
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: const [
FaIcon(FontAwesomeIcons.envelope, color: Colors.white, size: 40),
SizedBox(width: 15),
Text('Envelope', style: TextStyle(fontSize: 40, color: Colors.white),)
children: [
FaIcon(
FontAwesomeIcons.envelope,
color: Theme.of(context).colorScheme.onBackground,
size: 40
),
const SizedBox(width: 15),
Text(
'Envelope',
style: TextStyle(
fontSize: 40,
color: Theme.of(context).colorScheme.onBackground,
),
)
] ]
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Padding( Padding(
padding: const EdgeInsets.all(15),
padding: const EdgeInsets.only(
top: 15,
bottom: 15,
left: 20,
right: 20,
),
child: Column ( child: Column (
children: [ children: [
ElevatedButton( ElevatedButton(


+ 104
- 36
mobile/lib/views/main/conversation_detail.dart View File

@ -1,3 +1,4 @@
import 'package:Envelope/views/main/conversation_settings.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '/models/conversations.dart'; import '/models/conversations.dart';
@ -56,7 +57,13 @@ class _ConversationDetailState extends State<ConversationDetail> {
Widget usernameOrFailedToSend(int index) { Widget usernameOrFailedToSend(int index) {
if (messages[index].senderUsername != username) { if (messages[index].senderUsername != username) {
return Text(messages[index].senderUsername);
return Text(
messages[index].senderUsername,
style: TextStyle(
fontSize: 12,
color: Colors.grey[300],
),
);
} }
if (messages[index].failedToSend) { if (messages[index].failedToSend) {
@ -86,7 +93,6 @@ class _ConversationDetailState extends State<ConversationDetail> {
appBar: AppBar( appBar: AppBar(
elevation: 0, elevation: 0,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
backgroundColor: Colors.white,
flexibleSpace: SafeArea( flexibleSpace: SafeArea(
child: Container( child: Container(
padding: const EdgeInsets.only(right: 16), padding: const EdgeInsets.only(right: 16),
@ -96,7 +102,10 @@ class _ConversationDetailState extends State<ConversationDetail> {
onPressed: (){ onPressed: (){
Navigator.pop(context); Navigator.pop(context);
}, },
icon: const Icon(Icons.arrow_back,color: Colors.black,),
icon: Icon(
Icons.arrow_back,
color: Theme.of(context).appBarTheme.iconTheme?.color,
),
), ),
const SizedBox(width: 2,), const SizedBox(width: 2,),
Expanded( Expanded(
@ -106,14 +115,26 @@ class _ConversationDetailState extends State<ConversationDetail> {
children: <Widget>[ children: <Widget>[
Text( Text(
widget.conversation.name, widget.conversation.name,
style: const TextStyle(
style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600),
fontWeight: FontWeight.w600,
color: Theme.of(context).appBarTheme.toolbarTextStyle?.color
),
), ),
], ],
), ),
), ),
const Icon(Icons.settings,color: Colors.black54),
IconButton(
onPressed: (){
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ConversationSettings(conversation: widget.conversation)),
);
},
icon: Icon(
Icons.settings,
color: Theme.of(context).appBarTheme.iconTheme?.color,
),
),
], ],
), ),
), ),
@ -128,7 +149,7 @@ class _ConversationDetailState extends State<ConversationDetail> {
reverse: true, reverse: true,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return Container( return Container(
padding: const EdgeInsets.only(left: 14,right: 14,top: 0,bottom: 10),
padding: const EdgeInsets.only(left: 14,right: 14,top: 0,bottom: 0),
child: Align( child: Align(
alignment: ( alignment: (
messages[index].senderUsername == username ? messages[index].senderUsername == username ?
@ -145,23 +166,54 @@ class _ConversationDetailState extends State<ConversationDetail> {
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
color: ( color: (
messages[index].senderUsername == username ? messages[index].senderUsername == username ?
Colors.blue[200] :
Colors.grey.shade200
Theme.of(context).colorScheme.primary :
Theme.of(context).colorScheme.tertiary
), ),
), ),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Text(messages[index].data, style: const TextStyle(fontSize: 15)),
child: Text(
messages[index].data,
style: TextStyle(
fontSize: 15,
color: messages[index].senderUsername == username ?
Theme.of(context).colorScheme.onPrimary :
Theme.of(context).colorScheme.onTertiary,
)
),
), ),
usernameOrFailedToSend(index),
Text(
convertToAgo(messages[index].createdAt),
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
const SizedBox(height: 1.5),
Row(
mainAxisAlignment: messages[index].senderUsername == username ?
MainAxisAlignment.end :
MainAxisAlignment.start,
children: <Widget>[
const SizedBox(width: 10),
usernameOrFailedToSend(index),
],
),
const SizedBox(height: 1.5),
Row(
mainAxisAlignment: messages[index].senderUsername == username ?
MainAxisAlignment.end :
MainAxisAlignment.start,
children: <Widget>[
const SizedBox(width: 10),
Text(
convertToAgo(messages[index].createdAt),
textAlign: messages[index].senderUsername == username ?
TextAlign.left :
TextAlign.right,
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
),
),
],
), ),
),
]
index != 0 ?
const SizedBox(height: 20) :
const SizedBox.shrink(),
],
) )
), ),
); );
@ -177,7 +229,7 @@ class _ConversationDetailState extends State<ConversationDetail> {
padding: const EdgeInsets.only(left: 10,bottom: 10,top: 10), padding: const EdgeInsets.only(left: 10,bottom: 10,top: 10),
// height: 60, // height: 60,
width: double.infinity, width: double.infinity,
color: Colors.white,
color: Theme.of(context).backgroundColor,
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
GestureDetector( GestureDetector(
@ -187,18 +239,24 @@ class _ConversationDetailState extends State<ConversationDetail> {
height: 30, height: 30,
width: 30, width: 30,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.lightBlue,
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(30), borderRadius: BorderRadius.circular(30),
), ),
child: const Icon(Icons.add, color: Colors.white, size: 20, ),
child: Icon(
Icons.add,
color: Theme.of(context).colorScheme.onPrimary,
size: 20
),
), ),
), ),
const SizedBox(width: 15,), const SizedBox(width: 15,),
Expanded( Expanded(
child: TextField( child: TextField(
decoration: const InputDecoration(
decoration: InputDecoration(
hintText: "Write message...", hintText: "Write message...",
hintStyle: TextStyle(color: Colors.black54),
hintStyle: TextStyle(
color: Theme.of(context).hintColor,
),
border: InputBorder.none, border: InputBorder.none,
), ),
maxLines: null, maxLines: null,
@ -206,18 +264,28 @@ class _ConversationDetailState extends State<ConversationDetail> {
), ),
), ),
const SizedBox(width: 15), const SizedBox(width: 15),
FloatingActionButton(
onPressed: () async {
if (msgController.text == '') {
return;
}
await sendMessage(widget.conversation, msgController.text);
messages = await getMessagesForThread(widget.conversation);
setState(() {});
msgController.text = '';
},
child: const Icon(Icons.send,color: Colors.white,size: 18,),
backgroundColor: Colors.blue,
Container(
width: 45,
height: 45,
child: FittedBox(
child: FloatingActionButton(
onPressed: () async {
if (msgController.text == '') {
return;
}
await sendMessage(widget.conversation, msgController.text);
messages = await getMessagesForThread(widget.conversation);
setState(() {});
msgController.text = '';
},
child: Icon(
Icons.send,
color: Theme.of(context).colorScheme.onPrimary,
size: 22
),
backgroundColor: Theme.of(context).primaryColor,
),
),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
], ],


+ 7
- 29
mobile/lib/views/main/conversation_list.dart View File

@ -80,23 +80,8 @@ class _ConversationListState extends State<ConversationList> {
padding: const EdgeInsets.only(left: 16,right: 16,top: 10), padding: const EdgeInsets.only(left: 16,right: 16,top: 10),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
const Text("Conversations",style: TextStyle(fontSize: 32,fontWeight: FontWeight.bold),),
Container(
padding: const EdgeInsets.only(left: 8,right: 8,top: 2,bottom: 2),
height: 30,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: Colors.pink[50],
),
child: Row(
children: const <Widget>[
Icon(Icons.add,color: Colors.pink,size: 20,),
SizedBox(width: 2,),
Text("Add",style: TextStyle(fontSize: 14,fontWeight: FontWeight.bold),),
],
),
)
children: const <Widget>[
Text("Conversations",style: TextStyle(fontSize: 32,fontWeight: FontWeight.bold),),
], ],
), ),
), ),
@ -104,20 +89,13 @@ class _ConversationListState extends State<ConversationList> {
Padding( Padding(
padding: const EdgeInsets.only(top: 16,left: 16,right: 16), padding: const EdgeInsets.only(top: 16,left: 16,right: 16),
child: TextField( child: TextField(
decoration: InputDecoration(
decoration: const InputDecoration(
hintText: "Search...", hintText: "Search...",
hintStyle: TextStyle(color: Colors.grey.shade600),
prefixIcon: Icon(Icons.search,color: Colors.grey.shade600, size: 20,),
filled: true,
fillColor: Colors.grey.shade100,
contentPadding: const EdgeInsets.all(8),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide(
color: Colors.grey.shade100
)
prefixIcon: Icon(
Icons.search,
size: 20
), ),
),
),
onChanged: (value) => filterSearchResults(value.toLowerCase()) onChanged: (value) => filterSearchResults(value.toLowerCase())
), ),
), ),


+ 124
- 0
mobile/lib/views/main/conversation_settings.dart View File

@ -0,0 +1,124 @@
import 'package:Envelope/models/conversation_users.dart';
import 'package:flutter/material.dart';
import '/views/main/conversation_settings_user_list_item.dart';
import '/models/conversations.dart';
import 'package:Envelope/components/custom_circle_avatar.dart';
class ConversationSettings extends StatefulWidget {
final Conversation conversation;
const ConversationSettings({
Key? key,
required this.conversation,
}) : super(key: key);
@override
State<ConversationSettings> createState() => _ConversationSettingsState();
}
class _ConversationSettingsState extends State<ConversationSettings> {
final _formKey = GlobalKey<FormState>();
List<ConversationUser> users = [];
TextEditingController nameController = TextEditingController();
@override
void initState() {
nameController.text = widget.conversation.name;
super.initState();
getUsers();
}
Future<void> getUsers() async {
users = await getConversationUsers(widget.conversation);
setState(() {});
}
Widget conversationName() {
return Row(
children: <Widget> [
const CustomCircleAvatar(
icon: Icon(Icons.people, size: 40),
imagePath: null, // TODO: Add image here
radius: 30,
),
const SizedBox(width: 10),
Text(
widget.conversation.name,
style: const TextStyle(
fontSize: 25,
fontWeight: FontWeight.w500,
),
),
],
);
}
Widget usersList() {
return ListView.builder(
itemCount: users.length,
shrinkWrap: true,
padding: const EdgeInsets.only(top: 16),
itemBuilder: (context, i) {
return ConversationSettingsUserListItem(
user: users[i],
isAdmin: widget.conversation.admin,
);
}
);
}
@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(
widget.conversation.name + " Settings",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600
),
),
],
),
),
],
),
),
),
),
body: Padding(
padding: const EdgeInsets.all(15),
child: Column(
children: <Widget> [
const SizedBox(height: 30),
conversationName(),
const SizedBox(height: 10),
const Text('Users', style: TextStyle(fontSize: 20)),
usersList(),
],
),
),
);
}
}

+ 105
- 0
mobile/lib/views/main/conversation_settings_user_list_item.dart View File

@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:Envelope/models/conversation_users.dart';
import 'package:Envelope/components/custom_circle_avatar.dart';
class ConversationSettingsUserListItem extends StatefulWidget{
final ConversationUser user;
final bool isAdmin;
const ConversationSettingsUserListItem({
Key? key,
required this.user,
required this.isAdmin,
}) : super(key: key);
@override
_ConversationSettingsUserListItemState createState() => _ConversationSettingsUserListItemState();
}
class _ConversationSettingsUserListItemState extends State<ConversationSettingsUserListItem> {
Widget admin() {
if (widget.user.admin) {
return const SizedBox.shrink();
}
return Row(
children: const <Widget> [
SizedBox(width: 5),
Icon(Icons.circle, size: 6),
SizedBox(width: 5),
Text(
'Admin',
style: TextStyle(
fontSize: 14,
),
),
]
);
}
Widget adminUserActions() {
if (!widget.isAdmin) {
return const SizedBox.shrink();
}
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget> [
IconButton(icon: const Icon(Icons.cancel),
padding: const EdgeInsets.all(0),
onPressed:() => {
print('Cancel')
}
),
IconButton(icon: const Icon(Icons.admin_panel_settings),
padding: const EdgeInsets.all(0),
onPressed:() => {
print('Admin')
}
)
],
);
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.only(left: 16,right: 16,top: 10,bottom: 10),
child: Row(
children: <Widget>[
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
CustomCircleAvatar(
initials: widget.user.username[0].toUpperCase(),
imagePath: null, // TODO: Add image here
radius: 15,
),
const SizedBox(width: 16),
Expanded(
child: Align(
alignment: Alignment.centerLeft,
child: Container(
color: Colors.transparent,
child: Row(
children: <Widget>[
Text(
widget.user.username,
style: const TextStyle(fontSize: 16)
),
admin(),
],
),
),
),
),
],
),
),
adminUserActions(),
],
),
);
}
}

+ 20
- 17
mobile/lib/views/main/friend_list.dart View File

@ -88,14 +88,24 @@ class _FriendListState extends State<FriendList> {
padding: const EdgeInsets.only(left: 8,right: 8,top: 2,bottom: 2), padding: const EdgeInsets.only(left: 8,right: 8,top: 2,bottom: 2),
height: 30, height: 30,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: Colors.pink[50],
borderRadius: BorderRadius.circular(20),
color: Theme.of(context).colorScheme.tertiary
), ),
child: Row( child: Row(
children: const <Widget>[
Icon(Icons.add,color: Colors.pink,size: 20,),
SizedBox(width: 2,),
Text("Add",style: TextStyle(fontSize: 14,fontWeight: FontWeight.bold),),
children: <Widget>[
Icon(
Icons.add,
color: Theme.of(context).primaryColor,
size: 20
),
const SizedBox(width: 2,),
const Text(
"Add",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold
)
),
], ],
), ),
) )
@ -106,18 +116,11 @@ class _FriendListState extends State<FriendList> {
Padding( Padding(
padding: const EdgeInsets.only(top: 16,left: 16,right: 16), padding: const EdgeInsets.only(top: 16,left: 16,right: 16),
child: TextField( child: TextField(
decoration: InputDecoration(
decoration: const InputDecoration(
hintText: "Search...", hintText: "Search...",
hintStyle: TextStyle(color: Colors.grey.shade600),
prefixIcon: Icon(Icons.search,color: Colors.grey.shade600, size: 20,),
filled: true,
fillColor: Colors.grey.shade100,
contentPadding: const EdgeInsets.all(8),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide(
color: Colors.grey.shade100
)
prefixIcon: Icon(
Icons.search,
size: 20
), ),
), ),
onChanged: (value) => filterSearchResults(value.toLowerCase()) onChanged: (value) => filterSearchResults(value.toLowerCase())


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

@ -24,44 +24,44 @@ class _FriendListItemState extends State<FriendListItem> {
onTap: (){ onTap: (){
}, },
child: Container( 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
// ),
// ),
],
),
),
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
// ),
// ),
],
), ),
), ),
],
),
),
],
),
),
],
),
), ),
),
);
],
),
),
);
} }
} }

+ 27
- 26
mobile/lib/views/main/home.dart View File

@ -11,6 +11,7 @@ import '/utils/storage/messages.dart';
import '/utils/storage/session_cookie.dart'; import '/utils/storage/session_cookie.dart';
import '/models/conversations.dart'; import '/models/conversations.dart';
import '/models/friends.dart'; import '/models/friends.dart';
import '/models/my_profile.dart';
class Home extends StatefulWidget { class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key); const Home({Key? key}) : super(key: key);
@ -33,12 +34,14 @@ class _HomeState extends State<Home> {
@override @override
void initState() { void initState() {
super.initState();
updateData(); updateData();
super.initState();
} }
void updateData() async { void updateData() async {
await checkLogin();
if (!await checkLogin()) {
return;
}
await updateFriends(); await updateFriends();
await updateConversations(); await updateConversations();
await updateMessageThreads(); await updateMessageThreads();
@ -56,23 +59,23 @@ class _HomeState extends State<Home> {
}); });
} }
Future<void> checkLogin() async {
SharedPreferences preferences = await SharedPreferences.getInstance();
Future<bool> checkLogin() async {
bool isLoggedIn = false;
var loggedInTime = preferences.getString('logged_in_at');
if (loggedInTime == null) {
preferences.remove('logged_in_at');
preferences.remove('username');
preferences.remove('userId');
preferences.remove('asymmetricPublicKey');
try {
isLoggedIn = await MyProfile.isLoggedIn();
} catch (Exception) {
Navigator.pushNamedAndRemoveUntil(context, '/landing', ModalRoute.withName('/landing')); Navigator.pushNamedAndRemoveUntil(context, '/landing', ModalRoute.withName('/landing'));
return;
return false;
} }
DateTime loggedInAt = DateTime.parse(loggedInTime);
bool isAfter = loggedInAt.isAfter((DateTime.now()).add(const Duration(hours: 12)));
int statusCode = 200;
if (!isLoggedIn) {
await MyProfile.logout();
Navigator.pushNamedAndRemoveUntil(context, '/landing', ModalRoute.withName('/landing'));
return false;
}
int statusCode = 200;
try { try {
var resp = await http.get( var resp = await http.get(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/check'), Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/check'),
@ -82,20 +85,18 @@ class _HomeState extends State<Home> {
); );
statusCode = resp.statusCode; statusCode = resp.statusCode;
} catch(SocketException) { } catch(SocketException) {
if (!isAfter) {
return;
if (await MyProfile.isLoggedIn()) {
return true;
} }
} }
if (!isAfter && statusCode == 200) {
return;
if (isLoggedIn && statusCode == 200) {
return true;
} }
preferences.remove('logged_in_at');
preferences.remove('username');
preferences.remove('userId');
preferences.remove('asymmetricPublicKey');
MyProfile.logout();
Navigator.pushNamedAndRemoveUntil(context, '/landing', ModalRoute.withName('/landing')); Navigator.pushNamedAndRemoveUntil(context, '/landing', ModalRoute.withName('/landing'));
return false;
} }
void _onItemTapped(int index) { void _onItemTapped(int index) {
@ -130,13 +131,13 @@ class _HomeState extends State<Home> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return WillPopScope( return WillPopScope(
onWillPop: () async => false, onWillPop: () async => false,
child: Scaffold(
body: isLoading ? loading() : _widgetOptions.elementAt(_selectedIndex),
child: isLoading ? loading() : Scaffold(
body: _widgetOptions.elementAt(_selectedIndex),
bottomNavigationBar: isLoading ? const SizedBox.shrink() : BottomNavigationBar( bottomNavigationBar: isLoading ? const SizedBox.shrink() : BottomNavigationBar(
currentIndex: _selectedIndex, currentIndex: _selectedIndex,
onTap: _onItemTapped, onTap: _onItemTapped,
selectedItemColor: Colors.red,
unselectedItemColor: Colors.grey.shade600,
selectedItemColor: Theme.of(context).primaryColor,
unselectedItemColor: Theme.of(context).hintColor,
selectedLabelStyle: const TextStyle(fontWeight: FontWeight.w600), selectedLabelStyle: const TextStyle(fontWeight: FontWeight.w600),
unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.w600), unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.w600),
type: BottomNavigationBarType.fixed, type: BottomNavigationBarType.fixed,


+ 67
- 58
mobile/lib/views/main/profile.dart View File

@ -1,68 +1,77 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '/utils/storage/encryption_keys.dart';
import '/utils/storage/database.dart'; import '/utils/storage/database.dart';
import '/models/my_profile.dart';
class Profile extends StatefulWidget { class Profile extends StatefulWidget {
const Profile({Key? key}) : super(key: key);
const Profile({Key? key}) : super(key: key);
@override
State<Profile> createState() => _ProfileState();
@override
State<Profile> createState() => _ProfileState();
} }
class _ProfileState extends State<Profile> { class _ProfileState extends State<Profile> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SafeArea(
child: Padding(
padding: const EdgeInsets.only(left: 16,right: 16,top: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
const Text("Profile",style: TextStyle(fontSize: 32,fontWeight: FontWeight.bold),),
Container(
padding: const EdgeInsets.only(left: 8,right: 8,top: 2,bottom: 2),
height: 30,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: Colors.pink[50],
),
child: GestureDetector(
onTap: () async {
deleteDb();
final preferences = await SharedPreferences.getInstance();
preferences.remove('logged_in_at');
preferences.remove('username');
preferences.remove('userId');
preferences.remove(rsaPrivateKeyName);
Navigator.pushNamedAndRemoveUntil(context, '/landing', ModalRoute.withName('/landing'));
},
child: Row(
children: const <Widget>[
Icon(Icons.logout, color: Colors.pink, size: 20,),
SizedBox(width: 2,),
Text("Logout",style: TextStyle(fontSize: 14,fontWeight: FontWeight.bold),),
],
),
),
)
],
),
),
),
const Padding(
padding: EdgeInsets.only(top: 16,left: 16,right: 16),
child: Text('Test'),
),
],
),
),
);
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SafeArea(
child: Padding(
padding: const EdgeInsets.only(left: 16,right: 16,top: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
const Text("Profile",style: TextStyle(fontSize: 32,fontWeight: FontWeight.bold),),
Container(
padding: const EdgeInsets.only(left: 8,right: 8,top: 2,bottom: 2),
height: 30,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: Theme.of(context).colorScheme.tertiary
),
child: GestureDetector(
onTap: () {
deleteDb();
MyProfile.logout();
Navigator.pushNamedAndRemoveUntil(context, '/landing', ModalRoute.withName('/landing'));
},
child: Row(
children: <Widget>[
Icon(
Icons.logout,
color: Theme.of(context).primaryColor,
size: 20
),
const SizedBox(width: 2,),
const Text(
'Logout',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold
)
),
],
),
),
)
],
),
),
),
Padding(
padding: const EdgeInsets.only(top: 16,left: 16,right: 16),
child: Row(
children: const <Widget>[
Text('FUCK'),
],
)
),
],
),
),
);
} }
} }

Loading…
Cancel
Save