message deletion

This commit is contained in:
otsmr 2025-10-27 00:18:05 +01:00
parent c67bd6b464
commit 8f8f2cabe0
12 changed files with 120 additions and 56 deletions

View file

@ -8,6 +8,7 @@ import 'package:flutter/services.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/app.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/connection.provider.dart';
@ -19,8 +20,6 @@ import 'package:twonly/src/services/fcm.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'app.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await initFCMService(); await initFCMService();

View file

@ -155,12 +155,15 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
} }
Future<void> handleMessageDeletion( Future<void> handleMessageDeletion(
int contactId, int? contactId,
String messageId, String messageId,
DateTime timestamp, DateTime timestamp,
) async { ) async {
final msg = await getMessageById(messageId).getSingleOrNull(); final msg = await getMessageById(messageId).getSingleOrNull();
if (msg == null || msg.senderId != contactId) return; if (msg == null || msg.senderId != contactId) {
Log.error('Message does not exists or contact is not owner.');
return;
}
if (msg.mediaId != null) { if (msg.mediaId != null) {
await (delete(mediaFiles)..where((t) => t.mediaId.equals(msg.mediaId!))) await (delete(mediaFiles)..where((t) => t.mediaId.equals(msg.mediaId!)))
.go(); .go();
@ -176,7 +179,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
await (update(messages) await (update(messages)
..where( ..where(
(t) => t.messageId.equals(messageId) & t.senderId.equals(contactId), (t) => t.messageId.equals(messageId),
)) ))
.write( .write(
const MessagesCompanion( const MessagesCompanion(

View file

@ -20,7 +20,7 @@ class Messages extends Table {
TextColumn get content => text().nullable()(); TextColumn get content => text().nullable()();
TextColumn get mediaId => text() TextColumn get mediaId => text()
.nullable() .nullable()
.references(MediaFiles, #mediaId, onDelete: KeyAction.cascade)(); .references(MediaFiles, #mediaId, onDelete: KeyAction.setNull)();
BoolColumn get mediaStored => boolean().withDefault(const Constant(false))(); BoolColumn get mediaStored => boolean().withDefault(const Constant(false))();

View file

@ -2243,7 +2243,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
type: DriftSqlType.string, type: DriftSqlType.string,
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways( defaultConstraints: GeneratedColumn.constraintIsAlways(
'REFERENCES media_files (media_id) ON DELETE CASCADE')); 'REFERENCES media_files (media_id) ON DELETE SET NULL'));
static const VerificationMeta _mediaStoredMeta = static const VerificationMeta _mediaStoredMeta =
const VerificationMeta('mediaStored'); const VerificationMeta('mediaStored');
@override @override
@ -6464,7 +6464,7 @@ abstract class _$TwonlyDB extends GeneratedDatabase {
on: TableUpdateQuery.onTableName('media_files', on: TableUpdateQuery.onTableName('media_files',
limitUpdateKind: UpdateKind.delete), limitUpdateKind: UpdateKind.delete),
result: [ result: [
TableUpdate('messages', kind: UpdateKind.delete), TableUpdate('messages', kind: UpdateKind.update),
], ],
), ),
WritePropagation( WritePropagation(

View file

@ -291,7 +291,8 @@
"tutorialChatMessagesReopenMessageDesc": "Wenn dein Freund dir ein Bild oder Video mit unendlicher Anzeigezeit gesendet hat, kannst du es bis zum Neustart der App jederzeit erneut öffnen. Um dies zu tun, musst du einfach doppelt auf die Nachricht klicken. Dein Freund erhält dann eine Benachrichtigung, dass du das Bild erneut angesehen hast.", "tutorialChatMessagesReopenMessageDesc": "Wenn dein Freund dir ein Bild oder Video mit unendlicher Anzeigezeit gesendet hat, kannst du es bis zum Neustart der App jederzeit erneut öffnen. Um dies zu tun, musst du einfach doppelt auf die Nachricht klicken. Dein Freund erhält dann eine Benachrichtigung, dass du das Bild erneut angesehen hast.",
"memoriesEmpty": "Sobald du Bilder oder Videos speicherst, landen sie hier in deinen Erinnerungen.", "memoriesEmpty": "Sobald du Bilder oder Videos speicherst, landen sie hier in deinen Erinnerungen.",
"deleteTitle": "Bist du dir sicher?", "deleteTitle": "Bist du dir sicher?",
"deleteOkBtn": "Für mich löschen", "deleteOkBtnForAll": "Für alle löschen",
"deleteOkBtnForMe": "Für mich löschen",
"deleteImageTitle": "Bist du dir sicher?", "deleteImageTitle": "Bist du dir sicher?",
"deleteImageBody": "Das Bild wird unwiderruflich gelöscht.", "deleteImageBody": "Das Bild wird unwiderruflich gelöscht.",
"backupNoticeTitle": "Kein Backup konfiguriert", "backupNoticeTitle": "Kein Backup konfiguriert",

View file

@ -443,7 +443,8 @@
"tutorialChatMessagesReopenMessageDesc": "If your friend has sent you a picture or video with infinite display time, you can open it again at any time until you restart the app. To do this, simply double-click on the message. Your friend will then receive a notification that you have viewed the picture again.", "tutorialChatMessagesReopenMessageDesc": "If your friend has sent you a picture or video with infinite display time, you can open it again at any time until you restart the app. To do this, simply double-click on the message. Your friend will then receive a notification that you have viewed the picture again.",
"memoriesEmpty": "As soon as you save pictures or videos, they end up here in your memories.", "memoriesEmpty": "As soon as you save pictures or videos, they end up here in your memories.",
"deleteTitle": "Are you sure?", "deleteTitle": "Are you sure?",
"deleteOkBtn": "Delete for me", "deleteOkBtnForAll": "Delete for all",
"deleteOkBtnForMe": "Delete for me",
"deleteImageTitle": "Are you sure?", "deleteImageTitle": "Are you sure?",
"deleteImageBody": "The image will be irrevocably deleted.", "deleteImageBody": "The image will be irrevocably deleted.",
"settingsBackup": "Backup", "settingsBackup": "Backup",

View file

@ -1760,11 +1760,17 @@ abstract class AppLocalizations {
/// **'Are you sure?'** /// **'Are you sure?'**
String get deleteTitle; String get deleteTitle;
/// No description provided for @deleteOkBtn. /// No description provided for @deleteOkBtnForAll.
///
/// In en, this message translates to:
/// **'Delete for all'**
String get deleteOkBtnForAll;
/// No description provided for @deleteOkBtnForMe.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Delete for me'** /// **'Delete for me'**
String get deleteOkBtn; String get deleteOkBtnForMe;
/// No description provided for @deleteImageTitle. /// No description provided for @deleteImageTitle.
/// ///

View file

@ -930,7 +930,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get deleteTitle => 'Bist du dir sicher?'; String get deleteTitle => 'Bist du dir sicher?';
@override @override
String get deleteOkBtn => 'Für mich löschen'; String get deleteOkBtnForAll => 'Für alle löschen';
@override
String get deleteOkBtnForMe => 'Für mich löschen';
@override @override
String get deleteImageTitle => 'Bist du dir sicher?'; String get deleteImageTitle => 'Bist du dir sicher?';

View file

@ -924,7 +924,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get deleteTitle => 'Are you sure?'; String get deleteTitle => 'Are you sure?';
@override @override
String get deleteOkBtn => 'Delete for me'; String get deleteOkBtnForAll => 'Delete for all';
@override
String get deleteOkBtnForMe => 'Delete for me';
@override @override
String get deleteImageTitle => 'Are you sure?'; String get deleteImageTitle => 'Are you sure?';

View file

@ -118,6 +118,14 @@ class _ChatListEntryState extends State<ChatListEntry> {
alignment: alignment:
right ? Alignment.centerRight : Alignment.centerLeft, right ? Alignment.centerRight : Alignment.centerLeft,
children: [ children: [
if (widget.message.isDeletedFromSender)
ChatTextEntry(
message: widget.message,
nextMessage: widget.nextMessage,
borderRadius: borderRadius,
minWidth: reactionsForWidth * 43,
)
else
Column( Column(
children: [ children: [
ResponseContainer( ResponseContainer(
@ -146,6 +154,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
const SizedBox(height: 20, width: 10), const SizedBox(height: 20, width: 10),
], ],
), ),
if (!widget.message.isDeletedFromSender)
Positioned( Positioned(
bottom: -20, bottom: -20,
left: 5, left: 5,

View file

@ -23,7 +23,12 @@ class ChatTextEntry extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final text = message.content ?? ''; var text = message.content ?? '';
if (message.isDeletedFromSender) {
text = 'Nachricht wurde gelöscht.';
}
if (EmojiAnimation.supported(text)) { if (EmojiAnimation.supported(text)) {
return Container( return Container(
constraints: const BoxConstraints( constraints: const BoxConstraints(
@ -37,11 +42,24 @@ class ChatTextEntry extends StatelessWidget {
); );
} }
final displayTime = !combineTextMessageWithNext(message, nextMessage); var displayTime = !combineTextMessageWithNext(message, nextMessage);
var spacerWidth = minWidth - measureTextWidth(text) - 53; var spacerWidth = minWidth - measureTextWidth(text) - 53;
if (spacerWidth < 0) spacerWidth = 0; if (spacerWidth < 0) spacerWidth = 0;
Color? color;
var expanded = false;
if (message.quotesMessageId == null) {
color = getMessageColor(message);
}
if (message.isDeletedFromSender) {
color = context.color.surfaceBright;
displayTime = false;
} else if (measureTextWidth(text) > 270 ||
message.quotesMessageId != null) {
expanded = true;
}
return Container( return Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.8, maxWidth: MediaQuery.of(context).size.width * 0.8,
@ -49,15 +67,14 @@ class ChatTextEntry extends StatelessWidget {
), ),
padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10), padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: color: color,
message.quotesMessageId == null ? getMessageColor(message) : null,
borderRadius: borderRadius, borderRadius: borderRadius,
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
if (measureTextWidth(text) > 270 || message.quotesMessageId != null) if (expanded)
Expanded( Expanded(
child: BetterText(text: text), child: BetterText(text: text),
) )

View file

@ -85,10 +85,32 @@ class MessageContextMenu extends StatelessWidget {
context, context,
context.lang.deleteTitle, context.lang.deleteTitle,
null, null,
customOk: context.lang.deleteOkBtn, customOk:
(message.senderId == null && !message.isDeletedFromSender)
? context.lang.deleteOkBtnForAll
: context.lang.deleteOkBtnForMe,
); );
if (delete) { if (delete) {
await twonlyDB.messagesDao.deleteMessagesById(message.messageId); if (message.senderId == null && !message.isDeletedFromSender) {
await twonlyDB.messagesDao.handleMessageDeletion(
null,
message.messageId,
DateTime.now(),
);
await sendCipherTextToGroup(
message.groupId,
pb.EncryptedContent(
messageUpdate: pb.EncryptedContent_MessageUpdate(
type: pb.EncryptedContent_MessageUpdate_Type.DELETE,
senderMessageId: message.messageId,
),
),
null,
);
} else {
await twonlyDB.messagesDao
.deleteMessagesById(message.messageId);
}
} }
}, },
child: const FaIcon(FontAwesomeIcons.trash), child: const FaIcon(FontAwesomeIcons.trash),