Encrypted messaging app
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

285 lines
8.5 KiB

  1. import 'dart:convert';
  2. import 'package:Envelope/components/flash_message.dart';
  3. import 'package:Envelope/utils/storage/session_cookie.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter_dotenv/flutter_dotenv.dart';
  6. import 'package:qr_flutter/qr_flutter.dart';
  7. import 'package:sliding_up_panel/sliding_up_panel.dart';
  8. import 'package:http/http.dart' as http;
  9. import '/components/select_message_ttl.dart';
  10. import '/components/custom_circle_avatar.dart';
  11. import '/components/custom_title_bar.dart';
  12. import '/models/my_profile.dart';
  13. import '/utils/encryption/crypto_utils.dart';
  14. import '/utils/storage/database.dart';
  15. import '/views/main/profile/change_password.dart';
  16. import '/views/main/profile/change_server_url.dart';
  17. class Profile extends StatefulWidget {
  18. final MyProfile profile;
  19. const Profile({
  20. Key? key,
  21. required this.profile,
  22. }) : super(key: key);
  23. @override
  24. State<Profile> createState() => _ProfileState();
  25. }
  26. class _ProfileState extends State<Profile> {
  27. final PanelController _panelController = PanelController();
  28. @override
  29. Widget build(BuildContext context) {
  30. return Scaffold(
  31. appBar: const CustomTitleBar(
  32. title: Text(
  33. 'Profile',
  34. style: TextStyle(
  35. fontSize: 32,
  36. fontWeight: FontWeight.bold
  37. )
  38. ),
  39. showBack: false,
  40. backgroundColor: Colors.transparent,
  41. ),
  42. body: SlidingUpPanel(
  43. controller: _panelController,
  44. slideDirection: SlideDirection.DOWN,
  45. defaultPanelState: PanelState.CLOSED,
  46. color: Theme.of(context).scaffoldBackgroundColor,
  47. backdropTapClosesPanel: true,
  48. backdropEnabled: true,
  49. backdropOpacity: 0.2,
  50. minHeight: 0,
  51. maxHeight: 450,
  52. panel: Center(
  53. child: _profileQrCode(),
  54. ),
  55. body: Padding(
  56. padding: const EdgeInsets.only(top: 16,left: 16,right: 16),
  57. child: Column(
  58. children: <Widget>[
  59. usernameHeading(),
  60. const SizedBox(height: 30),
  61. settings(),
  62. const SizedBox(height: 30),
  63. logout(),
  64. ],
  65. )
  66. ),
  67. ),
  68. );
  69. }
  70. Widget usernameHeading() {
  71. return Row(
  72. children: <Widget> [
  73. const CustomCircleAvatar(
  74. icon: Icon(Icons.person, size: 40),
  75. imagePath: null, // TODO: Add image here
  76. radius: 30,
  77. ),
  78. const SizedBox(width: 20),
  79. Expanded(
  80. flex: 1,
  81. child: Text(
  82. widget.profile.username,
  83. style: const TextStyle(
  84. fontSize: 25,
  85. fontWeight: FontWeight.w500,
  86. ),
  87. ),
  88. ),
  89. IconButton(
  90. onPressed: () => _panelController.open(),
  91. icon: const Icon(Icons.qr_code_2),
  92. ),
  93. ],
  94. );
  95. }
  96. Widget logout() {
  97. bool isTesting = dotenv.env['ENVIRONMENT'] == 'development';
  98. return Align(
  99. alignment: Alignment.centerLeft,
  100. child: Column(
  101. crossAxisAlignment: CrossAxisAlignment.stretch,
  102. children: [
  103. TextButton.icon(
  104. label: const Text(
  105. 'Logout',
  106. style: TextStyle(fontSize: 16)
  107. ),
  108. icon: const Icon(Icons.exit_to_app),
  109. style: const ButtonStyle(
  110. alignment: Alignment.centerLeft,
  111. ),
  112. onPressed: () {
  113. deleteDb();
  114. MyProfile.logout();
  115. Navigator.pushNamedAndRemoveUntil(context, '/landing', ModalRoute.withName('/landing'));
  116. }
  117. ),
  118. isTesting ? TextButton.icon(
  119. label: const Text(
  120. 'Delete Database',
  121. style: TextStyle(fontSize: 16)
  122. ),
  123. icon: const Icon(Icons.delete_forever),
  124. style: const ButtonStyle(
  125. alignment: Alignment.centerLeft,
  126. ),
  127. onPressed: () {
  128. deleteDb();
  129. }
  130. ) : const SizedBox.shrink(),
  131. ],
  132. ),
  133. );
  134. }
  135. Widget settings() {
  136. return Align(
  137. alignment: Alignment.centerLeft,
  138. child: Column(
  139. crossAxisAlignment: CrossAxisAlignment.stretch,
  140. children: [
  141. const SizedBox(height: 5),
  142. TextButton.icon(
  143. label: const Text(
  144. 'Disappearing Messages',
  145. style: TextStyle(fontSize: 16)
  146. ),
  147. icon: const Icon(Icons.timer),
  148. style: ButtonStyle(
  149. alignment: Alignment.centerLeft,
  150. foregroundColor: MaterialStateProperty.resolveWith<Color>(
  151. (Set<MaterialState> states) {
  152. return Theme.of(context).colorScheme.onBackground;
  153. },
  154. )
  155. ),
  156. onPressed: () {
  157. Navigator.of(context).push(
  158. MaterialPageRoute(builder: (context) => SelectMessageTTL(
  159. widgetTitle: 'Message Expiry',
  160. currentSelected: widget.profile.messageExpiryDefault,
  161. backCallback: (String messageExpiry) async {
  162. widget.profile.messageExpiryDefault = messageExpiry;
  163. http.post(
  164. await MyProfile.getServerUrl('api/v1/auth/message_expiry'),
  165. headers: {
  166. 'cookie': await getSessionCookie(),
  167. },
  168. body: jsonEncode({
  169. 'message_expiry': messageExpiry,
  170. }),
  171. ).then((http.Response response) {
  172. if (response.statusCode == 200) {
  173. return;
  174. }
  175. showMessage(
  176. 'Could not change your default message expiry, please try again later.',
  177. context,
  178. );
  179. });
  180. },
  181. ))
  182. );
  183. }
  184. ),
  185. const SizedBox(height: 5),
  186. TextButton.icon(
  187. label: const Text(
  188. 'Server URL',
  189. style: TextStyle(fontSize: 16)
  190. ),
  191. icon: const Icon(Icons.dataset_linked_outlined),
  192. style: ButtonStyle(
  193. alignment: Alignment.centerLeft,
  194. foregroundColor: MaterialStateProperty.resolveWith<Color>(
  195. (Set<MaterialState> states) {
  196. return Theme.of(context).colorScheme.onBackground;
  197. },
  198. )
  199. ),
  200. onPressed: () {
  201. Navigator.of(context).push(
  202. MaterialPageRoute(builder: (context) => const ChangeServerUrl())
  203. );
  204. }
  205. ),
  206. const SizedBox(height: 5),
  207. TextButton.icon(
  208. label: const Text(
  209. 'Change Password',
  210. style: TextStyle(fontSize: 16)
  211. ),
  212. icon: const Icon(Icons.password),
  213. style: ButtonStyle(
  214. alignment: Alignment.centerLeft,
  215. foregroundColor: MaterialStateProperty.resolveWith<Color>(
  216. (Set<MaterialState> states) {
  217. return Theme.of(context).colorScheme.onBackground;
  218. },
  219. )
  220. ),
  221. onPressed: () {
  222. Navigator.of(context).push(
  223. MaterialPageRoute(builder: (context) => ChangePassword(
  224. privateKey: widget.profile.privateKey!,
  225. ))
  226. );
  227. }
  228. ),
  229. ],
  230. ),
  231. );
  232. }
  233. Widget _profileQrCode() {
  234. String payload = jsonEncode({
  235. 'i': widget.profile.id,
  236. 'u': widget.profile.username,
  237. 'k': base64.encode(
  238. CryptoUtils.encodeRSAPublicKeyToPem(widget.profile.publicKey!).codeUnits
  239. ),
  240. });
  241. return Column(
  242. children: [
  243. Padding(
  244. padding: const EdgeInsets.all(20),
  245. child: QrImage(
  246. backgroundColor: Theme.of(context).colorScheme.primary,
  247. data: payload,
  248. version: QrVersions.auto,
  249. gapless: true,
  250. ),
  251. ),
  252. Align(
  253. alignment: Alignment.centerRight,
  254. child: Padding(
  255. padding: const EdgeInsets.only(right: 20),
  256. child: IconButton(
  257. onPressed: () => _panelController.close(),
  258. icon: const Icon(Icons.arrow_upward),
  259. ),
  260. ),
  261. ),
  262. ]
  263. );
  264. }
  265. }