diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 92986e4..3565b03 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -198,6 +198,12 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { .watchSingleOrNull(); } + Stream watchDirectChat(int contactId) { + final groupId = getUUIDforDirectChat(contactId, gUser.userId); + return (select(groups)..where((t) => t.groupId.equals(groupId))) + .watchSingleOrNull(); + } + Stream> watchGroupsForChatList() { return (select(groups) ..where((t) => t.deletedContent.equals(false)) diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 3c1f557..b2f5f4b 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -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." } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index bb5db7b..c014f78 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -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." } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 5567323..f329ac9 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -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 diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index adc25c5..a58f148 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -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.'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 19de928..ff1a8e0 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -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.'; } diff --git a/lib/src/services/api/client2client/groups.c2c.dart b/lib/src/services/api/client2client/groups.c2c.dart index f193911..a20b27f 100644 --- a/lib/src/services/api/client2client/groups.c2c.dart +++ b/lib/src/services/api/client2client/groups.c2c.dart @@ -138,6 +138,15 @@ Future 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 handleGroupUpdate( break; } - unawaited(fetchGroupState(group)); + if (!group.isDirectChat) { + unawaited(fetchGroupState(group)); + } } Future handleGroupJoin( diff --git a/lib/src/services/group.services.dart b/lib/src/services/group.services.dart index a80d22b..3d81340 100644 --- a/lib/src/services/group.services.dart +++ b/lib/src/services/group.services.dart @@ -654,6 +654,48 @@ Future updateGroupName(Group group, String groupName) async { return (await fetchGroupState(group)) != null; } +Future 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 addNewGroupMembers( Group group, List newGroupMemberIds, diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 5a5dd35..b121035 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -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); } } diff --git a/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart b/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart index 8bd31d3..011c294 100644 --- a/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart +++ b/lib/src/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart @@ -64,7 +64,6 @@ class _AllReactionsViewState extends State { ), ), ); - // if (mounted) Navigator.pop(context); } @override diff --git a/lib/src/views/chats/chat_messages_components/chat_group_action.dart b/lib/src/views/chats/chat_messages_components/chat_group_action.dart index e547d6e..e711ded 100644 --- a/lib/src/views/chats/chat_messages_components/chat_group_action.dart +++ b/lib/src/views/chats/chat_messages_components/chat_group_action.dart @@ -58,12 +58,12 @@ class _ChatGroupActionState extends State { 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!) diff --git a/lib/src/views/components/better_list_title.dart b/lib/src/views/components/better_list_title.dart index 45aadfa..fe408a5 100644 --- a/lib/src/views/components/better_list_title.dart +++ b/lib/src/views/components/better_list_title.dart @@ -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, diff --git a/lib/src/views/components/select_chat_deletion_time.comp.dart b/lib/src/views/components/select_chat_deletion_time.comp.dart new file mode 100644 index 0000000..279a4c7 --- /dev/null +++ b/lib/src/views/components/select_chat_deletion_time.comp.dart @@ -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 createState() => + _SelectChatDeletionTimeListTitleState(); +} + +class _SelectChatDeletionTimeListTitleState + extends State { + Group? group; + + late StreamSubscription 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 _showDialog(Widget child) async { + await showCupertinoModalPopup( + 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.generate(_getOptions().length, (int index) { + return Center( + child: Text(_getOptions()[index].$2), + ); + }), + ), + ), + ); + } +} diff --git a/lib/src/views/contact/contact.view.dart b/lib/src/views/contact/contact.view.dart index c37f514..960bbbe 100644 --- a/lib/src/views/contact/contact.view.dart +++ b/lib/src/views/contact/contact.view.dart @@ -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 { 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 { }, ), const Divider(), + SelectChatDeletionTimeListTitle( + groupId: getUUIDforDirectChat(widget.userId, gUser.userId), + ), + const Divider(), BetterListTile( icon: FontAwesomeIcons.shieldHeart, text: context.lang.contactVerifyNumberTitle, diff --git a/lib/src/views/groups/group.view.dart b/lib/src/views/groups/group.view.dart index 851d5ca..4d495ac 100644 --- a/lib/src/views/groups/group.view.dart +++ b/lib/src/views/groups/group.view.dart @@ -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 { return; } } - // If not admin -> append to the server state - - // -> Inform the other users } @override @@ -188,6 +186,10 @@ class _GroupViewState extends State { text: context.lang.groupNameInput, onTap: _updateGroupName, ), + SelectChatDeletionTimeListTitle( + groupId: widget.group.groupId, + disabled: !group.isGroupAdmin, + ), const Divider(), ListTile( title: Padding(