mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 16:08:40 +00:00
new push system does work on android #53
This commit is contained in:
parent
773f076abd
commit
85dbac37fb
16 changed files with 601 additions and 122 deletions
|
|
@ -41,6 +41,7 @@ void main() async {
|
||||||
|
|
||||||
apiProvider = ApiProvider();
|
apiProvider = ApiProvider();
|
||||||
twonlyDatabase = TwonlyDatabase();
|
twonlyDatabase = TwonlyDatabase();
|
||||||
|
setupNotificationWithUsers();
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
MultiProvider(
|
MultiProvider(
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,7 @@ enum MessageKind {
|
||||||
acceptRequest,
|
acceptRequest,
|
||||||
opened,
|
opened,
|
||||||
ack,
|
ack,
|
||||||
pushKey,
|
pushKey
|
||||||
pushKeyAck,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DownloadState {
|
enum DownloadState {
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,8 @@ class MessageContent {
|
||||||
return ProfileContent.fromJson(json);
|
return ProfileContent.fromJson(json);
|
||||||
case MessageKind.storedMediaFile:
|
case MessageKind.storedMediaFile:
|
||||||
return StoredMediaFileContent.fromJson(json);
|
return StoredMediaFileContent.fromJson(json);
|
||||||
|
case MessageKind.pushKey:
|
||||||
|
return PushKeyContent.fromJson(json);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -203,3 +205,24 @@ class ProfileContent extends MessageContent {
|
||||||
return {'avatarSvg': avatarSvg, 'displayName': displayName};
|
return {'avatarSvg': avatarSvg, 'displayName': displayName};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PushKeyContent extends MessageContent {
|
||||||
|
int keyId;
|
||||||
|
List<int> key;
|
||||||
|
PushKeyContent({required this.keyId, required this.key});
|
||||||
|
|
||||||
|
static PushKeyContent fromJson(Map json) {
|
||||||
|
return PushKeyContent(
|
||||||
|
keyId: json['keyId'],
|
||||||
|
key: List<int>.from(json['key']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map toJson() {
|
||||||
|
return {
|
||||||
|
'keyId': keyId,
|
||||||
|
'key': key,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,10 @@
|
||||||
"settingsPrivacyBlockUsersDesc": "Blockierte Benutzer können nicht mit dir kommunizieren. Du kannst einen blockierten Benutzer jederzeit wieder entsperren.",
|
"settingsPrivacyBlockUsersDesc": "Blockierte Benutzer können nicht mit dir kommunizieren. Du kannst einen blockierten Benutzer jederzeit wieder entsperren.",
|
||||||
"settingsPrivacyBlockUsersCount": "{len} Kontakt(e)",
|
"settingsPrivacyBlockUsersCount": "{len} Kontakt(e)",
|
||||||
"settingsNotification": "Benachrichtigung",
|
"settingsNotification": "Benachrichtigung",
|
||||||
|
"settingsNotifyTroubleshooting": "Fehlersuche",
|
||||||
|
"settingsNotifyTroubleshootingDesc": "Hier klicken, wenn Probleme beim Empfang von Push-Benachrichtigungen auftreten.",
|
||||||
|
"settingsNotifyTroubleshootingNoProblem": "Kein Problem festgestellt",
|
||||||
|
"settingsNotifyTroubleshootingNoProblemDesc": "Klicke auf OK, um eine Testbenachrichtigung zu erhalten. Wenn du auch nach 10 Minuten warten keine Nachricht erhältst, sende uns bitte dein Diagnoseprotokoll unter Einstellungen > Hilfe > Diagnoseprotokoll, damit wir uns das Problem ansehen können.",
|
||||||
"settingsHelp": "Hilfe",
|
"settingsHelp": "Hilfe",
|
||||||
"settingsHelpSupport": "Support-Center",
|
"settingsHelpSupport": "Support-Center",
|
||||||
"settingsHelpDiagnostics": "Diagnoseprotokoll",
|
"settingsHelpDiagnostics": "Diagnoseprotokoll",
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,14 @@
|
||||||
},
|
},
|
||||||
"settingsNotification": "Notification",
|
"settingsNotification": "Notification",
|
||||||
"@settingsNotification": {},
|
"@settingsNotification": {},
|
||||||
|
"settingsNotifyTroubleshooting": "Troubleshooting",
|
||||||
|
"@settingsNotifyTroubleshooting": {},
|
||||||
|
"settingsNotifyTroubleshootingDesc": "Click here if you have problems receiving push notifications.",
|
||||||
|
"@settingsNotifyTroubleshootingDesc": {},
|
||||||
|
"settingsNotifyTroubleshootingNoProblem": "No problem detected",
|
||||||
|
"@settingsNotifyTroubleshootingNoProblem": {},
|
||||||
|
"settingsNotifyTroubleshootingNoProblemDesc": "Press OK to receive a test notification. When you receive no message even after waiting for 10 minutes, please send us your debug log in Settings > Help > Debug log, so we can look at that issue.",
|
||||||
|
"@settingsNotifyTroubleshootingNoProblemDesc": {},
|
||||||
"settingsHelp": "Help",
|
"settingsHelp": "Help",
|
||||||
"@settingsHelp": {},
|
"@settingsHelp": {},
|
||||||
"settingsHelpDiagnostics": "Diagnostic protocol",
|
"settingsHelpDiagnostics": "Diagnostic protocol",
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import 'package:twonly/src/json_models/userdata.dart';
|
||||||
import 'package:twonly/src/proto/api/error.pb.dart';
|
import 'package:twonly/src/proto/api/error.pb.dart';
|
||||||
import 'package:twonly/src/providers/api/api_utils.dart';
|
import 'package:twonly/src/providers/api/api_utils.dart';
|
||||||
import 'package:twonly/src/providers/hive.dart';
|
import 'package:twonly/src/providers/hive.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;
|
||||||
import 'package:twonly/src/utils/storage.dart';
|
import 'package:twonly/src/utils/storage.dart';
|
||||||
|
|
@ -31,7 +32,9 @@ Future tryTransmitMessages() async {
|
||||||
Result resp = await apiProvider.sendTextMessage(
|
Result resp = await apiProvider.sendTextMessage(
|
||||||
msg.userId,
|
msg.userId,
|
||||||
msg.bytes,
|
msg.bytes,
|
||||||
|
msg.pushData,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (resp.isSuccess) {
|
if (resp.isSuccess) {
|
||||||
if (msg.messageId != null) {
|
if (msg.messageId != null) {
|
||||||
await twonlyDatabase.messagesDao.updateMessageByMessageId(
|
await twonlyDatabase.messagesDao.updateMessageByMessageId(
|
||||||
|
|
@ -53,8 +56,13 @@ class RetransmitMessage {
|
||||||
int? messageId;
|
int? messageId;
|
||||||
int userId;
|
int userId;
|
||||||
Uint8List bytes;
|
Uint8List bytes;
|
||||||
RetransmitMessage(
|
List<int>? pushData;
|
||||||
{this.messageId, required this.userId, required this.bytes});
|
RetransmitMessage({
|
||||||
|
this.messageId,
|
||||||
|
required this.userId,
|
||||||
|
required this.bytes,
|
||||||
|
this.pushData,
|
||||||
|
});
|
||||||
|
|
||||||
// From JSON constructor
|
// From JSON constructor
|
||||||
factory RetransmitMessage.fromJson(Map<String, dynamic> json) {
|
factory RetransmitMessage.fromJson(Map<String, dynamic> json) {
|
||||||
|
|
@ -62,6 +70,7 @@ class RetransmitMessage {
|
||||||
messageId: json['messageId'],
|
messageId: json['messageId'],
|
||||||
userId: json['userId'],
|
userId: json['userId'],
|
||||||
bytes: base64Decode(json['bytes']),
|
bytes: base64Decode(json['bytes']),
|
||||||
|
pushData: json['pushData'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,6 +80,7 @@ class RetransmitMessage {
|
||||||
'messageId': messageId,
|
'messageId': messageId,
|
||||||
'userId': userId,
|
'userId': userId,
|
||||||
'bytes': base64Encode(bytes),
|
'bytes': base64Encode(bytes),
|
||||||
|
'pushData': pushData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -88,7 +98,8 @@ Future<Map<String, dynamic>> getAllMessagesForRetransmitting() async {
|
||||||
|
|
||||||
// this functions ensures that the message is received by the server and in case of errors will try again later
|
// this functions ensures that the message is received by the server and in case of errors will try again later
|
||||||
Future<Result> encryptAndSendMessage(
|
Future<Result> encryptAndSendMessage(
|
||||||
int? messageId, int userId, MessageJson msg) async {
|
int? messageId, int userId, MessageJson msg,
|
||||||
|
{PushKind? pushKind}) async {
|
||||||
Uint8List? bytes = await SignalHelper.encryptMessage(msg, userId);
|
Uint8List? bytes = await SignalHelper.encryptMessage(msg, userId);
|
||||||
|
|
||||||
if (bytes == null) {
|
if (bytes == null) {
|
||||||
|
|
@ -99,6 +110,11 @@ Future<Result> encryptAndSendMessage(
|
||||||
String stateId = (messageId ?? (60001 + Random().nextInt(100000))).toString();
|
String stateId = (messageId ?? (60001 + Random().nextInt(100000))).toString();
|
||||||
Box box = await getMediaStorage();
|
Box box = await getMediaStorage();
|
||||||
|
|
||||||
|
List<int>? pushData;
|
||||||
|
if (pushKind != null) {
|
||||||
|
pushData = await getPushData(userId, pushKind);
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
var retransmit = await getAllMessagesForRetransmitting();
|
var retransmit = await getAllMessagesForRetransmitting();
|
||||||
|
|
||||||
|
|
@ -106,12 +122,13 @@ Future<Result> encryptAndSendMessage(
|
||||||
messageId: messageId,
|
messageId: messageId,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
bytes: bytes,
|
bytes: bytes,
|
||||||
|
pushData: pushData,
|
||||||
).toJson());
|
).toJson());
|
||||||
|
|
||||||
box.put("messages-to-retransmit", jsonEncode(retransmit));
|
box.put("messages-to-retransmit", jsonEncode(retransmit));
|
||||||
}
|
}
|
||||||
|
|
||||||
Result resp = await apiProvider.sendTextMessage(userId, bytes);
|
Result resp = await apiProvider.sendTextMessage(userId, bytes, pushData);
|
||||||
|
|
||||||
if (resp.isSuccess) {
|
if (resp.isSuccess) {
|
||||||
if (messageId != null) {
|
if (messageId != null) {
|
||||||
|
|
@ -133,7 +150,8 @@ Future<Result> encryptAndSendMessage(
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future sendTextMessage(int target, TextMessageContent content) async {
|
Future sendTextMessage(
|
||||||
|
int target, TextMessageContent content, PushKind? pushKind) async {
|
||||||
DateTime messageSendAt = DateTime.now();
|
DateTime messageSendAt = DateTime.now();
|
||||||
|
|
||||||
int? messageId = await twonlyDatabase.messagesDao.insertMessage(
|
int? messageId = await twonlyDatabase.messagesDao.insertMessage(
|
||||||
|
|
@ -158,7 +176,7 @@ Future sendTextMessage(int target, TextMessageContent content) async {
|
||||||
timestamp: messageSendAt,
|
timestamp: messageSendAt,
|
||||||
);
|
);
|
||||||
|
|
||||||
encryptAndSendMessage(messageId, target, msg);
|
encryptAndSendMessage(messageId, target, msg, pushKind: pushKind);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future notifyContactAboutOpeningMessage(
|
Future notifyContactAboutOpeningMessage(
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import 'package:twonly/src/proto/api/server_to_client.pb.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/providers/hive.dart';
|
import 'package:twonly/src/providers/hive.dart';
|
||||||
|
import 'package:twonly/src/services/notification_service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
Future tryDownloadAllMediaFiles() async {
|
Future tryDownloadAllMediaFiles() async {
|
||||||
|
|
@ -280,6 +281,7 @@ class ImageUploader {
|
||||||
),
|
),
|
||||||
timestamp: metadata.messageSendAt,
|
timestamp: metadata.messageSendAt,
|
||||||
),
|
),
|
||||||
|
pushKind: PushKind.image,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,6 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
||||||
case MessageKind.acceptRequest:
|
case MessageKind.acceptRequest:
|
||||||
final update = ContactsCompanion(accepted: Value(true));
|
final update = ContactsCompanion(accepted: Value(true));
|
||||||
await twonlyDatabase.contactsDao.updateContact(fromUserId, update);
|
await twonlyDatabase.contactsDao.updateContact(fromUserId, update);
|
||||||
localPushNotificationNewMessage(fromUserId.toInt(), message, 8888888);
|
|
||||||
notifyContactsAboutProfileChange();
|
notifyContactsAboutProfileChange();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -214,6 +213,14 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case MessageKind.pushKey:
|
||||||
|
if (message.content != null) {
|
||||||
|
final pushKey = message.content!;
|
||||||
|
if (pushKey is PushKeyContent) {
|
||||||
|
await handleNewPushKey(fromUserId, pushKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if (message.kind != MessageKind.textMessage &&
|
if (message.kind != MessageKind.textMessage &&
|
||||||
message.kind != MessageKind.media &&
|
message.kind != MessageKind.media &&
|
||||||
|
|
@ -299,7 +306,6 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
localPushNotificationNewMessage(fromUserId, message, messageId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var ok = client.Response_Ok()..none = true;
|
var ok = client.Response_Ok()..none = true;
|
||||||
|
|
@ -328,21 +334,13 @@ Future<client.Response> handleContactRequest(
|
||||||
if (username.isSuccess) {
|
if (username.isSuccess) {
|
||||||
Uint8List name = username.value.userdata.username;
|
Uint8List name = username.value.userdata.username;
|
||||||
|
|
||||||
int added = await twonlyDatabase.contactsDao.insertContact(
|
await twonlyDatabase.contactsDao.insertContact(
|
||||||
ContactsCompanion(
|
ContactsCompanion(
|
||||||
username: Value(utf8.decode(name)),
|
username: Value(utf8.decode(name)),
|
||||||
userId: Value(fromUserId),
|
userId: Value(fromUserId),
|
||||||
requested: Value(true),
|
requested: Value(true),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (added > 0) {
|
|
||||||
localPushNotificationNewMessage(
|
|
||||||
fromUserId,
|
|
||||||
message,
|
|
||||||
999999,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
var ok = client.Response_Ok()..none = true;
|
var ok = client.Response_Ok()..none = true;
|
||||||
return client.Response()..ok = ok;
|
return client.Response()..ok = ok;
|
||||||
|
|
|
||||||
|
|
@ -385,11 +385,16 @@ class ApiProvider {
|
||||||
return await sendRequestSync(req);
|
return await sendRequestSync(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Result> sendTextMessage(int target, Uint8List msg) async {
|
Future<Result> sendTextMessage(
|
||||||
|
int target, Uint8List msg, List<int>? pushData) async {
|
||||||
var testMessage = ApplicationData_TextMessage()
|
var testMessage = ApplicationData_TextMessage()
|
||||||
..userId = Int64(target)
|
..userId = Int64(target)
|
||||||
..body = msg;
|
..body = msg;
|
||||||
|
|
||||||
|
if (pushData != null) {
|
||||||
|
testMessage.pushData = pushData;
|
||||||
|
}
|
||||||
|
|
||||||
var appData = ApplicationData()..textmessage = testMessage;
|
var appData = ApplicationData()..textmessage = testMessage;
|
||||||
var req = createClientToServerFromApplicationData(appData);
|
var req = createClientToServerFromApplicationData(appData);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@ import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/app.dart';
|
import 'package:twonly/src/app.dart';
|
||||||
import 'package:twonly/src/database/twonly_database.dart';
|
import 'package:twonly/src/services/notification_service.dart';
|
||||||
import 'package:twonly/src/providers/api_provider.dart';
|
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'dart:io' show Platform;
|
import 'dart:io' show Platform;
|
||||||
import '../../firebase_options.dart';
|
import '../../firebase_options.dart';
|
||||||
|
|
@ -70,38 +69,38 @@ Future initFCMService() async {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// APNS token is available, make FCM plugin API requests...
|
|
||||||
|
|
||||||
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
|
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
|
||||||
print('Got a message whilst in the foreground!');
|
if (!Platform.isAndroid) {
|
||||||
|
Logger("firebase-notification").shout("Got message in Dart while on iOS");
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger("firebase-notification")
|
||||||
|
.finer('Got a message while in the foreground!');
|
||||||
|
|
||||||
print('Message data: ${message.data}');
|
print('Message data: ${message.data}');
|
||||||
|
|
||||||
if (message.notification != null) {
|
if (message.notification != null) {
|
||||||
print('Message also contained a notification: ${message.notification}');
|
print('Message also contained a notification: ${message.notification}');
|
||||||
|
String title = message.notification!.title ?? "";
|
||||||
|
String body = message.notification!.body ?? "";
|
||||||
|
customLocalPushNotification(title, body);
|
||||||
|
}
|
||||||
|
if (message.data["push_data"] != null) {
|
||||||
|
handlePushData(message.data["push_data"]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||||
// Wenn Tasks länger als 30 Sekunden ausgeführt werden, wird der Prozess möglicherweise automatisch vom Gerät beendet.
|
|
||||||
// -> offer backend via http?
|
|
||||||
|
|
||||||
Logger("firebase-background")
|
Logger("firebase-background")
|
||||||
.shout('Handling a background message: ${message.messageId}');
|
.shout('Handling a background message: ${message.messageId}');
|
||||||
|
|
||||||
twonlyDatabase = TwonlyDatabase();
|
if (!Platform.isAndroid) {
|
||||||
|
Logger("firebase-notification").shout("Got message in Dart while on iOS");
|
||||||
|
}
|
||||||
|
|
||||||
apiProvider = ApiProvider();
|
Logger("firebase-notification")
|
||||||
await apiProvider.connect();
|
.finer('Got a message while in the background!');
|
||||||
|
print('Message data: ${message.data}');
|
||||||
final stopwatch = Stopwatch()..start();
|
|
||||||
while (true) {
|
|
||||||
if (stopwatch.elapsed >= Duration(seconds: 20)) {
|
|
||||||
Logger("firebase-background").shout('Exiting background handler');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await Future.delayed(Duration(milliseconds: 10));
|
|
||||||
}
|
|
||||||
await apiProvider.close(() {});
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,349 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||||
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:flutter_svg/svg.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/database/daos/contacts_dao.dart';
|
import 'package:twonly/src/database/daos/contacts_dao.dart';
|
||||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||||
import 'package:twonly/src/database/twonly_database.dart';
|
import 'package:twonly/src/database/twonly_database.dart';
|
||||||
import 'package:twonly/src/json_models/message.dart' as my;
|
import 'package:twonly/src/json_models/message.dart' as my;
|
||||||
|
import 'package:twonly/src/providers/api/api.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
|
class PushUser {
|
||||||
|
String displayName;
|
||||||
|
List<PushKeyMeta> keys;
|
||||||
|
|
||||||
|
PushUser({
|
||||||
|
required this.displayName,
|
||||||
|
required this.keys,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Factory method to create a User from JSON
|
||||||
|
factory PushUser.fromJson(Map<String, dynamic> json) {
|
||||||
|
return PushUser(
|
||||||
|
displayName: json['displayName'],
|
||||||
|
keys: (json['keys'] as List)
|
||||||
|
.map((keyJson) => PushKeyMeta.fromJson(keyJson))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to convert User to JSON
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'displayName': displayName,
|
||||||
|
'keys': keys.map((key) => key.toJson()).toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PushKeyMeta {
|
||||||
|
int id;
|
||||||
|
List<int> key;
|
||||||
|
DateTime createdAt;
|
||||||
|
|
||||||
|
PushKeyMeta({
|
||||||
|
required this.id,
|
||||||
|
required this.key,
|
||||||
|
required this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Factory method to create Keys from JSON
|
||||||
|
factory PushKeyMeta.fromJson(Map<String, dynamic> json) {
|
||||||
|
return PushKeyMeta(
|
||||||
|
id: json['id'],
|
||||||
|
key: List<int>.from(json['key']),
|
||||||
|
createdAt: DateTime.fromMillisecondsSinceEpoch(json['createdAt']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to convert Keys to JSON
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'key': key,
|
||||||
|
'createdAt': createdAt.millisecondsSinceEpoch, // Store as timestamp
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function must be called after the database is setup
|
||||||
|
Future setupNotificationWithUsers({bool force = false}) async {
|
||||||
|
var pushKeys = await getPushKeys("receivingPushKeys");
|
||||||
|
|
||||||
|
var wasChanged = false;
|
||||||
|
|
||||||
|
final random = Random.secure();
|
||||||
|
|
||||||
|
final contacts = await twonlyDatabase.contactsDao.getAllNotBlockedContacts();
|
||||||
|
for (final contact in contacts) {
|
||||||
|
if (pushKeys.containsKey(contact.userId)) {
|
||||||
|
// make it harder to predict the change of the key
|
||||||
|
final timeBefore =
|
||||||
|
DateTime.now().subtract(Duration(days: 5 + random.nextInt(5)));
|
||||||
|
final lastKey = pushKeys[contact.userId]!.keys.last;
|
||||||
|
if (force || lastKey.createdAt.isBefore(timeBefore)) {
|
||||||
|
final pushKey = PushKeyMeta(
|
||||||
|
id: lastKey.id + 1,
|
||||||
|
key: List<int>.generate(32, (index) => random.nextInt(256)),
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
sendNewPushKey(contact.userId, pushKey);
|
||||||
|
pushKeys[contact.userId]!.keys.add(pushKey);
|
||||||
|
pushKeys[contact.userId]!.displayName = getContactDisplayName(contact);
|
||||||
|
wasChanged = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/// Insert a new pushuser
|
||||||
|
final pushKey = PushKeyMeta(
|
||||||
|
id: 1,
|
||||||
|
key: List<int>.generate(32, (index) => random.nextInt(256)),
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
sendNewPushKey(contact.userId, pushKey);
|
||||||
|
final pushUser = PushUser(
|
||||||
|
displayName: getContactDisplayName(contact),
|
||||||
|
keys: [pushKey],
|
||||||
|
);
|
||||||
|
pushKeys[contact.userId] = pushUser;
|
||||||
|
wasChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wasChanged) {
|
||||||
|
await setPushKeys("receivingPushKeys", pushKeys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future sendNewPushKey(int userId, PushKeyMeta pushKey) async {
|
||||||
|
await encryptAndSendMessage(
|
||||||
|
null,
|
||||||
|
userId,
|
||||||
|
my.MessageJson(
|
||||||
|
kind: MessageKind.pushKey,
|
||||||
|
content: my.PushKeyContent(keyId: pushKey.id, key: pushKey.key),
|
||||||
|
timestamp: pushKey.createdAt,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async {
|
||||||
|
var pushKeys = await getPushKeys("sendingPushKeys");
|
||||||
|
|
||||||
|
if (pushKeys[fromUserId] == null) {
|
||||||
|
pushKeys[fromUserId] = PushUser(displayName: "-", keys: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// only store the newest key...
|
||||||
|
pushKeys[fromUserId]!.keys = [
|
||||||
|
PushKeyMeta(
|
||||||
|
id: pushKey.keyId,
|
||||||
|
key: pushKey.key,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
await setPushKeys("sendingPushKeys", pushKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PushKind {
|
||||||
|
reaction,
|
||||||
|
text,
|
||||||
|
video,
|
||||||
|
twonly,
|
||||||
|
image,
|
||||||
|
contactRequest,
|
||||||
|
acceptRequest,
|
||||||
|
storedMediaFile,
|
||||||
|
testNotification
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PushKindExtension on PushKind {
|
||||||
|
String get name => toString().split('.').last;
|
||||||
|
|
||||||
|
static PushKind fromString(String name) {
|
||||||
|
return PushKind.values.firstWhere((e) => e.name == name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PushNotification {
|
||||||
|
final int keyId;
|
||||||
|
final List<int> nonce;
|
||||||
|
final List<int> cipherText;
|
||||||
|
final List<int> mac;
|
||||||
|
|
||||||
|
PushNotification({
|
||||||
|
required this.keyId,
|
||||||
|
required this.nonce,
|
||||||
|
required this.cipherText,
|
||||||
|
required this.mac,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert a PushNotification instance to a Map
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'keyId': keyId,
|
||||||
|
'nonce': base64Encode(nonce),
|
||||||
|
'cipherText': base64Encode(cipherText),
|
||||||
|
'mac': base64Encode(mac),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a PushNotification instance from a Map
|
||||||
|
factory PushNotification.fromJson(Map<String, dynamic> json) {
|
||||||
|
return PushNotification(
|
||||||
|
keyId: json['keyId'],
|
||||||
|
nonce: base64Decode(json['nonce']),
|
||||||
|
cipherText: base64Decode(json['cipherText']),
|
||||||
|
mac: base64Decode(json['mac']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// this will trigger a push notification
|
||||||
|
/// push notification only containing the message kind and username
|
||||||
|
Future<List<int>?> getPushData(int toUserId, PushKind kind) async {
|
||||||
|
final Map<int, PushUser> pushKeys = await getPushKeys("sendingPushKeys");
|
||||||
|
|
||||||
|
List<int> key = "InsecureOnlyUsedForAddingContact".codeUnits;
|
||||||
|
int keyId = 0;
|
||||||
|
|
||||||
|
if (pushKeys[toUserId] == null) {
|
||||||
|
// user does not have send any push keys
|
||||||
|
// only allow accept request and contactrequest to be send in an insecure way :/
|
||||||
|
// In future find a better way, e.g. use the signal protocol in a native way..
|
||||||
|
if (kind != PushKind.acceptRequest &&
|
||||||
|
kind != PushKind.contactRequest &&
|
||||||
|
kind != PushKind.testNotification) {
|
||||||
|
// this will be enforced after every app uses this system... :/
|
||||||
|
// return null;
|
||||||
|
Logger("notification_service").shout(
|
||||||
|
"Using insecure key as the receiver does not send a push key!");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
key = pushKeys[toUserId]!.keys.last.key;
|
||||||
|
keyId = pushKeys[toUserId]!.keys.last.id;
|
||||||
|
} catch (e) {
|
||||||
|
Logger("notification_service")
|
||||||
|
.shout("No push notification key found for user $toUserId");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final chacha20 = Chacha20.poly1305Aead();
|
||||||
|
final nonce = chacha20.newNonce();
|
||||||
|
final secretBox = await chacha20.encrypt(
|
||||||
|
kind.name.codeUnits,
|
||||||
|
secretKey: SecretKeyData(key),
|
||||||
|
nonce: nonce,
|
||||||
|
);
|
||||||
|
final res = PushNotification(
|
||||||
|
keyId: keyId,
|
||||||
|
nonce: nonce,
|
||||||
|
cipherText: secretBox.cipherText,
|
||||||
|
mac: secretBox.mac.bytes,
|
||||||
|
);
|
||||||
|
|
||||||
|
return jsonEncode(res.toJson()).codeUnits;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<PushKind?> tryDecryptMessage(
|
||||||
|
List<int> key, PushNotification noti) async {
|
||||||
|
try {
|
||||||
|
final chacha20 = Chacha20.poly1305Aead();
|
||||||
|
SecretKeyData secretKeyData = SecretKeyData(key);
|
||||||
|
|
||||||
|
SecretBox secretBox = SecretBox(
|
||||||
|
noti.cipherText,
|
||||||
|
nonce: noti.nonce,
|
||||||
|
mac: Mac(noti.mac),
|
||||||
|
);
|
||||||
|
|
||||||
|
final plaintext =
|
||||||
|
await chacha20.decrypt(secretBox, secretKey: secretKeyData);
|
||||||
|
final plaintextString = utf8.decode(plaintext);
|
||||||
|
return PushKindExtension.fromString(plaintextString);
|
||||||
|
} catch (e) {
|
||||||
|
Logger("notification-service").shout(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future handlePushData(String pushDataJson) async {
|
||||||
|
try {
|
||||||
|
String jsonString = utf8.decode(base64.decode(pushDataJson));
|
||||||
|
final pushData = PushNotification.fromJson(jsonDecode(jsonString));
|
||||||
|
|
||||||
|
PushKind? pushKind;
|
||||||
|
int? fromUserId;
|
||||||
|
|
||||||
|
if (pushData.keyId == 0) {
|
||||||
|
List<int> key = "InsecureOnlyUsedForAddingContact".codeUnits;
|
||||||
|
pushKind = await tryDecryptMessage(key, pushData);
|
||||||
|
} else {
|
||||||
|
var pushKeys = await getPushKeys("receivingPushKeys");
|
||||||
|
for (final userId in pushKeys.keys) {
|
||||||
|
for (final key in pushKeys[userId]!.keys) {
|
||||||
|
if (key.id == pushData.keyId) {
|
||||||
|
pushKind = await tryDecryptMessage(key.key, pushData);
|
||||||
|
if (pushKind != null) {
|
||||||
|
fromUserId = userId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// found correct key and user
|
||||||
|
if (fromUserId != null) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pushKind != null) {
|
||||||
|
if (pushKind == PushKind.testNotification) {
|
||||||
|
customLocalPushNotification(
|
||||||
|
"Test notification", "This is a test notification.");
|
||||||
|
} else if (fromUserId != null) {
|
||||||
|
showLocalPushNotification(fromUserId, pushKind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Logger("notification-service").shout(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<int, PushUser>> getPushKeys(String storageKey) async {
|
||||||
|
var storage = getSecureStorage();
|
||||||
|
String? pushKeysJson = await storage.read(key: storageKey);
|
||||||
|
Map<int, PushUser> pushKeys = <int, PushUser>{};
|
||||||
|
if (pushKeysJson != null) {
|
||||||
|
Map<String, dynamic> jsonMap = jsonDecode(pushKeysJson);
|
||||||
|
jsonMap.forEach((key, value) {
|
||||||
|
pushKeys[int.parse(key)] = PushUser.fromJson(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
print("read: $storageKey: $pushKeys");
|
||||||
|
return pushKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future setPushKeys(String storageKey, Map<int, PushUser> pushKeys) async {
|
||||||
|
var storage = getSecureStorage();
|
||||||
|
Map<String, dynamic> jsonToSend = {};
|
||||||
|
pushKeys.forEach((key, value) {
|
||||||
|
jsonToSend[key.toString()] = value.toJson();
|
||||||
|
});
|
||||||
|
|
||||||
|
String jsonString = jsonEncode(jsonToSend);
|
||||||
|
print("write: $storageKey: $pushKeys");
|
||||||
|
await storage.write(key: storageKey, value: jsonString);
|
||||||
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
|
|
@ -96,102 +432,81 @@ Future<void> setupPushNotification() async {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String getPushNotificationText(String key, String userName) {
|
Future<String?> getAvatarIcon(Contact user) async {
|
||||||
String systemLanguage = Platform.localeName;
|
if (user.avatarSvg == null) return null;
|
||||||
|
|
||||||
Map<String, String> pushNotificationText;
|
final PictureInfo pictureInfo =
|
||||||
|
await vg.loadPicture(SvgStringLoader(user.avatarSvg!), null);
|
||||||
|
|
||||||
if (systemLanguage.contains("de")) {
|
final ui.Image image = await pictureInfo.picture.toImage(300, 300);
|
||||||
pushNotificationText = {
|
|
||||||
"newTextMessage": "%userName% hat dir eine Nachricht gesendet.",
|
final ByteData? byteData =
|
||||||
"newTwonly": "%userName% hat dir ein twonly gesendet.",
|
await image.toByteData(format: ui.ImageByteFormat.png);
|
||||||
"newVideo": "%userName% hat dir ein Video gesendet.",
|
final Uint8List pngBytes = byteData!.buffer.asUint8List();
|
||||||
"newImage": "%userName% hat dir ein Bild gesendet.",
|
|
||||||
"contactRequest": "%userName% möchte sich mir dir vernetzen.",
|
// Get the directory to save the image
|
||||||
"acceptRequest": "%userName% ist jetzt mit dir vernetzt.",
|
final directory = await getApplicationDocumentsDirectory();
|
||||||
"storedMediaFile": "%userName% hat dein Bild gespeichert."
|
final avatarsDirectory = Directory('${directory.path}/avatars');
|
||||||
};
|
|
||||||
} else {
|
// Create the avatars directory if it does not exist
|
||||||
pushNotificationText = {
|
if (!await avatarsDirectory.exists()) {
|
||||||
"newTextMessage": "%userName% has sent you a message.",
|
await avatarsDirectory.create(recursive: true);
|
||||||
"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.",
|
|
||||||
"storedMediaFile": "%userName% has stored your image."
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace %userName% with the actual user name
|
final filePath = '${avatarsDirectory.path}/${user.userId}.png';
|
||||||
return pushNotificationText[key]?.replaceAll("%userName%", userName) ?? "";
|
final file = File(filePath);
|
||||||
|
await file.writeAsBytes(pngBytes);
|
||||||
|
|
||||||
|
pictureInfo.picture.dispose();
|
||||||
|
|
||||||
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future localPushNotificationNewMessage(
|
Future showLocalPushNotification(
|
||||||
int fromUserId, my.MessageJson message, int messageId) async {
|
int fromUserId,
|
||||||
|
PushKind pushKind,
|
||||||
|
) async {
|
||||||
|
String? title;
|
||||||
|
String? body;
|
||||||
|
|
||||||
Contact? user = await twonlyDatabase.contactsDao
|
Contact? user = await twonlyDatabase.contactsDao
|
||||||
.getContactByUserId(fromUserId)
|
.getContactByUserId(fromUserId)
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
|
|
||||||
if (user == null) return;
|
if (user == null) return;
|
||||||
|
|
||||||
String msg = "";
|
title = getContactDisplayName(user);
|
||||||
|
body = getPushNotificationText(pushKind);
|
||||||
final content = message.content;
|
if (body == "") {
|
||||||
|
|
||||||
if (content is my.TextMessageContent) {
|
|
||||||
msg =
|
|
||||||
getPushNotificationText("newTextMessage", getContactDisplayName(user));
|
|
||||||
} else if (content is my.MediaMessageContent) {
|
|
||||||
if (content.isRealTwonly) {
|
|
||||||
msg = getPushNotificationText("newTwonly", getContactDisplayName(user));
|
|
||||||
} else if (content.isVideo) {
|
|
||||||
msg = getPushNotificationText("newVideo", getContactDisplayName(user));
|
|
||||||
} else {
|
|
||||||
msg = getPushNotificationText("newImage", getContactDisplayName(user));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.kind == MessageKind.contactRequest) {
|
|
||||||
msg =
|
|
||||||
getPushNotificationText("contactRequest", getContactDisplayName(user));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.kind == MessageKind.acceptRequest) {
|
|
||||||
msg = getPushNotificationText("acceptRequest", getContactDisplayName(user));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.kind == MessageKind.storedMediaFile) {
|
|
||||||
msg =
|
|
||||||
getPushNotificationText("storedMediaFile", getContactDisplayName(user));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg == "") {
|
|
||||||
Logger("localPushNotificationNewMessage")
|
Logger("localPushNotificationNewMessage")
|
||||||
.shout("No push notification type defined!");
|
.shout("No push notification type defined!");
|
||||||
}
|
}
|
||||||
|
|
||||||
const AndroidNotificationDetails androidNotificationDetails =
|
FilePathAndroidBitmap? styleInformation;
|
||||||
AndroidNotificationDetails(
|
String? avatarPath = await getAvatarIcon(user);
|
||||||
'0',
|
if (avatarPath != null) {
|
||||||
'Messages',
|
styleInformation = FilePathAndroidBitmap(avatarPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
AndroidNotificationDetails androidNotificationDetails =
|
||||||
|
AndroidNotificationDetails('0', 'Messages',
|
||||||
channelDescription: 'Messages from other users.',
|
channelDescription: 'Messages from other users.',
|
||||||
importance: Importance.max,
|
importance: Importance.max,
|
||||||
priority: Priority.max,
|
priority: Priority.max,
|
||||||
ticker: 'You got a new message.',
|
ticker: 'You got a new message.',
|
||||||
);
|
largeIcon: styleInformation);
|
||||||
|
|
||||||
const DarwinNotificationDetails darwinNotificationDetails =
|
const DarwinNotificationDetails darwinNotificationDetails =
|
||||||
DarwinNotificationDetails();
|
DarwinNotificationDetails();
|
||||||
const NotificationDetails notificationDetails = NotificationDetails(
|
NotificationDetails notificationDetails = NotificationDetails(
|
||||||
android: androidNotificationDetails, iOS: darwinNotificationDetails);
|
android: androidNotificationDetails, iOS: darwinNotificationDetails);
|
||||||
|
|
||||||
await flutterLocalNotificationsPlugin.show(
|
await flutterLocalNotificationsPlugin.show(
|
||||||
messageId,
|
fromUserId,
|
||||||
getContactDisplayName(user),
|
title,
|
||||||
msg,
|
body,
|
||||||
notificationDetails,
|
notificationDetails,
|
||||||
payload: message.kind.index.toString(),
|
payload: pushKind.name,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,8 +514,8 @@ Future customLocalPushNotification(String title, String msg) async {
|
||||||
const AndroidNotificationDetails androidNotificationDetails =
|
const AndroidNotificationDetails androidNotificationDetails =
|
||||||
AndroidNotificationDetails(
|
AndroidNotificationDetails(
|
||||||
'1',
|
'1',
|
||||||
'Error',
|
'System',
|
||||||
channelDescription: 'Error messages.',
|
channelDescription: 'System messages.',
|
||||||
importance: Importance.max,
|
importance: Importance.max,
|
||||||
priority: Priority.max,
|
priority: Priority.max,
|
||||||
);
|
);
|
||||||
|
|
@ -211,9 +526,40 @@ Future customLocalPushNotification(String title, String msg) async {
|
||||||
android: androidNotificationDetails, iOS: darwinNotificationDetails);
|
android: androidNotificationDetails, iOS: darwinNotificationDetails);
|
||||||
|
|
||||||
await flutterLocalNotificationsPlugin.show(
|
await flutterLocalNotificationsPlugin.show(
|
||||||
897898,
|
999999 + Random.secure().nextInt(9999),
|
||||||
title,
|
title,
|
||||||
msg,
|
msg,
|
||||||
notificationDetails,
|
notificationDetails,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String getPushNotificationText(PushKind pushKind) {
|
||||||
|
String systemLanguage = Platform.localeName;
|
||||||
|
|
||||||
|
Map<String, String> pushNotificationText;
|
||||||
|
|
||||||
|
if (systemLanguage.contains("de")) {
|
||||||
|
pushNotificationText = {
|
||||||
|
PushKind.text.name: "hat dir eine Nachricht gesendet.",
|
||||||
|
PushKind.twonly.name: "hat dir ein twonly gesendet.",
|
||||||
|
PushKind.video.name: "hat dir ein Video gesendet.",
|
||||||
|
PushKind.image.name: "hat dir ein Bild gesendet.",
|
||||||
|
PushKind.contactRequest.name: "möchte sich mir dir vernetzen.",
|
||||||
|
PushKind.acceptRequest.name: "ist jetzt mit dir vernetzt.",
|
||||||
|
PushKind.storedMediaFile.name: "hat dein Bild gespeichert.",
|
||||||
|
PushKind.reaction.name: "hat auf dein Bild reagiert."
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
pushNotificationText = {
|
||||||
|
PushKind.text.name: "has sent you a message.",
|
||||||
|
PushKind.twonly.name: "has sent you a twonly.",
|
||||||
|
PushKind.video.name: "has sent you a video.",
|
||||||
|
PushKind.image.name: "has sent you an image.",
|
||||||
|
PushKind.contactRequest.name: "wants to connect with you.",
|
||||||
|
PushKind.acceptRequest.name: "is now connected with you.",
|
||||||
|
PushKind.storedMediaFile.name: "has stored your image.",
|
||||||
|
PushKind.reaction.name: "has reacted to your image."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return pushNotificationText[pushKind.name] ?? "";
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -280,6 +280,7 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
|
||||||
TextMessageContent(
|
TextMessageContent(
|
||||||
text: newMessageController.text,
|
text: newMessageController.text,
|
||||||
),
|
),
|
||||||
|
PushKind.text,
|
||||||
);
|
);
|
||||||
newMessageController.clear();
|
newMessageController.clear();
|
||||||
currentInputText = "";
|
currentInputText = "";
|
||||||
|
|
|
||||||
|
|
@ -323,6 +323,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
responseToMessageId:
|
responseToMessageId:
|
||||||
allMediaFiles.first.messageOtherId,
|
allMediaFiles.first.messageOtherId,
|
||||||
),
|
),
|
||||||
|
PushKind.reaction,
|
||||||
);
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
selectedShortReaction = index;
|
selectedShortReaction = index;
|
||||||
|
|
@ -393,6 +394,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
),
|
),
|
||||||
timestamp: DateTime.now(),
|
timestamp: DateTime.now(),
|
||||||
),
|
),
|
||||||
|
pushKind: PushKind.storedMediaFile,
|
||||||
);
|
);
|
||||||
final res = await saveImageToGallery(imageBytes!);
|
final res = await saveImageToGallery(imageBytes!);
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:twonly/src/components/alert_dialog.dart';
|
||||||
import 'package:twonly/src/database/daos/contacts_dao.dart';
|
import 'package:twonly/src/database/daos/contacts_dao.dart';
|
||||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||||
import 'package:twonly/src/database/twonly_database.dart';
|
import 'package:twonly/src/database/twonly_database.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/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/components/headline.dart';
|
import 'package:twonly/src/components/headline.dart';
|
||||||
|
|
@ -67,6 +68,7 @@ class _SearchUsernameView extends State<SearchUsernameView> {
|
||||||
timestamp: DateTime.now(),
|
timestamp: DateTime.now(),
|
||||||
content: MessageContent(),
|
content: MessageContent(),
|
||||||
),
|
),
|
||||||
|
pushKind: PushKind.contactRequest,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -238,6 +240,7 @@ class _ContactsListViewState extends State<ContactsListView> {
|
||||||
timestamp: DateTime.now(),
|
timestamp: DateTime.now(),
|
||||||
content: MessageContent(),
|
content: MessageContent(),
|
||||||
),
|
),
|
||||||
|
pushKind: PushKind.acceptRequest,
|
||||||
);
|
);
|
||||||
notifyContactsAboutProfileChange();
|
notifyContactsAboutProfileChange();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
64
lib/src/views/settings/notification_view.dart
Normal file
64
lib/src/views/settings/notification_view.dart
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:twonly/globals.dart';
|
||||||
|
import 'package:twonly/src/components/alert_dialog.dart';
|
||||||
|
import 'package:twonly/src/services/fcm_service.dart';
|
||||||
|
import 'package:twonly/src/services/notification_service.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/utils/storage.dart';
|
||||||
|
|
||||||
|
class NotificationView extends StatelessWidget {
|
||||||
|
const NotificationView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(context.lang.settingsNotification),
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.lang.settingsNotifyTroubleshooting),
|
||||||
|
subtitle: Text(context.lang.settingsNotifyTroubleshootingDesc),
|
||||||
|
onTap: () async {
|
||||||
|
await initFCMAfterAuthenticated();
|
||||||
|
final storage = getSecureStorage();
|
||||||
|
String? storedToken = await storage.read(key: "google_fcm");
|
||||||
|
//await setupNotificationWithUsers(force: true);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
if (storedToken == null) {
|
||||||
|
final platform = Platform.isAndroid ? "Google's" : "Apple's";
|
||||||
|
showAlertDialog(context, "Problem detected",
|
||||||
|
"twonly is not able to register your app to $platform push server infrastrukture. For Android that can happen when you do not have the Google Play Services installed. If you theses installed and want to help us to fix the issue please send us your debug log in Settings > Help > Debug log.");
|
||||||
|
} else {
|
||||||
|
final run = await showAlertDialog(
|
||||||
|
context,
|
||||||
|
context.lang.settingsNotifyTroubleshootingNoProblem,
|
||||||
|
context.lang.settingsNotifyTroubleshootingNoProblemDesc);
|
||||||
|
|
||||||
|
if (run) {
|
||||||
|
final user = await getUser();
|
||||||
|
if (user != null) {
|
||||||
|
final pushData = await getPushData(
|
||||||
|
user.userId,
|
||||||
|
PushKind.testNotification,
|
||||||
|
);
|
||||||
|
await apiProvider.sendTextMessage(
|
||||||
|
user.userId,
|
||||||
|
Uint8List(0),
|
||||||
|
pushData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ 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/notification_view.dart';
|
||||||
import 'package:twonly/src/views/settings/profile_view.dart';
|
import 'package:twonly/src/views/settings/profile_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';
|
import 'package:twonly/src/views/settings/privacy_view.dart';
|
||||||
|
|
@ -129,11 +130,16 @@ class _SettingsMainViewState extends State<SettingsMainView> {
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
// BetterListTile(
|
BetterListTile(
|
||||||
// icon: FontAwesomeIcons.bell,
|
icon: FontAwesomeIcons.bell,
|
||||||
// text: context.lang.settingsNotification,
|
text: context.lang.settingsNotification,
|
||||||
// onTap: () async {},
|
onTap: () async {
|
||||||
// ),
|
Navigator.push(context,
|
||||||
|
MaterialPageRoute(builder: (context) {
|
||||||
|
return NotificationView();
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
BetterListTile(
|
BetterListTile(
|
||||||
icon: FontAwesomeIcons.circleQuestion,
|
icon: FontAwesomeIcons.circleQuestion,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue