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
- Send a picture first to only one person -> Kamera button
- Context Menu
- Pro Invitation codes
- Push Notification (Android)
- Invitation codes
- Settings
- Notification
- Real deployment aufsetzen, direkt auf Netcup?
- Firebase Push Notification
- MediaView:
- 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
- Settings
- Subscription
- Notification
- Webpage
- Instagam & Marketing vorbereiten
- IT-Startup der TU Darmstadt anschreiben

View file

@ -33,7 +33,7 @@ class MyApp extends StatefulWidget {
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
Future<bool> _isUserCreated = isUserCreated();
bool _showOnboarding = true;
bool _isConnected = false;
@ -44,6 +44,7 @@ class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_startColorAnimation();
// init change provider to load data from the database
@ -73,8 +74,18 @@ class _MyAppState extends State<MyApp> {
apiProvider.connect();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
print("STATE: $state");
if (state == AppLifecycleState.resumed) {
apiProvider.connect();
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
// disable globalCallbacks to the flutter tree
globalCallbackConnectionState = (a) {};
globalCallBackOnDownloadChange = (a, b) {};
@ -165,7 +176,8 @@ class _MyAppState extends State<MyApp> {
} else {
return Container();
}
}),
},
),
if (!_isConnected)
Positioned(
top: 3, // Position it at the top
@ -178,7 +190,8 @@ class _MyAppState extends State<MyApp> {
color: Colors.red[600]!.withAlpha(redColorOpacity),
width: 2.0), // Red border
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 {
try {
var users = await dbProvider.db!.query(tableName, columns: [
@ -130,23 +177,7 @@ class DbContacts extends CvModelBase {
columnCreatedAt
]);
if (users.isEmpty) return [];
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;
return _parseContacts(users);
} catch (e) {
Logger("contacts_model/getUsers").shout("$e");
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/providers/api/api.dart';
import 'package:twonly/src/providers/api/api_utils.dart';
import 'package:twonly/src/services/notification_service.dart';
// ignore: library_prefixes
import 'package:twonly/src/utils/signal.dart' as SignalHelper;
@ -103,6 +104,7 @@ Future<client.Response> handleNewMessage(
Uint8List name = username.value.userdata.username;
DbContacts.insertNewContact(
utf8.decode(name), fromUserId.toInt(), true);
localPushNotificationNewMessage(fromUserId.toInt(), message, 999999);
}
break;
case MessageKind.opened:
@ -117,6 +119,7 @@ Future<client.Response> handleNewMessage(
break;
case MessageKind.acceptRequest:
DbContacts.acceptUser(fromUserId.toInt());
localPushNotificationNewMessage(fromUserId.toInt(), message, 8888888);
break;
case MessageKind.ack:
DbMessages.acknowledgeMessageByUser(
@ -164,6 +167,8 @@ Future<client.Response> handleNewMessage(
tryDownloadMedia(downloadToken);
}
}
localPushNotificationNewMessage(
fromUserId.toInt(), message, messageId);
}
}
}

View file

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

View file

@ -1,7 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/services.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
/// since the plugin is initialized in the `main` function
@ -130,3 +134,86 @@ Future<void> setupPushNotification() async {
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:twonly/src/components/better_list_title.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';
@ -119,26 +117,7 @@ class _ProfileViewState extends State<ProfileView> {
BetterListTile(
icon: FontAwesomeIcons.bell,
text: context.lang.settingsNotification,
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');
},
onTap: () async {},
),
const Divider(),
BetterListTile(