This commit is contained in:
otsmr 2025-04-29 22:26:38 +02:00
parent 98b4c60e21
commit 586c4aa16a
14 changed files with 129 additions and 22 deletions

View file

@ -32,7 +32,7 @@ class NotificationService: UNNotificationServiceExtension {
bestAttemptContent.body = data!.body; bestAttemptContent.body = data!.body;
bestAttemptContent.threadIdentifier = String(format: "%d", data!.notificationId) bestAttemptContent.threadIdentifier = String(format: "%d", data!.notificationId)
} else { } else {
bestAttemptContent.title = "\(bestAttemptContent.title) [failed to decrypt]" bestAttemptContent.title = "\(bestAttemptContent.title) [10]"
} }
contentHandler(bestAttemptContent) contentHandler(bestAttemptContent)
@ -60,6 +60,7 @@ enum PushKind: String, Codable {
case storedMediaFile case storedMediaFile
case reaction case reaction
case testNotification case testNotification
case reopenedMedia
} }
import CryptoKit import CryptoKit
@ -106,8 +107,6 @@ func getPushNotificationData(pushDataJson: String) -> (title: String, body: Stri
// Handle the push notification based on the pushKind // Handle the push notification based on the pushKind
if let pushKind = pushKind { if let pushKind = pushKind {
let bestAttemptContent = UNMutableNotificationContent()
if pushKind == .testNotification { if pushKind == .testNotification {
return ("Test Notification", "This is a test notification.", 0) return ("Test Notification", "This is a test notification.", 0)
} else if displayName != nil && fromUserId != nil { } else if displayName != nil && fromUserId != nil {
@ -180,6 +179,8 @@ func determinePushKind(from message: String) -> PushKind? {
return .reaction return .reaction
} else if message.contains("testNotification") { } else if message.contains("testNotification") {
return .testNotification return .testNotification
} else if message.contains("reopenedMedia") {
return .reopenedMedia
} else { } else {
return nil // Unknown PushKind return nil // Unknown PushKind
} }
@ -319,7 +320,8 @@ func getPushNotificationText(pushKind: PushKind) -> String {
.contactRequest: "möchte sich mit dir vernetzen.", .contactRequest: "möchte sich mit dir vernetzen.",
.acceptRequest: "ist jetzt mit dir vernetzt.", .acceptRequest: "ist jetzt mit dir vernetzt.",
.storedMediaFile: "hat dein Bild gespeichert.", .storedMediaFile: "hat dein Bild gespeichert.",
.reaction: "hat auf dein Bild reagiert." .reaction: "hat auf dein Bild reagiert.",
.reopenedMedia: "Dein Bild wurde erneut geöffnet."
] ]
} else { // Default to English } else { // Default to English
pushNotificationText = [ pushNotificationText = [
@ -330,7 +332,8 @@ func getPushNotificationText(pushKind: PushKind) -> String {
.contactRequest: "wants to connect with you.", .contactRequest: "wants to connect with you.",
.acceptRequest: "is now connected with you.", .acceptRequest: "is now connected with you.",
.storedMediaFile: "has stored your image.", .storedMediaFile: "has stored your image.",
.reaction: "has reacted to your image." .reaction: "has reacted to your image.",
.reopenedMedia: "Your image was reopened."
] ]
} }
@ -353,7 +356,8 @@ func getPushNotificationTextWithoutUserId(pushKind: PushKind) -> String {
.contactRequest: "Du hast eine Kontaktanfrage erhalten.", .contactRequest: "Du hast eine Kontaktanfrage erhalten.",
.acceptRequest: "Deine Kontaktanfrage wurde angenommen.", .acceptRequest: "Deine Kontaktanfrage wurde angenommen.",
.storedMediaFile: "Dein Bild wurde gespeichert.", .storedMediaFile: "Dein Bild wurde gespeichert.",
.reaction: "Du hast eine Reaktion auf dein Bild erhalten." .reaction: "Du hast eine Reaktion auf dein Bild erhalten.",
.reopenedMedia: "hat dein Bild erneut geöffnet."
] ]
} else { // Default to English } else { // Default to English
pushNotificationText = [ pushNotificationText = [
@ -364,7 +368,8 @@ func getPushNotificationTextWithoutUserId(pushKind: PushKind) -> String {
.contactRequest: "You got a contact request.", .contactRequest: "You got a contact request.",
.acceptRequest: "Your contact request has been accepted.", .acceptRequest: "Your contact request has been accepted.",
.storedMediaFile: "Your image has been saved.", .storedMediaFile: "Your image has been saved.",
.reaction: "You got a reaction to your image." .reaction: "You got a reaction to your image.",
.reopenedMedia: "has reopened your image."
] ]
} }

View file

@ -4,6 +4,7 @@ import 'package:twonly/src/database/tables/contacts_table.dart';
enum MessageKind { enum MessageKind {
textMessage, textMessage,
storedMediaFile, storedMediaFile,
reopenedMedia,
media, media,
contactRequest, contactRequest,
profileChange, profileChange,

View file

@ -80,7 +80,9 @@
"messageSendState_Sending": "Wird gesendet", "messageSendState_Sending": "Wird gesendet",
"messageSendState_TapToLoad": "Tippe zum Laden", "messageSendState_TapToLoad": "Tippe zum Laden",
"messageSendState_Loading": "Herunterladen", "messageSendState_Loading": "Herunterladen",
"messageStoredInGalery": "In Gallerie gespeichert", "messageStoredInGalery": "Gespeichert",
"messageReopened": "Erneut geöffnet",
"@messageReopened": {},
"imageEditorDrawOk": "Zeichnung machen", "imageEditorDrawOk": "Zeichnung machen",
"settingsTitle": "Einstellungen", "settingsTitle": "Einstellungen",
"settingsChats": "Chats", "settingsChats": "Chats",

View file

@ -136,6 +136,8 @@
"@messageSendState_Loading": {}, "@messageSendState_Loading": {},
"messageStoredInGalery": "Stored in gallery", "messageStoredInGalery": "Stored in gallery",
"@messageStoredInGalery": {}, "@messageStoredInGalery": {},
"messageReopened": "Re-opened",
"@messageReopened": {},
"imageEditorDrawOk": "Take drawing", "imageEditorDrawOk": "Take drawing",
"@imageEditorDrawOk": {}, "@imageEditorDrawOk": {},
"settingsTitle": "Settings", "settingsTitle": "Settings",

View file

@ -491,6 +491,12 @@ abstract class AppLocalizations {
/// **'Stored in gallery'** /// **'Stored in gallery'**
String get messageStoredInGalery; String get messageStoredInGalery;
/// No description provided for @messageReopened.
///
/// In en, this message translates to:
/// **'Re-opened'**
String get messageReopened;
/// No description provided for @imageEditorDrawOk. /// No description provided for @imageEditorDrawOk.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View file

@ -206,7 +206,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get messageSendState_Loading => 'Herunterladen'; String get messageSendState_Loading => 'Herunterladen';
@override @override
String get messageStoredInGalery => 'In Gallerie gespeichert'; String get messageStoredInGalery => 'Gespeichert';
@override
String get messageReopened => 'Erneut geöffnet';
@override @override
String get imageEditorDrawOk => 'Zeichnung machen'; String get imageEditorDrawOk => 'Zeichnung machen';

View file

@ -208,6 +208,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get messageStoredInGalery => 'Stored in gallery'; String get messageStoredInGalery => 'Stored in gallery';
@override
String get messageReopened => 'Re-opened';
@override @override
String get imageEditorDrawOk => 'Take drawing'; String get imageEditorDrawOk => 'Take drawing';

View file

@ -19,7 +19,7 @@ Color getMessageColorFromType(MessageContent content, BuildContext context) {
} }
} }
} else { } else {
return Colors.black; return (isDarkMode(context)) ? Colors.white : Colors.black;
} }
} }
return color; return color;
@ -85,6 +85,8 @@ class MessageContent {
return StoredMediaFileContent.fromJson(json); return StoredMediaFileContent.fromJson(json);
case MessageKind.pushKey: case MessageKind.pushKey:
return PushKeyContent.fromJson(json); return PushKeyContent.fromJson(json);
case MessageKind.reopenedMedia:
return ReopenedMediaFileContent.fromJson(json);
default: default:
return null; return null;
} }
@ -188,6 +190,20 @@ class StoredMediaFileContent extends MessageContent {
} }
} }
class ReopenedMediaFileContent extends MessageContent {
int messageId;
ReopenedMediaFileContent({required this.messageId});
static ReopenedMediaFileContent fromJson(Map json) {
return ReopenedMediaFileContent(messageId: json['messageId']);
}
@override
Map toJson() {
return {'messageId': messageId};
}
}
class ProfileContent extends MessageContent { class ProfileContent extends MessageContent {
String avatarSvg; String avatarSvg;
String displayName; String displayName;

View file

@ -274,6 +274,12 @@ Future<Uint8List?> readMediaFile(int mediaId, String type) async {
return await file.readAsBytes(); return await file.readAsBytes();
} }
Future<bool> existsMediaFile(int mediaId, String type) async {
String basePath = await getMediaFilePath(mediaId, "received");
File file = File("$basePath.$type");
return await file.exists();
}
Future<void> writeMediaFile(int mediaId, String type, Uint8List data) async { Future<void> writeMediaFile(int mediaId, String type, Uint8List data) async {
String basePath = await getMediaFilePath(mediaId, "received"); String basePath = await getMediaFilePath(mediaId, "received");
File file = File("$basePath.$type"); File file = File("$basePath.$type");

View file

@ -130,7 +130,8 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
default: default:
if (message.kind != MessageKind.textMessage && if (message.kind != MessageKind.textMessage &&
message.kind != MessageKind.media && message.kind != MessageKind.media &&
message.kind != MessageKind.storedMediaFile) { message.kind != MessageKind.storedMediaFile &&
message.kind != MessageKind.reopenedMedia) {
Logger("handleServerMessages") Logger("handleServerMessages")
.shout("Got unknown MessageKind $message"); .shout("Got unknown MessageKind $message");
} else if (message.content == null || message.messageId == null) { } else if (message.content == null || message.messageId == null) {
@ -147,7 +148,8 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
bool acknowledgeByUser = false; bool acknowledgeByUser = false;
DateTime? openedAt; DateTime? openedAt;
if (message.kind == MessageKind.storedMediaFile) { if (message.kind == MessageKind.storedMediaFile ||
message.kind == MessageKind.reopenedMedia) {
acknowledgeByUser = true; acknowledgeByUser = true;
openedAt = DateTime.now(); openedAt = DateTime.now();
} }
@ -159,6 +161,9 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
if (content is TextMessageContent) { if (content is TextMessageContent) {
responseToMessageId = content.responseToMessageId; responseToMessageId = content.responseToMessageId;
} }
if (content is ReopenedMediaFileContent) {
responseToMessageId = content.messageId;
}
if (content is StoredMediaFileContent) { if (content is StoredMediaFileContent) {
responseToMessageId = content.messageId; responseToMessageId = content.messageId;
await twonlyDatabase.messagesDao.updateMessageByOtherUser( await twonlyDatabase.messagesDao.updateMessageByOtherUser(

View file

@ -163,7 +163,8 @@ enum PushKind {
contactRequest, contactRequest,
acceptRequest, acceptRequest,
storedMediaFile, storedMediaFile,
testNotification testNotification,
reopenedMedia
} }
extension PushKindExtension on PushKind { extension PushKindExtension on PushKind {
@ -532,7 +533,8 @@ String getPushNotificationTextWithoutUserId(PushKind pushKind) {
PushKind.contactRequest.name: "Du hast eine Kontaktanfrage erhalten.", PushKind.contactRequest.name: "Du hast eine Kontaktanfrage erhalten.",
PushKind.acceptRequest.name: "Deine Kontaktanfrage wurde angenommen.", PushKind.acceptRequest.name: "Deine Kontaktanfrage wurde angenommen.",
PushKind.storedMediaFile.name: "Dein Bild wurde gespeichert.", PushKind.storedMediaFile.name: "Dein Bild wurde gespeichert.",
PushKind.reaction.name: "Du hast eine Reaktion auf dein Bild erhalten." PushKind.reaction.name: "Du hast eine Reaktion auf dein Bild erhalten.",
PushKind.reopenedMedia.name: "Dein Bild wurde erneut geöffnet."
}; };
} else { } else {
pushNotificationText = { pushNotificationText = {
@ -543,7 +545,8 @@ String getPushNotificationTextWithoutUserId(PushKind pushKind) {
PushKind.contactRequest.name: "You got a contact request.", PushKind.contactRequest.name: "You got a contact request.",
PushKind.acceptRequest.name: "Your contact request has been accepted.", PushKind.acceptRequest.name: "Your contact request has been accepted.",
PushKind.storedMediaFile.name: "Your image has been saved.", PushKind.storedMediaFile.name: "Your image has been saved.",
PushKind.reaction.name: "You got a reaction to your image." PushKind.reaction.name: "You got a reaction to your image.",
PushKind.reopenedMedia.name: "Your image was reopened."
}; };
} }
return pushNotificationText[pushKind.name] ?? ""; return pushNotificationText[pushKind.name] ?? "";
@ -563,7 +566,8 @@ String getPushNotificationText(PushKind pushKind) {
PushKind.contactRequest.name: "möchte sich mir dir vernetzen.", PushKind.contactRequest.name: "möchte sich mir dir vernetzen.",
PushKind.acceptRequest.name: "ist jetzt mit dir vernetzt.", PushKind.acceptRequest.name: "ist jetzt mit dir vernetzt.",
PushKind.storedMediaFile.name: "hat dein Bild gespeichert.", PushKind.storedMediaFile.name: "hat dein Bild gespeichert.",
PushKind.reaction.name: "hat auf dein Bild reagiert." PushKind.reaction.name: "hat auf dein Bild reagiert.",
PushKind.reopenedMedia.name: "hat dein Bild erneut geöffnet."
}; };
} else { } else {
pushNotificationText = { pushNotificationText = {
@ -574,7 +578,8 @@ String getPushNotificationText(PushKind pushKind) {
PushKind.contactRequest.name: "wants to connect with you.", PushKind.contactRequest.name: "wants to connect with you.",
PushKind.acceptRequest.name: "is now connected with you.", PushKind.acceptRequest.name: "is now connected with you.",
PushKind.storedMediaFile.name: "has stored your image.", PushKind.storedMediaFile.name: "has stored your image.",
PushKind.reaction.name: "has reacted to your image." PushKind.reaction.name: "has reacted to your image.",
PushKind.reopenedMedia.name: "has reopened your image."
}; };
} }
return pushNotificationText[pushKind.name] ?? ""; return pushNotificationText[pushKind.name] ?? "";

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
@ -118,11 +119,35 @@ class ChatListEntry extends StatelessWidget {
Widget getReactionRow() { Widget getReactionRow() {
List<Widget> children = []; List<Widget> children = [];
bool hasOneTextReaction = false; bool hasOneTextReaction = false;
bool hasOneStored = false;
bool hasOneReopened = false;
for (final reaction in reactions) { for (final reaction in reactions) {
MessageContent? content = MessageContent.fromJson( MessageContent? content = MessageContent.fromJson(
reaction.kind, jsonDecode(reaction.contentJson!)); reaction.kind, jsonDecode(reaction.contentJson!));
if (content is StoredMediaFileContent || message.mediaStored) { // if (content is StoredMediaFileContent || message.mediaStored) {
// if (hasOneStored) continue;
// hasOneStored = true;
// children.add(
// Expanded(
// child: Align(
// alignment: Alignment.bottomRight,
// child: Padding(
// padding: EdgeInsets.only(right: 3),
// child: FaIcon(
// FontAwesomeIcons.floppyDisk,
// size: 12,
// color: Colors.blue,
// ),
// ),
// ),
// ),
// );
// }
if (content is ReopenedMediaFileContent) {
if (hasOneReopened) continue;
hasOneReopened = true;
children.add( children.add(
Expanded( Expanded(
child: Align( child: Align(
@ -130,16 +155,15 @@ class ChatListEntry extends StatelessWidget {
child: Padding( child: Padding(
padding: EdgeInsets.only(right: 3), padding: EdgeInsets.only(right: 3),
child: FaIcon( child: FaIcon(
FontAwesomeIcons.floppyDisk, FontAwesomeIcons.repeat,
size: 12, size: 12,
color: Colors.blue, color: Colors.yellow,
), ),
), ),
), ),
), ),
); );
} }
// only show one reaction // only show one reaction
if (hasOneTextReaction) continue; if (hasOneTextReaction) continue;
@ -282,6 +306,30 @@ class ChatListEntry extends StatelessWidget {
); );
child = GestureDetector( child = GestureDetector(
onDoubleTap: () async {
if (message.openedAt == null && message.messageOtherId != null) {
return;
}
if (await existsMediaFile(message.messageId, "png")) {
encryptAndSendMessage(
null,
contact.userId,
MessageJson(
kind: MessageKind.reopenedMedia,
messageId: message.messageId,
content: ReopenedMediaFileContent(
messageId: message.messageOtherId!,
),
timestamp: DateTime.now(),
),
pushKind: PushKind.reopenedMedia,
);
await twonlyDatabase.messagesDao.updateMessageByMessageId(
message.messageId,
MessagesCompanion(openedAt: Value(null)),
);
}
},
onTap: () { onTap: () {
if (message.kind == MessageKind.media) { if (message.kind == MessageKind.media) {
if (message.downloadState == DownloadState.downloaded && if (message.downloadState == DownloadState.downloaded &&

View file

@ -103,7 +103,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
progressTimer?.cancel(); progressTimer?.cancel();
if (allMediaFiles.isNotEmpty) { if (allMediaFiles.isNotEmpty) {
try { try {
if (!imageSaved) { if (!imageSaved && maxShowTime != gMediaShowInfinite) {
await deleteMediaFile(allMediaFiles.first.messageId, "mp4"); await deleteMediaFile(allMediaFiles.first.messageId, "mp4");
await deleteMediaFile(allMediaFiles.first.messageId, "png"); await deleteMediaFile(allMediaFiles.first.messageId, "png");
} }

View file

@ -144,6 +144,11 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
text = context.lang.messageStoredInGalery; text = context.lang.messageStoredInGalery;
} }
if (message.kind == MessageKind.reopenedMedia) {
icon = FaIcon(FontAwesomeIcons.repeat, size: 12, color: color);
text = context.lang.messageReopened;
}
if (message.errorWhileSending) { if (message.errorWhileSending) {
icon = icon =
FaIcon(FontAwesomeIcons.circleExclamation, size: 12, color: color); FaIcon(FontAwesomeIcons.circleExclamation, size: 12, color: color);