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.

335 lines
10 KiB

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