mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 09:08:40 +00:00
local push notification
This commit is contained in:
parent
ee2dcd788a
commit
fb26379201
7 changed files with 196 additions and 73 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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 [];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue