This commit is contained in:
otsmr 2025-11-06 00:23:29 +01:00
parent 8d45c8e9ce
commit 0c6099cd92
15 changed files with 369 additions and 48 deletions

View file

@ -198,6 +198,12 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
.watchSingleOrNull(); .watchSingleOrNull();
} }
Stream<Group?> watchDirectChat(int contactId) {
final groupId = getUUIDforDirectChat(contactId, gUser.userId);
return (select(groups)..where((t) => t.groupId.equals(groupId)))
.watchSingleOrNull();
}
Stream<List<Group>> watchGroupsForChatList() { Stream<List<Group>> watchGroupsForChatList() {
return (select(groups) return (select(groups)
..where((t) => t.deletedContent.equals(false)) ..where((t) => t.deletedContent.equals(false))

View file

@ -689,9 +689,9 @@
"@durationShortSecond": {}, "@durationShortSecond": {},
"durationShortMinute": "Min.", "durationShortMinute": "Min.",
"@durationShortMinute": {}, "@durationShortMinute": {},
"durationShortHour": "Std", "durationShortHour": "Std.",
"@durationShortHour": {}, "@durationShortHour": {},
"durationShortDays": "Tagen", "durationShortDays": "{count, plural, =1{1 Tag} other{{count} Tage}}",
"@durationShortDays": {}, "@durationShortDays": {},
"contacts": "Kontakte", "contacts": "Kontakte",
"groups": "Gruppen", "groups": "Gruppen",
@ -805,6 +805,13 @@
"leaveGroupSureTitle": "Gruppe verlassen", "leaveGroupSureTitle": "Gruppe verlassen",
"leaveGroupSureBody": "Willst du die Gruppe wirklich verlassen?", "leaveGroupSureBody": "Willst du die Gruppe wirklich verlassen?",
"leaveGroupSureOkBtn": "Gruppe verlassen", "leaveGroupSureOkBtn": "Gruppe verlassen",
"changeDisplayMaxTime": "{username} hat das Zeitlimit für verschwindende Nachrichten auf {time}.", "changeDisplayMaxTime": "Chats werden ab jetzt nach {time} gelöscht ({username}).",
"youChangedDisplayMaxTime": "Du hat das Zeitlimit für verschwindende Nachrichten auf {time}." "youChangedDisplayMaxTime": "Chats werden ab jetzt nach {time} gelöscht.",
"userGotReported": "Benutzer wurde gemeldet.",
"deleteChatAfter": "Chat löschen nach...",
"deleteChatAfterAnHour": "einer Stunde.",
"deleteChatAfterADay": "einem Tag.",
"deleteChatAfterAWeek": "einer Woche.",
"deleteChatAfterAMonth": "einem Monat.",
"deleteChatAfterAYear": "einem Jahr."
} }

View file

@ -516,7 +516,7 @@
"durationShortSecond": "Sec.", "durationShortSecond": "Sec.",
"durationShortMinute": "Min.", "durationShortMinute": "Min.",
"durationShortHour": "Hrs.", "durationShortHour": "Hrs.",
"durationShortDays": "Days", "durationShortDays": "{count, plural, =1{1 Day} other{{count} Days}}",
"contacts": "Contacts", "contacts": "Contacts",
"groups": "Groups", "groups": "Groups",
"newGroup": "New group", "newGroup": "New group",
@ -583,6 +583,13 @@
"leaveGroupSureTitle": "Leave group", "leaveGroupSureTitle": "Leave group",
"leaveGroupSureBody": "Do you really want to leave the group?", "leaveGroupSureBody": "Do you really want to leave the group?",
"leaveGroupSureOkBtn": "Leave group", "leaveGroupSureOkBtn": "Leave group",
"changeDisplayMaxTime": "{username} has set the time limit for disappearing messages to {time}.", "changeDisplayMaxTime": "Chats will now be deleted after {time} ({username}).",
"youChangedDisplayMaxTime": "You have set the time limit for disappearing messages to {time}." "youChangedDisplayMaxTime": "Chats will now be deleted after {time}.",
"userGotReported": "User has been reported.",
"deleteChatAfter": "Delete chat after...",
"deleteChatAfterAnHour": "one hour.",
"deleteChatAfterADay": "one day.",
"deleteChatAfterAWeek": "one week.",
"deleteChatAfterAMonth": "one month.",
"deleteChatAfterAYear": "one year."
} }

