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.

382 lines
12 KiB

  1. import 'dart:convert';
  2. import 'dart:io';
  3. import 'package:Envelope/utils/storage/session_cookie.dart';
  4. import 'package:Envelope/views/main/conversation/permissions.dart';
  5. import 'package:flutter/material.dart';
  6. import 'package:http/http.dart' as http;
  7. import '/components/custom_title_bar.dart';
  8. import '/components/flash_message.dart';
  9. import '/components/select_message_ttl.dart';
  10. import '/exceptions/update_data_exception.dart';
  11. import '/models/friends.dart';
  12. import '/utils/encryption/crypto_utils.dart';
  13. import '/utils/storage/write_file.dart';
  14. import '/views/main/conversation/create_add_users.dart';
  15. import '/models/conversation_users.dart';
  16. import '/models/conversations.dart';
  17. import '/models/my_profile.dart';
  18. import '/views/main/conversation/settings_user_list_item.dart';
  19. import '/views/main/conversation/edit_details.dart';
  20. import '/components/custom_circle_avatar.dart';
  21. import '/utils/storage/database.dart';
  22. import '/utils/storage/conversations.dart';
  23. class ConversationSettings extends StatefulWidget {
  24. const ConversationSettings({
  25. Key? key,
  26. required this.conversation,
  27. }) : super(key: key);
  28. final Conversation conversation;
  29. @override
  30. State<ConversationSettings> createState() => _ConversationSettingsState();
  31. }
  32. class _ConversationSettingsState extends State<ConversationSettings> {
  33. List<ConversationUser> users = [];
  34. MyProfile? profile;
  35. TextEditingController nameController = TextEditingController();
  36. @override
  37. Widget build(BuildContext context) {
  38. return Scaffold(
  39. appBar: CustomTitleBar(
  40. title: Text(
  41. '${widget.conversation.name} Settings',
  42. style: TextStyle(
  43. fontSize: 16,
  44. fontWeight: FontWeight.w600,
  45. color: Theme.of(context).appBarTheme.toolbarTextStyle?.color
  46. ),
  47. ),
  48. showBack: true,
  49. ),
  50. body: Padding(
  51. padding: const EdgeInsets.all(15),
  52. child: SingleChildScrollView(
  53. child: Column(
  54. children: <Widget> [
  55. const SizedBox(height: 30),
  56. conversationName(),
  57. const SizedBox(height: 25),
  58. widget.conversation.admin ?
  59. sectionTitle('Settings') :
  60. const SizedBox.shrink(),
  61. widget.conversation.admin ?
  62. settings() :
  63. const SizedBox.shrink(),
  64. widget.conversation.admin ?
  65. const SizedBox(height: 25) :
  66. const SizedBox.shrink(),
  67. sectionTitle('Members', showUsersAdd: widget.conversation.admin && !widget.conversation.twoUser),
  68. usersList(),
  69. const SizedBox(height: 25),
  70. myAccess(),
  71. ],
  72. ),
  73. ),
  74. ),
  75. );
  76. }
  77. Widget conversationName() {
  78. return Row(
  79. children: <Widget> [
  80. CustomCircleAvatar(
  81. icon: const Icon(Icons.people, size: 40),
  82. radius: 30,
  83. image: widget.conversation.icon,
  84. ),
  85. const SizedBox(width: 10),
  86. Text(
  87. widget.conversation.name,
  88. style: const TextStyle(
  89. fontSize: 25,
  90. fontWeight: FontWeight.w500,
  91. ),
  92. ),
  93. (widget.conversation.admin && widget.conversation.adminEditInfo) && !widget.conversation.twoUser ? IconButton(
  94. iconSize: 20,
  95. icon: const Icon(Icons.edit),
  96. padding: const EdgeInsets.all(5.0),
  97. splashRadius: 25,
  98. onPressed: () {
  99. Navigator.of(context).push(
  100. MaterialPageRoute(builder: (context) => ConversationEditDetails(
  101. // TODO: Move saveCallback to somewhere else
  102. saveCallback: (String conversationName, File? file) async {
  103. bool updatedImage = false;
  104. File? writtenFile;
  105. if (file != null) {
  106. updatedImage = file.hashCode != widget.conversation.icon.hashCode;
  107. writtenFile = await writeImage(
  108. widget.conversation.id,
  109. file.readAsBytesSync(),
  110. );
  111. }
  112. widget.conversation.name = conversationName;
  113. widget.conversation.icon = writtenFile;
  114. await saveConversation();
  115. await 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. Navigator.pop(context);
  125. },
  126. conversation: widget.conversation,
  127. )),
  128. ).then(onGoBack);
  129. },
  130. ) : const SizedBox.shrink(),
  131. ],
  132. );
  133. }
  134. Future<void> getUsers() async {
  135. users = await getConversationUsers(widget.conversation);
  136. profile = await MyProfile.getProfile();
  137. setState(() {});
  138. }
  139. @override
  140. void initState() {
  141. nameController.text = widget.conversation.name;
  142. super.initState();
  143. getUsers();
  144. }
  145. Widget myAccess() {
  146. return Align(
  147. alignment: Alignment.centerLeft,
  148. child: Column(
  149. crossAxisAlignment: CrossAxisAlignment.stretch,
  150. children: [
  151. TextButton.icon(
  152. label: const Text(
  153. 'Leave Conversation',
  154. style: TextStyle(fontSize: 16)
  155. ),
  156. icon: const Icon(Icons.exit_to_app),
  157. style: ButtonStyle(
  158. foregroundColor: MaterialStateProperty.all<Color>(Theme.of(context).colorScheme.error),
  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. Navigator.of(context).push(
  236. MaterialPageRoute(builder: (context) => SelectMessageTTL(
  237. widgetTitle: 'Message Expiry',
  238. currentSelected: widget.conversation.messageExpiryDefault,
  239. backCallback: (String messageExpiry) async {
  240. widget.conversation.messageExpiryDefault = messageExpiry;
  241. http.post(
  242. await MyProfile.getServerUrl(
  243. 'api/v1/auth/conversations/${widget.conversation.id}/message_expiry'
  244. ),
  245. headers: {
  246. 'cookie': await getSessionCookie(),
  247. },
  248. body: jsonEncode({
  249. 'message_expiry': messageExpiry,
  250. }),
  251. ).then((http.Response response) {
  252. if (response.statusCode == 204) {
  253. return;
  254. }
  255. showMessage(
  256. 'Could not change the default message expiry, please try again later.',
  257. context,
  258. );
  259. });
  260. saveConversation();
  261. }
  262. ))
  263. );
  264. }
  265. ),
  266. TextButton.icon(
  267. label: const Text(
  268. 'Permissions',
  269. style: TextStyle(fontSize: 16)
  270. ),
  271. icon: const Icon(Icons.lock),
  272. style: ButtonStyle(
  273. alignment: Alignment.centerLeft,
  274. foregroundColor: MaterialStateProperty.resolveWith<Color>(
  275. (Set<MaterialState> states) {
  276. return Theme.of(context).colorScheme.onBackground;
  277. },
  278. )
  279. ),
  280. onPressed: () {
  281. Navigator.of(context).push(
  282. MaterialPageRoute(builder: (context) => ConversationPermissions(
  283. conversation: widget.conversation,
  284. ))
  285. );
  286. }
  287. ),
  288. ],
  289. ),
  290. );
  291. }
  292. Widget usersList() {
  293. return ListView.builder(
  294. itemCount: users.length,
  295. shrinkWrap: true,
  296. padding: const EdgeInsets.only(top: 5, bottom: 0),
  297. physics: const NeverScrollableScrollPhysics(),
  298. itemBuilder: (context, i) {
  299. return ConversationSettingsUserListItem(
  300. user: users[i],
  301. isAdmin: widget.conversation.admin,
  302. twoUser: widget.conversation.twoUser,
  303. profile: profile!,
  304. );
  305. }
  306. );
  307. }
  308. Future<List<Friend>> unselectedFriends() async {
  309. final db = await getDatabaseConnection();
  310. List<String> notInArgs = [];
  311. for (var user in users) {
  312. notInArgs.add(user.userId);
  313. }
  314. final List<Map<String, dynamic>> maps = await db.query(
  315. 'friends',
  316. where: 'friend_id not in (${List.filled(notInArgs.length, '?').join(',')})',
  317. whereArgs: notInArgs,
  318. orderBy: 'username',
  319. );
  320. return List.generate(maps.length, (i) {
  321. return Friend(
  322. id: maps[i]['id'],
  323. userId: maps[i]['user_id'],
  324. friendId: maps[i]['friend_id'],
  325. friendSymmetricKey: maps[i]['symmetric_key'],
  326. publicKey: CryptoUtils.rsaPublicKeyFromPem(maps[i]['asymmetric_public_key']),
  327. acceptedAt: maps[i]['accepted_at'],
  328. username: maps[i]['username'],
  329. );
  330. });
  331. }
  332. onGoBack(dynamic value) async {
  333. nameController.text = widget.conversation.name;
  334. getUsers();
  335. setState(() {});
  336. }
  337. saveConversation() async {
  338. final db = await getDatabaseConnection();
  339. db.update(
  340. 'conversations',
  341. widget.conversation.toMap(),
  342. where: 'id = ?',
  343. whereArgs: [widget.conversation.id],
  344. );
  345. }
  346. }