211 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			211 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
import 'dart:async';
 | 
						|
import 'dart:io';
 | 
						|
import 'package:firebase_messaging/firebase_messaging.dart';
 | 
						|
import 'package:flutter/foundation.dart';
 | 
						|
import 'package:flutter/material.dart';
 | 
						|
import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
						|
import 'package:go_router/go_router.dart';
 | 
						|
import 'package:island/main.dart';
 | 
						|
import 'package:island/pods/config.dart';
 | 
						|
import 'package:island/route.dart';
 | 
						|
import 'package:island/models/account.dart';
 | 
						|
import 'package:island/pods/websocket.dart';
 | 
						|
import 'package:island/talker.dart';
 | 
						|
import 'package:island/widgets/app_notification.dart';
 | 
						|
import 'package:top_snackbar_flutter/top_snack_bar.dart';
 | 
						|
import 'package:url_launcher/url_launcher_string.dart';
 | 
						|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
 | 
						|
import 'package:windows_notification/windows_notification.dart' as winty;
 | 
						|
import 'package:windows_notification/notification_message.dart';
 | 
						|
 | 
						|
import 'package:dio/dio.dart';
 | 
						|
 | 
						|
// Windows notification instance
 | 
						|
winty.WindowsNotification? windowsNotification;
 | 
						|
 | 
						|
AppLifecycleState _appLifecycleState = AppLifecycleState.resumed;
 | 
						|
 | 
						|
void _onAppLifecycleChanged(AppLifecycleState state) {
 | 
						|
  _appLifecycleState = state;
 | 
						|
}
 | 
						|
 | 
						|
