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.

372 lines
12 KiB

  1. import 'dart:convert';
  2. import 'dart:io';
  3. import 'package:Envelope/database/repositories/conversation_users_repository.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:http/http.dart' as http;
  6. import '/components/custom_title_bar.dart';
  7. import '/components/flash_message.dart';
  8. import '/components/select_message_ttl.dart';
  9. import '/exceptions/update_data_exception.dart';
  10. import '/utils/encryption/crypto_utils.dart';
  11. import '/utils/storage/write_file.dart';
  12. import '/views/main/conversation/create_add_users.dart';
  13. import '/database/models/friends.dart';
  14. import '/database/models/conversation_users.dart';
  15. import '/database/models/conversations.dart';
  16. import '/database/models/my_profile.dart';
  17. import '/views/main/conversation/settings_user_list_item.dart';
  18. import '/views/main/conversation/edit_details.dart';
  19. import '/components/custom_circle_avatar.dart';
  20. import '/utils/storage/database.dart';
  21. import '/services/conversations_service.dart';
  22. import '/utils/storage/session_cookie.dart';
  23. import '/views/main/conversation/permissions.dart';
  24. class ConversationSettings extends StatefulWidget {
  25. const ConversationSettings({
  26. Key? key,
  27. required this.conversation,
  28. }) : super(key: key);
  29. final Conversation conversation;
  30. @override
  31. State<ConversationSettings> createState() => _ConversationSettingsState();
  32. }
  33. class _ConversationSettingsState extends State<ConversationSettings> {
  34. List<ConversationUser> users = [];
  35. MyProfile? profile;
  36. TextEditingController nameController = TextEditingController();
  37. @override
  38. Widget build(BuildContext context) {
  39. return Scaffold(
  40. appBar: CustomTitleBar(
  41. title: Text(
  42. '${widget.conversation.name} Settings',
  43. style: TextStyle(
  44. fontSize: 16,
  45. fontWeight: FontWeight.w600,
  46. color: Theme.of(context).appBarTheme.toolbarTextStyle?.color
  47. ),
  48. ),
  49. showBack: true,
  50. ),
  51. body: Padding(
  52. padding: const EdgeInsets.all(15),
  53. child: SingleChildScrollView(
  54. child: Column(
  55. children: <Widget> [
  56. const SizedBox(height: 30),
  57. conversationName(),
  58. const SizedBox(height: 25),
  59. widget.conversation.admin ?
  60. sectionTitle('Settings') :
  61. const SizedBox.shrink(),
  62. widget.conversation.admin ?
  63. settings() :
  64. const SizedBox.shrink(),
  65. widget.conversation.admin ?
  66. const SizedBox(height: 25) :
  67. const SizedBox.shrink(),
  68. sectionTitle('Members', showUsersAdd: widget.conversation.admin && !widget.conversation.twoUser),
  69. usersList(),
  70. const SizedBox(height: 25),
  71. myAccess(),
  72. ],
  73. ),
  74. ),
  75. ),
  76. );
  77. }
  78. Widget conversationName() {
  79. return Row(
  80. children: <Widget> [
  81. CustomCircleAvatar(
  82. icon: const Icon(Icons.people, size: 40),
  83. radius: 30,
  84. image: widget.conversation.icon,
  85. ),
  86. const SizedBox(width: 10),
  87. Text(
  88. widget.conversation.name,
  89. style: const TextStyle(
  90. fontSize: 25,
  91. fontWeight: FontWeight.w500,
  92. ),
  93. ),
  94. (widget.conversation.admin && widget.conversation.adminEditInfo) && !widget.conversation.twoUser ? IconButton(
  95. iconSize: 20,
  96. icon: const Icon(Icons.edit),
  97. padding: const EdgeInsets.all(5.0),
  98. splashRadius: 25,
  99. onPressed: () {
  100. Navigator.of(context).push(
  101. MaterialPageRoute(builder: (context) => ConversationEditDetails(
  102. // TODO: Move saveCallback to somewhere else
  103. saveCallback: (String conversationName, File? file) async {
  104. bool updatedImage = false;
  105. File? writtenFile;
  106. if (file != null) {
  107. updatedImage = file.hashCode != widget.conversation.icon.hashCode;
  108. writtenFile = await writeImage(
  109. widget.conversation.id,
  110. file.readAsBytesSync(),
  111. );
  112. }
  113. widget.conversation.name = conversationName;
  114. widget.conversation.icon = writtenFile;
  115. await ConversationsService.updateConversation(widget.conversation, updatedImage: updatedImage)
  116. .catchError((error) {
  117. String message = error.toString();
  118. if (error.runtimeType != UpdateDataException) {
  119. message = 'An error occured, please try again later';
  120. }
  121. showMessage(message, context);
  122. });
  123. setState(() {});
  124. if (!mounted) {
  125. return;
  126. }
  127. Navigator.pop(context);
  128. },
  129. conversation: widget.conversation,
  130. )),
  131. ).then(onGoBack);
  132. },
  133. ) : const SizedBox.shrink(),
  134. ],
  135. );
  136. }
  137. Future<void> getUsers() async {
  138. users = await ConversationUsersRepository.getConversationUsers(widget.conversation);
  139. profile = await MyProfile.getProfile();
  140. setState(() {});
  141. }
  142. @override
  143. void initState() {
  144. nameController.text = widget.conversation.name;
  145. super.initState();
  146. getUsers();
  147. }
  148. Widget myAccess() {
  149. return Align(
  150. alignment: Alignment.centerLeft,
  151. child: Column(
  152. crossAxisAlignment: CrossAxisAlignment.stretch,
  153. children: [
  154. TextButton.icon(
  155. label: const Text(
  156. 'Leave Conversation',
  157. style: TextStyle(fontSize: 16)
  158. ),
  159. icon: const Icon(Icons.exit_to_app),
  160. style: ButtonStyle(
  161. foregroundColor: MaterialStateProperty.all<Color>(Theme.of(context).colorScheme.error),
  162. alignment: Alignment.centerLeft,
  163. ),
  164. onPressed: () {
  165. print('Leave Group');
  166. }
  167. ),
  168. ],
  169. ),
  170. );
  171. }
  172. Widget sectionTitle(String title, { bool showUsersAdd = false}) {
  173. return Align(
  174. alignment: Alignment.centerLeft,
  175. child: Padding(
  176. padding: const EdgeInsets.only(right: 6),
  177. child: Row(
  178. children: [
  179. Expanded(
  180. child: Container(
  181. padding: const EdgeInsets.only(left: 12),
  182. child: Text(
  183. title,
  184. style: const TextStyle(fontSize: 20),
  185. ),
  186. ),
  187. ),
  188. !showUsersAdd ?
  189. const SizedBox.shrink() :
  190. IconButton(
  191. icon: const Icon(Icons.add),
  192. padding: const EdgeInsets.all(0),
  193. onPressed: () async {
  194. List<Friend> friends = await unselectedFriends();
  195. if (!mounted) {
  196. return;
  197. }
  198. Navigator.of(context).push(
  199. MaterialPageRoute(builder: (context) => ConversationAddFriendsList(
  200. friends: friends,
  201. saveCallback: (List<Friend> selectedFriends) async {
  202. ConversationsService.addUsersToConversation(
  203. widget.conversation,
  204. selectedFriends,
  205. );
  206. await ConversationsService.updateConversation(widget.conversation, includeUsers: true);
  207. await getUsers();
  208. if (!mounted) {
  209. return;
  210. }
  211. Navigator.pop(context);
  212. },
  213. ))
  214. );
  215. },
  216. ),
  217. ],
  218. )
  219. )
  220. );
  221. }
  222. Widget settings() {
  223. return Align(
  224. alignment: Alignment.centerLeft,
  225. child: Column(
  226. crossAxisAlignment: CrossAxisAlignment.stretch,
  227. children: [
  228. const SizedBox(height: 5),
  229. TextButton.icon(
  230. label: const Text(
  231. 'Disappearing Messages',
  232. style: TextStyle(fontSize: 16)
  233. ),
  234. icon: const Icon(Icons.timer),
  235. style: ButtonStyle(
  236. alignment: Alignment.centerLeft,
  237. foregroundColor: MaterialStateProperty.resolveWith<Color>(
  238. (Set<MaterialState> states) {
  239. return Theme.of(context).colorScheme.onBackground;
  240. },
  241. )
  242. ),
  243. onPressed: () {
  244. Navigator.of(context).push(
  245. MaterialPageRoute(builder: (context) => SelectMessageTTL(
  246. widgetTitle: 'Message Expiry',
  247. currentSelected: widget.conversation.messageExpiryDefault,
  248. backCallback: (String messageExpiry) async {
  249. widget.conversation.messageExpiryDefault = messageExpiry;
  250. ConversationsService.updateMessageExpiry(widget.conversation.id, messageExpiry)
  251. .catchError((dynamic) {
  252. showMessage(
  253. 'Could not change the default message expiry, please try again later.',
  254. context,
  255. );
  256. });
  257. ConversationsService.saveConversation(widget.conversation);
  258. }
  259. ))
  260. );
  261. }
  262. ),
  263. TextButton.icon(
  264. label: const Text(
  265. 'Permissions',
  266. style: TextStyle(fontSize: 16)
  267. ),
  268. icon: const Icon(Icons.lock),
  269. style: ButtonStyle(
  270. alignment: Alignment.centerLeft,
  271. foregroundColor: MaterialStateProperty.resolveWith<Color>(
  272. (Set<MaterialState> states) {
  273. return Theme.of(context).colorScheme.onBackground;
  274. },
  275. )
  276. ),
  277. onPressed: () {
  278. Navigator.of(context).push(
  279. MaterialPageRoute(builder: (context) => ConversationPermissions(
  280. conversation: widget.conversation,
  281. ))
  282. );
  283. }
  284. ),
  285. ],
  286. ),
  287. );
  288. }
  289. Widget usersList() {
  290. return ListView.builder(
  291. itemCount: users.length,
  292. shrinkWrap: true,
  293. padding: const EdgeInsets.only(top: 5, bottom: 0),
  294. physics: const NeverScrollableScrollPhysics(),
  295. itemBuilder: (context, i) {
  296. return ConversationSettingsUserListItem(
  297. user: users[i],
  298. isAdmin: widget.conversation.admin,
  299. twoUser: widget.conversation.twoUser,
  300. profile: profile!,
  301. );
  302. }
  303. );
  304. }
  305. Future<List<Friend>> unselectedFriends() async {
  306. final db = await getDatabaseConnection();
  307. List<String> notInArgs = [];
  308. for (var user in users) {
  309. notInArgs.add(user.userId);
  310. }
  311. final List<Map<String, dynamic>> maps = await db.query(
  312. 'friends',
  313. where: 'friend_id not in (${List.filled(notInArgs.length, '?').join(',')})',
  314. whereArgs: notInArgs,
  315. orderBy: 'username',
  316. );
  317. return List.generate(maps.length, (i) {
  318. return Friend(
  319. id: maps[i]['id'],
  320. userId: maps[i]['user_id'],
  321. friendId: maps[i]['friend_id'],
  322. friendSymmetricKey: maps[i]['symmetric_key'],
  323. publicKey: CryptoUtils.rsaPublicKeyFromPem(maps[i]['asymmetric_public_key']),
  324. acceptedAt: maps[i]['accepted_at'],
  325. username: maps[i]['username'],
  326. );
  327. });
  328. }
  329. onGoBack(dynamic value) async {
  330. nameController.text = widget.conversation.name;
  331. getUsers();
  332. setState(() {});
  333. }
  334. }