diff --git a/README.md b/README.md index 5f17958..1012e83 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,22 @@ Don't be lonely, get twonly! Send pictures to a friend in real time and be sure ## TODOS bevor first beta -- Settings - - Delete and Block active users - MessageKind -> Ausbauen? - Nachrichten nach 24h Stunden löschen -- Real deployment aufsetzen, direkt auf Netcup? - Pro Invitation codes - Push Notification (Android) -- FIX: Problem Bild falsch, wenn handy schräg... +- Settings + - Notification +- Real deployment aufsetzen, direkt auf Netcup? - MediaView: - - Bei weiteren geladenen Bildern -> Direkt anzeigen ohne zu popen + - Bei weiteren geladenen Bildern -> Direkt anzeigen ohne pop + +## Not my issues +- FIX: Problem Bild falsch, wenn handy schräg... -> Issue already openend ## TODOS bevor first release +- Settings + - Subscription - Webpage - Instagam & Marketing vorbereiten - IT-Startup der TU Darmstadt anschreiben diff --git a/lib/main.dart b/lib/main.dart index 2a1143f..b3983e9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,4 @@ -import 'dart:async'; - import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:provider/provider.dart'; import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api_provider.dart'; @@ -13,141 +9,13 @@ import 'package:twonly/src/providers/download_change_provider.dart'; import 'package:twonly/src/providers/messages_change_provider.dart'; import 'package:twonly/src/providers/contacts_change_provider.dart'; import 'package:twonly/src/providers/settings_change_provider.dart'; +import 'package:twonly/src/services/notification_service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'src/app.dart'; late DbProvider dbProvider; late ApiProvider apiProvider; -/// Streams are created so that app can respond to notification-related events -/// since the plugin is initialized in the `main` function -final StreamController selectNotificationStream = - StreamController.broadcast(); - -const MethodChannel platform = - MethodChannel('dexterx.dev/flutter_local_notifications_example'); - -const String portName = 'notification_send_port'; - -class ReceivedNotification { - ReceivedNotification({ - required this.id, - required this.title, - required this.body, - required this.payload, - this.data, - }); - - final int id; - final String? title; - final String? body; - final String? payload; - final Map? data; -} - -String? selectedNotificationPayload; - -/// A notification action which triggers a url launch event -const String urlLaunchActionId = 'id_1'; - -/// A notification action which triggers a App navigation event -const String navigationActionId = 'id_3'; - -/// Defines a iOS/MacOS notification category for text input actions. -const String darwinNotificationCategoryText = 'textCategory'; - -/// Defines a iOS/MacOS notification category for plain actions. -const String darwinNotificationCategoryPlain = 'plainCategory'; - -@pragma('vm:entry-point') -void notificationTapBackground(NotificationResponse notificationResponse) { - // ignore: avoid_print - print('notification(${notificationResponse.id}) action tapped: ' - '${notificationResponse.actionId} with' - ' payload: ${notificationResponse.payload}'); - if (notificationResponse.input?.isNotEmpty ?? false) { - // ignore: avoid_print - print( - 'notification action tapped with input: ${notificationResponse.input}'); - } -} - -final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = - FlutterLocalNotificationsPlugin(); - -int id = 0; - -Future setupPushNotification() async { - const AndroidInitializationSettings initializationSettingsAndroid = - AndroidInitializationSettings("logo"); - - final List darwinNotificationCategories = - [ - DarwinNotificationCategory( - darwinNotificationCategoryText, - actions: [ - DarwinNotificationAction.text( - 'text_1', - 'Action 1', - buttonTitle: 'Send', - placeholder: 'Placeholder', - ), - ], - ), - DarwinNotificationCategory( - darwinNotificationCategoryPlain, - actions: [ - DarwinNotificationAction.plain('id_1', 'Action 1'), - DarwinNotificationAction.plain( - 'id_2', - 'Action 2 (destructive)', - options: { - DarwinNotificationActionOption.destructive, - }, - ), - DarwinNotificationAction.plain( - navigationActionId, - 'Action 3 (foreground)', - options: { - DarwinNotificationActionOption.foreground, - }, - ), - DarwinNotificationAction.plain( - 'id_4', - 'Action 4 (auth required)', - options: { - DarwinNotificationActionOption.authenticationRequired, - }, - ), - ], - options: { - DarwinNotificationCategoryOption.hiddenPreviewShowTitle, - }, - ) - ]; - - /// Note: permissions aren't requested here just to demonstrate that can be - /// done later - final DarwinInitializationSettings initializationSettingsDarwin = - DarwinInitializationSettings( - requestAlertPermission: false, - requestBadgePermission: false, - requestSoundPermission: false, - notificationCategories: darwinNotificationCategories, - ); - - final InitializationSettings initializationSettings = InitializationSettings( - android: initializationSettingsAndroid, - iOS: initializationSettingsDarwin, - ); - - await flutterLocalNotificationsPlugin.initialize( - initializationSettings, - onDidReceiveNotificationResponse: selectNotificationStream.add, - onDidReceiveBackgroundNotificationResponse: notificationTapBackground, - ); -} - void main() async { final settingsController = SettingsChangeProvider(); @@ -167,8 +35,7 @@ void main() async { } }); - setupPushNotification(); - + await setupPushNotification(); await initMediaStorage(); dbProvider = DbProvider(); diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index f69187b..0b3da00 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -35,6 +35,9 @@ "settingsSubscription": "Subscription", "settingsAppearance": "Appearance", "settingsPrivacy": "Privacy", + "settingsPrivacyBlockUsers": "Block users", + "settingsPrivacyBlockUsersDesc": "Blocked users will not be able to communicate with you. You can unblock a blocked user at any time.", + "settingsPrivacyBlockUsersCount": "{len} contact(s)", "settingsNotification": "Notification", "settingsHelp": "Help", "settingsHelpSupport": "Support Center", diff --git a/lib/src/model/contacts_model.dart b/lib/src/model/contacts_model.dart index 3399c19..4e7e2c6 100644 --- a/lib/src/model/contacts_model.dart +++ b/lib/src/model/contacts_model.dart @@ -10,12 +10,14 @@ class Contact { {required this.userId, required this.displayName, required this.accepted, + required this.blocked, required this.totalMediaCounter, required this.requested}); final Int64 userId; final String displayName; final bool accepted; final bool requested; + final bool blocked; final int totalMediaCounter; } @@ -64,7 +66,21 @@ class DbContacts extends CvModelBase { [userId, displayName, accepted, requested, blocked, createdAt]; static Future> getActiveUsers() async { - return (await getUsers()).where((u) => u.accepted).toList(); + return (await _getAllUsers()) + .where((u) => u.accepted && !u.blocked) + .toList(); + } + + static Future> getBlockedUsers() async { + return (await _getAllUsers()).where((u) => u.blocked).toList(); + } + + static Future> getUsers() async { + return (await _getAllUsers()).where((u) => !u.blocked).toList(); + } + + static Future> getAllUsers() async { + return await _getAllUsers(); } static Future checkAndUpdateFlames(int userId, {DateTime? timestamp}) async { @@ -96,18 +112,17 @@ class DbContacts extends CvModelBase { ); } - static Future> getUsers() async { + static Future> _getAllUsers() async { try { - var users = await dbProvider.db!.query(tableName, - columns: [ - columnUserId, - columnDisplayName, - columnAccepted, - columnRequested, - columnTotalMediaCounter, - columnCreatedAt - ], - where: "$columnBlocked = 0"); + var users = await dbProvider.db!.query(tableName, columns: [ + columnUserId, + columnDisplayName, + columnAccepted, + columnRequested, + columnBlocked, + columnTotalMediaCounter, + columnCreatedAt + ]); if (users.isEmpty) return []; List parsedUsers = []; @@ -119,6 +134,7 @@ class DbContacts extends CvModelBase { totalMediaCounter: users.cast()[i][columnTotalMediaCounter], displayName: users.cast()[i][columnDisplayName], accepted: users[i][columnAccepted] == 1, + blocked: users[i][columnBlocked] == 1, requested: users[i][columnRequested] == 1, ), ); @@ -130,9 +146,9 @@ class DbContacts extends CvModelBase { } } - static Future blockUser(int userId) async { + static Future blockUser(int userId, {bool unblock = false}) async { Map valuesToUpdate = { - columnBlocked: 1, + columnBlocked: unblock ? 0 : 1, }; await dbProvider.db!.update( tableName, diff --git a/lib/src/services/notification_service.dart b/lib/src/services/notification_service.dart new file mode 100644 index 0000000..bb168e5 --- /dev/null +++ b/lib/src/services/notification_service.dart @@ -0,0 +1,132 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + +/// Streams are created so that app can respond to notification-related events +/// since the plugin is initialized in the `main` function +final StreamController selectNotificationStream = + StreamController.broadcast(); + +const MethodChannel platform = MethodChannel('twonly.eu/notifications'); + +const String portName = 'notification_send_port'; + +class ReceivedNotification { + ReceivedNotification({ + required this.id, + required this.title, + required this.body, + required this.payload, + this.data, + }); + + final int id; + final String? title; + final String? body; + final String? payload; + final Map? data; +} + +String? selectedNotificationPayload; + +/// A notification action which triggers a url launch event +const String urlLaunchActionId = 'id_1'; + +/// A notification action which triggers a App navigation event +const String navigationActionId = 'id_3'; + +/// Defines a iOS/MacOS notification category for text input actions. +const String darwinNotificationCategoryText = 'textCategory'; + +/// Defines a iOS/MacOS notification category for plain actions. +const String darwinNotificationCategoryPlain = 'plainCategory'; + +@pragma('vm:entry-point') +void notificationTapBackground(NotificationResponse notificationResponse) { + // ignore: avoid_print + print('notification(${notificationResponse.id}) action tapped: ' + '${notificationResponse.actionId} with' + ' payload: ${notificationResponse.payload}'); + if (notificationResponse.input?.isNotEmpty ?? false) { + // ignore: avoid_print + print( + 'notification action tapped with input: ${notificationResponse.input}'); + } +} + +final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + +int id = 0; + +Future setupPushNotification() async { + const AndroidInitializationSettings initializationSettingsAndroid = + AndroidInitializationSettings("logo"); + + final List darwinNotificationCategories = + [ + DarwinNotificationCategory( + darwinNotificationCategoryText, + actions: [ + DarwinNotificationAction.text( + 'text_1', + 'Action 1', + buttonTitle: 'Send', + placeholder: 'Placeholder', + ), + ], + ), + DarwinNotificationCategory( + darwinNotificationCategoryPlain, + actions: [ + DarwinNotificationAction.plain('id_1', 'Action 1'), + DarwinNotificationAction.plain( + 'id_2', + 'Action 2 (destructive)', + options: { + DarwinNotificationActionOption.destructive, + }, + ), + DarwinNotificationAction.plain( + navigationActionId, + 'Action 3 (foreground)', + options: { + DarwinNotificationActionOption.foreground, + }, + ), + DarwinNotificationAction.plain( + 'id_4', + 'Action 4 (auth required)', + options: { + DarwinNotificationActionOption.authenticationRequired, + }, + ), + ], + options: { + DarwinNotificationCategoryOption.hiddenPreviewShowTitle, + }, + ) + ]; + + /// Note: permissions aren't requested here just to demonstrate that can be + /// done later + final DarwinInitializationSettings initializationSettingsDarwin = + DarwinInitializationSettings( + requestAlertPermission: false, + requestBadgePermission: false, + requestSoundPermission: false, + notificationCategories: darwinNotificationCategories, + ); + + final InitializationSettings initializationSettings = InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsDarwin, + ); + + await flutterLocalNotificationsPlugin.initialize( + initializationSettings, + onDidReceiveNotificationResponse: selectNotificationStream.add, + onDidReceiveBackgroundNotificationResponse: notificationTapBackground, + ); +} diff --git a/lib/src/views/camera_to_share/share_image_view.dart b/lib/src/views/camera_to_share/share_image_view.dart index 8c5a7b5..ad0a454 100644 --- a/lib/src/views/camera_to_share/share_image_view.dart +++ b/lib/src/views/camera_to_share/share_image_view.dart @@ -44,12 +44,10 @@ class _ShareImageView extends State { } Future _loadAsync() async { - final users = await DbContacts.getActiveUsers(); - setState(() { - _users = users; - _updateUsers(_users); - }); + _users = await DbContacts.getActiveUsers(); + _updateUsers(_users); imageBytes = await widget.imageBytesFuture; + setState(() {}); } Future _updateUsers(List users) async { diff --git a/lib/src/views/settings/privacy_view.dart b/lib/src/views/settings/privacy_view.dart new file mode 100644 index 0000000..2689575 --- /dev/null +++ b/lib/src/views/settings/privacy_view.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/model/contacts_model.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/settings/privacy_view_block_users.dart'; + +class PrivacyView extends StatefulWidget { + const PrivacyView({super.key}); + + @override + State createState() => _PrivacyViewState(); +} + +class _PrivacyViewState extends State { + List blockedUsers = []; + + @override + void initState() { + super.initState(); + updateBlockedUsers(); + } + + Future updateBlockedUsers() async { + blockedUsers = await DbContacts.getBlockedUsers(); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(context.lang.settingsPrivacy), + ), + body: ListView( + children: [ + ListTile( + title: Text(context.lang.settingsPrivacyBlockUsers), + subtitle: Text( + context.lang.settingsPrivacyBlockUsersCount(blockedUsers.length), + ), + onTap: () async { + await Navigator.push(context, + MaterialPageRoute(builder: (context) { + return PrivacyViewBlockUsers(); + })); + updateBlockedUsers(); + }, + ), + ], + ), + ); + } +} diff --git a/lib/src/views/settings/privacy_view_block_users.dart b/lib/src/views/settings/privacy_view_block_users.dart new file mode 100644 index 0000000..0183c0b --- /dev/null +++ b/lib/src/views/settings/privacy_view_block_users.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/components/initialsavatar.dart'; +import 'package:twonly/src/model/contacts_model.dart'; +import 'package:twonly/src/utils/misc.dart'; + +class PrivacyViewBlockUsers extends StatefulWidget { + const PrivacyViewBlockUsers({super.key}); + + @override + State createState() => _PrivacyViewBlockUsers(); +} + +class _PrivacyViewBlockUsers extends State { + List allUsers = []; + List filteredUsers = []; + String lastQuery = ""; + + @override + void initState() { + super.initState(); + loadAsync(); + } + + Future loadAsync() async { + allUsers = await DbContacts.getAllUsers(); + _filterUsers(lastQuery); + } + + Future _filterUsers(String query) async { + lastQuery = query; + if (query.isEmpty) { + filteredUsers = allUsers; + return; + } + filteredUsers = allUsers + .where((user) => + user.displayName.toLowerCase().contains(query.toLowerCase())) + .toList(); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(context.lang.settingsPrivacyBlockUsers), + ), + body: Padding( + padding: EdgeInsets.only(bottom: 20, left: 10, top: 20, right: 10), + child: Column( + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: TextField( + onChanged: _filterUsers, + decoration: getInputDecoration( + context, + context.lang.searchUsernameInput, + ), + ), + ), + const SizedBox(height: 20), + Text( + context.lang.settingsPrivacyBlockUsersDesc, + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + Expanded( + child: UserList( + List.from(filteredUsers), + updateStatus: () { + loadAsync(); + }, + ), + ) + ], + ), + ), + ); + } +} + +class UserList extends StatelessWidget { + const UserList(this.users, {super.key, required this.updateStatus}); + final List users; + final Function updateStatus; + + Future block(bool? value, int userId) async { + if (value == null) return; + await DbContacts.blockUser(userId, unblock: !value); + updateStatus(); + } + + @override + Widget build(BuildContext context) { + // Step 1: Sort the users alphabetically + users.sort((a, b) => a.displayName.compareTo(b.displayName)); + + return ListView.builder( + restorationId: 'new_message_users_list', + itemCount: users.length, + itemBuilder: (BuildContext context, int i) { + Contact user = users[i]; + print(user.blocked); + return ListTile( + title: Row(children: [ + Text(user.displayName), + ]), + leading: InitialsAvatar( + displayName: user.displayName, + fontSize: 15, + ), + trailing: Checkbox( + value: user.blocked, + onChanged: (bool? value) { + print(value); + block(value, user.userId.toInt()); + }, + ), + onTap: () { + block(!user.blocked, user.userId.toInt()); + }, + ); + }, + ); + } +} diff --git a/lib/src/views/settings/settings_main_view.dart b/lib/src/views/settings/settings_main_view.dart index 46b6d88..d85500c 100644 --- a/lib/src/views/settings/settings_main_view.dart +++ b/lib/src/views/settings/settings_main_view.dart @@ -1,14 +1,15 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:twonly/main.dart'; import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/model/json/user_data.dart'; import 'package:flutter/material.dart'; +import 'package:twonly/src/services/notification_service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/settings/account_view.dart'; import 'package:twonly/src/views/settings/appearance_view.dart'; import 'package:twonly/src/views/settings/help_view.dart'; +import 'package:twonly/src/views/settings/privacy_view.dart'; class ProfileView extends StatefulWidget { const ProfileView({super.key}); @@ -107,7 +108,12 @@ class _ProfileViewState extends State { SettingsListTile( icon: FontAwesomeIcons.lock, text: context.lang.settingsPrivacy, - onTap: () {}, + onTap: () { + Navigator.push(context, + MaterialPageRoute(builder: (context) { + return PrivacyView(); + })); + }, ), SettingsListTile( icon: FontAwesomeIcons.bell,