mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2024-12-24 14:32:37 +01:00
Merge branch 'soru/single-isolate-push-alt' into 'main'
fix: Use single-isolate push Closes #207, #94, and #294 See merge request famedly/fluffychat!377
This commit is contained in:
commit
99dd2dac71
@ -85,6 +85,7 @@ dependencies {
|
||||
androidTestImplementation 'androidx.test:runner:1.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 'com.github.UnifiedPush:android-connector:1.0.0-beta3' // needed for unifiedpush
|
||||
}
|
||||
|
||||
if(file("google-services.json").exists()){
|
||||
|
@ -74,6 +74,24 @@
|
||||
|
||||
|
||||
</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.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
|
@ -0,0 +1,35 @@
|
||||
package chat.fluffy.fluffychat
|
||||
|
||||
import com.famedly.fcm_shared_isolate.FcmSharedIsolateService
|
||||
|
||||
import chat.fluffy.fluffychat.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
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugins.GeneratedPluginRegistrant
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
||||
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
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
|
||||
|
||||
import org.unifiedpush.flutter.connector.UnifiedPushHandler
|
||||
|
||||
import chat.fluffy.fluffychat.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 : UnifiedPushHandler() {
|
||||
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)
|
@ -6,7 +6,6 @@ import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
import 'package:famedlysdk/encryption.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'package:fluffychat/utils/firebase_controller.dart';
|
||||
import 'package:fluffychat/utils/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/utils/sentry_controller.dart';
|
||||
@ -33,6 +32,8 @@ import 'dialogs/key_verification_dialog.dart';
|
||||
import '../utils/platform_infos.dart';
|
||||
import '../app_config.dart';
|
||||
import '../config/setting_keys.dart';
|
||||
import '../utils/fluffy_client.dart';
|
||||
import '../utils/background_push.dart';
|
||||
|
||||
class Matrix extends StatefulWidget {
|
||||
static const String callNamespace = 'chat.fluffy.jitsi_call';
|
||||
@ -58,12 +59,14 @@ class Matrix extends StatefulWidget {
|
||||
Provider.of<MatrixState>(context, listen: false);
|
||||
}
|
||||
|
||||
class MatrixState extends State<Matrix> {
|
||||
Client client;
|
||||
class MatrixState extends State<Matrix> with WidgetsBindingObserver {
|
||||
FluffyClient client;
|
||||
Store store = Store();
|
||||
@override
|
||||
BuildContext get context => widget.context;
|
||||
|
||||
BackgroundPush _backgroundPush;
|
||||
|
||||
Map<String, dynamic> get shareContent => _shareContent;
|
||||
set shareContent(Map<String, dynamic> content) {
|
||||
_shareContent = content;
|
||||
@ -75,15 +78,7 @@ class MatrixState extends State<Matrix> {
|
||||
final StreamController<Map<String, dynamic>> onShareContentChanged =
|
||||
StreamController.broadcast();
|
||||
|
||||
String activeRoomId;
|
||||
File wallpaper;
|
||||
String clientName;
|
||||
|
||||
void clean() async {
|
||||
if (!kIsWeb) return;
|
||||
|
||||
await store.deleteItem(clientName);
|
||||
}
|
||||
|
||||
void _initWithStore() async {
|
||||
try {
|
||||
@ -181,7 +176,7 @@ class MatrixState extends State<Matrix> {
|
||||
|
||||
void _showLocalNotification(EventUpdate eventUpdate) async {
|
||||
final roomId = eventUpdate.roomID;
|
||||
if (webHasFocus && activeRoomId == roomId) return;
|
||||
if (webHasFocus && client.activeRoomId == roomId) return;
|
||||
final room = client.getRoomById(roomId);
|
||||
if (room.notificationCount == 0) return;
|
||||
final event = Event.fromJson(eventUpdate.content, room);
|
||||
@ -228,6 +223,7 @@ class MatrixState extends State<Matrix> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
initMatrix();
|
||||
if (PlatformInfos.isWeb) {
|
||||
initConfig().then((_) => initSettings());
|
||||
@ -261,28 +257,7 @@ class MatrixState extends State<Matrix> {
|
||||
});
|
||||
});
|
||||
}
|
||||
clientName =
|
||||
'${AppConfig.applicationName} ${kIsWeb ? 'Web' : Platform.operatingSystem}';
|
||||
final Set verificationMethods = <KeyVerificationMethod>{
|
||||
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
|
||||
},
|
||||
);
|
||||
client = FluffyClient();
|
||||
LoadingDialog.defaultTitle = L10n.of(context).loadingPleaseWait;
|
||||
LoadingDialog.defaultBackLabel = L10n.of(context).close;
|
||||
LoadingDialog.defaultOnError = (Object e) => e.toLocalizedString(context);
|
||||
@ -348,16 +323,10 @@ class MatrixState extends State<Matrix> {
|
||||
if (loginState != state) {
|
||||
loginState = state;
|
||||
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);
|
||||
if (kIsWeb || Platform.isLinux) {
|
||||
if (PlatformInfos.isWeb || PlatformInfos.isLinux) {
|
||||
client.onSync.stream.first.then((s) {
|
||||
html.Notification.requestPermission();
|
||||
onNotification ??= client.onEvent.stream
|
||||
@ -369,6 +338,26 @@ class MatrixState extends State<Matrix> {
|
||||
.listen(_showLocalNotification);
|
||||
});
|
||||
}
|
||||
|
||||
if (PlatformInfos.isMobile) {
|
||||
_backgroundPush = BackgroundPush(client, context, widget.apl);
|
||||
}
|
||||
}
|
||||
|
||||
bool _firstStartup = true;
|
||||
|
||||
@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;
|
||||
if (_firstStartup) {
|
||||
_firstStartup = false;
|
||||
_backgroundPush?.setupPush();
|
||||
}
|
||||
}
|
||||
|
||||
void initSettings() {
|
||||
@ -401,12 +390,16 @@ class MatrixState extends State<Matrix> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
|
||||
onRoomKeyRequestSub?.cancel();
|
||||
onKeyVerificationRequestSub?.cancel();
|
||||
onLoginStateChanged?.cancel();
|
||||
onNotification?.cancel();
|
||||
onFocusSub?.cancel();
|
||||
onBlurSub?.cancel();
|
||||
_backgroundPush?.onLogin?.cancel();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -16,4 +16,5 @@ abstract class SettingKeys {
|
||||
static const String unifiedPushRegistered =
|
||||
'chat.fluffy.unifiedpush.registered';
|
||||
static const String unifiedPushEndpoint = 'chat.fluffy.unifiedpush.endpoint';
|
||||
static const String notificationCurrentIds = 'chat.fluffy.notification_ids';
|
||||
}
|
||||
|
@ -19,12 +19,24 @@ import 'package:universal_html/prefer_universal/html.dart' as html;
|
||||
import 'components/matrix.dart';
|
||||
import 'config/themes.dart';
|
||||
import 'app_config.dart';
|
||||
import 'utils/fluffy_client.dart';
|
||||
import 'utils/platform_infos.dart';
|
||||
import 'utils/background_push.dart';
|
||||
|
||||
void main() async {
|
||||
// Our background push shared isolate accesses flutter-internal things very early in the startup proccess
|
||||
// To make sure that the parts of flutter needed are started up already, we need to ensure that the
|
||||
// widget bindings are initialized already.
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
SystemUiOverlayStyle(statusBarColor: Colors.transparent));
|
||||
FlutterError.onError = (FlutterErrorDetails details) =>
|
||||
Zone.current.handleUncaughtError(details.exception, details.stack);
|
||||
|
||||
if (PlatformInfos.isMobile) {
|
||||
BackgroundPush.clientOnly(FluffyClient());
|
||||
}
|
||||
|
||||
runZonedGuarded(
|
||||
() => runApp(PlatformInfos.isMobile
|
||||
? AppLock(
|
||||
|
811
lib/utils/background_push.dart
Normal file
811
lib/utils/background_push.dart
Normal file
@ -0,0 +1,811 @@
|
||||
/*
|
||||
* 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:ui';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
|
||||
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:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:unifiedpush/unifiedpush.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 'platform_infos.dart';
|
||||
import '../app_config.dart';
|
||||
import '../config/setting_keys.dart';
|
||||
import 'famedlysdk_store.dart';
|
||||
import 'fluffy_client.dart';
|
||||
import 'matrix_locals.dart';
|
||||
|
||||
class NoTokenException implements Exception {
|
||||
String get cause => 'Cannot get firebase token';
|
||||
}
|
||||
|
||||
class BackgroundPush {
|
||||
static BackgroundPush _instance;
|
||||
final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
FluffyClient client;
|
||||
BuildContext context;
|
||||
GlobalKey<AdaptivePageLayoutState> apl;
|
||||
String _fcmToken;
|
||||
LoginState _loginState;
|
||||
L10n l10n;
|
||||
Store _store;
|
||||
Store get store => _store ??= Store();
|
||||
Future<void> loadLocale() async {
|
||||
// inspired by _lookupL10n in .dart_tool/flutter_gen/gen_l10n/l10n.dart
|
||||
l10n ??= (context != null ? L10n.of(context) : null) ??
|
||||
(await L10n.delegate.load(window.locale)) ??
|
||||
L10nEn();
|
||||
}
|
||||
|
||||
final pendingTests = <String, Completer<void>>{};
|
||||
|
||||
void Function() _onContextInit;
|
||||
|
||||
DateTime lastReceivedPush;
|
||||
|
||||
BackgroundPush._(this.client) {
|
||||
onLogin ??=
|
||||
client.onLoginStateChanged.stream.listen(handleLoginStateChanged);
|
||||
onRoomSync ??= client.onSync.stream
|
||||
.where((s) => s.hasRoomUpdate)
|
||||
.listen((s) => _onClearingPush(getFromServer: false));
|
||||
_firebaseMessaging.setListeners(
|
||||
onMessage: _onFcmMessage,
|
||||
onNewToken: _newFcmToken,
|
||||
);
|
||||
UnifiedPush.initializeWithReceiver(
|
||||
onNewEndpoint: _newUpEndpoint,
|
||||
onRegistrationFailed: _upUnregistered,
|
||||
onRegistrationRefused: _upUnregistered,
|
||||
onUnregistered: _upUnregistered,
|
||||
onMessage: _onUpMessage,
|
||||
);
|
||||
}
|
||||
|
||||
factory BackgroundPush.clientOnly(FluffyClient client) {
|
||||
_instance ??= BackgroundPush._(client);
|
||||
return _instance;
|
||||
}
|
||||
|
||||
factory BackgroundPush(FluffyClient _client, BuildContext _context,
|
||||
GlobalKey<AdaptivePageLayoutState> _apl) {
|
||||
final instance = BackgroundPush.clientOnly(_client);
|
||||
instance.context = _context;
|
||||
instance.apl = _apl;
|
||||
instance.fullInit();
|
||||
return instance;
|
||||
}
|
||||
|
||||
Future<void> fullInit() async {
|
||||
_onContextInit?.call();
|
||||
_onContextInit = null;
|
||||
// ignore: unawaited_futures
|
||||
setupPush();
|
||||
}
|
||||
|
||||
void handleLoginStateChanged(LoginState state) {
|
||||
_loginState = state;
|
||||
setupPush();
|
||||
}
|
||||
|
||||
void _newFcmToken(String token) {
|
||||
_fcmToken = token;
|
||||
setupPush();
|
||||
}
|
||||
|
||||
final _firebaseMessaging = FcmSharedIsolate();
|
||||
|
||||
StreamSubscription<LoginState> onLogin;
|
||||
StreamSubscription<SyncUpdate> onRoomSync;
|
||||
|
||||
Future<void> setupPusher({
|
||||
String gatewayUrl,
|
||||
String token,
|
||||
Set<String> oldTokens,
|
||||
bool useDeviceSpecificAppId = false,
|
||||
}) 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;
|
||||
// Just the plain app id, we add the .data_message suffix later
|
||||
var appId = AppConfig.pushNotificationsAppId;
|
||||
// we need the deviceAppId to remove potential legacy UP pusher
|
||||
var deviceAppId = '$appId.${client.deviceID}';
|
||||
// appId may only be up to 64 chars as per spec
|
||||
if (deviceAppId.length > 64) {
|
||||
deviceAppId = deviceAppId.substring(0, 64);
|
||||
}
|
||||
if (!useDeviceSpecificAppId && PlatformInfos.isAndroid) {
|
||||
appId += '.data_message';
|
||||
}
|
||||
final thisAppId = useDeviceSpecificAppId ? deviceAppId : appId;
|
||||
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 == thisAppId &&
|
||||
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 ((token != null &&
|
||||
pusher.pushkey != token &&
|
||||
deviceAppId == pusher.appId) ||
|
||||
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,
|
||||
thisAppId,
|
||||
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 ||
|
||||
context == null) {
|
||||
return;
|
||||
}
|
||||
if (!PlatformInfos.isIOS &&
|
||||
(await UnifiedPush.getDistributors()).isNotEmpty) {
|
||||
await setupUp();
|
||||
} else {
|
||||
await setupFirebase();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _noFcmWarning() async {
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
if (await store.getItemBool(SettingKeys.showNoGoogle, true)) {
|
||||
await loadLocale();
|
||||
await FlushbarHelper.createError(
|
||||
message: l10n.noGoogleServicesWarning,
|
||||
duration: Duration(seconds: 15),
|
||||
).show(context);
|
||||
if (null == await store.getItem(SettingKeys.showNoGoogle)) {
|
||||
await 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 (context == null) {
|
||||
_onContextInit = sendTestMessageGUI;
|
||||
} else if (kReleaseMode) {
|
||||
// ignore: unawaited_futures
|
||||
sendTestMessageGUI();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> goToRoom(String roomId) async {
|
||||
try {
|
||||
if (apl == null) {
|
||||
return;
|
||||
}
|
||||
await apl.currentState.pushNamedAndRemoveUntilIsFirst('/rooms/$roomId');
|
||||
} catch (e, s) {
|
||||
Logs().e('[Push] Failed to open room', e, s);
|
||||
}
|
||||
}
|
||||
|
||||
bool _notificationsPluginSetUp = false;
|
||||
Future<void> setupLocalNotificationsPlugin() async {
|
||||
if (_notificationsPluginSetUp) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
_notificationsPluginSetUp = true;
|
||||
}
|
||||
|
||||
Future<void> setupUp() async {
|
||||
await setupLocalNotificationsPlugin();
|
||||
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);
|
||||
await _showDefaultNotification(data);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
useDeviceSpecificAppId: true,
|
||||
);
|
||||
await store.setItem(SettingKeys.unifiedPushEndpoint, newEndpoint);
|
||||
await store.setItemBool(SettingKeys.unifiedPushRegistered, true);
|
||||
}
|
||||
|
||||
Future<void> _upUnregistered() async {
|
||||
Logs().i('[Push] Removing UnifiedPush endpoint...');
|
||||
final oldEndpoint = await store.getItem(SettingKeys.unifiedPushEndpoint);
|
||||
await store.setItemBool(SettingKeys.unifiedPushRegistered, false);
|
||||
await store.deleteItem(SettingKeys.unifiedPushEndpoint);
|
||||
if (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);
|
||||
await _showDefaultNotification(data);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onMessage(Map<String, dynamic> data) async {
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
await _onClearingPush();
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
do {
|
||||
Logs().v('[Push] getting ' + roomId + ', event ' + eventId);
|
||||
await client
|
||||
.oneShotSync()
|
||||
.catchError((e) => Logs().v('[Push] Error one-shot syncing', e));
|
||||
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();
|
||||
}
|
||||
await _showNotification(roomId, eventId);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// 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.
|
||||
Map<String, int> idMap;
|
||||
Future<void> _loadIdMap() async {
|
||||
idMap ??= Map<String, int>.from(json.decode(
|
||||
(await store.getItem(SettingKeys.notificationCurrentIds)) ?? '{}'));
|
||||
}
|
||||
|
||||
Future<int> mapRoomIdToInt(String roomId) async {
|
||||
await _loadIdMap();
|
||||
int currentInt;
|
||||
try {
|
||||
currentInt = idMap[roomId];
|
||||
} catch (_) {
|
||||
currentInt = null;
|
||||
}
|
||||
if (currentInt != null) {
|
||||
return currentInt;
|
||||
}
|
||||
currentInt = 0;
|
||||
while (idMap.values.contains(currentInt)) {
|
||||
currentInt++;
|
||||
}
|
||||
idMap[roomId] = currentInt;
|
||||
await store.setItem(SettingKeys.notificationCurrentIds, json.encode(idMap));
|
||||
return currentInt;
|
||||
}
|
||||
|
||||
bool _clearingPushLock = false;
|
||||
Future<void> _onClearingPush({bool getFromServer = true}) async {
|
||||
if (_clearingPushLock) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
_clearingPushLock = true;
|
||||
Iterable<String> emptyRooms;
|
||||
if (getFromServer) {
|
||||
Logs().v('[Push] Got new clearing push');
|
||||
var syncErrored = false;
|
||||
if (client.syncPending) {
|
||||
Logs().v('[Push] waiting for existing sync');
|
||||
// we need to catchError here as the Future might be in a different execution zone
|
||||
await client.oneShotSync().catchError((e) {
|
||||
syncErrored = true;
|
||||
Logs().v('[Push] Error one-shot syncing', e);
|
||||
});
|
||||
}
|
||||
if (!syncErrored) {
|
||||
Logs().v('[Push] single oneShotSync');
|
||||
// we need to catchError here as the Future might be in a different execution zone
|
||||
await client.oneShotSync().catchError((e) {
|
||||
syncErrored = true;
|
||||
Logs().v('[Push] Error one-shot syncing', e);
|
||||
});
|
||||
if (!syncErrored) {
|
||||
emptyRooms = client.rooms
|
||||
.where((r) => r.notificationCount == 0)
|
||||
.map((r) => r.id);
|
||||
}
|
||||
}
|
||||
if (syncErrored) {
|
||||
try {
|
||||
Logs().v(
|
||||
'[Push] failed to sync for fallback push, fetching notifications endpoint...');
|
||||
final notifications = await client.requestNotifications(limit: 20);
|
||||
final notificationRooms =
|
||||
notifications.notifications.map((n) => n.roomId).toSet();
|
||||
emptyRooms = client.rooms
|
||||
.where((r) => !notificationRooms.contains(r.id))
|
||||
.map((r) => r.id);
|
||||
} catch (e) {
|
||||
Logs().v(
|
||||
'[Push] failed to fetch pending notifications for clearing push, falling back...',
|
||||
e);
|
||||
emptyRooms = client.rooms
|
||||
.where((r) => r.notificationCount == 0)
|
||||
.map((r) => r.id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
emptyRooms = client.rooms
|
||||
.where((r) => r.notificationCount == 0)
|
||||
.map((r) => r.id);
|
||||
}
|
||||
await _loadIdMap();
|
||||
var changed = false;
|
||||
for (final roomId in emptyRooms) {
|
||||
if (idMap[roomId] != null) {
|
||||
final id = idMap[roomId];
|
||||
idMap.remove(roomId);
|
||||
changed = true;
|
||||
await _flutterLocalNotificationsPlugin?.cancel(id);
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
await store.setItem(
|
||||
SettingKeys.notificationCurrentIds, json.encode(idMap));
|
||||
}
|
||||
} finally {
|
||||
_clearingPushLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showNotification(String roomId, String eventId) async {
|
||||
await setupLocalNotificationsPlugin();
|
||||
final room = client.getRoomById(roomId);
|
||||
await room.postLoad();
|
||||
final event = await client.database.getEventById(client.id, eventId, room);
|
||||
if (room == null) {
|
||||
throw 'Room not found';
|
||||
}
|
||||
|
||||
if (((client.activeRoomId?.isNotEmpty ?? false) &&
|
||||
client.activeRoomId == room.id &&
|
||||
client.syncPresence == null) ||
|
||||
(event != null && room.notificationCount == 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// load the locale
|
||||
await loadLocale();
|
||||
|
||||
// Count all unread events
|
||||
var unreadEvents = 0;
|
||||
client.rooms.forEach((Room room) => unreadEvents += room.notificationCount);
|
||||
|
||||
// Calculate title
|
||||
final title = l10n.unreadMessages(room.notificationCount.toString());
|
||||
|
||||
// Calculate the body
|
||||
final body = event?.getLocalizedBody(
|
||||
MatrixLocals(l10n),
|
||||
withSenderNamePrefix: true,
|
||||
hideReply: true,
|
||||
) ??
|
||||
l10n.openAppToReadMessages;
|
||||
|
||||
// The person object for the android message style notification
|
||||
final avatar = room.avatar == null
|
||||
? null
|
||||
: await downloadAndSaveAvatar(
|
||||
room.avatar,
|
||||
client,
|
||||
width: 126,
|
||||
height: 126,
|
||||
);
|
||||
final person = Person(
|
||||
name: room.getLocalizedDisplayname(MatrixLocals(l10n)),
|
||||
icon: avatar == null ? null : BitmapFilePathAndroidIcon(avatar),
|
||||
);
|
||||
|
||||
// Show notification
|
||||
var androidPlatformChannelSpecifics = _getAndroidNotificationDetails(
|
||||
styleInformation: MessagingStyleInformation(
|
||||
person,
|
||||
conversationTitle: title,
|
||||
messages: [
|
||||
Message(
|
||||
body,
|
||||
event?.originServerTs ?? DateTime.now(),
|
||||
person,
|
||||
)
|
||||
],
|
||||
),
|
||||
ticker: l10n.newMessageInFluffyChat,
|
||||
);
|
||||
var iOSPlatformChannelSpecifics = IOSNotificationDetails();
|
||||
var platformChannelSpecifics = NotificationDetails(
|
||||
android: androidPlatformChannelSpecifics,
|
||||
iOS: iOSPlatformChannelSpecifics,
|
||||
);
|
||||
await _flutterLocalNotificationsPlugin.show(
|
||||
await mapRoomIdToInt(room.id),
|
||||
room.getLocalizedDisplayname(MatrixLocals(l10n)),
|
||||
body,
|
||||
platformChannelSpecifics,
|
||||
payload: roomId,
|
||||
);
|
||||
}
|
||||
|
||||
Future<dynamic> _showDefaultNotification(Map<String, dynamic> data) async {
|
||||
try {
|
||||
await setupLocalNotificationsPlugin();
|
||||
|
||||
await loadLocale();
|
||||
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);
|
||||
if (unread == 0 || roomId == null || eventId == null) {
|
||||
await _onClearingPush();
|
||||
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(
|
||||
await mapRoomIdToInt(roomId),
|
||||
title,
|
||||
l10n.openAppToReadMessages,
|
||||
platformChannelSpecifics,
|
||||
payload: roomId,
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logs().e('[Push] Error while processing background notification', e, s);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
if (response.statusCode >= 300) {
|
||||
// we are not in the 2xx range
|
||||
return null;
|
||||
}
|
||||
var bytes = await consolidateHttpClientResponseBytes(response);
|
||||
await file.writeAsBytes(bytes);
|
||||
}
|
||||
|
||||
return file.path;
|
||||
}
|
||||
|
||||
AndroidNotificationDetails _getAndroidNotificationDetails(
|
||||
{MessagingStyleInformation styleInformation, String ticker}) {
|
||||
final color = (context != null ? Theme.of(context).primaryColor : null) ??
|
||||
Color(0xFF5625BA);
|
||||
|
||||
return AndroidNotificationDetails(
|
||||
AppConfig.pushNotificationsChannelId,
|
||||
AppConfig.pushNotificationsChannelName,
|
||||
AppConfig.pushNotificationsChannelDescription,
|
||||
styleInformation: styleInformation,
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
ticker: ticker,
|
||||
color: color,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> sendTestMessageGUI({bool verbose = false}) async {
|
||||
try {
|
||||
await sendTestMessage().timeout(Duration(seconds: 30));
|
||||
if (verbose) {
|
||||
await FlushbarHelper.createSuccess(
|
||||
message:
|
||||
'Push test was successful' /* l10n.pushTestSuccessful */)
|
||||
.show(context);
|
||||
}
|
||||
} catch (e, s) {
|
||||
var msg;
|
||||
// final l10n = 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(context);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> sendTestMessage() async {
|
||||
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>();
|
||||
if (PlatformInfos.isIOS) {
|
||||
// don't expect a reply, because fcm_shared_isolate can't receive on iOS
|
||||
completer.complete();
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
34
lib/utils/fluffy_client.dart
Normal file
34
lib/utils/fluffy_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';
|
||||
|
||||
class FluffyClient extends Client {
|
||||
static final FluffyClient _instance = FluffyClient._internal();
|
||||
|
||||
/// The ID of the currently active room, if there is one. May be null or emtpy
|
||||
String activeRoomId;
|
||||
|
||||
factory FluffyClient() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
FluffyClient._internal()
|
||||
: super(
|
||||
PlatformInfos.clientName,
|
||||
enableE2eeRecovery: true,
|
||||
verificationMethods: {
|
||||
KeyVerificationMethod.numbers,
|
||||
if (PlatformInfos.isMobile || PlatformInfos.isLinux)
|
||||
KeyVerificationMethod.emoji,
|
||||
},
|
||||
importantStateEvents: <String>{
|
||||
'im.ponies.room_emotes', // we want emotes to work properly
|
||||
},
|
||||
databaseBuilder: getDatabase,
|
||||
supportedLoginTypes: {
|
||||
AuthenticationTypes.password,
|
||||
if (PlatformInfos.isMobile) AuthenticationTypes.sso
|
||||
},
|
||||
);
|
||||
}
|
@ -11,21 +11,26 @@ import '../app_config.dart';
|
||||
|
||||
abstract class PlatformInfos {
|
||||
static bool get isWeb => kIsWeb;
|
||||
static bool get isLinux => !kIsWeb && Platform.isLinux;
|
||||
static bool get isWindows => !kIsWeb && Platform.isWindows;
|
||||
static bool get isMacOS => !kIsWeb && Platform.isMacOS;
|
||||
static bool get isIOS => !kIsWeb && Platform.isIOS;
|
||||
static bool get isAndroid => !kIsWeb && Platform.isAndroid;
|
||||
|
||||
static bool get isCupertinoStyle =>
|
||||
!kIsWeb && (Platform.isIOS || Platform.isMacOS);
|
||||
static bool get isCupertinoStyle => isIOS || isMacOS;
|
||||
|
||||
static bool get isMobile => !kIsWeb && (Platform.isAndroid || Platform.isIOS);
|
||||
static bool get isMobile => isAndroid || isIOS;
|
||||
|
||||
/// For desktops which don't support ChachedNetworkImage yet
|
||||
static bool get isBetaDesktop =>
|
||||
!kIsWeb && (Platform.isWindows || Platform.isLinux);
|
||||
static bool get isBetaDesktop => isWindows || isLinux;
|
||||
|
||||
static bool get isDesktop =>
|
||||
!kIsWeb && (Platform.isLinux || Platform.isWindows || Platform.isMacOS);
|
||||
static bool get isDesktop => isLinux || isWindows || isMacOS;
|
||||
|
||||
static bool get usesTouchscreen => !isMobile;
|
||||
|
||||
static String get clientName =>
|
||||
'${AppConfig.applicationName} ${isWeb ? 'Web' : Platform.operatingSystem}';
|
||||
|
||||
static Future<String> getVersion() async {
|
||||
var version = kIsWeb ? 'Web' : 'Unknown';
|
||||
try {
|
||||
|
@ -185,7 +185,7 @@ class _ChatState extends State<Chat> {
|
||||
void dispose() {
|
||||
timeline?.cancelSubscriptions();
|
||||
timeline = null;
|
||||
matrix.activeRoomId = '';
|
||||
matrix.client.activeRoomId = '';
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -510,7 +510,7 @@ class _ChatState extends State<Chat> {
|
||||
),
|
||||
);
|
||||
}
|
||||
matrix.activeRoomId = widget.id;
|
||||
matrix.client.activeRoomId = widget.id;
|
||||
|
||||
if (room.membership == Membership.invite) {
|
||||
showFutureLoadingDialog(context: context, future: () => room.join());
|
||||
|
@ -38,7 +38,7 @@ class _HomeserverPickerState extends State<HomeserverPicker> {
|
||||
type: AuthenticationTypes.token,
|
||||
userIdentifierType: null,
|
||||
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:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import '../utils/platform_infos.dart';
|
||||
|
||||
import '../app_config.dart';
|
||||
|
||||
@ -46,7 +47,7 @@ class _LoginState extends State<Login> {
|
||||
await matrix.client.login(
|
||||
user: usernameController.text,
|
||||
password: passwordController.text,
|
||||
initialDeviceDisplayName: matrix.clientName);
|
||||
initialDeviceDisplayName: PlatformInfos.clientName);
|
||||
} on MatrixException catch (exception) {
|
||||
setState(() => passwordError = exception.errorMessage);
|
||||
return setState(() => loading = false);
|
||||
|
@ -8,6 +8,7 @@ import 'package:fluffychat/components/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../utils/platform_infos.dart';
|
||||
|
||||
import '../app_config.dart';
|
||||
|
||||
@ -45,7 +46,7 @@ class _SignUpPasswordState extends State<SignUpPassword> {
|
||||
await matrix.client.register(
|
||||
username: widget.username,
|
||||
password: passwordController.text,
|
||||
initialDeviceDisplayName: matrix.clientName,
|
||||
initialDeviceDisplayName: PlatformInfos.clientName,
|
||||
auth: auth,
|
||||
);
|
||||
await waitForLogin;
|
||||
|
26
pubspec.lock
26
pubspec.lock
@ -227,6 +227,15 @@ packages:
|
||||
url: "https://gitlab.com/famedly/famedlysdk.git"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
fcm_shared_isolate:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: ios
|
||||
resolved-ref: "86519130e5e122a20fdd31de34013d62a88f106d"
|
||||
url: "https://gitlab.com/famedly/libraries/fcm_shared_isolate.git"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -269,34 +278,27 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
firebase:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "7.3.0"
|
||||
firebase_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.0+1"
|
||||
version: "0.5.3"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "2.1.0"
|
||||
firebase_core_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
version: "0.2.1+1"
|
||||
firebase_messaging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1161,8 +1163,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: main
|
||||
resolved-ref: f91b62772bd000863d1df2218198a83e71e6f87c
|
||||
ref: "8edc09a87d726d912926ac612e209573452c1273"
|
||||
resolved-ref: "8edc09a87d726d912926ac612e209573452c1273"
|
||||
url: "https://github.com/UnifiedPush/flutter-connector.git"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
|
@ -18,7 +18,13 @@ dependencies:
|
||||
unifiedpush:
|
||||
git:
|
||||
url: https://github.com/UnifiedPush/flutter-connector.git
|
||||
ref: main
|
||||
ref: 8edc09a87d726d912926ac612e209573452c1273
|
||||
|
||||
# Firebase Notifications
|
||||
fcm_shared_isolate:
|
||||
git:
|
||||
url: https://gitlab.com/famedly/libraries/fcm_shared_isolate.git
|
||||
ref: ios
|
||||
|
||||
cupertino_icons: any
|
||||
localstorage: ^3.0.6+9
|
||||
|
Loading…
Reference in New Issue
Block a user