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.

391 lines
12 KiB

  1. import 'dart:io';
  2. import 'package:Envelope/models/text_messages.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:image_picker/image_picker.dart';
  5. import '/components/custom_title_bar.dart';
  6. import '/components/file_picker.dart';
  7. import '/models/conversations.dart';
  8. import '/models/messages.dart';
  9. import '/models/my_profile.dart';
  10. import '/utils/storage/messages.dart';
  11. import '/utils/time.dart';
  12. import '/views/main/conversation/settings.dart';
  13. class ConversationDetail extends StatefulWidget{
  14. final Conversation conversation;
  15. const ConversationDetail({
  16. Key? key,
  17. required this.conversation,
  18. }) : super(key: key);
  19. @override
  20. _ConversationDetailState createState() => _ConversationDetailState();
  21. }
  22. class _ConversationDetailState extends State<ConversationDetail> {
  23. List<Message> messages = [];
  24. MyProfile profile = MyProfile(
  25. id: '',
  26. username: '',
  27. messageExpiryDefault: 'no_expiry',
  28. );
  29. TextEditingController msgController = TextEditingController();
  30. bool showFilePicker = false;
  31. List<File> selectedImages = [];
  32. @override
  33. Widget build(BuildContext context) {
  34. return Scaffold(
  35. appBar: CustomTitleBar(
  36. title: Text(
  37. widget.conversation.name,
  38. style: TextStyle(
  39. fontSize: 16,
  40. fontWeight: FontWeight.w600,
  41. color: Theme.of(context).appBarTheme.toolbarTextStyle?.color
  42. ),
  43. ),
  44. showBack: true,
  45. rightHandButton: IconButton(
  46. onPressed: (){
  47. Navigator.of(context).push(
  48. MaterialPageRoute(builder: (context) => ConversationSettings(
  49. conversation: widget.conversation
  50. )),
  51. );
  52. },
  53. icon: Icon(
  54. Icons.settings,
  55. color: Theme.of(context).appBarTheme.iconTheme?.color,
  56. ),
  57. ),
  58. ),
  59. body: Stack(
  60. children: <Widget>[
  61. messagesView(),
  62. newMessageContent(),
  63. ],
  64. ),
  65. );
  66. }
  67. Future<void> fetchMessages() async {
  68. profile = await MyProfile.getProfile();
  69. messages = await getMessagesForThread(widget.conversation);
  70. setState(() {});
  71. }
  72. @override
  73. void initState() {
  74. super.initState();
  75. fetchMessages();
  76. }
  77. Widget usernameOrFailedToSend(int index) {
  78. if (messages[index].senderUsername != profile.username) {
  79. return Text(
  80. messages[index].senderUsername,
  81. style: TextStyle(
  82. fontSize: 12,
  83. color: Colors.grey[300],
  84. ),
  85. );
  86. }
  87. if (messages[index].failedToSend) {
  88. return Row(
  89. mainAxisAlignment: MainAxisAlignment.end,
  90. children: const <Widget>[
  91. Icon(
  92. Icons.warning_rounded,
  93. color: Colors.red,
  94. size: 20,
  95. ),
  96. Text(
  97. 'Failed to send',
  98. style: TextStyle(color: Colors.red, fontSize: 12),
  99. textAlign: TextAlign.right,
  100. ),
  101. ],
  102. );
  103. }
  104. return const SizedBox.shrink();
  105. }
  106. Widget messagesView() {
  107. if (messages.isEmpty) {
  108. return const Center(
  109. child: Text('No Messages'),
  110. );
  111. }
  112. return ListView.builder(
  113. itemCount: messages.length,
  114. shrinkWrap: true,
  115. padding: const EdgeInsets.only(top: 10,bottom: 90),
  116. reverse: true,
  117. itemBuilder: (context, index) {
  118. return Container(
  119. padding: const EdgeInsets.only(left: 14,right: 14,top: 0,bottom: 0),
  120. child: Align(
  121. alignment: (
  122. messages[index].senderUsername == profile.username ?
  123. Alignment.topRight :
  124. Alignment.topLeft
  125. ),
  126. child: Column(
  127. crossAxisAlignment: messages[index].senderUsername == profile.username ?
  128. CrossAxisAlignment.end :
  129. CrossAxisAlignment.start,
  130. children: <Widget>[
  131. Container(
  132. decoration: BoxDecoration(
  133. borderRadius: BorderRadius.circular(20),
  134. color: (
  135. messages[index].senderUsername == profile.username ?
  136. Theme.of(context).colorScheme.primary :
  137. Theme.of(context).colorScheme.tertiary
  138. ),
  139. ),
  140. padding: const EdgeInsets.all(12),
  141. child: messageContent(index),
  142. ),
  143. const SizedBox(height: 1.5),
  144. Row(
  145. mainAxisAlignment: messages[index].senderUsername == profile.username ?
  146. MainAxisAlignment.end :
  147. MainAxisAlignment.start,
  148. children: <Widget>[
  149. const SizedBox(width: 10),
  150. usernameOrFailedToSend(index),
  151. ],
  152. ),
  153. const SizedBox(height: 1.5),
  154. Row(
  155. mainAxisAlignment: messages[index].senderUsername == profile.username ?
  156. MainAxisAlignment.end :
  157. MainAxisAlignment.start,
  158. children: <Widget>[
  159. const SizedBox(width: 10),
  160. Text(
  161. convertToAgo(messages[index].createdAt),
  162. textAlign: messages[index].senderUsername == profile.username ?
  163. TextAlign.left :
  164. TextAlign.right,
  165. style: TextStyle(
  166. fontSize: 12,
  167. color: Colors.grey[500],
  168. ),
  169. ),
  170. ],
  171. ),
  172. index != 0 ?
  173. const SizedBox(height: 20) :
  174. const SizedBox.shrink(),
  175. ],
  176. )
  177. ),
  178. );
  179. },
  180. );
  181. }
  182. Widget messageContent(int index) {
  183. return Text(
  184. messages[index].getContent(),
  185. style: TextStyle(
  186. fontSize: 15,
  187. color: messages[index].senderUsername == profile.username ?
  188. Theme.of(context).colorScheme.onPrimary :
  189. Theme.of(context).colorScheme.onTertiary,
  190. )
  191. );
  192. }
  193. Widget showSelectedImages() {
  194. if (selectedImages.isEmpty) {
  195. return const SizedBox.shrink();
  196. }
  197. return SizedBox(
  198. height: 80,
  199. width: double.infinity,
  200. child: ListView.builder(
  201. itemCount: selectedImages.length,
  202. shrinkWrap: true,
  203. scrollDirection: Axis.horizontal,
  204. padding: const EdgeInsets.all(5),
  205. itemBuilder: (context, i) {
  206. return Stack(
  207. children: [
  208. Column(
  209. children: [
  210. const SizedBox(height: 5),
  211. Container(
  212. alignment: Alignment.center,
  213. height: 65,
  214. width: 65,
  215. child: Image.file(
  216. selectedImages[i],
  217. fit: BoxFit.fill,
  218. ),
  219. ),
  220. ],
  221. ),
  222. SizedBox(
  223. height: 60,
  224. width: 70,
  225. child: Align(
  226. alignment: Alignment.topRight,
  227. child: GestureDetector(
  228. onTap: () {
  229. setState(() {
  230. selectedImages.removeAt(i);
  231. });
  232. },
  233. child: Container(
  234. height: 20,
  235. width: 20,
  236. decoration: BoxDecoration(
  237. color: Theme.of(context).colorScheme.onPrimary,
  238. borderRadius: BorderRadius.circular(30),
  239. ),
  240. child: Icon(
  241. Icons.cancel,
  242. color: Theme.of(context).primaryColor,
  243. size: 20
  244. ),
  245. ),
  246. ),
  247. ),
  248. ),
  249. ],
  250. );
  251. },
  252. )
  253. );
  254. }
  255. Widget newMessageContent() {
  256. return Align(
  257. alignment: Alignment.bottomLeft,
  258. child: ConstrainedBox(
  259. constraints: BoxConstraints(
  260. maxHeight: selectedImages.isEmpty ?
  261. 200.0 :
  262. 270.0,
  263. ),
  264. child: Container(
  265. padding: const EdgeInsets.only(left: 10,bottom: 10,top: 10),
  266. width: double.infinity,
  267. color: Theme.of(context).backgroundColor,
  268. child: Column(
  269. mainAxisSize: MainAxisSize.min,
  270. children: [
  271. showSelectedImages(),
  272. Row(
  273. children: <Widget>[
  274. GestureDetector(
  275. onTap: (){
  276. setState(() {
  277. showFilePicker = !showFilePicker;
  278. });
  279. },
  280. child: Container(
  281. height: 30,
  282. width: 30,
  283. decoration: BoxDecoration(
  284. color: Theme.of(context).primaryColor,
  285. borderRadius: BorderRadius.circular(30),
  286. ),
  287. child: Icon(
  288. Icons.add,
  289. color: Theme.of(context).colorScheme.onPrimary,
  290. size: 20
  291. ),
  292. ),
  293. ),
  294. const SizedBox(width: 15,),
  295. Expanded(
  296. child: TextField(
  297. decoration: InputDecoration(
  298. hintText: 'Write message...',
  299. hintStyle: TextStyle(
  300. color: Theme.of(context).hintColor,
  301. ),
  302. border: InputBorder.none,
  303. ),
  304. maxLines: null,
  305. controller: msgController,
  306. ),
  307. ),
  308. const SizedBox(width: 15),
  309. SizedBox(
  310. width: 45,
  311. height: 45,
  312. child: FittedBox(
  313. child: FloatingActionButton(
  314. onPressed: () async {
  315. if (msgController.text == '' || selectedImages.isEmpty) {
  316. return;
  317. }
  318. await sendMessage(
  319. widget.conversation,
  320. data: msgController.text != '' ? msgController.text : null,
  321. files: selectedImages,
  322. );
  323. messages = await getMessagesForThread(widget.conversation);
  324. setState(() {});
  325. msgController.text = '';
  326. },
  327. child: Icon(
  328. Icons.send,
  329. color: Theme.of(context).colorScheme.onPrimary,
  330. size: 22
  331. ),
  332. backgroundColor: Theme.of(context).primaryColor,
  333. ),
  334. ),
  335. ),
  336. const SizedBox(width: 10),
  337. ],
  338. ),
  339. showFilePicker ?
  340. FilePicker(
  341. cameraHandle: () {},
  342. galleryHandleMultiple: (List<XFile> images) async {
  343. for (var img in images) {
  344. selectedImages.add(File(img.path));
  345. }
  346. setState(() {
  347. showFilePicker = false;
  348. });
  349. },
  350. fileHandle: () {},
  351. ) :
  352. const SizedBox.shrink(),
  353. ],
  354. ),
  355. ),
  356. ),
  357. );
  358. }
  359. }