From cfd6bd92cbbffdc896e823bb9e55e836bdf0728a Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 9 Mar 2025 11:37:22 +0100 Subject: [PATCH] fix more --- lib/src/components/user_context_menu.dart | 2 +- lib/src/database/database.dart | 28 ++- lib/src/model/json/message.dart | 3 +- lib/src/providers/api/api.dart | 8 +- .../share_image_editor_view.dart | 24 ++- .../views/chats/chat_item_details_view.dart | 193 ++++++++++-------- lib/src/views/chats/chat_list_view.dart | 4 +- lib/src/views/chats/media_viewer_view.dart | 83 ++++---- 8 files changed, 192 insertions(+), 153 deletions(-) diff --git a/lib/src/components/user_context_menu.dart b/lib/src/components/user_context_menu.dart index 3158b72..50e5c47 100644 --- a/lib/src/components/user_context_menu.dart +++ b/lib/src/components/user_context_menu.dart @@ -44,7 +44,7 @@ class _UserContextMenuState extends State { onSelect: () { Navigator.push(context, MaterialPageRoute( builder: (context) { - return ChatItemDetailsView(user: widget.contact); + return ChatItemDetailsView(widget.contact.userId); }, )); }, diff --git a/lib/src/database/database.dart b/lib/src/database/database.dart index 8a328c9..0dc3656 100644 --- a/lib/src/database/database.dart +++ b/lib/src/database/database.dart @@ -26,20 +26,35 @@ class TwonlyDatabase extends _$TwonlyDatabase { // ------------ - Stream> watchMessageNotOpened(int userId) { + Stream> watchMessageNotOpened(int contactId) { return (select(messages) - ..where((t) => t.openedAt.isNull() & t.contactId.equals(userId))) + ..where((t) => t.openedAt.isNull() & t.contactId.equals(contactId))) .watch(); } - Stream watchLastMessage(int userId) { + Stream watchLastMessage(int contactId) { return (select(messages) - ..where((t) => t.contactId.equals(userId)) + ..where((t) => t.contactId.equals(contactId)) ..orderBy([(t) => OrderingTerm.desc(t.sendAt)]) ..limit(1)) .watchSingleOrNull(); } + Stream> watchAllMessagesFrom(int contactId) { + return (select(messages)..where((t) => t.contactId.equals(contactId))) + .watch(); + } + + Future openedAllTextMessages(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))) + .write(updates); + } + // ------------ Future insertContact(ContactsCompanion contact) { @@ -72,6 +87,11 @@ class TwonlyDatabase extends _$TwonlyDatabase { return (select(contacts)..where((t) => t.accepted.equals(false))).watch(); } + Stream watchContact(int userid) { + return (select(contacts)..where((t) => t.userId.equals(userid))) + .watchSingle(); + } + Stream> watchContactsForChatList() { return (select(contacts) ..where((t) => t.accepted.equals(true) & t.blocked.equals(false)) diff --git a/lib/src/model/json/message.dart b/lib/src/model/json/message.dart index 9622865..8337415 100644 --- a/lib/src/model/json/message.dart +++ b/lib/src/model/json/message.dart @@ -10,10 +10,9 @@ enum MessageKind { ack } -Color getMessageColorFromType(MessageJson msg, Color primary) { +Color getMessageColorFromType(MessageContent content, Color primary) { Color color; - final content = msg.content; if (content is TextMessageContent) { color = Colors.lightBlue; } else { diff --git a/lib/src/providers/api/api.dart b/lib/src/providers/api/api.dart index 40b3a42..7ad566d 100644 --- a/lib/src/providers/api/api.dart +++ b/lib/src/providers/api/api.dart @@ -301,12 +301,12 @@ Future tryDownloadMedia(int messageId, int fromUserId, List mediaToken, apiProvider.triggerDownload(mediaToken, offset); } -Future userOpenedOtherMessage(int fromUserId, int messageOtherId) async { - await DbMessages.userOpenedOtherMessage(fromUserId, messageOtherId); +Future notifyContactAboutOpeningMessage(int fromUserId, int messageOtherId) async { + //await DbMessages.userOpenedOtherMessage(fromUserId, messageOtherId); encryptAndSendMessage( - Int64(fromUserId), - Message( + fromUserId, + MessageJson( kind: MessageKind.opened, messageId: messageOtherId, content: MessageContent(), 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 f7df2a1..43a338c 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 @@ -1,12 +1,12 @@ -import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; import 'package:twonly/src/components/image_editor/action_button.dart'; import 'package:twonly/src/components/media_view_sizing.dart'; import 'package:twonly/src/components/notification_badge.dart'; +import 'package:twonly/src/database/contacts_db.dart'; +import 'package:twonly/src/database/database.dart'; import 'package:twonly/src/providers/api/api.dart'; -import 'package:twonly/src/providers/contacts_change_provider.dart'; import 'package:twonly/src/providers/send_next_media_to.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera_to_share/share_image_view.dart'; @@ -35,6 +35,7 @@ class _ShareImageEditorView extends State { bool _imageSaving = false; bool _isRealTwonly = false; int _maxShowTime = 18; + String? sendNextMediaToUserName; ImageItem currentImage = ImageItem(); ScreenshotController screenshotController = ScreenshotController(); @@ -51,6 +52,15 @@ class _ShareImageEditorView extends State { super.dispose(); } + Future updateAsync(int userId) async { + if (sendNextMediaToUserName != null) return; + Contact? contact = + await context.db.getContactByUserId(userId).getSingleOrNull(); + if (contact != null) { + sendNextMediaToUserName = getContactDisplayName(contact); + } + } + List get actionsAtTheRight { if (layers.isNotEmpty && layers.last.isEditing && @@ -229,13 +239,9 @@ class _ShareImageEditorView extends State { int? sendNextMediaToUserId = context.watch().sendNextMediaToUserId; - String? sendNextMediaToUserName; + if (sendNextMediaToUserId != null) { - sendNextMediaToUserName = context - .watch() - .allContacts - .firstWhere((x) => x.userId == sendNextMediaToUserId) - .displayName; + updateAsync(sendNextMediaToUserId); } return Scaffold( @@ -406,7 +412,7 @@ class _ShareImageEditorView extends State { label: Text( (sendNextMediaToUserName == null) ? context.lang.shareImagedEditorShareWith - : sendNextMediaToUserName, + : sendNextMediaToUserName!, style: TextStyle(fontSize: 17), ), ), diff --git a/lib/src/views/chats/chat_item_details_view.dart b/lib/src/views/chats/chat_item_details_view.dart index f2c7133..b3c136b 100644 --- a/lib/src/views/chats/chat_item_details_view.dart +++ b/lib/src/views/chats/chat_item_details_view.dart @@ -1,4 +1,6 @@ +import 'dart:async'; import 'dart:collection'; +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; @@ -6,13 +8,11 @@ import 'package:twonly/src/components/animate_icon.dart'; import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/components/message_send_state_icon.dart'; import 'package:twonly/src/components/verified_shield.dart'; -import '../../../../.blocked/archives/contacts_model.dart'; +import 'package:twonly/src/database/contacts_db.dart'; +import 'package:twonly/src/database/database.dart'; +import 'package:twonly/src/database/messages_db.dart'; import 'package:twonly/src/model/json/message.dart'; -import '../../../../.blocked/archives/messages_model.dart'; import 'package:twonly/src/providers/api/api.dart'; -import 'package:twonly/src/providers/contacts_change_provider.dart'; -import 'package:twonly/src/providers/download_change_provider.dart'; -import 'package:twonly/src/providers/messages_change_provider.dart'; import 'package:twonly/src/providers/send_next_media_to.dart'; import 'package:twonly/src/services/notification_service.dart'; import 'package:twonly/src/views/chats/media_viewer_view.dart'; @@ -21,27 +21,25 @@ import 'package:twonly/src/views/contact/contact_view.dart'; import 'package:twonly/src/views/home_view.dart'; class ChatListEntry extends StatelessWidget { - const ChatListEntry(this.message, this.user, this.lastMessageFromSameUser, + const ChatListEntry(this.message, this.userId, this.lastMessageFromSameUser, {super.key}); - final DbMessage message; - final Contact user; + final Message message; + final int userId; final bool lastMessageFromSameUser; @override Widget build(BuildContext context) { bool right = message.messageOtherId == null; - MessageSendState state = message.getSendState(); + MessageSendState state = messageSendStateFromMessage(message); bool isDownloading = false; List token = []; - final content = message.messageContent; - if (message.messageReceived && content is MediaMessageContent) { + final messageJson = MessageJson.fromJson(jsonDecode(message.contentJson!)); + final content = messageJson.content; + if (message.messageOtherId != null && content is MediaMessageContent) { token = content.downloadToken; - isDownloading = context - .watch() - .currentlyDownloading - .contains(token.toString()); + isDownloading = message.downloadState == DownloadState.downloading; } Widget child = Container(); @@ -81,20 +79,21 @@ class ChatListEntry extends StatelessWidget { ); } } else if (content is MediaMessageContent && !content.isVideo) { - Color color = message.messageContent - .getColor(Theme.of(context).colorScheme.primary); + Color color = getMessageColorFromType( + content, Theme.of(context).colorScheme.primary); + child = GestureDetector( onTap: () { if (state == MessageSendState.received && !isDownloading) { - if (message.isDownloaded) { + if (message.downloadState == DownloadState.downloaded) { Navigator.push( context, MaterialPageRoute(builder: (context) { - return MediaViewerView(user, message); + return MediaViewerView(userId); }), ); } else { - tryDownloadMedia(message.messageId, message.otherUserId, token, + tryDownloadMedia(message.messageId, message.contactId, token, force: true); } } @@ -112,7 +111,7 @@ class ChatListEntry extends StatelessWidget { child: Align( alignment: Alignment.centerRight, child: MessageSendStateIcon( - message, + [message], mainAxisAlignment: right ? MainAxisAlignment.center : MainAxisAlignment.center, ), @@ -134,9 +133,9 @@ class ChatListEntry extends StatelessWidget { /// Displays detailed information about a SampleItem. class ChatItemDetailsView extends StatefulWidget { - const ChatItemDetailsView({super.key, required this.user}); + const ChatItemDetailsView(this.userid, {super.key}); - final Contact user; + final int userid; @override State createState() => _ChatItemDetailsViewState(); @@ -145,86 +144,99 @@ class ChatItemDetailsView extends StatefulWidget { class _ChatItemDetailsViewState extends State { TextEditingController newMessageController = TextEditingController(); HashSet alreadyReportedOpened = HashSet(); - late Contact user; + Contact? user; String currentInputText = ""; + late StreamSubscription userSub; + late StreamSubscription> messageSub; + List messages = []; @override void initState() { super.initState(); - user = widget.user; - context - .read() - .loadMessagesForUser(user.userId.toInt()); - initAsync(); + initStreams(); } - Future initAsync() async { - context - .read() - .loadMessagesForUser(user.userId.toInt(), force: true); - setState(() {}); + @override + void dispose() { + super.dispose(); + userSub.cancel(); + messageSub.cancel(); + } + + Future initStreams() async { + Stream contact = context.db.watchContact(widget.userid); + userSub = contact.listen((contact) { + setState(() { + user = contact; + }); + }); + + Stream> msgStream = + context.db.watchAllMessagesFrom(widget.userid); + messageSub = msgStream.listen((msgs) { + if (!context.mounted) return; + var updated = false; + for (Message msg in msgs) { + if (msg.kind == MessageKind.textMessage && + msg.messageOtherId != null && + msg.openedAt == null) { + updated = true; + flutterLocalNotificationsPlugin.cancel(msg.messageId); + notifyContactAboutOpeningMessage(widget.userid, msg.messageOtherId!); + } + } + if (updated) { + context.db.openedAllTextMessages(widget.userid); + } else { + // The stream should be get an update, so only update the UI when all are opened + setState(() { + messages = msgs; + }); + } + }); } Future _sendMessage() async { - if (newMessageController.text == "") return; - setState(() {}); - await sendTextMessage(user.userId, newMessageController.text); + if (newMessageController.text == "" || user == null) return; + await sendTextMessage(user!.userId, newMessageController.text); newMessageController.clear(); currentInputText = ""; + setState(() {}); } @override Widget build(BuildContext context) { - user = context - .watch() - .allContacts - .firstWhere((c) => c.userId == widget.user.userId); - - List messages = context - .watch() - .allMessagesFromUser[user.userId.toInt()] ?? - []; - - messages.where((x) => x.messageOpenedAt == null).forEach((message) { - if (message.messageOtherId != null && - message.messageContent is TextMessageContent) { - if (!alreadyReportedOpened.contains(message.messageOtherId!)) { - userOpenedOtherMessage(message.otherUserId, message.messageOtherId!); - flutterLocalNotificationsPlugin.cancel(message.messageId); - alreadyReportedOpened.add(message.messageOtherId!); - } - } - }); - return Scaffold( appBar: AppBar( title: GestureDetector( onTap: () { Navigator.push(context, MaterialPageRoute(builder: (context) { - return ContactView(user.userId.toInt()); + return ContactView(widget.userid); })); }, - child: Row( - children: [ - InitialsAvatar( - displayName: user.displayName, - fontSize: 19, - ), - SizedBox(width: 10), - Expanded( - child: Container( - color: Colors.transparent, - child: Row( - children: [ - Text(user.displayName), - SizedBox(width: 10), - VerifiedShield(user), - ], - ), + child: (user == null) + ? Container() + : Row( + children: [ + InitialsAvatar( + getContactDisplayName(user!), + fontSize: 19, + ), + SizedBox(width: 10), + Expanded( + child: Container( + color: Colors.transparent, + child: Row( + children: [ + Text(getContactDisplayName(user!)), + SizedBox(width: 10), + VerifiedShield(user!), + ], + ), + ), + ), + ], ), - ), - ], - ), ), ), body: Column( @@ -242,17 +254,17 @@ class _ChatItemDetailsViewState extends State { (messages[i - 1].messageOtherId != null && messages[i].messageOtherId != null); } - if (messages[i].messageOpenedAt != null) { - if (calculateTimeDifference( - DateTime.now(), messages[i].messageOpenedAt!) - .inHours >= - 24) { - return Container(); - } - } + // if (messages[i].openedAt != null) { + // if (calculateTimeDifference( + // DateTime.now(), messages[i].openedAt!) + // .inHours >= + // 24) { + // return Container(); + // } + // } return ChatListEntry( messages[i], - user, + widget.userid, lastMessageFromSameUser, ); }, @@ -310,8 +322,9 @@ class _ChatItemDetailsViewState extends State { : IconButton( icon: FaIcon(FontAwesomeIcons.camera), onPressed: () { - context.read().updateSendNextMediaTo( - widget.user.userId.toInt()); + context + .read() + .updateSendNextMediaTo(widget.userid); globalUpdateOfHomeViewPageIndex(0); Navigator.popUntil(context, (route) => route.isFirst); }, diff --git a/lib/src/views/chats/chat_list_view.dart b/lib/src/views/chats/chat_list_view.dart index dc58642..f2ce88c 100644 --- a/lib/src/views/chats/chat_list_view.dart +++ b/lib/src/views/chats/chat_list_view.dart @@ -283,7 +283,7 @@ class _UserListItem extends State { Navigator.push( context, MaterialPageRoute(builder: (context) { - return MediaViewerView(widget.user, msg); + return MediaViewerView(widget.user.userId); }), ); return; @@ -291,7 +291,7 @@ class _UserListItem extends State { Navigator.push( context, MaterialPageRoute(builder: (context) { - return ChatItemDetailsView(user: widget.user); + return ChatItemDetailsView(widget.user.userId); }), ); }, diff --git a/lib/src/views/chats/media_viewer_view.dart b/lib/src/views/chats/media_viewer_view.dart index 37c04a6..b58b15c 100644 --- a/lib/src/views/chats/media_viewer_view.dart +++ b/lib/src/views/chats/media_viewer_view.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -7,11 +8,10 @@ import 'package:no_screenshot/no_screenshot.dart'; import 'package:provider/provider.dart'; import 'package:twonly/src/components/animate_icon.dart'; import 'package:twonly/src/components/media_view_sizing.dart'; -import '../../../../.blocked/archives/contacts_model.dart'; +import 'package:twonly/src/database/database.dart'; +import 'package:twonly/src/database/messages_db.dart'; import 'package:twonly/src/model/json/message.dart'; -import '../../../../.blocked/archives/messages_model.dart'; import 'package:twonly/src/providers/api/api.dart'; -import 'package:twonly/src/providers/messages_change_provider.dart'; import 'package:twonly/src/providers/send_next_media_to.dart'; import 'package:twonly/src/services/notification_service.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -21,9 +21,8 @@ import 'package:twonly/src/views/home_view.dart'; final _noScreenshot = NoScreenshot.instance; class MediaViewerView extends StatefulWidget { - final Contact otherUser; - final DbMessage message; - const MediaViewerView(this.otherUser, this.message, {super.key}); + final int userId; + const MediaViewerView(this.userId, {super.key}); @override State createState() => _MediaViewerViewState(); @@ -44,35 +43,29 @@ class _MediaViewerViewState extends State { bool isRealTwonly = false; bool isDownloading = false; - List allMediaFiles = []; + List allMediaFiles = []; + late StreamSubscription> _subscription; @override void initState() { super.initState(); - allMediaFiles = [widget.message]; asyncLoadNextMedia(); loadCurrentMediaFile(); } Future asyncLoadNextMedia() async { - await context - .read() - .loadMessagesForUser(widget.otherUser.userId.toInt()); - if (!context.mounted) return; - final allMessages = context - .read() - .allMessagesFromUser[widget.otherUser.userId.toInt()]; - if (allMessages == null) { - return; - } - final nextMediaFiles = allMessages.where((x) => - x.isMedia() && - x.messageOtherId != null && - x.messageOpenedAt == null && - x.messageId != widget.message.messageId); - allMediaFiles.addAll(nextMediaFiles.map((x) => x)); - setState(() {}); + Stream> messages = + context.db.watchMessageNotOpened(widget.userId); + + _subscription = messages.listen((messages) { + for (Message msg in messages) { + if (!allMediaFiles.any((m) => m.messageId == msg.messageId)) { + allMediaFiles.add(msg); + } + } + setState(() {}); + }); } Future nextMediaOrExit() async { @@ -92,7 +85,10 @@ class _MediaViewerViewState extends State { await _noScreenshot.screenshotOff(); if (!context.mounted || allMediaFiles.isEmpty) return; - final DbMessage current = allMediaFiles.first; + final Message current = allMediaFiles.first; + final MessageJson messageJson = + MessageJson.fromJson(jsonDecode(current.contentJson!)); + final MessageContent? content = messageJson.content; setState(() { // reset current image values @@ -101,16 +97,19 @@ class _MediaViewerViewState extends State { maxShowTime = 999999; progress = 0; isDownloading = false; - isRealTwonly = current.isRealTwonly(); + isRealTwonly = false; }); - // This will show the extra screen for the twonly - if (current.isRealTwonly() && !showTwonly) { - return; - } - - final content = current.messageContent; if (content is MediaMessageContent) { + if (content.isRealTwonly) { + setState(() { + isRealTwonly = true; + }); + if (!showTwonly) { + return; + } + } + if (isRealTwonly) { bool isAuth = await authenticateUser(context.lang.mediaViewerAuthReason, force: false); @@ -120,22 +119,22 @@ class _MediaViewerViewState extends State { } } flutterLocalNotificationsPlugin.cancel(current.messageId); - if (!current.isDownloaded) { + if (current.downloadState == DownloadState.pending) { setState(() { isDownloading = true; }); await tryDownloadMedia( - current.messageId, current.otherUserId, content.downloadToken, + current.messageId, current.contactId, content.downloadToken, force: true); } do { if (isDownloading) { - await Future.delayed(Duration(milliseconds: 100)); + await Future.delayed(Duration(milliseconds: 10)); } imageBytes = await getDownloadedMedia( content.downloadToken, current.messageOtherId!, - current.otherUserId, + current.contactId, ); } while (isDownloading && imageBytes == null); @@ -181,6 +180,7 @@ class _MediaViewerViewState extends State { nextMediaTimer?.cancel(); progressTimer?.cancel(); _noScreenshot.screenshotOn(); + _subscription.cancel(); } @override @@ -303,7 +303,7 @@ class _MediaViewerViewState extends State { curve: Curves.linearToEaseOut, child: GestureDetector( onTap: () { - sendTextMessage(widget.otherUser.userId, emoji); + sendTextMessage(widget.userId, emoji); setState(() { selectedShortReaction = index; }); @@ -351,8 +351,9 @@ class _MediaViewerViewState extends State { IconButton.outlined( icon: FaIcon(FontAwesomeIcons.camera), onPressed: () async { - context.read().updateSendNextMediaTo( - widget.otherUser.userId.toInt()); + context + .read() + .updateSendNextMediaTo(widget.userId.toInt()); globalUpdateOfHomeViewPageIndex(0); Navigator.popUntil(context, (route) => route.isFirst); }, @@ -410,7 +411,7 @@ class _MediaViewerViewState extends State { Navigator.push( context, MaterialPageRoute(builder: (context) { - return ChatItemDetailsView(user: widget.otherUser); + return ChatItemDetailsView(widget.userId); }), ); },