local push notification

This commit is contained in:
otsmr 2025-02-08 23:07:36 +01:00
parent ee2dcd788a
commit fb26379201
7 changed files with 196 additions and 73 deletions

View file

@ -6,12 +6,10 @@ 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
- Send a picture first to only one person -> Kamera button - Send a picture first to only one person -> Kamera button
- Context Menu - Context Menu
- Invitation codes
- Pro Invitation codes
- Push Notification (Android)
- Settings - Settings
- Notification
- Real deployment aufsetzen, direkt auf Netcup? - Real deployment aufsetzen, direkt auf Netcup?
- Firebase Push Notification
- MediaView: - MediaView:
- Bei weiteren geladenen Bildern -> Direkt anzeigen ohne pop - Bei weiteren geladenen Bildern -> Direkt anzeigen ohne pop
@ -21,6 +19,7 @@ Don't be lonely, get twonly! Send pictures to a friend in real time and be sure
## TODOS bevor first release ## TODOS bevor first release
- Settings - Settings
- Subscription - Subscription
- Notification
- Webpage - Webpage
- Instagam & Marketing vorbereiten - Instagam & Marketing vorbereiten
- IT-Startup der TU Darmstadt anschreiben - IT-Startup der TU Darmstadt anschreiben

View file

@ -33,7 +33,7 @@ class MyApp extends StatefulWidget {
State<MyApp> createState() => _MyAppState(); State<MyApp> createState() => _MyAppState();
} }
class _MyAppState extends State<MyApp> { class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
Future<bool> _isUserCreated = isUserCreated(); Future<bool> _isUserCreated = isUserCreated();
bool _showOnboarding = true; bool _showOnboarding = true;
bool _isConnected = false; bool _isConnected = false;
@ -44,6 +44,7 @@ class _MyAppState extends State<MyApp> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this);
_startColorAnimation(); _startColorAnimation();
// init change provider to load data from the database // init change provider to load data from the database
@ -73,8 +74,18 @@ class _MyAppState extends State<MyApp> {
apiProvider.connect(); apiProvider.connect();
} }
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
print("STATE: $state");
if (state == AppLifecycleState.resumed) {
apiProvider.connect();
}
}
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this);
// disable globalCallbacks to the flutter tree // disable globalCallbacks to the flutter tree
globalCallbackConnectionState = (a) {}; globalCallbackConnectionState = (a) {};
globalCallBackOnDownloadChange = (a, b) {}; globalCallBackOnDownloadChange = (a, b) {};
@ -143,29 +154,30 @@ class _MyAppState extends State<MyApp> {
home: Stack( home: Stack(
children: [ children: [
FutureBuilder<bool>( FutureBuilder<bool>(
future: _isUserCreated, future: _isUserCreated,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
return snapshot.data! return snapshot.data!
? HomeView() ? HomeView()
: _showOnboarding : _showOnboarding
? OnboardingView( ? OnboardingView(
callbackOnSuccess: () { callbackOnSuccess: () {
setState(() { setState(() {
_showOnboarding = false; _showOnboarding = false;
}); });
}, },
) )
: RegisterView( : RegisterView(
callbackOnSuccess: () { callbackOnSuccess: () {
_isUserCreated = isUserCreated(); _isUserCreated = isUserCreated();
setState(() {}); setState(() {});
}, },
); );
} else { } else {
return Container(); return Container();
} }
}), },
),
if (!_isConnected) if (!_isConnected)
Positioned( Positioned(
top: 3, // Position it at the top top: 3, // Position it at the top
@ -178,7 +190,8 @@ class _MyAppState extends State<MyApp> {
color: Colors.red[600]!.withAlpha(redColorOpacity), color: Colors.red[600]!.withAlpha(redColorOpacity),
width: 2.0), // Red border width: 2.0), // Red border
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(
Radius.circular(10.0)), // Rounded top corners Radius.circular(10.0),
), // Rounded top corners
), ),
), ),
), ),

View file

