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.

332 lines
9.7 KiB

  1. import 'dart:io';
  2. import 'package:Envelope/database/repositories/messages_repository.dart';
  3. import 'package:Envelope/services/messages_service.dart';
  4. import 'package:Envelope/views/main/conversation/message.dart';
  5. import 'package:flutter/material.dart';
  6. import 'package:image_picker/image_picker.dart';
  7. import '/components/custom_title_bar.dart';
  8. import '/components/file_picker.dart';
  9. import '/database/models/conversations.dart';
  10. import '/database/models/messages.dart';
  11. import '/database/models/my_profile.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. late ScrollController _scrollController;
  24. List<Message> messages = [];
  25. MyProfile profile = MyProfile(
  26. id: '',
  27. username: '',
  28. messageExpiryDefault: 'no_expiry',
  29. );
  30. TextEditingController msgController = TextEditingController();
  31. bool showFilePicker = false;
  32. List<File> selectedImages = [];
  33. bool sendDisabled = false;
  34. @override
  35. Widget build(BuildContext context) {
  36. return Scaffold(
  37. appBar: CustomTitleBar(
  38. title: Text(
  39. widget.conversation.name,
  40. style: TextStyle(
  41. fontSize: 16,
  42. fontWeight: FontWeight.w600,
  43. color: Theme.of(context).appBarTheme.toolbarTextStyle?.color
  44. ),
  45. ),
  46. showBack: true,
  47. rightHandButton: IconButton(
  48. onPressed: (){
  49. Navigator.of(context).push(
  50. MaterialPageRoute(builder: (context) => ConversationSettings(
  51. conversation: widget.conversation
  52. )),
  53. );
  54. },
  55. icon: Icon(
  56. Icons.settings,
  57. color: Theme.of(context).appBarTheme.iconTheme?.color,
  58. ),
  59. ),
  60. ),
  61. body: Stack(
  62. children: <Widget>[
  63. messagesView(),
  64. newMessageContent(),
  65. ],
  66. ),
  67. );
  68. }
  69. Future<void> fetchMessages() async {
  70. profile = await MyProfile.getProfile();
  71. messages = await MessagesRepository.getMessagesForThread(widget.conversation);
  72. setState(() {});
  73. }
  74. @override
  75. void initState() {
  76. sendDisabled = widget.conversation.adminSendMessages && !widget.conversation.admin;
  77. _scrollController = ScrollController();
  78. _scrollController.addListener(_scrollListener);
  79. super.initState();
  80. fetchMessages();
  81. }
  82. Future<void> _scrollListener() async {
  83. if (!(_scrollController.offset >= _scrollController.position.maxScrollExtent)) {
  84. return;
  85. }
  86. int page = 0;
  87. if (messages.length > 19) {
  88. page = messages.length ~/ 20;
  89. }
  90. await MessagesService.updateMessageThread(widget.conversation, page: page);
  91. await fetchMessages();
  92. }
  93. Widget messagesView() {
  94. if (messages.isEmpty) {
  95. return const Center(
  96. child: Text('No Messages'),
  97. );
  98. }
  99. return ListView.builder(
  100. controller: _scrollController,
  101. itemCount: messages.length,
  102. shrinkWrap: true,
  103. padding: EdgeInsets.only(
  104. top: 10,
  105. bottom: selectedImages.isEmpty ? 90 : 160,
  106. ),
  107. reverse: true,
  108. itemBuilder: (context, index) {
  109. return ConversationMessage(
  110. message: messages[index],
  111. profile: profile,
  112. index: index,
  113. );
  114. },
  115. );
  116. }
  117. Widget showSelectedImages() {
  118. if (selectedImages.isEmpty) {
  119. return const SizedBox.shrink();
  120. }
  121. return SizedBox(
  122. height: 80,
  123. width: double.infinity,
  124. child: ListView.builder(
  125. itemCount: selectedImages.length,
  126. shrinkWrap: true,
  127. scrollDirection: Axis.horizontal,
  128. padding: const EdgeInsets.all(5),
  129. itemBuilder: (context, i) {
  130. return Stack(
  131. children: [
  132. Column(
  133. children: [
  134. const SizedBox(height: 5),
  135. Container(
  136. alignment: Alignment.center,
  137. height: 65,
  138. width: 65,
  139. child: Image.file(
  140. selectedImages[i],
  141. fit: BoxFit.fill,
  142. ),
  143. ),
  144. ],
  145. ),
  146. SizedBox(
  147. height: 60,
  148. width: 70,
  149. child: Align(
  150. alignment: Alignment.topRight,
  151. child: GestureDetector(
  152. onTap: () {
  153. setState(() {
  154. selectedImages.removeAt(i);
  155. });
  156. },
  157. child: Container(
  158. height: 20,
  159. width: 20,
  160. decoration: BoxDecoration(
  161. color: Theme.of(context).colorScheme.onPrimary,
  162. borderRadius: BorderRadius.circular(30),
  163. ),
  164. child: Icon(
  165. Icons.cancel,
  166. color: Theme.of(context).primaryColor,
  167. size: 20
  168. ),
  169. ),
  170. ),
  171. ),
  172. ),
  173. ],
  174. );
  175. },
  176. )
  177. );
  178. }
  179. Widget newMessageContent() {
  180. return Align(
  181. alignment: Alignment.bottomLeft,
  182. child: ConstrainedBox(
  183. constraints: BoxConstraints(
  184. maxHeight: selectedImages.isEmpty ?
  185. 200.0 :
  186. 270.0,
  187. ),
  188. child: Container(
  189. padding: const EdgeInsets.only(left: 10,bottom: 10,top: 10),
  190. width: double.infinity,
  191. color: Theme.of(context).backgroundColor,
  192. child: Column(
  193. mainAxisSize: MainAxisSize.min,
  194. children: [
  195. showSelectedImages(),
  196. Row(
  197. children: <Widget>[
  198. GestureDetector(
  199. onTap: (){
  200. setState(() {
  201. if (sendDisabled) {
  202. return;
  203. }
  204. showFilePicker = !showFilePicker;
  205. });
  206. },
  207. child: Container(
  208. height: 30,
  209. width: 30,
  210. decoration: BoxDecoration(
  211. color: Theme.of(context).primaryColor,
  212. borderRadius: BorderRadius.circular(30),
  213. ),
  214. child: Icon(
  215. Icons.add,
  216. color: Theme.of(context).colorScheme.onPrimary,
  217. size: 20
  218. ),
  219. ),
  220. ),
  221. const SizedBox(width: 15,),
  222. Expanded(
  223. child: TextField(
  224. enabled: !sendDisabled,
  225. decoration: InputDecoration(
  226. hintText: sendDisabled ?
  227. 'Messages disabled for non-admins' :
  228. 'Write message...',
  229. hintStyle: TextStyle(
  230. color: Theme.of(context).hintColor,
  231. ),
  232. border: InputBorder.none,
  233. ),
  234. maxLines: null,
  235. controller: msgController,
  236. ),
  237. ),
  238. const SizedBox(width: 15),
  239. SizedBox(
  240. width: 45,
  241. height: 45,
  242. child: FittedBox(
  243. child: FloatingActionButton(
  244. onPressed: () async {
  245. if ((msgController.text == '' && selectedImages.isEmpty) || sendDisabled) {
  246. return;
  247. }
  248. await MessagesService.sendMessage(
  249. widget.conversation,
  250. data: msgController.text != '' ? msgController.text : null,
  251. files: selectedImages,
  252. );
  253. messages = await MessagesRepository.getMessagesForThread(widget.conversation);
  254. setState(() {
  255. msgController.text = '';
  256. selectedImages = [];
  257. });
  258. },
  259. backgroundColor: Theme.of(context).primaryColor,
  260. child: Icon(
  261. Icons.send,
  262. color: Theme.of(context).colorScheme.onPrimary,
  263. size: 22
  264. ),
  265. ),
  266. ),
  267. ),
  268. const SizedBox(width: 10),
  269. ],
  270. ),
  271. AnimatedSwitcher(
  272. duration: const Duration(milliseconds: 250),
  273. transitionBuilder: (Widget child, Animation<double> animation) {
  274. return SizeTransition(sizeFactor: animation, child: child);
  275. },
  276. child: showFilePicker ?
  277. FilePicker(
  278. key: const Key('filePicker'),
  279. cameraHandle: (XFile image) {},
  280. galleryHandleMultiple: (List<XFile> images) async {
  281. for (var img in images) {
  282. selectedImages.add(File(img.path));
  283. }
  284. setState(() {
  285. showFilePicker = false;
  286. });
  287. },
  288. fileHandle: () {},
  289. ) :
  290. const SizedBox(height: 15),
  291. ),
  292. ],
  293. ),
  294. ),
  295. ),
  296. );
  297. }
  298. }