View file

@ -2201,8 +2201,8 @@ abstract class AppLocalizations {
/// No description provided for @durationShortDays. /// No description provided for @durationShortDays.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Days'** /// **'{count, plural, =1{1 Day} other{{count} Days}}'**
String get durationShortDays; String durationShortDays(num count);
/// No description provided for @contacts. /// No description provided for @contacts.
/// ///
@ -2603,14 +2603,56 @@ abstract class AppLocalizations {
/// No description provided for @changeDisplayMaxTime. /// No description provided for @changeDisplayMaxTime.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'{username} has set the time limit for disappearing messages to {time}.'** /// **'Chats will now be deleted after {time} ({username}).'**
String changeDisplayMaxTime(Object time, Object username); String changeDisplayMaxTime(Object time, Object username);
/// No description provided for @youChangedDisplayMaxTime. /// No description provided for @youChangedDisplayMaxTime.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'You have set the time limit for disappearing messages to {time}.'** /// **'Chats will now be deleted after {time}.'**
String youChangedDisplayMaxTime(Object time); String youChangedDisplayMaxTime(Object time);
/// No description provided for @userGotReported.
///
/// In en, this message translates to:
/// **'User has been reported.'**
String get userGotReported;
/// No description provided for @deleteChatAfter.
///
/// In en, this message translates to:
/// **'Delete chat after...'**
String get deleteChatAfter;
/// No description provided for @deleteChatAfterAnHour.
///
/// In en, this message translates to:
/// **'one hour.'**
String get deleteChatAfterAnHour;
/// No description provided for @deleteChatAfterADay.
///
/// In en, this message translates to:
/// **'one day.'**
String get deleteChatAfterADay;
/// No description provided for @deleteChatAfterAWeek.
///
/// In en, this message translates to:
/// **'one week.'**
String get deleteChatAfterAWeek;
/// No description provided for @deleteChatAfterAMonth.
///
/// In en, this message translates to:
/// **'one month.'**
String get deleteChatAfterAMonth;
/// No description provided for @deleteChatAfterAYear.
///
/// In en, this message translates to:
/// **'one year.'**
String get deleteChatAfterAYear;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View file

@ -1164,10 +1164,18 @@ class AppLocalizationsDe extends AppLocalizations {
String get durationShortMinute => 'Min.'; String get durationShortMinute => 'Min.';
@override @override
String get durationShortHour => 'Std'; String get durationShortHour => 'Std.';
@override @override
String get durationShortDays => 'Tagen'; String durationShortDays(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count Tage',
one: '1 Tag',
);
return '$_temp0';
}
@override @override
String get contacts => 'Kontakte'; String get contacts => 'Kontakte';
@ -1424,11 +1432,32 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String changeDisplayMaxTime(Object time, Object username) { String changeDisplayMaxTime(Object time, Object username) {
return '$username hat das Zeitlimit für verschwindende Nachrichten auf $time.'; return 'Chats werden ab jetzt nach $time gelöscht ($username).';
} }
@override @override
String youChangedDisplayMaxTime(Object time) { String youChangedDisplayMaxTime(Object time) {
return 'Du hat das Zeitlimit für verschwindende Nachrichten auf $time.'; return 'Chats werden ab jetzt nach $time gelöscht.';
} }
@override
String get userGotReported => 'Benutzer wurde gemeldet.';
@override
String get deleteChatAfter => 'Chat löschen nach...';
@override
String get deleteChatAfterAnHour => 'einer Stunde.';
@override
String get deleteChatAfterADay => 'einem Tag.';
@override
String get deleteChatAfterAWeek => 'einer Woche.';
@override
String get deleteChatAfterAMonth => 'einem Monat.';
@override
String get deleteChatAfterAYear => 'einem Jahr.';
} }

View file

