This commit is contained in:
otsmr 2025-03-09 11:37:22 +01:00
parent f7306fe7db
commit cfd6bd92cb
8 changed files with 192 additions and 153 deletions

View file

@ -44,7 +44,7 @@ class _UserContextMenuState extends State<UserContextMenu> {
onSelect: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return ChatItemDetailsView(user: widget.contact);
return ChatItemDetailsView(widget.contact.userId);
},
));
},

View file

@ -26,20 +26,35 @@ class TwonlyDatabase extends _$TwonlyDatabase {
// ------------
Stream<List<Message>> watchMessageNotOpened(int userId) {
Stream<List<Message>> 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<Message?> watchLastMessage(int userId) {
Stream<Message?> 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<List<Message>> 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<int> insertContact(ContactsCompanion contact) {
@ -72,6 +87,11 @@ class TwonlyDatabase extends _$TwonlyDatabase {
return (select(contacts)..where((t) => t.accepted.equals(false))).watch();
}
Stream<Contact> watchContact(int userid) {
return (select(contacts)..where((t) => t.userId.equals(userid)))
.watchSingle();
}
Stream<List<Contact>> watchContactsForChatList() {
return (select(contacts)
..where((t) => t.accepted.equals(true) & t.blocked.equals(false))

View file

@ -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 {

View file

@ -301,12 +301,12 @@ Future tryDownloadMedia(int messageId, int fromUserId, List<int> 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(),

View file

@ -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<ShareImageEditorView> {
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<ShareImageEditorView> {
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<Widget> get actionsAtTheRight {
if (layers.isNotEmpty &&
layers.last.isEditing &&
@ -229,13 +239,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
int? sendNextMediaToUserId =
context.watch<SendNextMediaTo>().sendNextMediaToUserId;
String? sendNextMediaToUserName;
if (sendNextMediaToUserId != null) {
sendNextMediaToUserName = context
.watch<ContactChangeProvider>()
.allContacts
.firstWhere((x) => x.userId == sendNextMediaToUserId)
.displayName;
updateAsync(sendNextMediaToUserId);
}
return Scaffold(
@ -406,7 +412,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
label: Text(
(sendNextMediaToUserName == null)
? context.lang.shareImagedEditorShareWith
: sendNextMediaToUserName,
: sendNextMediaToUserName!,
style: TextStyle(fontSize: 17),
),
),

View file

@ -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<int> 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<DownloadChangeProvider>()
.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<ChatItemDetailsView> createState() => _ChatItemDetailsViewState();
@ -145,69 +144,82 @@ class ChatItemDetailsView extends StatefulWidget {
class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
TextEditingController newMessageController = TextEditingController();
HashSet<int> alreadyReportedOpened = HashSet<int>();
late Contact user;
Contact? user;
String currentInputText = "";
late StreamSubscription<Contact> userSub;
late StreamSubscription<List<Message>> messageSub;
List<Message> messages = [];
@override
void initState() {
super.initState();
user = widget.user;
context
.read<MessagesChangeProvider>()
.loadMessagesForUser(user.userId.toInt());
initAsync();
initStreams();
}
Future initAsync() async {
context
.read<MessagesChangeProvider>()
.loadMessagesForUser(user.userId.toInt(), force: true);
setState(() {});
@override
void dispose() {
super.dispose();
userSub.cancel();
messageSub.cancel();
}
Future initStreams() async {
Stream<Contact> contact = context.db.watchContact(widget.userid);
userSub = contact.listen((contact) {
setState(() {
user = contact;
});
});
Stream<List<Message>> 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<ContactChangeProvider>()
.allContacts
.firstWhere((c) => c.userId == widget.user.userId);
List<DbMessage> messages = context
.watch<MessagesChangeProvider>()
.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(
child: (user == null)
? Container()
: Row(
children: [
InitialsAvatar(
displayName: user.displayName,
getContactDisplayName(user!),
fontSize: 19,
),
SizedBox(width: 10),
@ -216,9 +228,9 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
color: Colors.transparent,
child: Row(
children: [
Text(user.displayName),
Text(getContactDisplayName(user!)),
SizedBox(width: 10),
VerifiedShield(user),
VerifiedShield(user!),
],
),
),
@ -242,17 +254,17 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
(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<ChatItemDetailsView> {
: IconButton(
icon: FaIcon(FontAwesomeIcons.camera),
onPressed: () {
context.read<SendNextMediaTo>().updateSendNextMediaTo(
widget.user.userId.toInt());
context
.read<SendNextMediaTo>()
.updateSendNextMediaTo(widget.userid);
globalUpdateOfHomeViewPageIndex(0);
Navigator.popUntil(context, (route) => route.isFirst);
},

View file

@ -283,7 +283,7 @@ class _UserListItem extends State<UserListItem> {
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<UserListItem> {
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return ChatItemDetailsView(user: widget.user);
return ChatItemDetailsView(widget.user.userId);
}),
);
},

View file

@ -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<MediaViewerView> createState() => _MediaViewerViewState();
@ -44,35 +43,29 @@ class _MediaViewerViewState extends State<MediaViewerView> {
bool isRealTwonly = false;
bool isDownloading = false;
List<DbMessage> allMediaFiles = [];
List<Message> allMediaFiles = [];
late StreamSubscription<List<Message>> _subscription;
@override
void initState() {
super.initState();
allMediaFiles = [widget.message];
asyncLoadNextMedia();
loadCurrentMediaFile();
}
Future asyncLoadNextMedia() async {
await context
.read<MessagesChangeProvider>()
.loadMessagesForUser(widget.otherUser.userId.toInt());
if (!context.mounted) return;
final allMessages = context
.read<MessagesChangeProvider>()
.allMessagesFromUser[widget.otherUser.userId.toInt()];
if (allMessages == null) {
return;
Stream<List<Message>> 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);
}
}
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(() {});
});
}
Future nextMediaOrExit() async {
@ -92,7 +85,10 @@ class _MediaViewerViewState extends State<MediaViewerView> {
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<MediaViewerView> {
maxShowTime = 999999;
progress = 0;
isDownloading = false;
isRealTwonly = current.isRealTwonly();
isRealTwonly = false;
});
// This will show the extra screen for the twonly
if (current.isRealTwonly() && !showTwonly) {
if (content is MediaMessageContent) {
if (content.isRealTwonly) {
setState(() {
isRealTwonly = true;
});
if (!showTwonly) {
return;
}
}
final content = current.messageContent;
if (content is MediaMessageContent) {
if (isRealTwonly) {
bool isAuth = await authenticateUser(context.lang.mediaViewerAuthReason,
force: false);
@ -120,22 +119,22 @@ class _MediaViewerViewState extends State<MediaViewerView> {
}
}
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<MediaViewerView> {
nextMediaTimer?.cancel();
progressTimer?.cancel();
_noScreenshot.screenshotOn();
_subscription.cancel();
}
@override
@ -303,7 +303,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
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<MediaViewerView> {
IconButton.outlined(
icon: FaIcon(FontAwesomeIcons.camera),
onPressed: () async {
context.read<SendNextMediaTo>().updateSendNextMediaTo(
widget.otherUser.userId.toInt());
context
.read<SendNextMediaTo>()
.updateSendNextMediaTo(widget.userId.toInt());
globalUpdateOfHomeViewPageIndex(0);
Navigator.popUntil(context, (route) => route.isFirst);
},
@ -410,7 +411,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return ChatItemDetailsView(user: widget.otherUser);
return ChatItemDetailsView(widget.userId);
}),
);
},