feat(flutter): add poke notification service
This commit is contained in:
@@ -0,0 +1,179 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
|
||||||
|
import '../src/rust/api.dart' as rust;
|
||||||
|
|
||||||
|
class PokeNotificationService {
|
||||||
|
PokeNotificationService({FlutterLocalNotificationsPlugin? notifications})
|
||||||
|
: _notifications = notifications ?? FlutterLocalNotificationsPlugin();
|
||||||
|
|
||||||
|
static const _strongAndroidChannelId = 'chanora_pokes_strong_v1';
|
||||||
|
static const _defaultAndroidChannelId = 'chanora_pokes_default_v1';
|
||||||
|
static const _groupKey = 'chanora.pokes';
|
||||||
|
static const _darwinThreadId = 'chanora.pokes';
|
||||||
|
static const _windowsHeader = WindowsHeader(
|
||||||
|
id: 'chanora.pokes',
|
||||||
|
title: 'Pokes',
|
||||||
|
arguments: 'pokes',
|
||||||
|
);
|
||||||
|
static const _windowsAppUserModelId = 'Chanora.Client';
|
||||||
|
static const _windowsGuid = '6B7F3DCB-4418-4E0A-8CC7-02B7C95B675E';
|
||||||
|
|
||||||
|
final FlutterLocalNotificationsPlugin _notifications;
|
||||||
|
bool _initialized = false;
|
||||||
|
|
||||||
|
Future<void> init() async {
|
||||||
|
if (_initialized) return;
|
||||||
|
await _notifications.initialize(
|
||||||
|
settings: const InitializationSettings(
|
||||||
|
android: AndroidInitializationSettings('ic_chanora_notification'),
|
||||||
|
iOS: DarwinInitializationSettings(
|
||||||
|
requestAlertPermission: false,
|
||||||
|
requestBadgePermission: false,
|
||||||
|
// TODO(event-sounds): handled by future EventSoundService, not the OS channel.
|
||||||
|
requestSoundPermission: false,
|
||||||
|
// TODO(event-sounds): handled by future EventSoundService, not the OS channel.
|
||||||
|
defaultPresentSound: false,
|
||||||
|
),
|
||||||
|
macOS: DarwinInitializationSettings(
|
||||||
|
requestAlertPermission: false,
|
||||||
|
requestBadgePermission: false,
|
||||||
|
// TODO(event-sounds): handled by future EventSoundService, not the OS channel.
|
||||||
|
requestSoundPermission: false,
|
||||||
|
// TODO(event-sounds): handled by future EventSoundService, not the OS channel.
|
||||||
|
defaultPresentSound: false,
|
||||||
|
),
|
||||||
|
linux: LinuxInitializationSettings(
|
||||||
|
defaultActionName: 'Open',
|
||||||
|
// TODO(event-sounds): handled by future EventSoundService, not the OS channel.
|
||||||
|
defaultSuppressSound: true,
|
||||||
|
),
|
||||||
|
windows: WindowsInitializationSettings(
|
||||||
|
appName: 'Chanora',
|
||||||
|
appUserModelId: _windowsAppUserModelId,
|
||||||
|
guid: _windowsGuid,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> requestPermission() async {
|
||||||
|
await init();
|
||||||
|
if (kIsWeb) return true;
|
||||||
|
|
||||||
|
final android = _notifications
|
||||||
|
.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin
|
||||||
|
>();
|
||||||
|
if (android != null) {
|
||||||
|
return await android.requestNotificationsPermission() ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ios = _notifications
|
||||||
|
.resolvePlatformSpecificImplementation<
|
||||||
|
IOSFlutterLocalNotificationsPlugin
|
||||||
|
>();
|
||||||
|
if (ios != null) {
|
||||||
|
return await ios.requestPermissions(alert: true, badge: true) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final macOS = _notifications
|
||||||
|
.resolvePlatformSpecificImplementation<
|
||||||
|
MacOSFlutterLocalNotificationsPlugin
|
||||||
|
>();
|
||||||
|
if (macOS != null) {
|
||||||
|
return await macOS.requestPermissions(alert: true, badge: true) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> show({
|
||||||
|
required String senderName,
|
||||||
|
required String message,
|
||||||
|
required BigInt senderId,
|
||||||
|
required rust.BridgePokeStrength strength,
|
||||||
|
}) async {
|
||||||
|
await init();
|
||||||
|
final permitted = await requestPermission();
|
||||||
|
if (!permitted) return;
|
||||||
|
|
||||||
|
final trimmedMessage = message.trim();
|
||||||
|
final body = trimmedMessage.isEmpty
|
||||||
|
? '$senderName pokes you'
|
||||||
|
: trimmedMessage;
|
||||||
|
|
||||||
|
await _notifications.show(
|
||||||
|
id: senderId.toUnsigned(31).toInt(),
|
||||||
|
title: 'Poke from $senderName',
|
||||||
|
body: body,
|
||||||
|
notificationDetails: NotificationDetails(
|
||||||
|
android: _androidDetails(strength),
|
||||||
|
iOS: _darwinDetails(strength),
|
||||||
|
macOS: _darwinDetails(strength),
|
||||||
|
linux: _linuxDetails(strength),
|
||||||
|
windows: _windowsDetails(strength),
|
||||||
|
),
|
||||||
|
payload: 'poke:$senderId',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AndroidNotificationDetails _androidDetails(rust.BridgePokeStrength strength) {
|
||||||
|
final isStrong = strength == rust.BridgePokeStrength.strong;
|
||||||
|
return AndroidNotificationDetails(
|
||||||
|
isStrong ? _strongAndroidChannelId : _defaultAndroidChannelId,
|
||||||
|
isStrong ? 'Pokes' : 'Pokes (quiet)',
|
||||||
|
channelDescription: 'TeamSpeak poke notifications',
|
||||||
|
importance: isStrong ? Importance.max : Importance.defaultImportance,
|
||||||
|
priority: isStrong ? Priority.high : Priority.defaultPriority,
|
||||||
|
// TODO(event-sounds): handled by future EventSoundService, not the OS channel.
|
||||||
|
playSound: false,
|
||||||
|
// TODO(event-sounds): handled by future EventSoundService, not the OS channel.
|
||||||
|
silent: true,
|
||||||
|
groupKey: _groupKey,
|
||||||
|
category: AndroidNotificationCategory.message,
|
||||||
|
visibility: NotificationVisibility.private,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DarwinNotificationDetails _darwinDetails(rust.BridgePokeStrength strength) {
|
||||||
|
return DarwinNotificationDetails(
|
||||||
|
// TODO(event-sounds): handled by future EventSoundService, not the OS channel.
|
||||||
|
presentSound: false,
|
||||||
|
threadIdentifier: _darwinThreadId,
|
||||||
|
interruptionLevel: switch (strength) {
|
||||||
|
rust.BridgePokeStrength.strong => InterruptionLevel.timeSensitive,
|
||||||
|
rust.BridgePokeStrength.suppressed => InterruptionLevel.active,
|
||||||
|
rust.BridgePokeStrength.suppressedOverflow => InterruptionLevel.passive,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LinuxNotificationDetails _linuxDetails(rust.BridgePokeStrength strength) {
|
||||||
|
return LinuxNotificationDetails(
|
||||||
|
// TODO(event-sounds): handled by future EventSoundService, not the OS channel.
|
||||||
|
suppressSound: true,
|
||||||
|
urgency: switch (strength) {
|
||||||
|
rust.BridgePokeStrength.strong => LinuxNotificationUrgency.critical,
|
||||||
|
rust.BridgePokeStrength.suppressed => LinuxNotificationUrgency.normal,
|
||||||
|
rust.BridgePokeStrength.suppressedOverflow =>
|
||||||
|
LinuxNotificationUrgency.low,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowsNotificationDetails _windowsDetails(rust.BridgePokeStrength strength) {
|
||||||
|
return WindowsNotificationDetails(
|
||||||
|
// TODO(event-sounds): handled by future EventSoundService, not the OS channel.
|
||||||
|
audio: WindowsNotificationAudio.silent(),
|
||||||
|
header: _windowsHeader,
|
||||||
|
scenario: strength == rust.BridgePokeStrength.strong
|
||||||
|
? WindowsNotificationScenario.urgent
|
||||||
|
: null,
|
||||||
|
duration: strength == rust.BridgePokeStrength.strong
|
||||||
|
? WindowsNotificationDuration.long
|
||||||
|
: WindowsNotificationDuration.short,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
|
||||||
|
import 'package:chanora_flutter/services/poke_notification_service.dart';
|
||||||
|
import 'package:chanora_flutter/src/rust/api.dart' as rust;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
const channel = MethodChannel('dexterous.com/flutter/local_notifications');
|
||||||
|
late List<MethodCall> calls;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||||
|
AndroidFlutterLocalNotificationsPlugin.registerWith();
|
||||||
|
calls = <MethodCall>[];
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(channel, (call) async {
|
||||||
|
calls.add(call);
|
||||||
|
return switch (call.method) {
|
||||||
|
'initialize' => true,
|
||||||
|
'requestNotificationsPermission' => true,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
debugDefaultTargetPlatformOverride = null;
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(channel, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'show dispatches a silent poke notification with sender payload',
|
||||||
|
() async {
|
||||||
|
final service = PokeNotificationService();
|
||||||
|
|
||||||
|
await service.show(
|
||||||
|
senderName: 'Alice',
|
||||||
|
message: 'wake up',
|
||||||
|
senderId: BigInt.from(42),
|
||||||
|
strength: rust.BridgePokeStrength.strong,
|
||||||
|
);
|
||||||
|
|
||||||
|
final showCall = calls.singleWhere((call) => call.method == 'show');
|
||||||
|
final arguments = Map<Object?, Object?>.from(showCall.arguments as Map);
|
||||||
|
|
||||||
|
expect(arguments['id'], 42);
|
||||||
|
expect(arguments['title'], 'Poke from Alice');
|
||||||
|
expect(arguments['body'], 'wake up');
|
||||||
|
expect(arguments['payload'], 'poke:42');
|
||||||
|
final specifics = Map<Object?, Object?>.from(
|
||||||
|
arguments['platformSpecifics'] as Map,
|
||||||
|
);
|
||||||
|
expect(specifics['silent'], true);
|
||||||
|
expect(specifics['playSound'], false);
|
||||||
|
expect(specifics['groupKey'], 'chanora.pokes');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user