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.

245 lines
6.3 KiB

  1. import 'package:flutter/material.dart';
  2. import '/components/view_image.dart';
  3. import '/database/models/image_message.dart';
  4. import '/database/models/my_profile.dart';
  5. import '/database/models/messages.dart';
  6. import '/utils/time.dart';
  7. @immutable
  8. class ConversationMessage extends StatefulWidget {
  9. const ConversationMessage({
  10. Key? key,
  11. required this.message,
  12. required this.profile,
  13. required this.index,
  14. }) : super(key: key);
  15. final Message message;
  16. final MyProfile profile;
  17. final int index;
  18. @override
  19. _ConversationMessageState createState() => _ConversationMessageState();
  20. }
  21. class _ConversationMessageState extends State<ConversationMessage> {
  22. List<PopupMenuEntry<String>> menuItems = [];
  23. Offset? _tapPosition;
  24. bool showDownloadButton = false;
  25. bool showDeleteButton = false;
  26. @override
  27. void initState() {
  28. super.initState();
  29. showDownloadButton = widget.message.runtimeType == ImageMessage;
  30. showDeleteButton = widget.message.senderId == widget.profile.id;
  31. if (showDownloadButton) {
  32. menuItems.add(PopupMenuItem(
  33. value: 'download',
  34. child: Row(
  35. children: const [
  36. Icon(Icons.download),
  37. SizedBox(
  38. width: 10,
  39. ),
  40. Text('Download')
  41. ],
  42. ),
  43. ));
  44. }
  45. if (showDeleteButton) {
  46. menuItems.add(PopupMenuItem(
  47. value: 'delete',
  48. child: Row(
  49. children: const [
  50. Icon(Icons.delete),
  51. SizedBox(
  52. width: 10,
  53. ),
  54. Text('Delete')
  55. ],
  56. ),
  57. ));
  58. }
  59. setState(() {});
  60. }
  61. @override
  62. Widget build(BuildContext context) {
  63. return Container(
  64. padding: const EdgeInsets.only(left: 14,right: 14,top: 0,bottom: 0),
  65. child: Align(
  66. alignment: (
  67. widget.message.senderId == widget.profile.id ?
  68. Alignment.topRight :
  69. Alignment.topLeft
  70. ),
  71. child: Column(
  72. crossAxisAlignment: widget.message.senderId == widget.profile.id ?
  73. CrossAxisAlignment.end :
  74. CrossAxisAlignment.start,
  75. children: <Widget>[
  76. messageContent(context),
  77. const SizedBox(height: 1.5),
  78. Row(
  79. mainAxisAlignment: widget.message.senderId == widget.profile.id ?
  80. MainAxisAlignment.end :
  81. MainAxisAlignment.start,
  82. children: <Widget>[
  83. const SizedBox(width: 10),
  84. usernameOrFailedToSend(),
  85. ],
  86. ),
  87. const SizedBox(height: 1.5),
  88. Row(
  89. mainAxisAlignment: widget.message.senderId == widget.profile.id ?
  90. MainAxisAlignment.end :
  91. MainAxisAlignment.start,
  92. children: <Widget>[
  93. const SizedBox(width: 10),
  94. Text(
  95. convertToAgo(widget.message.createdAt),
  96. textAlign: widget.message.senderId == widget.profile.id ?
  97. TextAlign.left :
  98. TextAlign.right,
  99. style: TextStyle(
  100. fontSize: 12,
  101. color: Theme.of(context).hintColor,
  102. ),
  103. ),
  104. ],
  105. ),
  106. widget.index != 0 ?
  107. const SizedBox(height: 20) :
  108. const SizedBox.shrink(),
  109. ],
  110. )
  111. ),
  112. );
  113. }
  114. void _showCustomMenu() {
  115. if (menuItems.isEmpty) {
  116. return;
  117. }
  118. final Size overlay = MediaQuery.of(context).size;
  119. int addVerticalOffset = 75 * menuItems.length;
  120. // TODO: Implement download & delete methods
  121. showMenu(
  122. context: context,
  123. items: menuItems,
  124. position: RelativeRect.fromRect(
  125. Offset(_tapPosition!.dx, (_tapPosition!.dy - addVerticalOffset)) & const Size(40, 40),
  126. Offset.zero & overlay
  127. )
  128. )
  129. .then<void>((String? delta) async {
  130. if (delta == null) {
  131. return;
  132. }
  133. });
  134. }
  135. void _storePosition(TapDownDetails details) {
  136. _tapPosition = details.globalPosition;
  137. }
  138. Widget messageContent(BuildContext context) {
  139. if (widget.message.runtimeType == ImageMessage) {
  140. return GestureDetector(
  141. onTap: () {
  142. Navigator.push(context, MaterialPageRoute(builder: (context) {
  143. return ViewImage(
  144. message: (widget.message as ImageMessage)
  145. );
  146. }));
  147. },
  148. onLongPress: _showCustomMenu,
  149. onTapDown: _storePosition,
  150. child: ConstrainedBox(
  151. constraints: const BoxConstraints(maxHeight: 350, maxWidth: 250),
  152. child: ClipRRect(
  153. borderRadius: BorderRadius.circular(20), child: Image.file(
  154. (widget.message as ImageMessage).file,
  155. fit: BoxFit.fill,
  156. ),
  157. ),
  158. ),
  159. );
  160. }
  161. return GestureDetector(
  162. onLongPress: _showCustomMenu,
  163. onTapDown: _storePosition,
  164. child: Container(
  165. decoration: BoxDecoration(
  166. borderRadius: BorderRadius.circular(20),
  167. color: (
  168. widget.message.senderId == widget.profile.id ?
  169. Theme.of(context).colorScheme.primary :
  170. Theme.of(context).colorScheme.tertiary
  171. ),
  172. ),
  173. padding: const EdgeInsets.all(12),
  174. child: Text(
  175. widget.message.getContent(),
  176. style: TextStyle(
  177. fontSize: 15,
  178. color: widget.message.senderId == widget.profile.id ?
  179. Theme.of(context).colorScheme.onPrimary :
  180. Theme.of(context).colorScheme.onTertiary,
  181. ),
  182. ),
  183. ),
  184. );
  185. }
  186. Widget usernameOrFailedToSend() {
  187. if (widget.message.senderId != widget.profile.id) {
  188. return Text(
  189. widget.message.senderUsername,
  190. style: TextStyle(
  191. fontSize: 12,
  192. color: Theme.of(context).hintColor,
  193. ),
  194. );
  195. }
  196. if (widget.message.failedToSend) {
  197. return Row(
  198. mainAxisAlignment: MainAxisAlignment.end,
  199. children: const <Widget>[
  200. Icon(
  201. Icons.warning_rounded,
  202. color: Colors.red,
  203. size: 20,
  204. ),
  205. Text(
  206. 'Failed to send',
  207. style: TextStyle(color: Colors.red, fontSize: 12),
  208. textAlign: TextAlign.right,
  209. ),
  210. ],
  211. );
  212. }
  213. return const SizedBox.shrink();
  214. }
  215. }