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.

374 lines
11 KiB

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