mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2025-02-17 06:20:44 +01:00
fix: Use single-isolate push
This commit is contained in:
parent
2bd00e6bb3
commit
9aefc8aed8
@ -43,7 +43,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "chat.fluffy.fluffychat"
|
applicationId "chat.fluffy.fluffychat.dev"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
@ -85,8 +85,9 @@ dependencies {
|
|||||||
androidTestImplementation 'androidx.test:runner:1.1.1'
|
androidTestImplementation 'androidx.test:runner:1.1.1'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
|
||||||
implementation "net.zetetic:android-database-sqlcipher:4.4.0" // needed for moor_ffi w/ sqlcipher
|
implementation "net.zetetic:android-database-sqlcipher:4.4.0" // needed for moor_ffi w/ sqlcipher
|
||||||
|
implementation 'com.github.UnifiedPush:android-connector:1.0.0-beta3' // needed for unifiedpush
|
||||||
}
|
}
|
||||||
|
|
||||||
if(file("google-services.json").exists()){
|
if(file("google-services.json").exists()){
|
||||||
apply plugin: 'com.google.gms.google-services'
|
// apply plugin: 'com.google.gms.google-services'
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="chat.fluffy.fluffychat">
|
package="chat.fluffy.fluffychat.dev">
|
||||||
<!-- Flutter needs it to communicate with the running application
|
<!-- Flutter needs it to communicate with the running application
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
-->
|
-->
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="chat.fluffy.fluffychat">
|
package="chat.fluffy.fluffychat.dev">
|
||||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||||
In most cases you can leave this as-is, but you if you want to provide
|
In most cases you can leave this as-is, but you if you want to provide
|
||||||
@ -74,6 +74,24 @@
|
|||||||
|
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<service android:name=".FcmPushService"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<receiver android:exported="true" android:enabled="true" android:name=".UnifiedPushReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="org.unifiedpush.android.connector.MESSAGE"/>
|
||||||
|
<action android:name="org.unifiedpush.android.connector.UNREGISTERED"/>
|
||||||
|
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT"/>
|
||||||
|
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED" />
|
||||||
|
<action android:name="org.unifiedpush.android.connector.REGISTRATION_REFUSED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data
|
<meta-data
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package chat.fluffy.fluffychat
|
package chat.fluffy.fluffychat.dev
|
||||||
|
|
||||||
import io.flutter.app.FlutterApplication
|
import io.flutter.app.FlutterApplication
|
||||||
import io.flutter.plugin.common.PluginRegistry
|
import io.flutter.plugin.common.PluginRegistry
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
package chat.fluffy.fluffychat.dev
|
||||||
|
|
||||||
|
import com.famedly.fcm_shared_isolate.FcmSharedIsolateService
|
||||||
|
|
||||||
|
import chat.fluffy.fluffychat.dev.MainActivity
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
import io.flutter.view.FlutterMain
|
||||||
|
import io.flutter.embedding.engine.dart.DartExecutor.DartEntrypoint
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.WindowManager
|
||||||
|
|
||||||
|
class FcmPushService : FcmSharedIsolateService() {
|
||||||
|
override fun getEngine(): FlutterEngine {
|
||||||
|
return provideEngine(getApplicationContext())
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun provideEngine(context: Context): FlutterEngine {
|
||||||
|
var engine = MainActivity.engine
|
||||||
|
if (engine == null) {
|
||||||
|
engine = MainActivity.provideEngine(context)
|
||||||
|
engine.getLocalizationPlugin().sendLocalesToFlutter(
|
||||||
|
context.getResources().getConfiguration())
|
||||||
|
engine.getDartExecutor().executeDartEntrypoint(
|
||||||
|
DartEntrypoint.createDefault())
|
||||||
|
}
|
||||||
|
return engine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,36 @@
|
|||||||
package chat.fluffy.fluffychat
|
package chat.fluffy.fluffychat.dev
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugins.GeneratedPluginRegistrant
|
|
||||||
|
|
||||||
class MainActivity: FlutterActivity() {
|
import android.content.Context
|
||||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
import android.os.Bundle
|
||||||
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
import android.util.Log
|
||||||
|
import android.view.WindowManager
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
if (!BuildConfig.DEBUG) {
|
||||||
|
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE,
|
||||||
|
WindowManager.LayoutParams.FLAG_SECURE)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
override fun provideFlutterEngine(context: Context): FlutterEngine? {
|
||||||
|
return provideEngine(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
|
// do nothing, because the engine was been configured in provideEngine
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var engine: FlutterEngine? = null
|
||||||
|
fun provideEngine(context: Context): FlutterEngine {
|
||||||
|
var eng = engine ?: FlutterEngine(context, emptyArray(), true, false)
|
||||||
|
engine = eng
|
||||||
|
return eng
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
package chat.fluffy.fluffychat.dev
|
||||||
|
|
||||||
|
import org.unifiedpush.flutter.connector.UnifiedPushService
|
||||||
|
|
||||||
|
import chat.fluffy.fluffychat.dev.MainActivity
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
import io.flutter.view.FlutterMain
|
||||||
|
import io.flutter.embedding.engine.dart.DartExecutor.DartEntrypoint
|
||||||
|
import org.unifiedpush.android.connector.MessagingReceiver
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.WindowManager
|
||||||
|
|
||||||
|
val receiverHandler = object : UnifiedPushService() {
|
||||||
|
override fun getEngine(context: Context): FlutterEngine {
|
||||||
|
return provideEngine(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun provideEngine(context: Context): FlutterEngine {
|
||||||
|
var engine = MainActivity.engine
|
||||||
|
if (engine == null) {
|
||||||
|
engine = MainActivity.provideEngine(context)
|
||||||
|
engine.getLocalizationPlugin().sendLocalesToFlutter(
|
||||||
|
context.getResources().getConfiguration())
|
||||||
|
engine.getDartExecutor().executeDartEntrypoint(
|
||||||
|
DartEntrypoint.createDefault())
|
||||||
|
}
|
||||||
|
return engine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnifiedPushReceiver : MessagingReceiver(receiverHandler)
|
@ -1,5 +1,5 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="chat.fluffy.fluffychat">
|
package="chat.fluffy.fluffychat.dev">
|
||||||
<!-- Flutter needs it to communicate with the running application
|
<!-- Flutter needs it to communicate with the running application
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
-->
|
-->
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
json_key_file("keys.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
|
json_key_file("keys.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
|
||||||
package_name("chat.fluffy.fluffychat") # e.g. com.krausefx.app
|
package_name("chat.fluffy.fluffychat.dev") # e.g. com.krausefx.app
|
||||||
|
@ -6,7 +6,6 @@ import 'package:adaptive_dialog/adaptive_dialog.dart';
|
|||||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||||
import 'package:famedlysdk/encryption.dart';
|
import 'package:famedlysdk/encryption.dart';
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
import 'package:fluffychat/utils/firebase_controller.dart';
|
|
||||||
import 'package:fluffychat/utils/matrix_locals.dart';
|
import 'package:fluffychat/utils/matrix_locals.dart';
|
||||||
import 'package:fluffychat/utils/platform_infos.dart';
|
import 'package:fluffychat/utils/platform_infos.dart';
|
||||||
import 'package:fluffychat/utils/sentry_controller.dart';
|
import 'package:fluffychat/utils/sentry_controller.dart';
|
||||||
@ -33,6 +32,10 @@ import 'dialogs/key_verification_dialog.dart';
|
|||||||
import '../utils/platform_infos.dart';
|
import '../utils/platform_infos.dart';
|
||||||
import '../app_config.dart';
|
import '../app_config.dart';
|
||||||
import '../config/setting_keys.dart';
|
import '../config/setting_keys.dart';
|
||||||
|
import '../utils/famedlysdk_client.dart';
|
||||||
|
import '../utils/plugins/background_push_plugin.dart';
|
||||||
|
import '../utils/plugins/background_sync_plugin.dart';
|
||||||
|
import '../utils/plugins/local_notification_plugin.dart';
|
||||||
|
|
||||||
class Matrix extends StatefulWidget {
|
class Matrix extends StatefulWidget {
|
||||||
static const String callNamespace = 'chat.fluffy.jitsi_call';
|
static const String callNamespace = 'chat.fluffy.jitsi_call';
|
||||||
@ -64,6 +67,24 @@ class MatrixState extends State<Matrix> {
|
|||||||
@override
|
@override
|
||||||
BuildContext get context => widget.context;
|
BuildContext get context => widget.context;
|
||||||
|
|
||||||
|
BackgroundPushPlugin _backgroundPushPlugin;
|
||||||
|
// ignore: unused_field
|
||||||
|
BackgroundSyncPlugin _backgroundSyncPlugin;
|
||||||
|
LocalNotificationPlugin _localNotificationsPlugin;
|
||||||
|
|
||||||
|
L10n _l10n;
|
||||||
|
L10n get l10n {
|
||||||
|
if (_l10n != null) {
|
||||||
|
return _l10n;
|
||||||
|
}
|
||||||
|
final l10n = L10n.of(context);
|
||||||
|
if (l10n != null) {
|
||||||
|
_l10n = l10n;
|
||||||
|
return _l10n;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, dynamic> get shareContent => _shareContent;
|
Map<String, dynamic> get shareContent => _shareContent;
|
||||||
set shareContent(Map<String, dynamic> content) {
|
set shareContent(Map<String, dynamic> content) {
|
||||||
_shareContent = content;
|
_shareContent = content;
|
||||||
@ -77,13 +98,6 @@ class MatrixState extends State<Matrix> {
|
|||||||
|
|
||||||
String activeRoomId;
|
String activeRoomId;
|
||||||
File wallpaper;
|
File wallpaper;
|
||||||
String clientName;
|
|
||||||
|
|
||||||
void clean() async {
|
|
||||||
if (!kIsWeb) return;
|
|
||||||
|
|
||||||
await store.deleteItem(clientName);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _initWithStore() async {
|
void _initWithStore() async {
|
||||||
try {
|
try {
|
||||||
@ -98,15 +112,15 @@ class MatrixState extends State<Matrix> {
|
|||||||
await client.requestThirdPartyIdentifiers().then((l) {
|
await client.requestThirdPartyIdentifiers().then((l) {
|
||||||
if (l.isEmpty) {
|
if (l.isEmpty) {
|
||||||
Flushbar(
|
Flushbar(
|
||||||
title: L10n.of(context).warning,
|
title: l10n.warning,
|
||||||
message: L10n.of(context).noPasswordRecoveryDescription,
|
message: l10n.noPasswordRecoveryDescription,
|
||||||
mainButton: RaisedButton(
|
mainButton: RaisedButton(
|
||||||
elevation: 7,
|
elevation: 7,
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: Text(L10n.of(context).edit),
|
child: Text(l10n.edit),
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
AdaptivePageLayout.of(context).pushNamed('/settings/3pid'),
|
AdaptivePageLayout.of(context).pushNamed('/settings/3pid'),
|
||||||
),
|
),
|
||||||
@ -139,7 +153,7 @@ class MatrixState extends State<Matrix> {
|
|||||||
case AuthenticationTypes.password:
|
case AuthenticationTypes.password:
|
||||||
final input = await showTextInputDialog(
|
final input = await showTextInputDialog(
|
||||||
context: context,
|
context: context,
|
||||||
title: L10n.of(context).pleaseEnterYourPassword,
|
title: l10n.pleaseEnterYourPassword,
|
||||||
textFields: [
|
textFields: [
|
||||||
DialogTextField(
|
DialogTextField(
|
||||||
minLines: 1,
|
minLines: 1,
|
||||||
@ -165,10 +179,10 @@ class MatrixState extends State<Matrix> {
|
|||||||
);
|
);
|
||||||
if (OkCancelResult.ok ==
|
if (OkCancelResult.ok ==
|
||||||
await showOkCancelAlertDialog(
|
await showOkCancelAlertDialog(
|
||||||
message: L10n.of(context).pleaseFollowInstructionsOnWeb,
|
message: l10n.pleaseFollowInstructionsOnWeb,
|
||||||
context: context,
|
context: context,
|
||||||
okLabel: L10n.of(context).next,
|
okLabel: l10n.next,
|
||||||
cancelLabel: L10n.of(context).cancel,
|
cancelLabel: l10n.cancel,
|
||||||
)) {
|
)) {
|
||||||
return uiaRequest.completeStage(
|
return uiaRequest.completeStage(
|
||||||
AuthenticationData(session: uiaRequest.session),
|
AuthenticationData(session: uiaRequest.session),
|
||||||
@ -186,7 +200,7 @@ class MatrixState extends State<Matrix> {
|
|||||||
if (room.notificationCount == 0) return;
|
if (room.notificationCount == 0) return;
|
||||||
final event = Event.fromJson(eventUpdate.content, room);
|
final event = Event.fromJson(eventUpdate.content, room);
|
||||||
final body = event.getLocalizedBody(
|
final body = event.getLocalizedBody(
|
||||||
MatrixLocals(L10n.of(context)),
|
MatrixLocals(l10n),
|
||||||
withSenderNamePrefix:
|
withSenderNamePrefix:
|
||||||
!room.isDirectChat || room.lastEvent.senderId == client.userID,
|
!room.isDirectChat || room.lastEvent.senderId == client.userID,
|
||||||
);
|
);
|
||||||
@ -200,7 +214,7 @@ class MatrixState extends State<Matrix> {
|
|||||||
..autoplay = true
|
..autoplay = true
|
||||||
..load();
|
..load();
|
||||||
html.Notification(
|
html.Notification(
|
||||||
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))),
|
room.getLocalizedDisplayname(MatrixLocals(l10n)),
|
||||||
body: body,
|
body: body,
|
||||||
icon: icon,
|
icon: icon,
|
||||||
);
|
);
|
||||||
@ -208,7 +222,7 @@ class MatrixState extends State<Matrix> {
|
|||||||
/*var sessionBus = DBusClient.session();
|
/*var sessionBus = DBusClient.session();
|
||||||
var client = NotificationClient(sessionBus);
|
var client = NotificationClient(sessionBus);
|
||||||
_linuxNotificationIds[roomId] = await client.notify(
|
_linuxNotificationIds[roomId] = await client.notify(
|
||||||
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))),
|
room.getLocalizedDisplayname(MatrixLocals(l10n)),
|
||||||
body: body,
|
body: body,
|
||||||
replacesID: _linuxNotificationIds[roomId] ?? -1,
|
replacesID: _linuxNotificationIds[roomId] ?? -1,
|
||||||
appName: AppConfig.applicationName,
|
appName: AppConfig.applicationName,
|
||||||
@ -261,30 +275,9 @@ class MatrixState extends State<Matrix> {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
clientName =
|
client = getClient();
|
||||||
'${AppConfig.applicationName} ${kIsWeb ? 'Web' : Platform.operatingSystem}';
|
LoadingDialog.defaultTitle = l10n.loadingPleaseWait;
|
||||||
final Set verificationMethods = <KeyVerificationMethod>{
|
LoadingDialog.defaultBackLabel = l10n.close;
|
||||||
KeyVerificationMethod.numbers
|
|
||||||
};
|
|
||||||
if (PlatformInfos.isMobile || (!kIsWeb && Platform.isLinux)) {
|
|
||||||
// emojis don't show in web somehow
|
|
||||||
verificationMethods.add(KeyVerificationMethod.emoji);
|
|
||||||
}
|
|
||||||
client = Client(
|
|
||||||
clientName,
|
|
||||||
enableE2eeRecovery: true,
|
|
||||||
verificationMethods: verificationMethods,
|
|
||||||
importantStateEvents: <String>{
|
|
||||||
'im.ponies.room_emotes', // we want emotes to work properly
|
|
||||||
},
|
|
||||||
databaseBuilder: getDatabase,
|
|
||||||
supportedLoginTypes: {
|
|
||||||
AuthenticationTypes.password,
|
|
||||||
if (PlatformInfos.isMobile) AuthenticationTypes.sso
|
|
||||||
},
|
|
||||||
);
|
|
||||||
LoadingDialog.defaultTitle = L10n.of(context).loadingPleaseWait;
|
|
||||||
LoadingDialog.defaultBackLabel = L10n.of(context).close;
|
|
||||||
LoadingDialog.defaultOnError = (Object e) => e.toLocalizedString(context);
|
LoadingDialog.defaultOnError = (Object e) => e.toLocalizedString(context);
|
||||||
|
|
||||||
onRoomKeyRequestSub ??=
|
onRoomKeyRequestSub ??=
|
||||||
@ -296,11 +289,11 @@ class MatrixState extends State<Matrix> {
|
|||||||
final sender = room.getUserByMXIDSync(request.sender);
|
final sender = room.getUserByMXIDSync(request.sender);
|
||||||
if (await showOkCancelAlertDialog(
|
if (await showOkCancelAlertDialog(
|
||||||
context: context,
|
context: context,
|
||||||
title: L10n.of(context).requestToReadOlderMessages,
|
title: l10n.requestToReadOlderMessages,
|
||||||
message:
|
message:
|
||||||
'${sender.id}\n\n${L10n.of(context).device}:\n${request.requestingDevice.deviceId}\n\n${L10n.of(context).publicKey}:\n${request.requestingDevice.ed25519Key.beautified}',
|
'${sender.id}\n\n${l10n.device}:\n${request.requestingDevice.deviceId}\n\n${l10n.publicKey}:\n${request.requestingDevice.ed25519Key.beautified}',
|
||||||
okLabel: L10n.of(context).verify,
|
okLabel: l10n.verify,
|
||||||
cancelLabel: L10n.of(context).deny,
|
cancelLabel: l10n.deny,
|
||||||
) ==
|
) ==
|
||||||
OkCancelResult.ok) {
|
OkCancelResult.ok) {
|
||||||
await request.forwardKey();
|
await request.forwardKey();
|
||||||
@ -319,8 +312,8 @@ class MatrixState extends State<Matrix> {
|
|||||||
};
|
};
|
||||||
if (await showOkCancelAlertDialog(
|
if (await showOkCancelAlertDialog(
|
||||||
context: context,
|
context: context,
|
||||||
title: L10n.of(context).newVerificationRequest,
|
title: l10n.newVerificationRequest,
|
||||||
message: L10n.of(context).askVerificationRequest(request.userId),
|
message: l10n.askVerificationRequest(request.userId),
|
||||||
) ==
|
) ==
|
||||||
OkCancelResult.ok) {
|
OkCancelResult.ok) {
|
||||||
request.onUpdate = null;
|
request.onUpdate = null;
|
||||||
@ -328,7 +321,7 @@ class MatrixState extends State<Matrix> {
|
|||||||
await request.acceptVerification();
|
await request.acceptVerification();
|
||||||
await KeyVerificationDialog(
|
await KeyVerificationDialog(
|
||||||
request: request,
|
request: request,
|
||||||
l10n: L10n.of(context),
|
l10n: l10n,
|
||||||
).show(context);
|
).show(context);
|
||||||
} else {
|
} else {
|
||||||
request.onUpdate = null;
|
request.onUpdate = null;
|
||||||
@ -346,12 +339,6 @@ class MatrixState extends State<Matrix> {
|
|||||||
if (loginState != state) {
|
if (loginState != state) {
|
||||||
loginState = state;
|
loginState = state;
|
||||||
widget.apl.currentState.pushNamedAndRemoveAllOthers('/');
|
widget.apl.currentState.pushNamedAndRemoveAllOthers('/');
|
||||||
if (loginState == LoginState.logged) {
|
|
||||||
FirebaseController.context = context;
|
|
||||||
FirebaseController.matrix = this;
|
|
||||||
FirebaseController.setupFirebase(clientName)
|
|
||||||
.catchError(SentryController.captureException);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
onUiaRequest ??= client.onUiaRequest.stream.listen(_onUiaRequest);
|
onUiaRequest ??= client.onUiaRequest.stream.listen(_onUiaRequest);
|
||||||
@ -367,6 +354,16 @@ class MatrixState extends State<Matrix> {
|
|||||||
.listen(_showLocalNotification);
|
.listen(_showLocalNotification);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (PlatformInfos.isMobile) {
|
||||||
|
_backgroundPushPlugin = BackgroundPushPlugin(this);
|
||||||
|
//
|
||||||
|
_backgroundPushPlugin?.setupPush();
|
||||||
|
}
|
||||||
|
if (PlatformInfos.isMobile || PlatformInfos.isWeb) {
|
||||||
|
_localNotificationsPlugin = LocalNotificationPlugin(this);
|
||||||
|
}
|
||||||
|
_backgroundSyncPlugin = BackgroundSyncPlugin(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void initSettings() {
|
void initSettings() {
|
||||||
@ -402,6 +399,11 @@ class MatrixState extends State<Matrix> {
|
|||||||
onNotification?.cancel();
|
onNotification?.cancel();
|
||||||
onFocusSub?.cancel();
|
onFocusSub?.cancel();
|
||||||
onBlurSub?.cancel();
|
onBlurSub?.cancel();
|
||||||
|
_localNotificationsPlugin.onFocusSub?.cancel();
|
||||||
|
_localNotificationsPlugin.onBlurSub?.cancel();
|
||||||
|
_localNotificationsPlugin?.matrix = null;
|
||||||
|
_backgroundPushPlugin?.onLogin?.cancel();
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,4 +15,5 @@ abstract class SettingKeys {
|
|||||||
static const String unifiedPushRegistered =
|
static const String unifiedPushRegistered =
|
||||||
'chat.fluffy.unifiedpush.registered';
|
'chat.fluffy.unifiedpush.registered';
|
||||||
static const String unifiedPushEndpoint = 'chat.fluffy.unifiedpush.endpoint';
|
static const String unifiedPushEndpoint = 'chat.fluffy.unifiedpush.endpoint';
|
||||||
|
static const String notificationCurrentIds = 'chat.fluffy.notification_ids';
|
||||||
}
|
}
|
||||||
|
@ -19,12 +19,26 @@ import 'package:universal_html/prefer_universal/html.dart' as html;
|
|||||||
import 'components/matrix.dart';
|
import 'components/matrix.dart';
|
||||||
import 'config/themes.dart';
|
import 'config/themes.dart';
|
||||||
import 'app_config.dart';
|
import 'app_config.dart';
|
||||||
|
import 'utils/famedlysdk_client.dart';
|
||||||
|
import 'utils/platform_infos.dart';
|
||||||
|
import 'utils/plugins/background_push_plugin.dart';
|
||||||
|
import 'utils/plugins/background_sync_plugin.dart';
|
||||||
|
import 'utils/plugins/local_notification_plugin.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
SystemUiOverlayStyle(statusBarColor: Colors.transparent));
|
SystemUiOverlayStyle(statusBarColor: Colors.transparent));
|
||||||
FlutterError.onError = (FlutterErrorDetails details) =>
|
FlutterError.onError = (FlutterErrorDetails details) =>
|
||||||
Zone.current.handleUncaughtError(details.exception, details.stack);
|
Zone.current.handleUncaughtError(details.exception, details.stack);
|
||||||
|
|
||||||
|
final client = getClient();
|
||||||
|
BackgroundSyncPlugin.clientOnly(client);
|
||||||
|
if (PlatformInfos.isMobile) {
|
||||||
|
LocalNotificationPlugin.clientOnly(client);
|
||||||
|
BackgroundPushPlugin.clientOnly(client);
|
||||||
|
}
|
||||||
|
|
||||||
runZonedGuarded(
|
runZonedGuarded(
|
||||||
() => runApp(PlatformInfos.isMobile
|
() => runApp(PlatformInfos.isMobile
|
||||||
? AppLock(
|
? AppLock(
|
||||||
|
34
lib/utils/famedlysdk_client.dart
Normal file
34
lib/utils/famedlysdk_client.dart
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:famedlysdk/encryption.dart';
|
||||||
|
import 'platform_infos.dart';
|
||||||
|
import 'famedlysdk_store.dart';
|
||||||
|
|
||||||
|
Client _client;
|
||||||
|
|
||||||
|
Client getClient() {
|
||||||
|
if (_client != null) {
|
||||||
|
return _client;
|
||||||
|
}
|
||||||
|
final clientName = PlatformInfos.clientName;
|
||||||
|
final Set verificationMethods = <KeyVerificationMethod>{
|
||||||
|
KeyVerificationMethod.numbers
|
||||||
|
};
|
||||||
|
if (PlatformInfos.isMobile || PlatformInfos.isLinux) {
|
||||||
|
// emojis don't show in web somehow
|
||||||
|
verificationMethods.add(KeyVerificationMethod.emoji);
|
||||||
|
}
|
||||||
|
_client = Client(
|
||||||
|
clientName,
|
||||||
|
enableE2eeRecovery: true,
|
||||||
|
verificationMethods: verificationMethods,
|
||||||
|
importantStateEvents: <String>{
|
||||||
|
'im.ponies.room_emotes', // we want emotes to work properly
|
||||||
|
},
|
||||||
|
databaseBuilder: getDatabase,
|
||||||
|
supportedLoginTypes: {
|
||||||
|
AuthenticationTypes.password,
|
||||||
|
if (PlatformInfos.isMobile) AuthenticationTypes.sso
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return _client;
|
||||||
|
}
|
@ -1,528 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:fluffychat/app_config.dart';
|
|
||||||
import 'package:fluffychat/utils/platform_infos.dart';
|
|
||||||
import 'package:flushbar/flushbar_helper.dart';
|
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
|
||||||
import 'package:fluffychat/components/matrix.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/l10n_en.dart';
|
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:unifiedpush/unifiedpush.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
|
|
||||||
import '../components/matrix.dart';
|
|
||||||
import '../config/setting_keys.dart';
|
|
||||||
import 'famedlysdk_store.dart';
|
|
||||||
import 'matrix_locals.dart';
|
|
||||||
|
|
||||||
abstract class FirebaseController {
|
|
||||||
static final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
|
|
||||||
static final FlutterLocalNotificationsPlugin
|
|
||||||
_flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
|
||||||
static BuildContext context;
|
|
||||||
static MatrixState matrix;
|
|
||||||
|
|
||||||
static Future<void> setupFirebase(String clientName) async {
|
|
||||||
if (!PlatformInfos.isMobile) return;
|
|
||||||
if (Platform.isIOS) iOS_Permission();
|
|
||||||
|
|
||||||
Function goToRoom = (dynamic message) async {
|
|
||||||
try {
|
|
||||||
String roomId;
|
|
||||||
if (message is String && message[0] == '{') {
|
|
||||||
message = json.decode(message);
|
|
||||||
}
|
|
||||||
if (message is String) {
|
|
||||||
roomId = message;
|
|
||||||
} else if (message is Map) {
|
|
||||||
roomId = (message['data'] ??
|
|
||||||
message['notification'] ??
|
|
||||||
message)['room_id'];
|
|
||||||
}
|
|
||||||
if (roomId?.isEmpty ?? true) throw ('Bad roomId');
|
|
||||||
await matrix.widget.apl.currentState
|
|
||||||
.pushNamedAndRemoveUntilIsFirst('/rooms/${roomId}');
|
|
||||||
} catch (_) {
|
|
||||||
await FlushbarHelper.createError(message: 'Failed to open chat...')
|
|
||||||
.show(context);
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
|
|
||||||
var initializationSettingsAndroid =
|
|
||||||
AndroidInitializationSettings('notifications_icon');
|
|
||||||
var initializationSettingsIOS =
|
|
||||||
IOSInitializationSettings(onDidReceiveLocalNotification: (i, a, b, c) {
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
var initializationSettings = InitializationSettings(
|
|
||||||
android: initializationSettingsAndroid,
|
|
||||||
iOS: initializationSettingsIOS,
|
|
||||||
);
|
|
||||||
await _flutterLocalNotificationsPlugin.initialize(initializationSettings,
|
|
||||||
onSelectNotification: goToRoom);
|
|
||||||
|
|
||||||
// ignore: unawaited_futures
|
|
||||||
_flutterLocalNotificationsPlugin
|
|
||||||
.getNotificationAppLaunchDetails()
|
|
||||||
.then((details) {
|
|
||||||
if (details == null || !details.didNotificationLaunchApp) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
goToRoom(details.payload);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!Platform.isIOS && (await UnifiedPush.getDistributors()).isNotEmpty) {
|
|
||||||
await setupUnifiedPush(clientName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String fcmToken;
|
|
||||||
try {
|
|
||||||
fcmToken = await _firebaseMessaging.getToken();
|
|
||||||
if (fcmToken?.isEmpty ?? true) {
|
|
||||||
throw '_firebaseMessaging.getToken() has not thrown an exception but returned no token';
|
|
||||||
}
|
|
||||||
} catch (e, s) {
|
|
||||||
Logs().w('Unable to get firebase token', e, s);
|
|
||||||
fcmToken = null;
|
|
||||||
}
|
|
||||||
if (fcmToken?.isEmpty ?? true) {
|
|
||||||
// no google services warning
|
|
||||||
if (await matrix.store.getItemBool(SettingKeys.showNoGoogle, true)) {
|
|
||||||
await FlushbarHelper.createError(
|
|
||||||
message: L10n.of(context).noGoogleServicesWarning,
|
|
||||||
duration: Duration(seconds: 15),
|
|
||||||
).show(context);
|
|
||||||
if (null == await matrix.store.getItemBool(SettingKeys.showNoGoogle)) {
|
|
||||||
await matrix.store.setItemBool(SettingKeys.showNoGoogle, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await setupPusher(
|
|
||||||
clientName: clientName,
|
|
||||||
gatewayUrl: AppConfig.pushNotificationsGatewayUrl,
|
|
||||||
token: fcmToken,
|
|
||||||
);
|
|
||||||
|
|
||||||
_firebaseMessaging.configure(
|
|
||||||
onMessage: _onFcmMessage,
|
|
||||||
onBackgroundMessage: _onFcmMessage,
|
|
||||||
onResume: goToRoom,
|
|
||||||
onLaunch: goToRoom,
|
|
||||||
);
|
|
||||||
Logs().i('[Push] Firebase initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> setupPusher({
|
|
||||||
String clientName,
|
|
||||||
String gatewayUrl,
|
|
||||||
String token,
|
|
||||||
Set<String> oldTokens,
|
|
||||||
}) async {
|
|
||||||
oldTokens ??= <String>{};
|
|
||||||
final client = matrix.client;
|
|
||||||
final pushers = await client.requestPushers().catchError((e) {
|
|
||||||
Logs().w('[Push] Unable to request pushers', e);
|
|
||||||
return <Pusher>[];
|
|
||||||
});
|
|
||||||
var setNewPusher = false;
|
|
||||||
if (gatewayUrl != null && token != null && clientName != null) {
|
|
||||||
final currentPushers = pushers.where((pusher) => pusher.pushkey == token);
|
|
||||||
if (currentPushers.length == 1 &&
|
|
||||||
currentPushers.first.kind == 'http' &&
|
|
||||||
currentPushers.first.appId == AppConfig.pushNotificationsAppId &&
|
|
||||||
currentPushers.first.appDisplayName == clientName &&
|
|
||||||
currentPushers.first.deviceDisplayName == client.deviceName &&
|
|
||||||
currentPushers.first.lang == 'en' &&
|
|
||||||
currentPushers.first.data.url.toString() == gatewayUrl &&
|
|
||||||
currentPushers.first.data.format ==
|
|
||||||
AppConfig.pushNotificationsPusherFormat) {
|
|
||||||
Logs().i('[Push] Pusher already set');
|
|
||||||
} else {
|
|
||||||
oldTokens.add(token);
|
|
||||||
setNewPusher = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (final pusher in pushers) {
|
|
||||||
if (oldTokens.contains(pusher.pushkey)) {
|
|
||||||
pusher.kind = null;
|
|
||||||
try {
|
|
||||||
await client.setPusher(
|
|
||||||
pusher,
|
|
||||||
append: true,
|
|
||||||
);
|
|
||||||
Logs().i('[Push] Removed legacy pusher for this device');
|
|
||||||
} catch (err) {
|
|
||||||
Logs().w('[Push] Failed to remove old pusher', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (setNewPusher) {
|
|
||||||
try {
|
|
||||||
await client.setPusher(
|
|
||||||
Pusher(
|
|
||||||
token,
|
|
||||||
AppConfig.pushNotificationsAppId,
|
|
||||||
clientName,
|
|
||||||
client.deviceName,
|
|
||||||
'en',
|
|
||||||
PusherData(
|
|
||||||
url: Uri.parse(gatewayUrl),
|
|
||||||
format: AppConfig.pushNotificationsPusherFormat,
|
|
||||||
),
|
|
||||||
kind: 'http',
|
|
||||||
),
|
|
||||||
append: false,
|
|
||||||
);
|
|
||||||
} catch (e, s) {
|
|
||||||
Logs().e('[Push] Unable to set pushers', e, s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> onUnifiedPushMessage(String payload) async {
|
|
||||||
Map<String, dynamic> data;
|
|
||||||
try {
|
|
||||||
data = Map<String, dynamic>.from(json.decode(payload)['notification']);
|
|
||||||
await _onMessage(data);
|
|
||||||
} catch (e, s) {
|
|
||||||
Logs().e('[Push] Failed to display message', e, s);
|
|
||||||
await _showDefaultNotification(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> onRemoveEndpoint() async {
|
|
||||||
Logs().i('[Push] Removing UnifiedPush endpoint...');
|
|
||||||
// we need our own store object as it is likely that we don't have a context here
|
|
||||||
final store = Store();
|
|
||||||
final oldEndpoint = await store.getItem(SettingKeys.unifiedPushEndpoint);
|
|
||||||
await store.setItemBool(SettingKeys.unifiedPushRegistered, false);
|
|
||||||
await store.deleteItem(SettingKeys.unifiedPushEndpoint);
|
|
||||||
if (matrix != null && (oldEndpoint?.isNotEmpty ?? false)) {
|
|
||||||
// remove the old pusher
|
|
||||||
await setupPusher(
|
|
||||||
oldTokens: {oldEndpoint},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> onBackgroundNewEndpoint(String endpoint) async {
|
|
||||||
// just remove the old endpoint. we'll deal with this when the app next starts up
|
|
||||||
await onRemoveEndpoint();
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> setupUnifiedPush(String clientName) async {
|
|
||||||
final onNewEndpoint = (String newEndpoint) async {
|
|
||||||
if (newEndpoint?.isEmpty ?? true) {
|
|
||||||
await onRemoveEndpoint();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var endpoint =
|
|
||||||
'https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify';
|
|
||||||
try {
|
|
||||||
final url = Uri.parse(newEndpoint)
|
|
||||||
.replace(
|
|
||||||
path: '/_matrix/push/v1/notify',
|
|
||||||
query: '',
|
|
||||||
)
|
|
||||||
.toString()
|
|
||||||
.split('?')
|
|
||||||
.first;
|
|
||||||
final res = json.decode(utf8.decode((await http.get(url)).bodyBytes));
|
|
||||||
if (res['gateway'] == 'matrix') {
|
|
||||||
endpoint = url;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
Logs().i('[Push] No self-hosted unified push gateway present: ' +
|
|
||||||
newEndpoint);
|
|
||||||
}
|
|
||||||
Logs().i('[Push] UnifiedPush using endpoint ' + endpoint);
|
|
||||||
final oldTokens = <String>{};
|
|
||||||
try {
|
|
||||||
final fcmToken = await _firebaseMessaging.getToken();
|
|
||||||
oldTokens.add(fcmToken);
|
|
||||||
} catch (_) {}
|
|
||||||
await setupPusher(
|
|
||||||
clientName: clientName,
|
|
||||||
gatewayUrl: endpoint,
|
|
||||||
token: newEndpoint,
|
|
||||||
oldTokens: oldTokens,
|
|
||||||
);
|
|
||||||
await matrix.store.setItem(SettingKeys.unifiedPushEndpoint, newEndpoint);
|
|
||||||
await matrix.store.setItemBool(SettingKeys.unifiedPushRegistered, true);
|
|
||||||
};
|
|
||||||
await UnifiedPush.initialize(
|
|
||||||
onNewEndpoint, // new endpoint
|
|
||||||
onRemoveEndpoint, // registration failed
|
|
||||||
onRemoveEndpoint, // registration removed
|
|
||||||
onRemoveEndpoint, // unregistered
|
|
||||||
onUnifiedPushMessage, // foreground message
|
|
||||||
onBackgroundNewEndpoint, // background new endpoint (be static)
|
|
||||||
onRemoveEndpoint, // background unregistered (be static)
|
|
||||||
onUnifiedPushMessage, // background push message (be static)
|
|
||||||
);
|
|
||||||
if (!(await matrix.store
|
|
||||||
.getItemBool(SettingKeys.unifiedPushRegistered, false))) {
|
|
||||||
Logs().i('[Push] UnifiedPush not registered, attempting to do so...');
|
|
||||||
await UnifiedPush.registerAppWithDialog();
|
|
||||||
} else {
|
|
||||||
// make sure the endpoint is up-to-date etc.
|
|
||||||
await onNewEndpoint(
|
|
||||||
await matrix.store.getItem(SettingKeys.unifiedPushEndpoint));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<dynamic> _onFcmMessage(Map<String, dynamic> message) async {
|
|
||||||
Map<String, dynamic> data;
|
|
||||||
try {
|
|
||||||
data = Map<String, dynamic>.from(message['data'] ?? message);
|
|
||||||
await _onMessage(data);
|
|
||||||
} catch (e, s) {
|
|
||||||
Logs().e('[Push] Error while processing notification', e, s);
|
|
||||||
await _showDefaultNotification(data);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<dynamic> _onMessage(Map<String, dynamic> data) async {
|
|
||||||
final String roomId = data['room_id'];
|
|
||||||
final String eventId = data['event_id'];
|
|
||||||
final unread = ((data['counts'] is String
|
|
||||||
? json.decode(data.tryGet<String>('counts', '{}'))
|
|
||||||
: data.tryGet<Map<String, dynamic>>(
|
|
||||||
'counts', <String, dynamic>{})) as Map<String, dynamic>)
|
|
||||||
.tryGet<int>('unread');
|
|
||||||
if ((roomId?.isEmpty ?? true) ||
|
|
||||||
(eventId?.isEmpty ?? true) ||
|
|
||||||
unread == 0) {
|
|
||||||
Logs().i('[Push] New clearing push');
|
|
||||||
await _flutterLocalNotificationsPlugin.cancelAll();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (matrix != null && matrix?.activeRoomId == roomId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Logs().i('[Push] New message received');
|
|
||||||
// FIXME unable to init without context currently https://github.com/flutter/flutter/issues/67092
|
|
||||||
// Locked on EN until issue resolved
|
|
||||||
final i18n = context == null ? L10nEn() : L10n.of(context);
|
|
||||||
|
|
||||||
// Get the client
|
|
||||||
var client = matrix?.client;
|
|
||||||
var tempClient = false;
|
|
||||||
if (client == null) {
|
|
||||||
tempClient = true;
|
|
||||||
final platform = kIsWeb ? 'Web' : Platform.operatingSystem;
|
|
||||||
final clientName = 'FluffyChat $platform';
|
|
||||||
client = Client(clientName, databaseBuilder: getDatabase)..init();
|
|
||||||
Logs().i('[Push] Use a temp client');
|
|
||||||
await client.onLoginStateChanged.stream
|
|
||||||
.firstWhere((l) => l == LoginState.logged)
|
|
||||||
.timeout(
|
|
||||||
Duration(seconds: 20),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final roomFuture =
|
|
||||||
client.onRoomUpdate.stream.where((u) => u.id == roomId).first;
|
|
||||||
final eventFuture = client.onEvent.stream
|
|
||||||
.where((u) => u.content['event_id'] == eventId)
|
|
||||||
.first;
|
|
||||||
|
|
||||||
// Get the room
|
|
||||||
var room = client.getRoomById(roomId);
|
|
||||||
if (room == null) {
|
|
||||||
Logs().i('[Push] Wait for the room');
|
|
||||||
await roomFuture.timeout(Duration(seconds: 5));
|
|
||||||
Logs().i('[Push] Room found');
|
|
||||||
room = client.getRoomById(roomId);
|
|
||||||
if (room == null) return null;
|
|
||||||
} else {
|
|
||||||
// cancel the future
|
|
||||||
// ignore: unawaited_futures
|
|
||||||
roomFuture.timeout(Duration(seconds: 0)).catchError((_) => null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the event
|
|
||||||
var event = await client.database.getEventById(client.id, eventId, room);
|
|
||||||
if (event == null) {
|
|
||||||
Logs().i('[Push] Wait for the event');
|
|
||||||
final eventUpdate = await eventFuture.timeout(Duration(seconds: 5));
|
|
||||||
Logs().i('[Push] Event found');
|
|
||||||
event = Event.fromJson(eventUpdate.content, room);
|
|
||||||
if (room == null) return null;
|
|
||||||
} else {
|
|
||||||
// cancel the future
|
|
||||||
// ignore: unawaited_futures
|
|
||||||
eventFuture.timeout(Duration(seconds: 0)).catchError((_) => null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count all unread events
|
|
||||||
var unreadEvents = 0;
|
|
||||||
client.rooms.forEach((Room room) => unreadEvents += room.notificationCount);
|
|
||||||
|
|
||||||
// Calculate title
|
|
||||||
final title = unread > 1
|
|
||||||
? i18n.unreadMessagesInChats(unreadEvents.toString(), unread.toString())
|
|
||||||
: i18n.unreadMessages(unreadEvents.toString());
|
|
||||||
|
|
||||||
// Calculate the body
|
|
||||||
final body = event.getLocalizedBody(
|
|
||||||
MatrixLocals(i18n),
|
|
||||||
withSenderNamePrefix: true,
|
|
||||||
hideReply: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
// The person object for the android message style notification
|
|
||||||
final person = Person(
|
|
||||||
name: room.getLocalizedDisplayname(MatrixLocals(i18n)),
|
|
||||||
icon: room.avatar == null
|
|
||||||
? null
|
|
||||||
: BitmapFilePathAndroidIcon(
|
|
||||||
await downloadAndSaveAvatar(
|
|
||||||
room.avatar,
|
|
||||||
client,
|
|
||||||
width: 126,
|
|
||||||
height: 126,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Show notification
|
|
||||||
var androidPlatformChannelSpecifics = _getAndroidNotificationDetails(
|
|
||||||
styleInformation: MessagingStyleInformation(
|
|
||||||
person,
|
|
||||||
conversationTitle: title,
|
|
||||||
messages: [
|
|
||||||
Message(
|
|
||||||
body,
|
|
||||||
event.originServerTs,
|
|
||||||
person,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
ticker: i18n.newMessageInFluffyChat,
|
|
||||||
);
|
|
||||||
var iOSPlatformChannelSpecifics = IOSNotificationDetails();
|
|
||||||
var platformChannelSpecifics = NotificationDetails(
|
|
||||||
android: androidPlatformChannelSpecifics,
|
|
||||||
iOS: iOSPlatformChannelSpecifics,
|
|
||||||
);
|
|
||||||
await _flutterLocalNotificationsPlugin.show(
|
|
||||||
0,
|
|
||||||
room.getLocalizedDisplayname(MatrixLocals(i18n)),
|
|
||||||
body,
|
|
||||||
platformChannelSpecifics,
|
|
||||||
payload: roomId);
|
|
||||||
|
|
||||||
if (tempClient) {
|
|
||||||
await client.dispose();
|
|
||||||
client = null;
|
|
||||||
Logs().i('[Push] Temp client disposed');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<dynamic> _showDefaultNotification(
|
|
||||||
Map<String, dynamic> data) async {
|
|
||||||
try {
|
|
||||||
var flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
|
||||||
// Init notifications framework
|
|
||||||
var initializationSettingsAndroid =
|
|
||||||
AndroidInitializationSettings('notifications_icon');
|
|
||||||
var initializationSettingsIOS = IOSInitializationSettings();
|
|
||||||
var initializationSettings = InitializationSettings(
|
|
||||||
android: initializationSettingsAndroid,
|
|
||||||
iOS: initializationSettingsIOS,
|
|
||||||
);
|
|
||||||
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
|
|
||||||
|
|
||||||
// FIXME unable to init without context currently https://github.com/flutter/flutter/issues/67092
|
|
||||||
// Locked on en for now
|
|
||||||
//final l10n = L10n(Platform.localeName);
|
|
||||||
final l10n = L10nEn();
|
|
||||||
String eventID = data['event_id'];
|
|
||||||
String roomID = data['room_id'];
|
|
||||||
final unread = ((data['counts'] is String
|
|
||||||
? json.decode(data.tryGet<String>('counts', '{}'))
|
|
||||||
: data.tryGet<Map<String, dynamic>>(
|
|
||||||
'counts', <String, dynamic>{})) as Map<String, dynamic>)
|
|
||||||
.tryGet<int>('unread', 1);
|
|
||||||
await flutterLocalNotificationsPlugin.cancelAll();
|
|
||||||
if (unread == 0 || roomID == null || eventID == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display notification
|
|
||||||
var androidPlatformChannelSpecifics = _getAndroidNotificationDetails();
|
|
||||||
var iOSPlatformChannelSpecifics = IOSNotificationDetails();
|
|
||||||
var platformChannelSpecifics = NotificationDetails(
|
|
||||||
android: androidPlatformChannelSpecifics,
|
|
||||||
iOS: iOSPlatformChannelSpecifics,
|
|
||||||
);
|
|
||||||
final title = l10n.unreadChats(unread.toString());
|
|
||||||
await flutterLocalNotificationsPlugin.show(
|
|
||||||
1, title, l10n.openAppToReadMessages, platformChannelSpecifics,
|
|
||||||
payload: roomID);
|
|
||||||
} catch (e, s) {
|
|
||||||
Logs().e('[Push] Error while processing background notification', e, s);
|
|
||||||
}
|
|
||||||
return Future<void>.value();
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<String> downloadAndSaveAvatar(Uri content, Client client,
|
|
||||||
{int width, int height}) async {
|
|
||||||
final thumbnail = width == null && height == null ? false : true;
|
|
||||||
final tempDirectory = (await getTemporaryDirectory()).path;
|
|
||||||
final prefix = thumbnail ? 'thumbnail' : '';
|
|
||||||
var file =
|
|
||||||
File('$tempDirectory/${prefix}_${content.toString().split("/").last}');
|
|
||||||
|
|
||||||
if (!file.existsSync()) {
|
|
||||||
final url = thumbnail
|
|
||||||
? content.getThumbnail(client, width: width, height: height)
|
|
||||||
: content.getDownloadLink(client);
|
|
||||||
var request = await HttpClient().getUrl(Uri.parse(url));
|
|
||||||
var response = await request.close();
|
|
||||||
var bytes = await consolidateHttpClientResponseBytes(response);
|
|
||||||
await file.writeAsBytes(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
return file.path;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void iOS_Permission() {
|
|
||||||
_firebaseMessaging.requestNotificationPermissions(
|
|
||||||
IosNotificationSettings(sound: true, badge: true, alert: true));
|
|
||||||
_firebaseMessaging.onIosSettingsRegistered
|
|
||||||
.listen((IosNotificationSettings settings) {
|
|
||||||
Logs().i('Settings registered: $settings');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static AndroidNotificationDetails _getAndroidNotificationDetails(
|
|
||||||
{MessagingStyleInformation styleInformation, String ticker}) {
|
|
||||||
final color =
|
|
||||||
context != null ? Theme.of(context).primaryColor : Color(0xFF5625BA);
|
|
||||||
|
|
||||||
return AndroidNotificationDetails(
|
|
||||||
AppConfig.pushNotificationsChannelId,
|
|
||||||
AppConfig.pushNotificationsChannelName,
|
|
||||||
AppConfig.pushNotificationsChannelDescription,
|
|
||||||
styleInformation: styleInformation,
|
|
||||||
importance: Importance.max,
|
|
||||||
priority: Priority.high,
|
|
||||||
ticker: ticker,
|
|
||||||
color: color,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -11,21 +11,26 @@ import '../app_config.dart';
|
|||||||
|
|
||||||
abstract class PlatformInfos {
|
abstract class PlatformInfos {
|
||||||
static bool get isWeb => kIsWeb;
|
static bool get isWeb => kIsWeb;
|
||||||
|
static bool get isLinux => Platform.isLinux;
|
||||||
|
static bool get isWindows => Platform.isWindows;
|
||||||
|
static bool get isMacOS => Platform.isMacOS;
|
||||||
|
static bool get isIOS => Platform.isIOS;
|
||||||
|
static bool get isAndroid => Platform.isAndroid;
|
||||||
|
|
||||||
static bool get isCupertinoStyle =>
|
static bool get isCupertinoStyle => !kIsWeb && (isIOS || isMacOS);
|
||||||
!kIsWeb && (Platform.isIOS || Platform.isMacOS);
|
|
||||||
|
|
||||||
static bool get isMobile => !kIsWeb && (Platform.isAndroid || Platform.isIOS);
|
static bool get isMobile => !kIsWeb && (isAndroid || isIOS);
|
||||||
|
|
||||||
/// For desktops which don't support ChachedNetworkImage yet
|
/// For desktops which don't support ChachedNetworkImage yet
|
||||||
static bool get isBetaDesktop =>
|
static bool get isBetaDesktop => !kIsWeb && (isWindows || isLinux);
|
||||||
!kIsWeb && (Platform.isWindows || Platform.isLinux);
|
|
||||||
|
|
||||||
static bool get isDesktop =>
|
static bool get isDesktop => !kIsWeb && (isLinux || isWindows || isMacOS);
|
||||||
!kIsWeb && (Platform.isLinux || Platform.isWindows || Platform.isMacOS);
|
|
||||||
|
|
||||||
static bool get usesTouchscreen => !isMobile;
|
static bool get usesTouchscreen => !isMobile;
|
||||||
|
|
||||||
|
static String get clientName =>
|
||||||
|
'${AppConfig.applicationName} ${isWeb ? 'Web' : Platform.operatingSystem}';
|
||||||
|
|
||||||
static Future<String> getVersion() async {
|
static Future<String> getVersion() async {
|
||||||
var version = kIsWeb ? 'Web' : 'Unknown';
|
var version = kIsWeb ? 'Web' : 'Unknown';
|
||||||
try {
|
try {
|
||||||
|
469
lib/utils/plugins/background_push_plugin.dart
Normal file
469
lib/utils/plugins/background_push_plugin.dart
Normal file
@ -0,0 +1,469 @@
|
|||||||
|
/*
|
||||||
|
* Famedly
|
||||||
|
* Copyright (C) 2020, 2021 Famedly GmbH
|
||||||
|
* Copyright (C) 2021 Fluffychat
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:fcm_shared_isolate/fcm_shared_isolate.dart';
|
||||||
|
import 'package:flushbar/flushbar_helper.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:pedantic/pedantic.dart';
|
||||||
|
import 'package:unifiedpush/unifiedpush.dart';
|
||||||
|
|
||||||
|
import '../../components/matrix.dart';
|
||||||
|
import '../platform_infos.dart';
|
||||||
|
import '../../app_config.dart';
|
||||||
|
import '../../config/setting_keys.dart';
|
||||||
|
import '../famedlysdk_store.dart';
|
||||||
|
|
||||||
|
class PreNotify {
|
||||||
|
PreNotify(this.roomId, this.eventId);
|
||||||
|
String roomId;
|
||||||
|
String eventId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoTokenException implements Exception {
|
||||||
|
String get cause => 'Cannot get firebase token';
|
||||||
|
}
|
||||||
|
|
||||||
|
class BackgroundPushPlugin {
|
||||||
|
static BackgroundPushPlugin _instance;
|
||||||
|
Client client;
|
||||||
|
MatrixState matrix;
|
||||||
|
String _fcmToken;
|
||||||
|
LoginState _loginState;
|
||||||
|
|
||||||
|
final StreamController<PreNotify> onPreNotify = StreamController.broadcast();
|
||||||
|
|
||||||
|
final pendingTests = <String, Completer<void>>{};
|
||||||
|
|
||||||
|
void Function() _onMatrixInit;
|
||||||
|
|
||||||
|
DateTime lastReceivedPush;
|
||||||
|
|
||||||
|
BackgroundPushPlugin._(this.client) {
|
||||||
|
onLogin ??=
|
||||||
|
client.onLoginStateChanged.stream.listen(handleLoginStateChanged);
|
||||||
|
_firebaseMessaging.setListeners(
|
||||||
|
onMessage: _onFcmMessage,
|
||||||
|
onNewToken: _newFcmToken,
|
||||||
|
);
|
||||||
|
UnifiedPush.setListeners(
|
||||||
|
onNewEndpoint: _newUpEndpoint,
|
||||||
|
onRegistrationFailed: _upUnregistered,
|
||||||
|
onRegistrationRefused: _upUnregistered,
|
||||||
|
onUnregistered: _upUnregistered,
|
||||||
|
onMessage: _onUpMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory BackgroundPushPlugin.clientOnly(Client client) {
|
||||||
|
_instance ??= BackgroundPushPlugin._(client);
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory BackgroundPushPlugin(MatrixState matrix) {
|
||||||
|
final instance = BackgroundPushPlugin.clientOnly(matrix.client);
|
||||||
|
unawaited(instance.initMatrix(matrix));
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> initMatrix(MatrixState matrix) async {
|
||||||
|
this.matrix = matrix;
|
||||||
|
_onMatrixInit?.call();
|
||||||
|
_onMatrixInit = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleLoginStateChanged(LoginState state) {
|
||||||
|
_loginState = state;
|
||||||
|
if (state == LoginState.logged && PlatformInfos.isMobile) {
|
||||||
|
setupPush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _newFcmToken(String token) {
|
||||||
|
_fcmToken = token;
|
||||||
|
if (_loginState == LoginState.logged && PlatformInfos.isMobile) {
|
||||||
|
setupPush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final _firebaseMessaging = FcmSharedIsolate();
|
||||||
|
|
||||||
|
StreamSubscription<LoginState> onLogin;
|
||||||
|
|
||||||
|
Future<void> setupPusher({
|
||||||
|
String gatewayUrl,
|
||||||
|
String token,
|
||||||
|
Set<String> oldTokens,
|
||||||
|
}) async {
|
||||||
|
final clientName = PlatformInfos.clientName;
|
||||||
|
oldTokens ??= <String>{};
|
||||||
|
final pushers = await client.requestPushers().catchError((e) {
|
||||||
|
Logs().w('[Push] Unable to request pushers', e);
|
||||||
|
return <Pusher>[];
|
||||||
|
});
|
||||||
|
var setNewPusher = false;
|
||||||
|
if (gatewayUrl != null && token != null && clientName != null) {
|
||||||
|
final currentPushers = pushers.where((pusher) => pusher.pushkey == token);
|
||||||
|
if (currentPushers.length == 1 &&
|
||||||
|
currentPushers.first.kind == 'http' &&
|
||||||
|
currentPushers.first.appId == AppConfig.pushNotificationsAppId &&
|
||||||
|
currentPushers.first.appDisplayName == clientName &&
|
||||||
|
currentPushers.first.deviceDisplayName == client.deviceName &&
|
||||||
|
currentPushers.first.lang == 'en' &&
|
||||||
|
currentPushers.first.data.url.toString() == gatewayUrl &&
|
||||||
|
currentPushers.first.data.format ==
|
||||||
|
AppConfig.pushNotificationsPusherFormat) {
|
||||||
|
Logs().i('[Push] Pusher already set');
|
||||||
|
} else {
|
||||||
|
oldTokens.add(token);
|
||||||
|
if (client.isLogged()) {
|
||||||
|
setNewPusher = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final pusher in pushers) {
|
||||||
|
if (oldTokens.contains(pusher.pushkey)) {
|
||||||
|
pusher.kind = null;
|
||||||
|
try {
|
||||||
|
await client.setPusher(
|
||||||
|
pusher,
|
||||||
|
append: true,
|
||||||
|
);
|
||||||
|
Logs().i('[Push] Removed legacy pusher for this device');
|
||||||
|
} catch (err) {
|
||||||
|
Logs().w('[Push] Failed to remove old pusher', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (setNewPusher) {
|
||||||
|
try {
|
||||||
|
await client.setPusher(
|
||||||
|
Pusher(
|
||||||
|
token,
|
||||||
|
AppConfig.pushNotificationsAppId,
|
||||||
|
clientName,
|
||||||
|
client.deviceName,
|
||||||
|
'en',
|
||||||
|
PusherData(
|
||||||
|
url: Uri.parse(gatewayUrl),
|
||||||
|
format: AppConfig.pushNotificationsPusherFormat,
|
||||||
|
),
|
||||||
|
kind: 'http',
|
||||||
|
),
|
||||||
|
append: false,
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs().e('[Push] Unable to set pushers', e, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setupPush() async {
|
||||||
|
if (_loginState != LoginState.logged || !PlatformInfos.isMobile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!PlatformInfos.isIOS &&
|
||||||
|
(await UnifiedPush.getDistributors()).isNotEmpty) {
|
||||||
|
await setupUp();
|
||||||
|
} else {
|
||||||
|
await setupFirebase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _noFcmWarning() async {
|
||||||
|
if (matrix?.context == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (await matrix.store.getItemBool(SettingKeys.showNoGoogle, true)) {
|
||||||
|
await FlushbarHelper.createError(
|
||||||
|
message: matrix.l10n.noGoogleServicesWarning,
|
||||||
|
duration: Duration(seconds: 15),
|
||||||
|
).show(matrix.context);
|
||||||
|
if (null == await matrix.store.getItem(SettingKeys.showNoGoogle)) {
|
||||||
|
await matrix.store.setItemBool(SettingKeys.showNoGoogle, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setupFirebase() async {
|
||||||
|
if (_fcmToken?.isEmpty ?? true) {
|
||||||
|
try {
|
||||||
|
_fcmToken = await _firebaseMessaging.getToken();
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs().e('[Push] cannot get token', e, s);
|
||||||
|
await _noFcmWarning();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await setupPusher(
|
||||||
|
gatewayUrl: AppConfig.pushNotificationsGatewayUrl,
|
||||||
|
token: _fcmToken,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matrix == null) {
|
||||||
|
_onMatrixInit = sendTestMessageGUI;
|
||||||
|
} else if (kReleaseMode) {
|
||||||
|
// ignore: unawaited_futures
|
||||||
|
sendTestMessageGUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setupUp() async {
|
||||||
|
final store = matrix?.store ?? Store();
|
||||||
|
if (!(await store.getItemBool(SettingKeys.unifiedPushRegistered, false))) {
|
||||||
|
Logs().i('[Push] UnifiedPush not registered, attempting to do so...');
|
||||||
|
await UnifiedPush.registerAppWithDialog();
|
||||||
|
} else {
|
||||||
|
// make sure the endpoint is up-to-date etc.
|
||||||
|
await _newUpEndpoint(
|
||||||
|
await store.getItem(SettingKeys.unifiedPushEndpoint));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onFcmMessage(Map<dynamic, dynamic> message) async {
|
||||||
|
Map<String, dynamic> data;
|
||||||
|
try {
|
||||||
|
data = Map<String, dynamic>.from(message['data'] ?? message);
|
||||||
|
await _onMessage(data);
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs().e('[Push] Error while processing notification', e, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _newUpEndpoint(String newEndpoint) async {
|
||||||
|
if (newEndpoint?.isEmpty ?? true) {
|
||||||
|
await _upUnregistered();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var endpoint =
|
||||||
|
'https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify';
|
||||||
|
try {
|
||||||
|
final url = Uri.parse(newEndpoint)
|
||||||
|
.replace(
|
||||||
|
path: '/_matrix/push/v1/notify',
|
||||||
|
query: '',
|
||||||
|
)
|
||||||
|
.toString()
|
||||||
|
.split('?')
|
||||||
|
.first;
|
||||||
|
final res = json.decode(utf8.decode((await http.get(url)).bodyBytes));
|
||||||
|
if (res['gateway'] == 'matrix') {
|
||||||
|
endpoint = url;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Logs().i(
|
||||||
|
'[Push] No self-hosted unified push gateway present: ' + newEndpoint);
|
||||||
|
}
|
||||||
|
Logs().i('[Push] UnifiedPush using endpoint ' + endpoint);
|
||||||
|
final oldTokens = <String>{};
|
||||||
|
try {
|
||||||
|
final fcmToken = await _firebaseMessaging.getToken();
|
||||||
|
oldTokens.add(fcmToken);
|
||||||
|
} catch (_) {}
|
||||||
|
await setupPusher(
|
||||||
|
gatewayUrl: endpoint,
|
||||||
|
token: newEndpoint,
|
||||||
|
oldTokens: oldTokens,
|
||||||
|
);
|
||||||
|
final store = matrix?.store ?? Store();
|
||||||
|
await store.setItem(SettingKeys.unifiedPushEndpoint, newEndpoint);
|
||||||
|
await store.setItemBool(SettingKeys.unifiedPushRegistered, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _upUnregistered() async {
|
||||||
|
Logs().i('[Push] Removing UnifiedPush endpoint...');
|
||||||
|
final store = matrix?.store ?? Store();
|
||||||
|
final oldEndpoint = await store.getItem(SettingKeys.unifiedPushEndpoint);
|
||||||
|
await store.setItemBool(SettingKeys.unifiedPushRegistered, false);
|
||||||
|
await store.deleteItem(SettingKeys.unifiedPushEndpoint);
|
||||||
|
if (matrix != null && (oldEndpoint?.isNotEmpty ?? false)) {
|
||||||
|
// remove the old pusher
|
||||||
|
await setupPusher(
|
||||||
|
oldTokens: {oldEndpoint},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onUpMessage(String message) async {
|
||||||
|
Map<String, dynamic> data;
|
||||||
|
try {
|
||||||
|
data = Map<String, dynamic>.from(json.decode(message)['notification']);
|
||||||
|
await _onMessage(data);
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs().e('[Push] Error while processing notification', e, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onMessage(Map<String, dynamic> data) async {
|
||||||
|
try {
|
||||||
|
Logs().v('[Push] _onMessage');
|
||||||
|
lastReceivedPush = DateTime.now();
|
||||||
|
final roomId = data['room_id'];
|
||||||
|
final eventId = data['event_id'];
|
||||||
|
if (roomId == 'test') {
|
||||||
|
Logs().v('[Push] Test $eventId was successful!');
|
||||||
|
pendingTests.remove(eventId)?.complete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (roomId != null && eventId != null) {
|
||||||
|
var giveUp = false;
|
||||||
|
var loaded = false;
|
||||||
|
final stopwatch = Stopwatch();
|
||||||
|
stopwatch.start();
|
||||||
|
final syncSubscription = client.onSync.stream.listen((r) {
|
||||||
|
if (stopwatch.elapsed.inSeconds >= 30) {
|
||||||
|
giveUp = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
final eventSubscription = client.onEvent.stream.listen((e) {
|
||||||
|
if (e.content['event_id'] == eventId) {
|
||||||
|
loaded = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
if (!(await eventExists(roomId, eventId)) && !loaded) {
|
||||||
|
onPreNotify.add(PreNotify(roomId, eventId));
|
||||||
|
do {
|
||||||
|
Logs().v('[Push] getting ' + roomId + ', event ' + eventId);
|
||||||
|
await client.oneShotSync();
|
||||||
|
if (stopwatch.elapsed.inSeconds >= 60) {
|
||||||
|
giveUp = true;
|
||||||
|
}
|
||||||
|
} while (!loaded && !giveUp);
|
||||||
|
}
|
||||||
|
Logs().v('[Push] ' +
|
||||||
|
(giveUp ? 'gave up on ' : 'got ') +
|
||||||
|
roomId +
|
||||||
|
', event ' +
|
||||||
|
eventId);
|
||||||
|
} finally {
|
||||||
|
await syncSubscription.cancel();
|
||||||
|
await eventSubscription.cancel();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (client.syncPending) {
|
||||||
|
Logs().v('[Push] waiting for existing sync');
|
||||||
|
await client.oneShotSync();
|
||||||
|
}
|
||||||
|
Logs().v('[Push] single oneShotSync');
|
||||||
|
await client.oneShotSync();
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs().e('[Push] Error proccessing push message: $e', s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> eventExists(String roomId, String eventId) async {
|
||||||
|
final room = client.getRoomById(roomId);
|
||||||
|
if (room == null) return false;
|
||||||
|
return (await client.database.getEventById(client.id, eventId, room)) !=
|
||||||
|
null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> sendTestMessageGUI({bool verbose = false}) async {
|
||||||
|
try {
|
||||||
|
await sendTestMessage().timeout(Duration(seconds: 30));
|
||||||
|
if (verbose) {
|
||||||
|
await FlushbarHelper.createSuccess(
|
||||||
|
message:
|
||||||
|
'Push test was successful' /* matrix.l10n.pushTestSuccessful */)
|
||||||
|
.show(matrix.context);
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
var msg;
|
||||||
|
// final l10n = matrix.l10n;
|
||||||
|
if (e is SocketException) {
|
||||||
|
msg = 'Push server is unreachable';
|
||||||
|
// msg = verbose ? l10n.pushServerUnreachable : null;
|
||||||
|
} else if (e is NoTokenException) {
|
||||||
|
msg = 'Push token is unavailable';
|
||||||
|
// msg = verbose ? l10n.pushTokenUnavailable : null;
|
||||||
|
} else {
|
||||||
|
msg = 'Push failed';
|
||||||
|
// msg = l10n.pushFail;
|
||||||
|
Logs().e('[Push] Test message failed: $e', s);
|
||||||
|
}
|
||||||
|
if (msg != null) {
|
||||||
|
await FlushbarHelper.createError(message: '$msg\n\n${e.toString()}')
|
||||||
|
.show(matrix.context);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> sendTestMessage() async {
|
||||||
|
final store = matrix?.store ?? Store();
|
||||||
|
|
||||||
|
if (!(await store.getItemBool(SettingKeys.unifiedPushRegistered, false)) &&
|
||||||
|
(_fcmToken?.isEmpty ?? true)) {
|
||||||
|
throw NoTokenException();
|
||||||
|
}
|
||||||
|
|
||||||
|
final random = Random.secure();
|
||||||
|
final randomId =
|
||||||
|
base64.encode(List<int>.generate(12, (i) => random.nextInt(256)));
|
||||||
|
final completer = Completer<void>();
|
||||||
|
pendingTests[randomId] = completer;
|
||||||
|
|
||||||
|
final endpoint = (await store.getItem(SettingKeys.unifiedPushEndpoint)) ??
|
||||||
|
AppConfig.pushNotificationsGatewayUrl;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final resp = await http.post(
|
||||||
|
endpoint,
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: jsonEncode(
|
||||||
|
{
|
||||||
|
'notification': {
|
||||||
|
'event_id': randomId,
|
||||||
|
'room_id': 'test',
|
||||||
|
'counts': {
|
||||||
|
'unread': 1,
|
||||||
|
},
|
||||||
|
'devices': [
|
||||||
|
{
|
||||||
|
'app_id': AppConfig.pushNotificationsAppId,
|
||||||
|
'pushkey': _fcmToken,
|
||||||
|
'pushkey_ts': 12345678,
|
||||||
|
'data': {},
|
||||||
|
'tweaks': {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (resp.statusCode < 200 || resp.statusCode >= 299) {
|
||||||
|
throw resp.body.isNotEmpty ? resp.body : resp.reasonPhrase;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
pendingTests.remove(randomId);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
}
|
54
lib/utils/plugins/background_sync_plugin.dart
Normal file
54
lib/utils/plugins/background_sync_plugin.dart
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* Famedly
|
||||||
|
* Copyright (C) 2020, 2021 Famedly GmbH
|
||||||
|
* Copyright (C) 2021 Fluffychat
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../components/matrix.dart';
|
||||||
|
|
||||||
|
class BackgroundSyncPlugin with WidgetsBindingObserver {
|
||||||
|
final Client client;
|
||||||
|
|
||||||
|
BackgroundSyncPlugin._(this.client) {
|
||||||
|
if (kIsWeb) return;
|
||||||
|
final wb = WidgetsBinding.instance;
|
||||||
|
wb.addObserver(this);
|
||||||
|
didChangeAppLifecycleState(wb.lifecycleState);
|
||||||
|
}
|
||||||
|
|
||||||
|
static BackgroundSyncPlugin _instance;
|
||||||
|
|
||||||
|
factory BackgroundSyncPlugin.clientOnly(Client client) {
|
||||||
|
_instance ??= BackgroundSyncPlugin._(client);
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory BackgroundSyncPlugin(MatrixState matrix) =>
|
||||||
|
BackgroundSyncPlugin.clientOnly(matrix.client);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
Logs().v('AppLifecycleState = $state');
|
||||||
|
final foreground = state != AppLifecycleState.detached &&
|
||||||
|
state != AppLifecycleState.paused;
|
||||||
|
client.backgroundSync = foreground;
|
||||||
|
client.syncPresence = foreground ? null : PresenceType.unavailable;
|
||||||
|
client.requestHistoryOnLimitedTimeline = !foreground;
|
||||||
|
}
|
||||||
|
}
|
366
lib/utils/plugins/local_notification_plugin.dart
Normal file
366
lib/utils/plugins/local_notification_plugin.dart
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
/*
|
||||||
|
* Famedly
|
||||||
|
* Copyright (C) 2020, 2021 Famedly GmbH
|
||||||
|
* Copyright (C) 2021 Fluffychat
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:ui';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:universal_html/prefer_universal/html.dart' as darthtml;
|
||||||
|
|
||||||
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/l10n_en.dart';
|
||||||
|
import 'background_push_plugin.dart';
|
||||||
|
import '../platform_infos.dart';
|
||||||
|
import '../../components/matrix.dart';
|
||||||
|
import '../matrix_locals.dart';
|
||||||
|
import '../../app_config.dart';
|
||||||
|
import '../famedlysdk_store.dart';
|
||||||
|
import '../../config/setting_keys.dart';
|
||||||
|
|
||||||
|
class LocalNotificationPlugin {
|
||||||
|
Client client;
|
||||||
|
L10n l10n;
|
||||||
|
Future<void> loadLocale() async {
|
||||||
|
// inspired by _lookupL10n in .dart_tool/flutter_gen/gen_l10n/l10n.dart
|
||||||
|
l10n ??=
|
||||||
|
matrix?.l10n ?? (await L10n.delegate.load(window.locale)) ?? L10nEn();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initNotifications;
|
||||||
|
bool reinited = false;
|
||||||
|
MatrixState matrix;
|
||||||
|
|
||||||
|
LocalNotificationPlugin._(this.client) {
|
||||||
|
Logs().v('[Notify] setup');
|
||||||
|
onSync ??= client.onSync.stream.listen((r) => update());
|
||||||
|
onEvent ??= client.onEvent.stream.listen((e) => event(e));
|
||||||
|
onPreNotify ??= BackgroundPushPlugin.clientOnly(client)
|
||||||
|
.onPreNotify
|
||||||
|
.stream
|
||||||
|
.listen(preNotify);
|
||||||
|
if (kIsWeb) {
|
||||||
|
onFocusSub = darthtml.window.onFocus.listen((_) => webHasFocus = true);
|
||||||
|
onBlurSub = darthtml.window.onBlur.listen((_) => webHasFocus = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_initNotifications = kIsWeb
|
||||||
|
? registerDesktopNotifications()
|
||||||
|
: _flutterLocalNotificationsPlugin.initialize(
|
||||||
|
InitializationSettings(
|
||||||
|
android: AndroidInitializationSettings('notifications_icon'),
|
||||||
|
iOS: IOSInitializationSettings(
|
||||||
|
// constructor is called early (before runApp),
|
||||||
|
// so don't request permissions here.
|
||||||
|
requestAlertPermission: false,
|
||||||
|
requestBadgePermission: false,
|
||||||
|
requestSoundPermission: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_initNotifications.then((_) => update());
|
||||||
|
}
|
||||||
|
|
||||||
|
static LocalNotificationPlugin _instance;
|
||||||
|
factory LocalNotificationPlugin.clientOnly(Client client) {
|
||||||
|
_instance ??= LocalNotificationPlugin._(client);
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory LocalNotificationPlugin(MatrixState matrix) {
|
||||||
|
_instance ??= LocalNotificationPlugin._(matrix.client);
|
||||||
|
_instance.matrix = matrix;
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
void update() {
|
||||||
|
// print('[Notify] updating');
|
||||||
|
client.rooms.forEach((r) async {
|
||||||
|
try {
|
||||||
|
await _notification(r);
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs().e('[Notify] Error processing update', e, s);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void reinit() {
|
||||||
|
if (reinited == true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Logs().v('[Notify] reinit');
|
||||||
|
_initNotifications = _flutterLocalNotificationsPlugin?.initialize(
|
||||||
|
InitializationSettings(
|
||||||
|
android: AndroidInitializationSettings('notifications_icon'),
|
||||||
|
iOS: IOSInitializationSettings(),
|
||||||
|
), onSelectNotification: (String payload) async {
|
||||||
|
Logs().v('[Notify] Selected: $payload');
|
||||||
|
_openRoom(payload);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
reinited = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void event(EventUpdate e) {
|
||||||
|
notYetLoaded[e.roomID]?.removeWhere((x) => x == e.content['event_id']);
|
||||||
|
dirtyRoomIds.add(e.roomID);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Set<String>> notYetLoaded = {};
|
||||||
|
|
||||||
|
/// Rooms that has been changed since app start
|
||||||
|
final dirtyRoomIds = <String>{};
|
||||||
|
|
||||||
|
void preNotify(PreNotify pn) {
|
||||||
|
final r = client.getRoomById(pn.roomId);
|
||||||
|
if (r == null) return;
|
||||||
|
notYetLoaded[pn.roomId] ??= {};
|
||||||
|
notYetLoaded[pn.roomId].add(pn.eventId);
|
||||||
|
dirtyRoomIds.add(pn.roomId);
|
||||||
|
_notification(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
final _flutterLocalNotificationsPlugin =
|
||||||
|
kIsWeb ? null : FlutterLocalNotificationsPlugin();
|
||||||
|
|
||||||
|
StreamSubscription<SyncUpdate> onSync;
|
||||||
|
StreamSubscription<EventUpdate> onEvent;
|
||||||
|
StreamSubscription<PreNotify> onPreNotify;
|
||||||
|
|
||||||
|
final roomEvent = <String, String>{};
|
||||||
|
|
||||||
|
void _openRoom(String roomId) async {
|
||||||
|
if (matrix == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await matrix.widget.apl.currentState
|
||||||
|
.pushNamedAndRemoveUntilIsFirst('/rooms/$roomId');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> _notification(Room room) async {
|
||||||
|
await _initNotifications;
|
||||||
|
|
||||||
|
final dirty = dirtyRoomIds.remove(room.id);
|
||||||
|
|
||||||
|
final notYet = notYetLoaded[room.id]?.length ?? 0;
|
||||||
|
if (notYet == 0 &&
|
||||||
|
room.notificationCount == 0 &&
|
||||||
|
room.membership != Membership.invite) {
|
||||||
|
if (!roomEvent.containsKey(room.id) || roomEvent[room.id] != null) {
|
||||||
|
Logs().v('[Notify] clearing ' + room.id);
|
||||||
|
roomEvent[room.id] = null;
|
||||||
|
final id = await mapRoomIdToInt(room.id);
|
||||||
|
await _flutterLocalNotificationsPlugin?.cancel(id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final event = notYet == 0 ? room.lastEvent : null;
|
||||||
|
final eventId = event?.eventId ??
|
||||||
|
(room.membership == Membership.invite ? 'invite' : '');
|
||||||
|
if (roomEvent[room.id] == eventId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (webHasFocus && room.id == matrix?.activeRoomId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadLocale();
|
||||||
|
|
||||||
|
// Calculate the body
|
||||||
|
final body = room.membership == Membership.invite
|
||||||
|
? l10n.youAreInvitedToThisChat
|
||||||
|
: event?.getLocalizedBody(
|
||||||
|
MatrixLocals(l10n),
|
||||||
|
withSenderNamePrefix: !room.isDirectChat ||
|
||||||
|
room.lastEvent.senderId == client.userID,
|
||||||
|
hideReply: true,
|
||||||
|
) ??
|
||||||
|
l10n.unreadMessages(room.notificationCount);
|
||||||
|
|
||||||
|
Logs().v('[Notify] showing ' + room.id);
|
||||||
|
roomEvent[room.id] = eventId;
|
||||||
|
|
||||||
|
// Show notification
|
||||||
|
if (_flutterLocalNotificationsPlugin != null) {
|
||||||
|
// The person object for the android message style notification
|
||||||
|
final person = Person(
|
||||||
|
name: room.getLocalizedDisplayname(MatrixLocals(l10n)),
|
||||||
|
icon: room.avatar == null || room.avatar.toString().isEmpty
|
||||||
|
? null
|
||||||
|
: BitmapFilePathAndroidIcon(
|
||||||
|
await downloadAndSaveAvatar(
|
||||||
|
room.avatar,
|
||||||
|
client,
|
||||||
|
width: 126,
|
||||||
|
height: 126,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
|
||||||
|
AppConfig.pushNotificationsChannelId,
|
||||||
|
AppConfig.pushNotificationsChannelName,
|
||||||
|
AppConfig.pushNotificationsChannelDescription,
|
||||||
|
styleInformation: MessagingStyleInformation(
|
||||||
|
person,
|
||||||
|
messages: [
|
||||||
|
Message(
|
||||||
|
body,
|
||||||
|
event?.originServerTs ?? DateTime.now(),
|
||||||
|
person,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
importance: Importance.max,
|
||||||
|
priority: Priority.high,
|
||||||
|
when: event?.originServerTs?.millisecondsSinceEpoch,
|
||||||
|
onlyAlertOnce: !dirty);
|
||||||
|
var iOSPlatformChannelSpecifics = IOSNotificationDetails();
|
||||||
|
var platformChannelSpecifics = NotificationDetails(
|
||||||
|
android: androidPlatformChannelSpecifics,
|
||||||
|
iOS: iOSPlatformChannelSpecifics,
|
||||||
|
);
|
||||||
|
final id = await mapRoomIdToInt(room.id);
|
||||||
|
await _flutterLocalNotificationsPlugin.show(
|
||||||
|
id,
|
||||||
|
room.getLocalizedDisplayname(MatrixLocals(l10n)),
|
||||||
|
body,
|
||||||
|
platformChannelSpecifics,
|
||||||
|
payload: room.id,
|
||||||
|
);
|
||||||
|
} else if (PlatformInfos.isWeb) {
|
||||||
|
sendDesktopNotification(
|
||||||
|
room.getLocalizedDisplayname(MatrixLocals(l10n)),
|
||||||
|
body,
|
||||||
|
roomId: room.id,
|
||||||
|
icon: event?.sender?.avatarUrl?.getThumbnail(client,
|
||||||
|
width: 64, height: 64, method: ThumbnailMethod.crop) ??
|
||||||
|
room?.avatar?.getThumbnail(client,
|
||||||
|
width: 64, height: 64, method: ThumbnailMethod.crop),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Logs().w('No platform support for notifications');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<String> downloadAndSaveAvatar(Uri content, Client client,
|
||||||
|
{int width, int height}) async {
|
||||||
|
final thumbnail = width == null && height == null ? false : true;
|
||||||
|
final tempDirectory = (await getTemporaryDirectory()).path;
|
||||||
|
final prefix = thumbnail ? 'thumbnail' : '';
|
||||||
|
var file =
|
||||||
|
File('$tempDirectory/${prefix}_${content.toString().split("/").last}');
|
||||||
|
|
||||||
|
if (!file.existsSync()) {
|
||||||
|
final url = Uri.parse(thumbnail
|
||||||
|
? content.getThumbnail(client, width: width, height: height)
|
||||||
|
: content.getDownloadLink(client));
|
||||||
|
if (url.host.isEmpty) return '';
|
||||||
|
final request = await HttpClient()
|
||||||
|
.getUrl(url)
|
||||||
|
.timeout(Duration(seconds: 5))
|
||||||
|
.catchError((e) => null);
|
||||||
|
if (request == null) return '';
|
||||||
|
final response = await request.close();
|
||||||
|
var bytes = await consolidateHttpClientResponseBytes(response)
|
||||||
|
.timeout(Duration(seconds: 5))
|
||||||
|
.catchError((e) => null);
|
||||||
|
if (bytes == null) return '';
|
||||||
|
await file.writeAsBytes(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Workaround for the problem that local notification IDs must be int but we
|
||||||
|
/// sort by [roomId] which is a String. To make sure that we don't have duplicated
|
||||||
|
/// IDs we map the [roomId] to a number and store this number.
|
||||||
|
Future<int> mapRoomIdToInt(String roomId) async {
|
||||||
|
final storage = matrix?.store ?? Store();
|
||||||
|
final idMap = json.decode(
|
||||||
|
(await storage.getItem(SettingKeys.notificationCurrentIds)) ?? '{}');
|
||||||
|
int currentInt;
|
||||||
|
try {
|
||||||
|
currentInt = idMap[roomId];
|
||||||
|
} catch (_) {
|
||||||
|
currentInt = null;
|
||||||
|
}
|
||||||
|
if (currentInt != null) {
|
||||||
|
return currentInt;
|
||||||
|
}
|
||||||
|
currentInt = idMap.keys.length + 1;
|
||||||
|
idMap[roomId] = currentInt;
|
||||||
|
await storage.setItem(
|
||||||
|
SettingKeys.notificationCurrentIds, json.encode(idMap));
|
||||||
|
return currentInt;
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamSubscription<darthtml.Event> onFocusSub;
|
||||||
|
StreamSubscription<darthtml.Event> onBlurSub;
|
||||||
|
|
||||||
|
bool webHasFocus = true;
|
||||||
|
|
||||||
|
void sendDesktopNotification(
|
||||||
|
String title,
|
||||||
|
String body, {
|
||||||
|
String icon,
|
||||||
|
String roomId,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
darthtml.AudioElement()
|
||||||
|
..src = 'assets/assets/sounds/pop.wav'
|
||||||
|
..autoplay = true
|
||||||
|
..load();
|
||||||
|
final notification = darthtml.Notification(
|
||||||
|
title,
|
||||||
|
body: body,
|
||||||
|
icon: icon,
|
||||||
|
);
|
||||||
|
notification.onClick.listen((e) => _openRoom(roomId));
|
||||||
|
} catch (e, s) {
|
||||||
|
Logs().e('[Notify] Error sending desktop notification', e, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> registerDesktopNotifications() async {
|
||||||
|
await client.onSync.stream.first;
|
||||||
|
await darthtml.Notification.requestPermission();
|
||||||
|
onSync ??= client.onSync.stream.listen(updateTabTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTabTitle(dynamic sync) {
|
||||||
|
var unreadEvents = 0;
|
||||||
|
client.rooms.forEach((Room room) {
|
||||||
|
if (room.membership == Membership.invite || room.notificationCount > 0) {
|
||||||
|
unreadEvents++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (unreadEvents > 0) {
|
||||||
|
darthtml.document.title = '($unreadEvents) FluffyChat';
|
||||||
|
} else {
|
||||||
|
darthtml.document.title = 'FluffyChat';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -37,7 +37,7 @@ class _HomeserverPickerState extends State<HomeserverPicker> {
|
|||||||
type: AuthenticationTypes.token,
|
type: AuthenticationTypes.token,
|
||||||
userIdentifierType: null,
|
userIdentifierType: null,
|
||||||
token: token,
|
token: token,
|
||||||
initialDeviceDisplayName: Matrix.of(context).clientName,
|
initialDeviceDisplayName: PlatformInfos.clientName,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import 'package:fluffychat/components/matrix.dart';
|
|||||||
import 'package:flushbar/flushbar_helper.dart';
|
import 'package:flushbar/flushbar_helper.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
import '../utils/platform_infos.dart';
|
||||||
|
|
||||||
class Login extends StatefulWidget {
|
class Login extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -44,7 +45,7 @@ class _LoginState extends State<Login> {
|
|||||||
await matrix.client.login(
|
await matrix.client.login(
|
||||||
user: usernameController.text,
|
user: usernameController.text,
|
||||||
password: passwordController.text,
|
password: passwordController.text,
|
||||||
initialDeviceDisplayName: matrix.clientName);
|
initialDeviceDisplayName: PlatformInfos.clientName);
|
||||||
} on MatrixException catch (exception) {
|
} on MatrixException catch (exception) {
|
||||||
setState(() => passwordError = exception.errorMessage);
|
setState(() => passwordError = exception.errorMessage);
|
||||||
return setState(() => loading = false);
|
return setState(() => loading = false);
|
||||||
|
@ -8,6 +8,7 @@ import 'package:fluffychat/components/matrix.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
import '../utils/platform_infos.dart';
|
||||||
|
|
||||||
class SignUpPassword extends StatefulWidget {
|
class SignUpPassword extends StatefulWidget {
|
||||||
final MatrixFile avatar;
|
final MatrixFile avatar;
|
||||||
@ -43,7 +44,7 @@ class _SignUpPasswordState extends State<SignUpPassword> {
|
|||||||
await matrix.client.register(
|
await matrix.client.register(
|
||||||
username: widget.username,
|
username: widget.username,
|
||||||
password: passwordController.text,
|
password: passwordController.text,
|
||||||
initialDeviceDisplayName: matrix.clientName,
|
initialDeviceDisplayName: PlatformInfos.clientName,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
);
|
);
|
||||||
await waitForLogin;
|
await waitForLogin;
|
||||||
|
30
pubspec.lock
30
pubspec.lock
@ -227,6 +227,15 @@ packages:
|
|||||||
url: "https://gitlab.com/famedly/famedlysdk.git"
|
url: "https://gitlab.com/famedly/famedlysdk.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
|
fcm_shared_isolate:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "."
|
||||||
|
ref: log
|
||||||
|
resolved-ref: "646eab0821cd94aaa221c3e6c700c62918dc41f3"
|
||||||
|
url: "https://gitlab.com/famedly/libraries/fcm_shared_isolate.git"
|
||||||
|
source: git
|
||||||
|
version: "0.0.1"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -269,34 +278,27 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.1"
|
||||||
firebase:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: firebase
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "7.3.0"
|
|
||||||
firebase_core:
|
firebase_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_core
|
name: firebase_core
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.0+1"
|
version: "0.5.3"
|
||||||
firebase_core_platform_interface:
|
firebase_core_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_core_platform_interface
|
name: firebase_core_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.1.0"
|
||||||
firebase_core_web:
|
firebase_core_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_core_web
|
name: firebase_core_web
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
version: "0.2.1+1"
|
||||||
firebase_messaging:
|
firebase_messaging:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1160,11 +1162,9 @@ packages:
|
|||||||
unifiedpush:
|
unifiedpush:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "/home/sorunome/repos/flutter-connector"
|
||||||
ref: main
|
relative: false
|
||||||
resolved-ref: "9ad5360bb63021f366d35facf217f053d465b46c"
|
source: path
|
||||||
url: "https://github.com/UnifiedPush/flutter-connector.git"
|
|
||||||
source: git
|
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
universal_html:
|
universal_html:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
|
11
pubspec.yaml
11
pubspec.yaml
@ -16,9 +16,16 @@ dependencies:
|
|||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
unifiedpush:
|
unifiedpush:
|
||||||
|
path: /home/sorunome/repos/flutter-connector
|
||||||
|
# git:
|
||||||
|
# url: https://github.com/UnifiedPush/flutter-connector.git
|
||||||
|
# ref: main
|
||||||
|
|
||||||
|
# Firebase Notifications
|
||||||
|
fcm_shared_isolate:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/UnifiedPush/flutter-connector.git
|
url: https://gitlab.com/famedly/libraries/fcm_shared_isolate.git
|
||||||
ref: main
|
ref: log
|
||||||
|
|
||||||
cupertino_icons: any
|
cupertino_icons: any
|
||||||
localstorage: ^3.0.6+9
|
localstorage: ^3.0.6+9
|
||||||
|
Loading…
x
Reference in New Issue
Block a user