purge text messages fix multiple bugs

This commit is contained in:
otsmr 2025-10-28 13:23:35 +01:00
parent 4914df5610
commit 071c5c2a0d
25 changed files with 207 additions and 69 deletions

View file

@ -57,13 +57,12 @@ void main() async {
await initFileDownloader();
unawaited(MediaFileService.purgeTempFolder());
await twonlyDB.messagesDao.purgeMessageTable();
// await twonlyDB.messagesDao.resetPendingDownloadState();
// await twonlyDB.messageRetransmissionDao.purgeOldRetransmissions();
// await twonlyDB.signalDao.purgeOutDatedPreKeys();
// Purge media files in the background
// unawaited(purgeReceivedMediaFiles());
// unawaited(purgeSendMediaFiles());
// unawaited(performTwonlySafeBackup());

View file

@ -121,7 +121,7 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
}
}
String getContactDisplayName(Contact user) {
String getContactDisplayName(Contact user, {int? maxLength}) {
var name = user.username;
if (user.nickName != null && user.nickName != '') {
name = user.nickName!;
@ -131,12 +131,19 @@ String getContactDisplayName(Contact user) {
if (user.accountDeleted) {
name = applyStrikethrough(name);
}
if (name.length > 27) {
return '${name.substring(0, 27 - 3)}...';
if (maxLength != null) {
name = substringBy(name, maxLength);
}
return name;
}
String substringBy(String string, int maxLength) {
if (string.length > maxLength) {
return '${string.substring(0, maxLength - 3)}...';
}
return string;
}
String getContactDisplayNameOld(old.Contact user) {
var name = user.username;
if (user.nickName != null && user.nickName != '') {

View file

@ -68,16 +68,20 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
}
Stream<List<Message>> watchByGroupId(String groupId) {
return ((select(messages)..where((t) => t.groupId.equals(groupId)))
return ((select(messages)
..where(
(t) =>
t.groupId.equals(groupId) &
(t.isDeletedFromSender.equals(true) |
((t.type.equals(MessageType.text.name) &
t.content.isNotNull()) |
(t.type.equals(MessageType.media.name) &
t.mediaId.isNotNull()))),
))
..orderBy([(t) => OrderingTerm.asc(t.createdAt)]))
.watch();
}
// Stream<List<GroupMember>> watchMembersByGroupId(String groupId) {
// return (select(groupMembers)..where((t) => t.groupId.equals(groupId)))
// .watch();
// }
Stream<List<(GroupMember, Contact)>> watchMembersByGroupId(String groupId) {
final query = (select(groupMembers).join([
leftOuterJoin(
@ -101,21 +105,31 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
.watchSingleOrNull();
}
// Future<void> removeOldMessages() {
// return (update(messages)
// ..where(
// (t) =>
// (t.openedAt.isSmallerThanValue(
// DateTime.now().subtract(const Duration(days: 1)),
// ) |
// (t.sendAt.isSmallerThanValue(
// DateTime.now().subtract(const Duration(days: 3)),
// ) &
// t.errorWhileSending.equals(true))) &
// t.kind.equals(MessageKind.textMessage.name),
// ))
// .write(const MessagesCompanion(contentJson: Value(null)));
// }
Future<void> purgeMessageTable() async {
final allGroups = await select(groups).get();
for (final group in allGroups) {
final deletionTime = DateTime.now().subtract(
Duration(
milliseconds: group.deleteMessagesAfterMilliseconds,
),
);
final affected = await (delete(messages)
..where(
(m) =>
m.groupId.equals(group.groupId) &
// m.messageId.equals(lastMessage.messageId).not() &
(m.mediaStored.equals(true) &
m.isDeletedFromSender.equals(true) |
m.mediaStored.equals(false)) &
(m.openedAt.isSmallerThanValue(deletionTime) |
(m.isDeletedFromSender.equals(true) &
m.createdAt.isSmallerThanValue(deletionTime))),
))
.go();
Log.info('Deleted $affected messages.');
}
}
// Future<List<Message>> getAllMessagesPendingDownloading() {
// return (select(messages)

View file

@ -18,7 +18,7 @@ class Groups extends Table {
boolean().withDefault(const Constant(false))();
IntColumn get deleteMessagesAfterMilliseconds =>
integer().withDefault(const Constant(1000 * 60 * 24))();
integer().withDefault(const Constant(1000 * 60 * 60 * 24))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();

View file

@ -734,7 +734,7 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> {
'delete_messages_after_milliseconds', aliasedName, false,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const Constant(1000 * 60 * 24));
defaultValue: const Constant(1000 * 60 * 60 * 24));
static const VerificationMeta _createdAtMeta =
const VerificationMeta('createdAt');
@override

View file

@ -352,5 +352,10 @@
"received": "Empfangen",
"opened": "Geöffnet",
"waitingForInternet": "Warten auf Internet",
"editHistory": "Bearbeitungshistorie"
"editHistory": "Bearbeitungshistorie",
"archivedChats": "Archivierte Chats",
"durationShortSecond": "Sek.",
"durationShortMinute": "Min.",
"durationShortHour": "Std",
"durationShortDays": "Tagen"
}

View file

@ -508,5 +508,10 @@
"received": "Received",
"opened": "Opened",
"waitingForInternet": "Waiting for internet",
"editHistory": "Edit history"
"editHistory": "Edit history",
"archivedChats": "Archived chats",
"durationShortSecond": "Sec.",
"durationShortMinute": "Min.",
"durationShortHour": "Hrs.",
"durationShortDays": "Days"
}

View file

@ -2155,6 +2155,36 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Edit history'**
String get editHistory;
/// No description provided for @archivedChats.
///
/// In en, this message translates to:
/// **'Archived chats'**
String get archivedChats;
/// No description provided for @durationShortSecond.
///
/// In en, this message translates to:
/// **'Sec.'**
String get durationShortSecond;
/// No description provided for @durationShortMinute.
///
/// In en, this message translates to:
/// **'Min.'**
String get durationShortMinute;
/// No description provided for @durationShortHour.
///
/// In en, this message translates to:
/// **'Hrs.'**
String get durationShortHour;
/// No description provided for @durationShortDays.
///
/// In en, this message translates to:
/// **'Days'**
String get durationShortDays;
}
class _AppLocalizationsDelegate

View file

@ -1143,4 +1143,19 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get editHistory => 'Bearbeitungshistorie';
@override
String get archivedChats => 'Archivierte Chats';
@override
String get durationShortSecond => 'Sek.';
@override
String get durationShortMinute => 'Min.';
@override
String get durationShortHour => 'Std';
@override
String get durationShortDays => 'Tagen';
}

View file

@ -1136,4 +1136,19 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get editHistory => 'Edit history';
@override
String get archivedChats => 'Archived chats';
@override
String get durationShortSecond => 'Sec.';
@override
String get durationShortMinute => 'Min.';
@override
String get durationShortHour => 'Hrs.';
@override
String get durationShortDays => 'Days';
}

View file

@ -71,7 +71,8 @@ Future<void> insertMediaFileInMessagesTable(
}
Future<void> startBackgroundMediaUpload(MediaFileService mediaService) async {
if (mediaService.mediaFile.uploadState == UploadState.initialized) {
if (mediaService.mediaFile.uploadState == UploadState.initialized ||
mediaService.mediaFile.uploadState == UploadState.preprocessing) {
await mediaService.setUploadState(UploadState.preprocessing);
if (!mediaService.tempPath.existsSync()) {
await mediaService.compressMedia();
@ -84,7 +85,9 @@ Future<void> startBackgroundMediaUpload(MediaFileService mediaService) async {
if (!mediaService.uploadRequestPath.existsSync()) {
await _createUploadRequest(mediaService);
}
await mediaService.setUploadState(UploadState.uploading);
if (mediaService.uploadRequestPath.existsSync()) {
await mediaService.setUploadState(UploadState.uploading);
}
}
if (mediaService.mediaFile.uploadState == UploadState.uploading) {
@ -109,8 +112,6 @@ Future<void> _encryptMediaFiles(MediaFileService mediaService) async {
mediaService.encryptedPath
.writeAsBytesSync(Uint8List.fromList(secretBox.cipherText));
await mediaService.setUploadState(UploadState.uploading);
}
Future<void> _createUploadRequest(MediaFileService media) async {
@ -121,6 +122,11 @@ Future<void> _createUploadRequest(MediaFileService media) async {
final messages =
await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaFile.mediaId);
if (messages.isEmpty) {
// There where no user selected who should receive the image, so waiting with this step...
return;
}
for (final message in messages) {
final groupMembers =
await twonlyDB.groupsDao.getGroupMembers(message.groupId);

View file

@ -53,6 +53,8 @@ class MediaFileService {
final messages =
await twonlyDB.messagesDao.getMessagesByMediaId(mediaId);
// in case messages in empty the file will be deleted, as delete is true by default
for (final message in messages) {
if (message.senderId == null) {
// Media was send by me

View file

@ -89,27 +89,26 @@ String errorCodeToText(BuildContext context, ErrorCode code) {
case ErrorCode.PlanUpgradeNotYearly:
return context.lang.errorPlanUpgradeNotYearly;
}
return code.toString(); // Fallback for unrecognized keys
return code.toString();
}
String formatDuration(int seconds) {
String formatDuration(BuildContext context, int seconds) {
if (seconds < 60) {
return '$seconds Sec.';
return '$seconds ${context.lang.durationShortSecond}';
} else if (seconds < 3600) {
final minutes = seconds ~/ 60;
return '$minutes Min.';
return '$minutes ${context.lang.durationShortMinute}';
} else if (seconds < 86400) {
final hours = seconds ~/ 3600;
return '$hours Hrs.'; // Assuming "Stu." is for hours
return '$hours ${context.lang.durationShortHour}';
} else {
final days = seconds ~/ 86400;
return '$days Days';
return '$days ${context.lang.durationShortDays}';
}
}
InputDecoration getInputDecoration(BuildContext context, String hintText) {
final primaryColor =
Theme.of(context).colorScheme.primary; // Get the primary color
final primaryColor = Theme.of(context).colorScheme.primary;
return InputDecoration(
hintText: hintText,
focusedBorder: OutlineInputBorder(

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/utils/misc.dart';
class SendToWidget extends StatelessWidget {
@ -40,7 +41,7 @@ class SendToWidget extends StatelessWidget {
style: textStyle,
),
Text(
sendTo,
substringBy(sendTo, 20),
textAlign: TextAlign.center,
style: boldTextStyle, // Use the bold text style here
),
@ -48,9 +49,4 @@ class SendToWidget extends StatelessWidget {
),
);
}
String getContactDisplayName(String contact) {
// Replace this with your actual logic to get the contact display name
return contact; // Placeholder implementation
}
}

View file

@ -7,6 +7,7 @@ import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:hashlib/random.dart';
import 'package:screenshot/screenshot.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
@ -75,8 +76,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
if (widget.imageBytesFuture != null) {
loadImage(widget.imageBytesFuture!);
} else {
if (widget.mediaFileService.storedPath.existsSync()) {
loadImage(widget.mediaFileService.storedPath.readAsBytes());
if (widget.mediaFileService.tempPath.existsSync()) {
loadImage(widget.mediaFileService.tempPath.readAsBytes());
}
}
}
@ -482,7 +483,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
label: Text(
(widget.sendToGroup == null)
? context.lang.shareImagedEditorShareWith
: widget.sendToGroup!.groupName,
: substringBy(widget.sendToGroup!.groupName, 15),
style: const TextStyle(fontSize: 17),
),
),

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/chats/chat_list_components/group_list_item.dart';
class ArchivedChatsView extends StatefulWidget {
@ -40,7 +41,7 @@ class _ArchivedChatsViewState extends State<ArchivedChatsView> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Archivierte Chats'),
title: Text(context.lang.archivedChats),
),
body: ListView(
children: _groupsArchived.map((group) {

View file

@ -252,7 +252,7 @@ class _ChatListViewState extends State<ChatListView> {
if (_groupsArchived.isEmpty) return Container();
return ListTile(
title: Text(
'Archivierte Chats (${_groupsArchived.length})',
'${context.lang.archivedChats} (${_groupsArchived.length})',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 13),
),

View file

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
@ -208,10 +209,12 @@ class _UserListItem extends State<GroupListItem> {
group: widget.group,
child: ListTile(
title: Text(
widget.group.groupName,
substringBy(widget.group.groupName, 30),
),
subtitle: (_currentMessage == null)
? Text(context.lang.chatsTapToSend)
? (widget.group.totalMediaCounter == 0)
? Text(context.lang.chatsTapToSend)
: LastMessageTime(dateTime: widget.group.lastMessageExchange)
: Row(
children: [
MessageSendStateIcon(
@ -222,7 +225,7 @@ class _UserListItem extends State<GroupListItem> {
const Text(''),
const SizedBox(width: 5),
if (_currentMessage != null)
LastMessageTime(message: _currentMessage!),
LastMessageTime(message: _currentMessage),
FlameCounterWidget(
groupId: widget.group.groupId,
prefix: true,

View file

@ -6,9 +6,10 @@ import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart';
class LastMessageTime extends StatefulWidget {
const LastMessageTime({required this.message, super.key});
const LastMessageTime({this.message, this.dateTime, super.key});
final Message message;
final Message? message;
final DateTime? dateTime;
@override
State<LastMessageTime> createState() => _LastMessageTimeState();
@ -24,12 +25,17 @@ class _LastMessageTimeState extends State<LastMessageTime> {
// Change the color every 200 milliseconds
updateTime =
Timer.periodic(const Duration(milliseconds: 500), (timer) async {
final lastAction = await twonlyDB.messagesDao
.getLastMessageAction(widget.message.messageId);
setState(() {
if (widget.message != null) {
final lastAction = await twonlyDB.messagesDao
.getLastMessageAction(widget.message!.messageId);
lastMessageInSeconds = DateTime.now()
.difference(lastAction?.actionAt ?? widget.message.createdAt)
.difference(lastAction?.actionAt ?? widget.message!.createdAt)
.inSeconds;
} else if (widget.dateTime != null) {
lastMessageInSeconds =
DateTime.now().difference(widget.dateTime!).inSeconds;
}
setState(() {
if (lastMessageInSeconds < 0) {
lastMessageInSeconds = 0;
}
@ -46,7 +52,7 @@ class _LastMessageTimeState extends State<LastMessageTime> {
@override
Widget build(BuildContext context) {
return Text(
formatDuration(lastMessageInSeconds),
formatDuration(context, lastMessageInSeconds),
style: const TextStyle(fontSize: 12),
);
}

View file

@ -5,6 +5,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:mutex/mutex.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/memory_item.model.dart';
@ -241,7 +242,9 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
color: Colors.transparent,
child: Row(
children: [
Text(group.groupName),
Text(
substringBy(widget.group.groupName, 20),
),
const SizedBox(width: 10),
VerifiedShield(key: verifyShieldKey, group: group),
const SizedBox(width: 10),

View file

@ -144,6 +144,16 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
case MessageSendState.sending:
icon = getLoaderIcon(color);
text = context.lang.messageSendState_Sending;
if (mediaFile != null) {
if (mediaFile.uploadState == UploadState.uploadLimitReached) {
text = 'Upload Limit erreicht';
}
if (mediaFile.uploadState == UploadState.preprocessing) {
text = 'Wird verarbeitet';
}
}
hasLoader = true;
case MessageSendState.receiving:
icon = getLoaderIcon(color);

View file

@ -24,7 +24,8 @@ class _ContactViewState extends State<ContactView> {
Future<void> handleUserRemoveRequest(Contact contact) async {
final remove = await showAlertDialog(
context,
context.lang.contactRemoveTitle(getContactDisplayName(contact)),
context.lang
.contactRemoveTitle(getContactDisplayName(contact, maxLength: 20)),
context.lang.contactRemoveBody,
);
if (remove) {
@ -124,7 +125,7 @@ class _ContactViewState extends State<ContactView> {
child: VerifiedShield(key: GlobalKey(), contact: contact),
),
Text(
getContactDisplayName(contact),
getContactDisplayName(contact, maxLength: 20),
style: const TextStyle(fontSize: 20),
),
// if (flameCounter > 0)

View file

@ -5,6 +5,8 @@ import 'package:photo_view/photo_view_gallery.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';
@ -117,12 +119,28 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
FilledButton.icon(
icon: const FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async {
final orgMediaService =
widget.galleryItems[currentIndex].mediaService;
final newMediaService = await initializeMediaUpload(
orgMediaService.mediaFile.type,
gUser.defaultShowTime,
);
if (newMediaService == null) {
Log.error('Could not create new mediaFIle');
return;
}
orgMediaService.storedPath
.copySync(newMediaService.tempPath.path);
if (!context.mounted) return;
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ShareImageEditorView(
mediaFileService: widget
.galleryItems[currentIndex].mediaService,
mediaFileService: newMediaService,
sharedFromGallery: true,
),
),

View file

@ -114,6 +114,7 @@ Future<String?> showDisplayNameChangeDialog(
content: TextField(
controller: controller,
autofocus: true,
maxLength: 30,
decoration: InputDecoration(
hintText: context.lang.settingsProfileEditDisplayNameNew,
),

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/better_list_title.dart';
@ -63,7 +64,7 @@ class _SettingsMainViewState extends State<SettingsMainView> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
gUser.displayName,
substringBy(gUser.displayName, 27),
style: const TextStyle(fontSize: 20),
textAlign: TextAlign.left,
),