@ -117,6 +117,53 @@ class DbContacts extends CvModelBase {
} }
} }
static List<Contact> _parseContacts(List<dynamic> users) {
List<Contact> parsedUsers = [];
for (int i = 0; i < users.length; i++) {
try {
int userId = users.cast()[i][columnUserId];
parsedUsers.add(
Contact(
userId: Int64(userId),
totalMediaCounter: users.cast()[i][columnTotalMediaCounter],
displayName: users.cast()[i][columnDisplayName],
accepted: users[i][columnAccepted] == 1,
blocked: users[i][columnBlocked] == 1,
verified: users[i][columnVerified] == 1,
requested: users[i][columnRequested] == 1,
),
);
} catch (e) {
Logger("contacts_model/parse_single_user").shout("$e");
return [];
}
}
return parsedUsers;
}
static Future<Contact?> getUserById(int userId) async {
try {
var user = await dbProvider.db!.query(tableName,
columns: [
columnUserId,
columnDisplayName,
columnAccepted,
columnRequested,
columnBlocked,
columnVerified,
columnTotalMediaCounter,
columnCreatedAt
],
where: "$columnUserId = ?",
whereArgs: [userId]);
if (user.isEmpty) return null;
return _parseContacts(user)[0];
} catch (e) {
Logger("contacts_model/getUserById").shout("$e");
return null;
}
}
static Future<List<Contact>> _getAllUsers() async { static Future<List<Contact>> _getAllUsers() async {
try { try {
var users = await dbProvider.db!.query(tableName, columns: [ var users = await dbProvider.db!.query(tableName, columns: [
@ -130,23 +177,7 @@ class DbContacts extends CvModelBase {
columnCreatedAt columnCreatedAt
]); ]);
if (users.isEmpty) return []; if (users.isEmpty) return [];
return _parseContacts(users);
List<Contact> parsedUsers = [];
for (int i = 0; i < users.length; i++) {
int userId = users.cast()[i][columnUserId];
parsedUsers.add(
Contact(
userId: Int64(userId),
totalMediaCounter: users.cast()[i][columnTotalMediaCounter],
displayName: users.cast()[i][columnDisplayName],
accepted: users[i][columnAccepted] == 1,
blocked: users[i][columnBlocked] == 1,
verified: users[i][columnVerified] == 1,
requested: users[i][columnRequested] == 1,
),
);
}
return parsedUsers;
} catch (e) { } catch (e) {
Logger("contacts_model/getUsers").shout("$e"); Logger("contacts_model/getUsers").shout("$e");
return []; return [];

View file

@ -17,6 +17,7 @@ import 'package:twonly/src/proto/api/server_to_client.pb.dart' as server;
import 'package:twonly/src/proto/api/server_to_client.pbserver.dart'; import 'package:twonly/src/proto/api/server_to_client.pbserver.dart';
import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/providers/api/api_utils.dart'; import 'package:twonly/src/providers/api/api_utils.dart';
import 'package:twonly/src/services/notification_service.dart';
// ignore: library_prefixes // ignore: library_prefixes
import 'package:twonly/src/utils/signal.dart' as SignalHelper; import 'package:twonly/src/utils/signal.dart' as SignalHelper;
@ -103,6 +104,7 @@ Future<client.Response> handleNewMessage(
Uint8List name = username.value.userdata.username; Uint8List name = username.value.userdata.username;
DbContacts.insertNewContact( DbContacts.insertNewContact(
utf8.decode(name), fromUserId.toInt(), true); utf8.decode(name), fromUserId.toInt(), true);
localPushNotificationNewMessage(fromUserId.toInt(), message, 999999);
} }
break; break;
case MessageKind.opened: case MessageKind.opened:
@ -117,6 +119,7 @@ Future<client.Response> handleNewMessage(
break; break;
case MessageKind.acceptRequest: case MessageKind.acceptRequest:
DbContacts.acceptUser(fromUserId.toInt()); DbContacts.acceptUser(fromUserId.toInt());
localPushNotificationNewMessage(fromUserId.toInt(), message, 8888888);
break; break;
case MessageKind.ack: case MessageKind.ack:
DbMessages.acknowledgeMessageByUser( DbMessages.acknowledgeMessageByUser(
@ -164,6 +167,8 @@ Future<client.Response> handleNewMessage(
tryDownloadMedia(downloadToken); tryDownloadMedia(downloadToken);
} }
} }
localPushNotificationNewMessage(
fromUserId.toInt(), message, messageId);
} }
} }
} }

