diff --git a/lib/src/components/message_send_state_icon.dart b/lib/src/components/message_send_state_icon.dart index 29c5cc2..46ba62d 100644 --- a/lib/src/components/message_send_state_icon.dart +++ b/lib/src/components/message_send_state_icon.dart @@ -85,6 +85,8 @@ class _MessageSendStateIconState extends State { for (final message in widget.messages) { if (icons.length == 2) break; + if (kindsAlreadyShown.contains(message.kind)) continue; + kindsAlreadyShown.add(message.kind); MessageSendState state = messageSendStateFromMessage(message); late Color color; @@ -94,14 +96,13 @@ class _MessageSendStateIconState extends State { getMessageColorFromType(TextMessageContent(text: ""), twonlyColor); } else { MessageContent? content = MessageContent.fromJson( - message.kind, jsonDecode(message.contentJson!)); + message.kind, + jsonDecode(message.contentJson!), + ); if (content == null) continue; color = getMessageColorFromType(content, twonlyColor); } - if (kindsAlreadyShown.contains(message.kind)) continue; - kindsAlreadyShown.add(message.kind); - Widget icon = Placeholder(); switch (state) { @@ -140,6 +141,11 @@ class _MessageSendStateIconState extends State { break; } + if (message.kind == MessageKind.storedMediaFile) { + icon = FaIcon(FontAwesomeIcons.floppyDisk, size: 12, color: color); + text = "Stored in gallery"; + } + icons.add(icon); } diff --git a/lib/src/database/daos/messages_dao.dart b/lib/src/database/daos/messages_dao.dart index d54ebb9..e1e1775 100644 --- a/lib/src/database/daos/messages_dao.dart +++ b/lib/src/database/daos/messages_dao.dart @@ -76,13 +76,13 @@ class MessagesDao extends DatabaseAccessor .get(); } - Future openedAllTextMessages(int contactId) { + Future openedAllNonMediaMessages(int contactId) { final updates = MessagesCompanion(openedAt: Value(DateTime.now())); return (update(messages) ..where((t) => t.contactId.equals(contactId) & t.openedAt.isNull() & - t.kind.equals(MessageKind.textMessage.name))) + t.kind.equals(MessageKind.media.name).not())) .write(updates); } diff --git a/lib/src/json_models/message.dart b/lib/src/json_models/message.dart index 951c90c..0be6d01 100644 --- a/lib/src/json_models/message.dart +++ b/lib/src/json_models/message.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; enum MessageKind { textMessage, + storedMediaFile, media, contactRequest, profileChange, @@ -20,7 +21,7 @@ Map messageKindColors = { Color getMessageColorFromType(MessageContent content, Color primary) { Color color; - if (content is TextMessageContent) { + if (content is TextMessageContent || content is StoredMediaFileContent) { color = messageKindColors["text"]!; } else { if (content is MediaMessageContent) { @@ -96,6 +97,8 @@ class MessageContent { return TextMessageContent.fromJson(json); case MessageKind.profileChange: return ProfileContent.fromJson(json); + case MessageKind.storedMediaFile: + return StoredMediaFileContent.fromJson(json); default: return null; } @@ -172,6 +175,20 @@ class TextMessageContent extends MessageContent { } } +class StoredMediaFileContent extends MessageContent { + int messageId; + StoredMediaFileContent({required this.messageId}); + + static StoredMediaFileContent fromJson(Map json) { + return StoredMediaFileContent(messageId: json['messageId']); + } + + @override + Map toJson() { + return {'messageId': messageId}; + } +} + class ProfileContent extends MessageContent { String avatarSvg; String displayName; diff --git a/lib/src/json_models/userdata.dart b/lib/src/json_models/userdata.dart index 07a2d49..281a928 100644 --- a/lib/src/json_models/userdata.dart +++ b/lib/src/json_models/userdata.dart @@ -15,6 +15,7 @@ class UserData { String? avatarSvg; String? avatarJson; int? avatarCounter; + int? defaultShowTime; final int userId; diff --git a/lib/src/json_models/userdata.g.dart b/lib/src/json_models/userdata.g.dart index 2d2e586..287ce51 100644 --- a/lib/src/json_models/userdata.g.dart +++ b/lib/src/json_models/userdata.g.dart @@ -13,7 +13,8 @@ UserData _$UserDataFromJson(Map json) => UserData( ) ..avatarSvg = json['avatarSvg'] as String? ..avatarJson = json['avatarJson'] as String? - ..avatarCounter = (json['avatarCounter'] as num?)?.toInt(); + ..avatarCounter = (json['avatarCounter'] as num?)?.toInt() + ..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt(); Map _$UserDataToJson(UserData instance) => { 'username': instance.username, @@ -21,5 +22,6 @@ Map _$UserDataToJson(UserData instance) => { 'avatarSvg': instance.avatarSvg, 'avatarJson': instance.avatarJson, 'avatarCounter': instance.avatarCounter, + 'defaultShowTime': instance.defaultShowTime, 'userId': instance.userId, }; diff --git a/lib/src/providers/api/server_messages.dart b/lib/src/providers/api/server_messages.dart index ea7be63..a47cc6f 100644 --- a/lib/src/providers/api/server_messages.dart +++ b/lib/src/providers/api/server_messages.dart @@ -221,18 +221,28 @@ Future handleNewMessage(int fromUserId, Uint8List body) async { break; default: if (message.kind != MessageKind.textMessage && - message.kind != MessageKind.media) { + message.kind != MessageKind.media && + message.kind != MessageKind.storedMediaFile) { Logger("handleServerMessages") .shout("Got unknown MessageKind $message"); } else { String content = jsonEncode(message.content!.toJson()); + bool acknowledgeByUser = false; + DateTime? openedAt; + if (message.kind == MessageKind.storedMediaFile) { + acknowledgeByUser = true; + openedAt = DateTime.now(); + } + final update = MessagesCompanion( contactId: Value(fromUserId), kind: Value(message.kind), messageOtherId: Value(message.messageId), contentJson: Value(content), acknowledgeByServer: Value(true), + acknowledgeByUser: Value(acknowledgeByUser), + openedAt: Value(openedAt), downloadState: Value(message.kind == MessageKind.media ? DownloadState.pending : DownloadState.downloaded), diff --git a/lib/src/services/notification_service.dart b/lib/src/services/notification_service.dart index 050ff35..0603f9b 100644 --- a/lib/src/services/notification_service.dart +++ b/lib/src/services/notification_service.dart @@ -149,6 +149,7 @@ String getPushNotificationText(String key, String userName) { "newImage": "%userName% hat dir ein Bild gesendet.", "contactRequest": "%userName% möchte sich mir dir vernetzen.", "acceptRequest": "%userName% ist jetzt mit dir vernetzt.", + "storedMediaFile": "%userName% hat dein Bild gespeichert." }; } else { pushNotificationText = { @@ -158,6 +159,7 @@ String getPushNotificationText(String key, String userName) { "newImage": "%userName% has sent you an image.", "contactRequest": "%userName% wants to connect with you.", "acceptRequest": "%userName% is now connected with you.", + "storedMediaFile": "%userName% has stored your image." }; } @@ -199,6 +201,11 @@ Future localPushNotificationNewMessage( msg = getPushNotificationText("acceptRequest", getContactDisplayName(user)); } + if (message.kind == my.MessageKind.storedMediaFile) { + msg = + getPushNotificationText("storedMediaFile", getContactDisplayName(user)); + } + if (msg == "") { Logger("localPushNotificationNewMessage") .shout("No push notification type defined!"); diff --git a/lib/src/views/camera_to_share/share_image_editor_view.dart b/lib/src/views/camera_to_share/share_image_editor_view.dart index cfe9666..58f8af9 100644 --- a/lib/src/views/camera_to_share/share_image_editor_view.dart +++ b/lib/src/views/camera_to_share/share_image_editor_view.dart @@ -10,6 +10,7 @@ import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/providers/api/media.dart'; import 'package:twonly/src/providers/send_next_media_to.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/camera_to_share/share_image_view.dart'; import 'dart:async'; import 'package:flutter/services.dart'; @@ -35,7 +36,7 @@ class _ShareImageEditorView extends State { bool _imageSaved = false; bool _imageSaving = false; bool _isRealTwonly = false; - int _maxShowTime = 18; + int maxShowTime = 999999; String? sendNextMediaToUserName; ImageItem currentImage = ImageItem(); @@ -44,9 +45,20 @@ class _ShareImageEditorView extends State { @override void initState() { super.initState(); + initAsync(); loadImage(widget.imageBytes); } + void initAsync() async { + final user = await getUser(); + if (user == null) return; + if (user.defaultShowTime != null) { + setState(() { + maxShowTime = user.defaultShowTime!; + }); + } + } + @override void dispose() { layers.clear(); @@ -114,24 +126,25 @@ class _ShareImageEditorView extends State { ), const SizedBox(height: 8), NotificationBadge( - count: _maxShowTime == 999999 ? "∞" : _maxShowTime.toString(), + count: maxShowTime == 999999 ? "∞" : maxShowTime.toString(), // count: "", child: ActionButton( FontAwesomeIcons.stopwatch, tooltipText: context.lang.protectAsARealTwonly, - // disable: _isRealTwonly, onPressed: () async { - if (_maxShowTime == 999999) { - _maxShowTime = 4; - } else if (_maxShowTime >= 22) { - _maxShowTime = 999999; + if (maxShowTime == 999999) { + maxShowTime = 4; + } else if (maxShowTime >= 22) { + maxShowTime = 999999; } else { - _maxShowTime = _maxShowTime + 4; + maxShowTime = maxShowTime + 8; } setState(() {}); - - // _maxShowTime = - // _isRealTwonly = !_isRealTwonly; + var user = await getUser(); + if (user != null) { + user.defaultShowTime = maxShowTime; + updateUser(user); + } }, ), ), @@ -145,7 +158,7 @@ class _ShareImageEditorView extends State { onPressed: () async { _isRealTwonly = !_isRealTwonly; if (_isRealTwonly) { - _maxShowTime = 12; + maxShowTime = 12; } setState(() {}); }, @@ -369,7 +382,7 @@ class _ShareImageEditorView extends State { builder: (context) => ShareImageView( imageBytesFuture: imageBytes, isRealTwonly: _isRealTwonly, - maxShowTime: _maxShowTime, + maxShowTime: maxShowTime, ), ), ); @@ -387,7 +400,7 @@ class _ShareImageEditorView extends State { [sendNextMediaToUserId], imageBytes!, _isRealTwonly, - _maxShowTime, + maxShowTime, ); Navigator.popUntil(context, (route) => route.isFirst); globalUpdateOfHomeViewPageIndex(1); @@ -402,7 +415,7 @@ class _ShareImageEditorView extends State { builder: (context) => ShareImageView( imageBytesFuture: imageBytes, isRealTwonly: _isRealTwonly, - maxShowTime: _maxShowTime, + maxShowTime: maxShowTime, ), ), ); diff --git a/lib/src/views/chats/chat_item_details_view.dart b/lib/src/views/chats/chat_item_details_view.dart index 37b1c81..ea486f3 100644 --- a/lib/src/views/chats/chat_item_details_view.dart +++ b/lib/src/views/chats/chat_item_details_view.dart @@ -74,7 +74,9 @@ class ChatListEntry extends StatelessWidget { } } else if (content is MediaMessageContent && !content.isVideo) { Color color = getMessageColorFromType( - content, Theme.of(context).colorScheme.primary); + content, + Theme.of(context).colorScheme.primary, + ); child = GestureDetector( onTap: () { @@ -112,6 +114,26 @@ class ChatListEntry extends StatelessWidget { ), ), ); + } else if (message.kind == MessageKind.storedMediaFile) { + child = Container( + padding: EdgeInsets.all(5), + width: 150, + decoration: BoxDecoration( + border: Border.all( + color: messageKindColors["text"]!, + width: 1.0, + ), + borderRadius: BorderRadius.circular(12.0), + ), + child: Align( + alignment: Alignment.centerRight, + child: MessageSendStateIcon( + [message], + mainAxisAlignment: + right ? MainAxisAlignment.center : MainAxisAlignment.center, + ), + ), + ); } return Align( @@ -181,9 +203,8 @@ class _ChatItemDetailsViewState extends State { notifyContactAboutOpeningMessage(widget.userid, msg.messageOtherId!); } } - if (updated) { - twonlyDatabase.messagesDao.openedAllTextMessages(widget.userid); - } else { + twonlyDatabase.messagesDao.openedAllNonMediaMessages(widget.userid); + if (!updated) { // The stream should be get an update, so only update the UI when all are opened setState(() { messages = msgs; diff --git a/lib/src/views/chats/media_viewer_view.dart b/lib/src/views/chats/media_viewer_view.dart index 5d123b2..bdb8c97 100644 --- a/lib/src/views/chats/media_viewer_view.dart +++ b/lib/src/views/chats/media_viewer_view.dart @@ -45,6 +45,9 @@ class _MediaViewerViewState extends State { bool isRealTwonly = false; bool isDownloading = false; + bool imageSaved = false; + bool imageSaving = false; + List allMediaFiles = []; late StreamSubscription> _subscription; @@ -95,10 +98,11 @@ class _MediaViewerViewState extends State { MediaMessageContent.fromJson(jsonDecode(current.contentJson!)); setState(() { - // reset current image values imageBytes = null; canBeSeenUntil = null; maxShowTime = 999999; + imageSaving = false; + imageSaved = false; progress = 0; isDownloading = false; isRealTwonly = false; @@ -281,7 +285,7 @@ class _MediaViewerViewState extends State { ), AnimatedPositioned( duration: Duration(milliseconds: 200), // Animation duration - bottom: showShortReactions ? 130 : 90, + bottom: showShortReactions ? 100 : 90, left: showShortReactions ? 0 : 150, right: showShortReactions ? 0 : 150, curve: Curves.linearToEaseOut, @@ -341,32 +345,65 @@ class _MediaViewerViewState extends State { ), if (imageBytes != null) Positioned( - bottom: 30, + bottom: 0, left: 0, right: 0, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - IconButton.outlined( - icon: FaIcon(FontAwesomeIcons.camera), - onPressed: () async { - context - .read() - .updateSendNextMediaTo(widget.userId.toInt()); - globalUpdateOfHomeViewPageIndex(0); - Navigator.popUntil(context, (route) => route.isFirst); - }, - style: ButtonStyle( - padding: WidgetStateProperty.all( - EdgeInsets.symmetric(vertical: 10, horizontal: 30), + if (maxShowTime == 999999) + OutlinedButton( + style: OutlinedButton.styleFrom( + iconColor: imageSaved + ? Theme.of(context).colorScheme.outline + : Theme.of(context).colorScheme.primary, + foregroundColor: imageSaved + ? Theme.of(context).colorScheme.outline + : Theme.of(context).colorScheme.primary, + ), + onPressed: () async { + setState(() { + imageSaving = true; + }); + encryptAndSendMessage( + null, + widget.userId, + MessageJson( + kind: MessageKind.storedMediaFile, + messageId: allMediaFiles.first.messageId, + content: StoredMediaFileContent( + messageId: allMediaFiles.first.messageId, + ), + timestamp: DateTime.now(), + ), + ); + final res = await saveImageToGallery(imageBytes!); + if (res == null) { + setState(() { + imageSaving = false; + imageSaved = true; + }); + } + }, + child: Row( + children: [ + imageSaving + ? SizedBox( + width: 10, + height: 10, + child: CircularProgressIndicator( + strokeWidth: 1)) + : imageSaved + ? Icon(Icons.check) + : FaIcon(FontAwesomeIcons.floppyDisk), + ], ), ), - ), SizedBox(width: 10), IconButton( icon: SizedBox( - width: 40, - height: 40, + width: 30, + height: 30, child: GridView.count( crossAxisCount: 2, children: List.generate( @@ -391,14 +428,10 @@ class _MediaViewerViewState extends State { showShortReactions = !showShortReactions; selectedShortReaction = -1; }); - // context.read().updateSendNextMediaTo( - // widget.otherUser.userId.toInt()); - // globalUpdateOfHomeViewPageIndex(0); - // Navigator.popUntil(context, (route) => route.isFirst); }, style: ButtonStyle( padding: WidgetStateProperty.all( - EdgeInsets.symmetric(vertical: 10, horizontal: 30), + EdgeInsets.symmetric(vertical: 10, horizontal: 20), ), ), ), @@ -416,7 +449,23 @@ class _MediaViewerViewState extends State { }, style: ButtonStyle( padding: WidgetStateProperty.all( - EdgeInsets.symmetric(vertical: 10, horizontal: 30), + EdgeInsets.symmetric(vertical: 10, horizontal: 20), + ), + ), + ), + SizedBox(width: 10), + IconButton.outlined( + icon: FaIcon(FontAwesomeIcons.camera), + onPressed: () async { + context + .read() + .updateSendNextMediaTo(widget.userId.toInt()); + globalUpdateOfHomeViewPageIndex(0); + Navigator.popUntil(context, (route) => route.isFirst); + }, + style: ButtonStyle( + padding: WidgetStateProperty.all( + EdgeInsets.symmetric(vertical: 10, horizontal: 20), ), ), ),