block users via settings

This commit is contained in:
otsmr 2025-02-08 11:43:56 +01:00
parent 1b47c11aaa
commit ac91e954f7
9 changed files with 366 additions and 161 deletions

View file

@ -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 ## TODOS bevor first beta
- Settings
- Delete and Block active users
- MessageKind -> Ausbauen? - MessageKind -> Ausbauen?
- Nachrichten nach 24h Stunden löschen - Nachrichten nach 24h Stunden löschen
- Real deployment aufsetzen, direkt auf Netcup?
- Pro Invitation codes - Pro Invitation codes
- Push Notification (Android) - Push Notification (Android)
- FIX: Problem Bild falsch, wenn handy schräg... - Settings
- Notification
- Real deployment aufsetzen, direkt auf Netcup?
- MediaView: - 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 ## TODOS bevor first release
- Settings
- Subscription
- Webpage - Webpage
- Instagam & Marketing vorbereiten - Instagam & Marketing vorbereiten
- IT-Startup der TU Darmstadt anschreiben - IT-Startup der TU Darmstadt anschreiben

View file

@ -1,8 +1,4 @@
import 'dart:async';
import 'package:flutter/foundation.dart'; 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:provider/provider.dart';
import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/providers/api_provider.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/messages_change_provider.dart';
import 'package:twonly/src/providers/contacts_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/providers/settings_change_provider.dart';
import 'package:twonly/src/services/notification_service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'src/app.dart'; import 'src/app.dart';
late DbProvider dbProvider; late DbProvider dbProvider;
late ApiProvider apiProvider; 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<NotificationResponse> selectNotificationStream =
StreamController<NotificationResponse>.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<String, dynamic>? 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<void> setupPushNotification() async {
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings("logo");
final List<DarwinNotificationCategory> darwinNotificationCategories =
<DarwinNotificationCategory>[
DarwinNotificationCategory(
darwinNotificationCategoryText,
actions: <DarwinNotificationAction>[
DarwinNotificationAction.text(
'text_1',
'Action 1',
buttonTitle: 'Send',
placeholder: 'Placeholder',
),
],
),
DarwinNotificationCategory(
darwinNotificationCategoryPlain,
actions: <DarwinNotificationAction>[
DarwinNotificationAction.plain('id_1', 'Action 1'),
DarwinNotificationAction.plain(
'id_2',
'Action 2 (destructive)',
options: <DarwinNotificationActionOption>{
DarwinNotificationActionOption.destructive,
},
),
DarwinNotificationAction.plain(
navigationActionId,
'Action 3 (foreground)',
options: <DarwinNotificationActionOption>{
DarwinNotificationActionOption.foreground,
},
),
DarwinNotificationAction.plain(
'id_4',
'Action 4 (auth required)',
options: <DarwinNotificationActionOption>{
DarwinNotificationActionOption.authenticationRequired,
},
),
],
options: <DarwinNotificationCategoryOption>{
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 { void main() async {
final settingsController = SettingsChangeProvider(); final settingsController = SettingsChangeProvider();
@ -167,8 +35,7 @@ void main() async {
} }
}); });
setupPushNotification(); await setupPushNotification();
await initMediaStorage(); await initMediaStorage();
dbProvider = DbProvider(); dbProvider = DbProvider();

View file

@ -35,6 +35,9 @@
"settingsSubscription": "Subscription", "settingsSubscription": "Subscription",
"settingsAppearance": "Appearance", "settingsAppearance": "Appearance",
"settingsPrivacy": "Privacy", "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", "settingsNotification": "Notification",
"settingsHelp": "Help", "settingsHelp": "Help",
"settingsHelpSupport": "Support Center", "settingsHelpSupport": "Support Center",

View file

@ -10,12 +10,14 @@ class Contact {
{required this.userId, {required this.userId,
required this.displayName, required this.displayName,
required this.accepted, required this.accepted,
required this.blocked,
required this.totalMediaCounter, required this.totalMediaCounter,
required this.requested}); required this.requested});
final Int64 userId; final Int64 userId;
final String displayName; final String displayName;
final bool accepted; final bool accepted;
final bool requested; final bool requested;
final bool blocked;
final int totalMediaCounter; final int totalMediaCounter;
} }
@ -64,7 +66,21 @@ class DbContacts extends CvModelBase {
[userId, displayName, accepted, requested, blocked, createdAt]; [userId, displayName, accepted, requested, blocked, createdAt];
static Future<List<Contact>> getActiveUsers() async { static Future<List<Contact>> getActiveUsers() async {
return (await getUsers()).where((u) => u.accepted).toList(); return (await _getAllUsers())
.where((u) => u.accepted && !u.blocked)
.toList();
}
static Future<List<Contact>> getBlockedUsers() async {
return (await _getAllUsers()).where((u) => u.blocked).toList();
}
static Future<List<Contact>> getUsers() async {
return (await _getAllUsers()).where((u) => !u.blocked).toList();
}
static Future<List<Contact>> getAllUsers() async {
return await _getAllUsers();
} }
static Future checkAndUpdateFlames(int userId, {DateTime? timestamp}) async { static Future checkAndUpdateFlames(int userId, {DateTime? timestamp}) async {
@ -96,18 +112,17 @@ class DbContacts extends CvModelBase {
); );
} }
static Future<List<Contact>> getUsers() async { static Future<List<Contact>> _getAllUsers() async {
try { try {
var users = await dbProvider.db!.query(tableName, var users = await dbProvider.db!.query(tableName, columns: [
columns: [
columnUserId, columnUserId,
columnDisplayName, columnDisplayName,
columnAccepted, columnAccepted,
columnRequested, columnRequested,
columnBlocked,
columnTotalMediaCounter, columnTotalMediaCounter,
columnCreatedAt columnCreatedAt
], ]);
where: "$columnBlocked = 0");
if (users.isEmpty) return []; if (users.isEmpty) return [];
List<Contact> parsedUsers = []; List<Contact> parsedUsers = [];
@ -119,6 +134,7 @@ class DbContacts extends CvModelBase {
totalMediaCounter: users.cast()[i][columnTotalMediaCounter], totalMediaCounter: users.cast()[i][columnTotalMediaCounter],
displayName: users.cast()[i][columnDisplayName], displayName: users.cast()[i][columnDisplayName],
accepted: users[i][columnAccepted] == 1, accepted: users[i][columnAccepted] == 1,
blocked: users[i][columnBlocked] == 1,
requested: users[i][columnRequested] == 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<String, dynamic> valuesToUpdate = { Map<String, dynamic> valuesToUpdate = {
columnBlocked: 1, columnBlocked: unblock ? 0 : 1,
}; };
await dbProvider.db!.update( await dbProvider.db!.update(
tableName, tableName,

View file

@ -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<NotificationResponse> selectNotificationStream =
StreamController<NotificationResponse>.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<String, dynamic>? 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<void> setupPushNotification() async {
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings("logo");
final List<DarwinNotificationCategory> darwinNotificationCategories =
<DarwinNotificationCategory>[
DarwinNotificationCategory(
darwinNotificationCategoryText,
actions: <DarwinNotificationAction>[
DarwinNotificationAction.text(
'text_1',
'Action 1',
buttonTitle: 'Send',
placeholder: 'Placeholder',
),
],
),
DarwinNotificationCategory(
darwinNotificationCategoryPlain,
actions: <DarwinNotificationAction>[
DarwinNotificationAction.plain('id_1', 'Action 1'),
DarwinNotificationAction.plain(
'id_2',
'Action 2 (destructive)',
options: <DarwinNotificationActionOption>{
DarwinNotificationActionOption.destructive,
},
),
DarwinNotificationAction.plain(
navigationActionId,
'Action 3 (foreground)',
options: <DarwinNotificationActionOption>{
DarwinNotificationActionOption.foreground,
},
),
DarwinNotificationAction.plain(
'id_4',
'Action 4 (auth required)',
options: <DarwinNotificationActionOption>{
DarwinNotificationActionOption.authenticationRequired,
},
),
],
options: <DarwinNotificationCategoryOption>{
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,
);
}

View file

@ -44,12 +44,10 @@ class _ShareImageView extends State<ShareImageView> {
} }
Future<void> _loadAsync() async { Future<void> _loadAsync() async {
final users = await DbContacts.getActiveUsers(); _users = await DbContacts.getActiveUsers();
setState(() {
_users = users;
_updateUsers(_users); _updateUsers(_users);
});
imageBytes = await widget.imageBytesFuture; imageBytes = await widget.imageBytesFuture;
setState(() {});
} }
Future _updateUsers(List<Contact> users) async { Future _updateUsers(List<Contact> users) async {

View file

@ -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<PrivacyView> createState() => _PrivacyViewState();
}
class _PrivacyViewState extends State<PrivacyView> {
List<Contact> 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();
},
),
],
),
);
}
}

View file

@ -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<PrivacyViewBlockUsers> createState() => _PrivacyViewBlockUsers();
}
class _PrivacyViewBlockUsers extends State<PrivacyViewBlockUsers> {
List<Contact> allUsers = [];
List<Contact> 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<Contact> 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());
},
);
},
);
}
}

View file

@ -1,14 +1,15 @@
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/components/initialsavatar.dart';
import 'package:twonly/src/model/json/user_data.dart'; import 'package:twonly/src/model/json/user_data.dart';
import 'package:flutter/material.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/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/settings/account_view.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/appearance_view.dart';
import 'package:twonly/src/views/settings/help_view.dart'; import 'package:twonly/src/views/settings/help_view.dart';
import 'package:twonly/src/views/settings/privacy_view.dart';
class ProfileView extends StatefulWidget { class ProfileView extends StatefulWidget {
const ProfileView({super.key}); const ProfileView({super.key});
@ -107,7 +108,12 @@ class _ProfileViewState extends State<ProfileView> {
SettingsListTile( SettingsListTile(
icon: FontAwesomeIcons.lock, icon: FontAwesomeIcons.lock,
text: context.lang.settingsPrivacy, text: context.lang.settingsPrivacy,
onTap: () {}, onTap: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) {
return PrivacyView();
}));
},
), ),
SettingsListTile( SettingsListTile(
icon: FontAwesomeIcons.bell, icon: FontAwesomeIcons.bell,