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.

366 lines
11 KiB

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