@ -1159,7 +1159,15 @@ class AppLocalizationsEn extends AppLocalizations {
String get durationShortHour => 'Hrs.'; String get durationShortHour => 'Hrs.';
@override @override
String get durationShortDays => 'Days'; String durationShortDays(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count Days',
one: '1 Day',
);
return '$_temp0';
}
@override @override
String get contacts => 'Contacts'; String get contacts => 'Contacts';
@ -1414,11 +1422,32 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String changeDisplayMaxTime(Object time, Object username) { String changeDisplayMaxTime(Object time, Object username) {
return '$username has set the time limit for disappearing messages to $time.'; return 'Chats will now be deleted after $time ($username).';
} }
@override @override
String youChangedDisplayMaxTime(Object time) { String youChangedDisplayMaxTime(Object time) {
return 'You have set the time limit for disappearing messages to $time.'; return 'Chats will now be deleted after $time.';
} }
@override
String get userGotReported => 'User has been reported.';
@override
String get deleteChatAfter => 'Delete chat after...';
@override
String get deleteChatAfterAnHour => 'one hour.';
@override
String get deleteChatAfterADay => 'one day.';
@override
String get deleteChatAfterAWeek => 'one week.';
@override
String get deleteChatAfterAMonth => 'one month.';
@override
String get deleteChatAfterAYear => 'one year.';
} }

View file

@ -138,6 +138,15 @@ Future<void> handleGroupUpdate(
contactId: Value(fromUserId), contactId: Value(fromUserId),
), ),
); );
if (group.isDirectChat) {
await twonlyDB.groupsDao.updateGroup(
group.groupId,
GroupsCompanion(
deleteMessagesAfterMilliseconds:
Value(update.newDeleteMessagesAfterMilliseconds.toInt()),
),
);
}
case GroupActionType.removedMember: case GroupActionType.removedMember:
case GroupActionType.addMember: case GroupActionType.addMember:
case GroupActionType.leftGroup: case GroupActionType.leftGroup:
@ -165,7 +174,9 @@ Future<void> handleGroupUpdate(
break; break;
} }
if (!group.isDirectChat) {
unawaited(fetchGroupState(group)); unawaited(fetchGroupState(group));
}
} }
Future<bool> handleGroupJoin( Future<bool> handleGroupJoin(

View file

@ -654,6 +654,48 @@ Future<bool> updateGroupName(Group group, String groupName) async {
return (await fetchGroupState(group)) != null; return (await fetchGroupState(group)) != null;
} }
Future<bool> updateChatDeletionTime(
Group group,
int deleteMessagesAfterMilliseconds,
) async {
// ensure the latest state is used
final currentState = await fetchGroupState(group);
if (currentState == null) return false;
final (versionId, state) = currentState;
state.deleteMessagesAfterMilliseconds =
Int64(deleteMessagesAfterMilliseconds);
// send new state to the server
if (!await _updateGroupState(group, state)) {
return false;
}
await sendCipherTextToGroup(
group.groupId,
EncryptedContent(
groupUpdate: EncryptedContent_GroupUpdate(
groupActionType: GroupActionType.changeDisplayMaxTime.name,
newDeleteMessagesAfterMilliseconds: Int64(
deleteMessagesAfterMilliseconds,
),
),
),
);
await twonlyDB.groupsDao.insertGroupAction(
GroupHistoriesCompanion(
groupId: Value(group.groupId),
type: const Value(GroupActionType.changeDisplayMaxTime),
newDeleteMessagesAfterMilliseconds:
Value(deleteMessagesAfterMilliseconds),
),
);
// Updates the groupName :)
return (await fetchGroupState(group)) != null;
}
Future<bool> addNewGroupMembers( Future<bool> addNewGroupMembers(
Group group, Group group,
List<int> newGroupMemberIds, List<int> newGroupMemberIds,

View file

@ -113,7 +113,7 @@ String formatDuration(BuildContext context, int seconds) {
return '$hours ${context.lang.durationShortHour}'; return '$hours ${context.lang.durationShortHour}';
} else { } else {
final days = seconds ~/ 86400; final days = seconds ~/ 86400;
return '$days ${context.lang.durationShortDays}'; return context.lang.durationShortDays(days);
} }
} }

View file

@ -64,7 +64,6 @@ class _AllReactionsViewState extends State<AllReactionsView> {
), ),
), ),
); );
// if (mounted) Navigator.pop(context);
} }
@override @override

View file