View file

@ -26,6 +26,7 @@ import 'package:web_socket_channel/web_socket_channel.dart';
class ApiProvider { class ApiProvider {
final String apiUrl; final String apiUrl;
final String? backupApiUrl; final String? backupApiUrl;
bool isAuthenticated = false;
ApiProvider({required this.apiUrl, required this.backupApiUrl}); ApiProvider({required this.apiUrl, required this.backupApiUrl});
final log = Logger("ApiProvider"); final log = Logger("ApiProvider");
@ -72,13 +73,13 @@ class ApiProvider {
log.info("Trying to connect to the backend $apiUrl!"); log.info("Trying to connect to the backend $apiUrl!");
if (await _connectTo(apiUrl)) { if (await _connectTo(apiUrl)) {
onConnected(); await onConnected();
return true; return true;
} }
if (backupApiUrl != null) { if (backupApiUrl != null) {
log.info("Trying to connect to the backup backend $backupApiUrl!"); log.info("Trying to connect to the backup backend $backupApiUrl!");
if (await _connectTo(backupApiUrl!)) { if (await _connectTo(backupApiUrl!)) {
onConnected(); await onConnected();
return true; return true;
} }
} }
@ -90,12 +91,14 @@ class ApiProvider {
void _onDone() { void _onDone() {
globalCallbackConnectionState(false); globalCallbackConnectionState(false);
_channel = null; _channel = null;
isAuthenticated = false;
tryToReconnect(); tryToReconnect();
} }
void _onError(dynamic e) { void _onError(dynamic e) {
globalCallbackConnectionState(false); globalCallbackConnectionState(false);
_channel = null; _channel = null;
isAuthenticated = false;
tryToReconnect(); tryToReconnect();
} }
@ -155,12 +158,16 @@ class ApiProvider {
_channel!.sink.add(response.writeToBuffer()); _channel!.sink.add(response.writeToBuffer());
} }
Future<Result> _sendRequestV0(ClientToServer request) async { Future<Result> _sendRequestV0(ClientToServer request,
{bool authenticated = true}) async {
if (_channel == null) { if (_channel == null) {
if (!await connect()) { if (!await connect()) {
return Result.error(ErrorCode.InternalError); return Result.error(ErrorCode.InternalError);
} }
} }
if (authenticated) {
await authenticate();
}
var seq = Int64(Random().nextInt(4294967296)); var seq = Int64(Random().nextInt(4294967296));
while (messagesV0.containsKey(seq)) { while (messagesV0.containsKey(seq)) {
seq = Int64(Random().nextInt(4294967296)); seq = Int64(Random().nextInt(4294967296));
@ -175,6 +182,7 @@ class ApiProvider {
} }
Future authenticate() async { Future authenticate() async {
if (isAuthenticated) return;
if (await SignalHelper.getSignalIdentity() == null) { if (await SignalHelper.getSignalIdentity() == null) {
return; return;
} }
@ -182,7 +190,7 @@ class ApiProvider {
var handshake = Handshake()..getchallenge = Handshake_GetChallenge(); var handshake = Handshake()..getchallenge = Handshake_GetChallenge();
var req = createClientToServerFromHandshake(handshake); var req = createClientToServerFromHandshake(handshake);
final result = await _sendRequestV0(req); final result = await _sendRequestV0(req, authenticated: false);
if (result.isError) { if (result.isError) {
log.shout("Error auth", result); log.shout("Error auth", result);
return; return;
@ -206,13 +214,14 @@ class ApiProvider {
var req2 = createClientToServerFromHandshake(opensession); var req2 = createClientToServerFromHandshake(opensession);
final result2 = await _sendRequestV0(req2); final result2 = await _sendRequestV0(req2, authenticated: false);
if (result2.isError) { if (result2.isError) {
log.shout("send request failed: ${result2.error}"); log.shout("send request failed: ${result2.error}");
return; return;
} }
log.info("Authenticated!"); log.info("Authenticated!");
isAuthenticated = true;
} }
Future<Result> register(String username, String? inviteCode) async { Future<Result> register(String username, String? inviteCode) async {

View file

@ -1,7 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:logging/logging.dart';
import 'package:twonly/src/model/contacts_model.dart';
import 'package:twonly/src/model/json/message.dart' as my;
/// Streams are created so that app can respond to notification-related events /// Streams are created so that app can respond to notification-related events
/// since the plugin is initialized in the `main` function /// since the plugin is initialized in the `main` function
@ -130,3 +134,86 @@ Future<void> setupPushNotification() async {
onDidReceiveBackgroundNotificationResponse: notificationTapBackground, onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
); );
} }
String getPushNotificationText(String key, String userName) {
String systemLanguage = Platform.localeName;
Map<String, String> pushNotificationText;
if (systemLanguage.contains("de")) {
pushNotificationText = {
"newTextMessage": "%userName% hat die eine Nachricht gesendet.",
"newTwonly": "%userName% hat dir einen twonly gesendet.",
"newVideo": "%userName% hat die eine Video gesendet.",
"newImage": "%userName% hat die eine Bild gesendet.",
"contactRequest": "%userName% möchte sich mir dir vernetzen.",
"acceptRequest": "%userName% ist jetzt mit dir vernetzt.",
};
} else {
pushNotificationText = {
"newTextMessage": "%userName% has sent you a message.",
"newTwonly": "%userName% has sent you a twonly.",
"newVideo": "%userName% has sent you a video.",
"newImage": "%userName% has sent you an image.",
"contactRequest": "%userName% wants to connect with you.",
"acceptRequest": "%userName% is now connected with you.",
};
}
// Replace %userName% with the actual user name
return pushNotificationText[key]?.replaceAll("%userName%", userName) ?? "";
}
Future localPushNotificationNewMessage(
int fromUserId, my.Message message, int messageId) async {
Contact? user = await DbContacts.getUserById(fromUserId);
if (user == null) return;
String msg = "";
final content = message.content;
if (content is my.TextMessageContent) {
msg = getPushNotificationText("newTextMessage", user.displayName);
} else if (content is my.MediaMessageContent) {
if (content.isRealTwonly) {
msg = getPushNotificationText("newTwonly", user.displayName);
} else if (content.isVideo) {
msg = getPushNotificationText("newVideo", user.displayName);
} else {
msg = getPushNotificationText("newImage", user.displayName);
}
}
if (message.kind == my.MessageKind.contactRequest) {
msg = getPushNotificationText("contactRequest", user.displayName);
}
if (message.kind == my.MessageKind.acceptRequest) {
msg = getPushNotificationText("acceptRequest", user.displayName);
}
if (msg == "") {
Logger("localPushNotificationNewMessage")
.shout("No push notification type defined!");
}
const AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails(
'0',
'Messages',
channelDescription: 'Messages from other users.',
importance: Importance.max,
priority: Priority.max,
ticker: 'You got a new message.',
);
const NotificationDetails notificationDetails =
NotificationDetails(android: androidNotificationDetails);
await flutterLocalNotificationsPlugin.show(
messageId,
user.displayName,
msg,
notificationDetails,
// payload: 'test',
);
}

View file

@ -1,10 +1,8 @@
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/src/components/better_list_title.dart'; import 'package:twonly/src/components/better_list_title.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';
@ -119,26 +117,7 @@ class _ProfileViewState extends State<ProfileView> {
BetterListTile( BetterListTile(
icon: FontAwesomeIcons.bell, icon: FontAwesomeIcons.bell,
text: context.lang.settingsNotification, text: context.lang.settingsNotification,
onTap: () async { onTap: () async {},
const AndroidNotificationDetails
androidNotificationDetails = AndroidNotificationDetails(
'0',
'Messages',
channelDescription: 'Messages from other users.',
importance: Importance.max,
priority: Priority.max,
ticker: 'You got a new message.',
);
const NotificationDetails notificationDetails =
NotificationDetails(
android: androidNotificationDetails);
await flutterLocalNotificationsPlugin.show(
0,
'New message from x',
'You got a new message from XX',
notificationDetails,
payload: 'test');
},
), ),
const Divider(), const Divider(),
BetterListTile( BetterListTile(