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();
}
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() {
return (select(groups)
..where((t) => t.deletedContent.equals(false))

View file

@ -689,9 +689,9 @@
"@durationShortSecond": {},
"durationShortMinute": "Min.",
"@durationShortMinute": {},
"durationShortHour": "Std",
"durationShortHour": "Std.",
"@durationShortHour": {},
"durationShortDays": "Tagen",
"durationShortDays": "{count, plural, =1{1 Tag} other{{count} Tage}}",
"@durationShortDays": {},
"contacts": "Kontakte",
"groups": "Gruppen",
@ -805,6 +805,13 @@
"leaveGroupSureTitle": "Gruppe verlassen",
"leaveGroupSureBody": "Willst du die Gruppe wirklich verlassen?",
"leaveGroupSureOkBtn": "Gruppe verlassen",
"changeDisplayMaxTime": "{username} hat das Zeitlimit für verschwindende Nachrichten auf {time}.",
"youChangedDisplayMaxTime": "Du hat das Zeitlimit für verschwindende Nachrichten auf {time}."
"changeDisplayMaxTime": "Chats werden ab jetzt nach {time} gelöscht ({username}).",
"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.",
"durationShortMinute": "Min.",
"durationShortHour": "Hrs.",
"durationShortDays": "Days",
"durationShortDays": "{count, plural, =1{1 Day} other{{count} Days}}",
"contacts": "Contacts",
"groups": "Groups",
"newGroup": "New group",
@ -583,6 +583,13 @@
"leaveGroupSureTitle": "Leave group",
"leaveGroupSureBody": "Do you really want to leave the group?",
"leaveGroupSureOkBtn": "Leave group",
"changeDisplayMaxTime": "{username} has set the time limit for disappearing messages to {time}.",
"youChangedDisplayMaxTime": "You have set the time limit for disappearing messages to {time}."
"changeDisplayMaxTime": "Chats will now be deleted after {time} ({username}).",
"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.
///
/// In en, this message translates to:
/// **'Days'**
String get durationShortDays;
/// **'{count, plural, =1{1 Day} other{{count} Days}}'**
String durationShortDays(num count);
/// No description provided for @contacts.
///
@ -2603,14 +2603,56 @@ abstract class AppLocalizations {
/// No description provided for @changeDisplayMaxTime.
///
/// 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);
/// No description provided for @youChangedDisplayMaxTime.
///
/// 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);
/// 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

View file

@ -1164,10 +1164,18 @@ class AppLocalizationsDe extends AppLocalizations {
String get durationShortMinute => 'Min.';
@override
String get durationShortHour => 'Std';
String get durationShortHour => 'Std.';
@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
String get contacts => 'Kontakte';
@ -1424,11 +1432,32 @@ class AppLocalizationsDe extends AppLocalizations {
@override
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
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.';
@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
String get contacts => 'Contacts';
@ -1414,11 +1422,32 @@ class AppLocalizationsEn extends AppLocalizations {
@override
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
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),
),
);
if (group.isDirectChat) {
await twonlyDB.groupsDao.updateGroup(
group.groupId,
GroupsCompanion(
deleteMessagesAfterMilliseconds:
Value(update.newDeleteMessagesAfterMilliseconds.toInt()),
),
);
}
case GroupActionType.removedMember:
case GroupActionType.addMember:
case GroupActionType.leftGroup:
@ -165,7 +174,9 @@ Future<void> handleGroupUpdate(
break;
}
unawaited(fetchGroupState(group));
if (!group.isDirectChat) {
unawaited(fetchGroupState(group));
}
}
Future<bool> handleGroupJoin(

View file

@ -654,6 +654,48 @@ Future<bool> updateGroupName(Group group, String groupName) async {
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(
Group group,
List<int> newGroupMemberIds,

View file

@ -113,7 +113,7 @@ String formatDuration(BuildContext context, int seconds) {
return '$hours ${context.lang.durationShortHour}';
} else {
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

View file

@ -58,12 +58,12 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
case GroupActionType.changeDisplayMaxTime:
final time = formatDuration(
context,
(widget.action.newDeleteMessagesAfterMilliseconds ?? 0 / 1000) as int,
(widget.action.newDeleteMessagesAfterMilliseconds ?? 0) ~/ 1000,
);
text = (contact == null)
? context.lang.youChangedDisplayMaxTime(time)
: context.lang.changeDisplayMaxTime(maker, time);
icon = FontAwesomeIcons.pencil;
: context.lang.changeDisplayMaxTime(time, maker);
icon = FontAwesomeIcons.stopwatch20;
case GroupActionType.updatedGroupName:
text = (contact == null)
? 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 {
const BetterListTile({
required this.text,
required this.onTap,
this.onTap,
this.icon,
this.leading,
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:flutter/material.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/better_list_title.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/contact/contact_verify.view.dart';
import 'package:twonly/src/views/groups/group.view.dart';
class ContactView extends StatefulWidget {
const ContactView(this.userId, {super.key});
@ -68,30 +71,14 @@ class _ContactViewState extends State<ContactView> {
if (!mounted) return;
if (res.isSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Benutzer wurde gemeldet.'),
duration: Duration(seconds: 3),
SnackBar(
content: Text(context.lang.userGotReported),
duration: const Duration(seconds: 3),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Es ist ein Fehler aufgetreten. Bitte versuche es später erneut.',
),
duration: Duration(seconds: 3),
),
);
showNetworkIssue(context);
}
// 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
@ -155,6 +142,10 @@ class _ContactViewState extends State<ContactView> {
},
),
const Divider(),
SelectChatDeletionTimeListTitle(
groupId: getUUIDforDirectChat(widget.userId, gUser.userId),
),
const Divider(),
BetterListTile(
icon: FontAwesomeIcons.shieldHeart,
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/avatar_icon.component.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/contact/contact.view.dart';
import 'package:twonly/src/views/groups/group_create_select_members.view.dart';
@ -148,9 +149,6 @@ class _GroupViewState extends State<GroupView> {
return;
}
}
// If not admin -> append to the server state
// -> Inform the other users
}
@override
@ -188,6 +186,10 @@ class _GroupViewState extends State<GroupView> {
text: context.lang.groupNameInput,
onTap: _updateGroupName,
),
SelectChatDeletionTimeListTitle(
groupId: widget.group.groupId,
disabled: !group.isGroupAdmin,
),
const Divider(),
ListTile(
title: Padding(