@ -58,12 +58,12 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
case GroupActionType.changeDisplayMaxTime: case GroupActionType.changeDisplayMaxTime:
final time = formatDuration( final time = formatDuration(
context, context,
(widget.action.newDeleteMessagesAfterMilliseconds ?? 0 / 1000) as int, (widget.action.newDeleteMessagesAfterMilliseconds ?? 0) ~/ 1000,
); );
text = (contact == null) text = (contact == null)
? context.lang.youChangedDisplayMaxTime(time) ? context.lang.youChangedDisplayMaxTime(time)
: context.lang.changeDisplayMaxTime(maker, time); : context.lang.changeDisplayMaxTime(time, maker);
icon = FontAwesomeIcons.pencil; icon = FontAwesomeIcons.stopwatch20;
case GroupActionType.updatedGroupName: case GroupActionType.updatedGroupName:
text = (contact == null) text = (contact == null)
? context.lang.youChangedGroupName(widget.action.newGroupName!) ? context.lang.youChangedGroupName(widget.action.newGroupName!)

View file

@ -4,7 +4,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class BetterListTile extends StatelessWidget { class BetterListTile extends StatelessWidget {
const BetterListTile({ const BetterListTile({
required this.text, required this.text,
required this.onTap, this.onTap,
this.icon, this.icon,
this.leading, this.leading,
super.key, super.key,

View file

@ -0,0 +1,156 @@
import 'dart:async';
import 'package:drift/drift.dart' show Value;
import 'package:fixnum/fixnum.dart';
import 'package:flutter/cupertino.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/better_list_title.dart';
import 'package:twonly/src/views/groups/group.view.dart';
class SelectChatDeletionTimeListTitle extends StatefulWidget {
const SelectChatDeletionTimeListTitle({
required this.groupId,
this.disabled = false,
super.key,
});
final String groupId;
final bool disabled;
@override
State<SelectChatDeletionTimeListTitle> createState() =>
_SelectChatDeletionTimeListTitleState();
}
class _SelectChatDeletionTimeListTitleState
extends State<SelectChatDeletionTimeListTitle> {
Group? group;
late StreamSubscription<Group?> groupSub;
int _selectedDeletionTime = 0;
@override
void initState() {
groupSub = twonlyDB.groupsDao.watchGroup(widget.groupId).listen((update) {
if (update == null) return;
group = update;
final selected = _getOptions().indexWhere(
(t) => t.$1 == update.deleteMessagesAfterMilliseconds,
);
setState(() {
_selectedDeletionTime = selected % _getOptions().length;
});
});
super.initState();
}
@override
void dispose() {
groupSub.cancel();
super.dispose();
}
Future<void> _showDialog(Widget child) async {
await showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) => Container(
height: 216,
padding: const EdgeInsets.only(top: 6),
// The Bottom margin is provided to align the popup above the system navigation bar.
margin:
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
// Provide a background color for the popup.
color: CupertinoColors.systemBackground.resolveFrom(context),
// Use a SafeArea widget to avoid system overlaps.
child: SafeArea(top: false, child: child),
),
);
if (group == null) return;
final selected = _getOptions()[_selectedDeletionTime].$1;
if (group!.deleteMessagesAfterMilliseconds != selected) {
if (group!.isDirectChat) {
await twonlyDB.groupsDao.updateGroup(
group!.groupId,
GroupsCompanion(
deleteMessagesAfterMilliseconds: Value(selected),
),
);
await sendCipherTextToGroup(
group!.groupId,
EncryptedContent(
groupUpdate: EncryptedContent_GroupUpdate(
groupActionType: GroupActionType.changeDisplayMaxTime.name,
newDeleteMessagesAfterMilliseconds: Int64(selected),
),
),
);
} else {
if (!await updateChatDeletionTime(group!, selected)) {
if (mounted) {
showNetworkIssue(context);
}
}
}
}
}
List<(int, String)> _getOptions() {
return getOptions(context);
}
static List<(int, String)> getOptions(BuildContext context) {
return <(int, String)>[
(1000 * 60 * 60, context.lang.deleteChatAfterAnHour),
(1000 * 60 * 60 * 24, context.lang.deleteChatAfterADay),
(1000 * 60 * 60 * 24 * 7, context.lang.deleteChatAfterAWeek),
(1000 * 60 * 60 * 24 * 30, context.lang.deleteChatAfterAMonth),
(1000 * 60 * 60 * 24 * 365, context.lang.deleteChatAfterAYear),
];
}
@override
Widget build(BuildContext context) {
return BetterListTile(
icon: FontAwesomeIcons.stopwatch20,
text: context.lang.deleteChatAfter,
trailing: Text(_getOptions()[_selectedDeletionTime].$2),
onTap: widget.disabled
? null
: () => _showDialog(
CupertinoPicker(
magnification: 1.22,
squeeze: 1.2,
useMagnifier: true,
itemExtent: 32,
// This sets the initial item.
scrollController: FixedExtentScrollController(
initialItem: _selectedDeletionTime,
),
// This is called when selected item is changed.
onSelectedItemChanged: (int selectedItem) {
setState(() {
_selectedDeletionTime = selectedItem;
});
},
children:
List<Widget>.generate(_getOptions().length, (int index) {
return Center(
child: Text(_getOptions()[index].$2),
);
}),
),
),
);
}
}

View file

@ -1,3 +1,4 @@
import 'dart:async';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
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';
@ -9,8 +10,10 @@ import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/better_list_title.dart'; import 'package:twonly/src/views/components/better_list_title.dart';
import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/select_chat_deletion_time.comp.dart';
import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/components/verified_shield.dart';
import 'package:twonly/src/views/contact/contact_verify.view.dart'; import 'package:twonly/src/views/contact/contact_verify.view.dart';
import 'package:twonly/src/views/groups/group.view.dart';
class ContactView extends StatefulWidget { class ContactView extends StatefulWidget {
const ContactView(this.userId, {super.key}); const ContactView(this.userId, {super.key});
@ -68,30 +71,14 @@ class _ContactViewState extends State<ContactView> {
if (!mounted) return; if (!mounted) return;
if (res.isSuccess) { if (res.isSuccess) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Benutzer wurde gemeldet.'), content: Text(context.lang.userGotReported),
duration: Duration(seconds: 3), duration: const Duration(seconds: 3),
), ),
); );
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( showNetworkIssue(context);
const SnackBar(
content: Text(
'Es ist ein Fehler aufgetreten. Bitte versuche es später erneut.',
),
duration: Duration(seconds: 3),
),
);
} }
// if (block) {
// const update = ContactsCompanion(blocked: Value(true));
// if (context.mounted) {
// await twonlyDB.contactsDao.updateContact(contact.userId, update);
// }
// if (mounted) {
// Navigator.popUntil(context, (route) => route.isFirst);
// }
// }
} }
@override @override
@ -155,6 +142,10 @@ class _ContactViewState extends State<ContactView> {
}, },
), ),
const Divider(), const Divider(),
SelectChatDeletionTimeListTitle(
groupId: getUUIDforDirectChat(widget.userId, gUser.userId),
),
const Divider(),
BetterListTile( BetterListTile(
icon: FontAwesomeIcons.shieldHeart, icon: FontAwesomeIcons.shieldHeart,
text: context.lang.contactVerifyNumberTitle, text: context.lang.contactVerifyNumberTitle,

View file

@ -11,6 +11,7 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/better_list_title.dart'; import 'package:twonly/src/views/components/better_list_title.dart';
import 'package:twonly/src/views/components/select_chat_deletion_time.comp.dart';
import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/components/verified_shield.dart';
import 'package:twonly/src/views/contact/contact.view.dart'; import 'package:twonly/src/views/contact/contact.view.dart';
import 'package:twonly/src/views/groups/group_create_select_members.view.dart'; import 'package:twonly/src/views/groups/group_create_select_members.view.dart';
@ -148,9 +149,6 @@ class _GroupViewState extends State<GroupView> {
return; return;
} }
} }
// If not admin -> append to the server state
// -> Inform the other users
} }
@override @override
@ -188,6 +186,10 @@ class _GroupViewState extends State<GroupView> {
text: context.lang.groupNameInput, text: context.lang.groupNameInput,
onTap: _updateGroupName, onTap: _updateGroupName,
), ),
SelectChatDeletionTimeListTitle(
groupId: widget.group.groupId,
disabled: !group.isGroupAdmin,
),
const Divider(), const Divider(),
ListTile( ListTile(
title: Padding( title: Padding(