Future<void> initializeLocalNotifications() async {
 | 
						|
  // Initialize Windows notification for Windows platform
 | 
						|
  windowsNotification = winty.WindowsNotification(
 | 
						|
    applicationId: "Solian",
 | 
						|
  );
 | 
						|
 | 
						|
  WidgetsBinding.instance.addObserver(
 | 
						|
    LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged),
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
class LifecycleEventHandler extends WidgetsBindingObserver {
 | 
						|
  final void Function(AppLifecycleState) onAppLifecycleChanged;
 | 
						|
 | 
						|
  LifecycleEventHandler({required this.onAppLifecycleChanged});
 | 
						|
 | 
						|
  @override
 | 
						|
  void didChangeAppLifecycleState(AppLifecycleState state) {
 | 
						|
    onAppLifecycleChanged(state);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
StreamSubscription<WebSocketPacket> setupNotificationListener(
 | 
						|
  BuildContext context,
 | 
						|
  WidgetRef ref,
 | 
						|
) {
 | 
						|
  final ws = ref.watch(websocketProvider);
 | 
						|
  return ws.dataStream.listen((pkt) async {
 | 
						|
    if (pkt.type == "notifications.new") {
 | 
						|
      final notification = SnNotification.fromJson(pkt.data!);
 | 
						|
      if (_appLifecycleState == AppLifecycleState.resumed) {
 | 
						|
        // App is focused, show in-app notification
 | 
						|
        talker.info(
 | 
						|
          '[Notification] Showing in-app notification: ${notification.title}',
 | 
						|
        );
 | 
						|
        showTopSnackBar(
 | 
						|
          globalOverlay.currentState!,
 | 
						|
          Center(
 | 
						|
            child: ConstrainedBox(
 | 
						|
              constraints: const BoxConstraints(maxWidth: 480),
 | 
						|
              child: NotificationCard(notification: notification),
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
          onTap: () {
 | 
						|
            if (notification.meta['action_uri'] != null) {
 | 
						|
              var uri = notification.meta['action_uri'] as String;
 | 
						|
              if (uri.startsWith('/')) {
 | 
						|
                // In-app routes
 | 
						|
                rootNavigatorKey.currentContext?.push(
 | 
						|
                  notification.meta['action_uri'],
 | 
						|
                );
 | 
						|
              } else {
 | 
						|
                // External URLs
 | 
						|
                launchUrlString(uri);
 | 
						|
              }
 | 
						|
            }
 | 
						|
          },
 | 
						|
          onDismissed: () {},
 | 
						|
          dismissType: DismissType.onSwipe,
 | 
						|
          displayDuration: const Duration(seconds: 5),
 | 
						|
          snackBarPosition: SnackBarPosition.top,
 | 
						|
          padding: EdgeInsets.only(
 | 
						|
            left: 16,
 | 
						|
            right: 16,
 | 
						|
            top: 28, // Windows specific padding
 | 
						|
            bottom: 16,
 | 
						|
          ),
 | 
						|
        );
 | 
						|
      } else {
 | 
						|
        // App is in background, show Windows system notification
 | 
						|
        talker.info(
 | 
						|
          '[Notification] Showing Windows system notification: ${notification.title}',
 | 
						|
        );
 | 
						|
 | 
						|
        if (windowsNotification != null) {
 | 
						|
          final serverUrl = ref.read(serverUrlProvider);
 | 
						|
          final pfp = notification.meta['pfp'] as String?;
 | 
						|
          final img = notification.meta['images'] as List<dynamic>?;
 | 
						|
          final actionUrl = notification.meta['action_uri'] as String?;
 | 
						|
 | 
						|
          // Download and cache images
 | 
						|
          String? imagePath;
 | 
						|
          String? largeImagePath;
 | 
						|
 | 
						|
          if (pfp != null) {
 | 
						|
            try {
 | 
						|
              final file = await DefaultCacheManager().getSingleFile(
 | 
						|
                '$serverUrl/drive/files/$pfp',
 | 
						|
              );
 | 
						|
              imagePath = file.path;
 | 
						|
            } catch (e) {
 | 
						|
              talker.error('Failed to download pfp image: $e');
 | 
						|
            }
 | 
						|
          }
 | 
						|
 | 
						|
          if (img != null && img.isNotEmpty) {
 | 
						|
            try {
 | 
						|
              final file = await DefaultCacheManager().getSingleFile(
 | 
						|
                '$serverUrl/drive/files/${img.firstOrNull}',
 | 
						|
              );
 | 
						|
              largeImagePath = file.path;
 | 
						|
            } catch (e) {
 | 
						|
              talker.error('Failed to download large image: $e');
 | 
						|
            }
 | 
						|
          }
 | 
						|
 | 
						|
          // Use Windows notification for Windows platform
 | 
						|
          final notificationMessage = NotificationMessage.fromPluginTemplate(
 | 
						|
            notification.id, // unique id
 | 
						|
            notification.title,
 | 
						|
            [notification.subtitle, notification.content].where((e) => e.isNotEmpty).join('\n'),
 | 
						|
            group: notification.topic,
 | 
						|
            image: imagePath,
 | 
						|
            largeImage: largeImagePath,
 | 
						|
            launch: actionUrl != null ? 'solian://$actionUrl' : null,
 | 
						|
          );
 | 
						|
          await windowsNotification!.showNotificationPluginTemplate(
 | 
						|
            notificationMessage,
 | 
						|
          );
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
Future<void> subscribePushNotification(
 | 
						|
  Dio apiClient, {
 | 
						|
  bool detailedErrors = false,
 | 
						|
}) async {
 | 
						|
  if (!kIsWeb && Platform.isLinux) {
 | 
						|
    return;
 | 
						|
  }
 | 
						|
  await FirebaseMessaging.instance.requestPermission(
 | 
						|
    alert: true,
 | 
						|
    badge: true,
 | 
						|
    sound: true,
 | 
						|
  );
 | 
						|
 | 
						|
  String? deviceToken;
 | 
						|
  if (kIsWeb) {
 | 
						|
    deviceToken = await FirebaseMessaging.instance.getToken(
 | 
						|
      vapidKey:
 | 
						|
          "BFN2mkqyeI6oi4d2PAV4pfNyG3Jy0FBEblmmPrjmP0r5lHOPrxrcqLIWhM21R_cicF-j4Xhtr1kyDyDgJYRPLgU",
 | 
						|
    );
 | 
						|
  } else if (Platform.isAndroid) {
 | 
						|
    deviceToken = await FirebaseMessaging.instance.getToken();
 | 
						|
  } else if (Platform.isIOS) {
 | 
						|
    deviceToken = await FirebaseMessaging.instance.getAPNSToken();
 | 
						|
  }
 | 
						|
 | 
						|
  FirebaseMessaging.instance.onTokenRefresh
 | 
						|
      .listen((fcmToken) {
 | 
						|
        _putTokenToRemote(apiClient, fcmToken, 1);
 | 
						|
      })
 | 
						|
      .onError((err) {
 | 
						|
        talker.error("Failed to get firebase cloud messaging push token: $err");
 | 
						|
      });
 | 
						|
 | 
						|
  if (deviceToken != null) {
 | 
						|
    _putTokenToRemote(
 | 
						|
      apiClient,
 | 
						|
      deviceToken,
 | 
						|
      !kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1,
 | 
						|
    );
 | 
						|
  } else if (detailedErrors) {
 | 
						|
    throw Exception("Failed to get device token for push notifications.");
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
Future<void> _putTokenToRemote(
 | 
						|
  Dio apiClient,
 | 
						|
  String token,
 | 
						|
  int provider,
 | 
						|
) async {
 | 
						|
  await apiClient.put(
 | 
						|
    "/ring/notifications/subscription",
 | 
						|
    data: {"provider": provider, "device_token": token},
 | 
						|
  );
 | 
						|
}
 |