Compare commits

..
Author SHA1 Message Date
Edison Jwa dc52092654 docs: remove trailing whitespace from continuation design 2026-06-08 23:32:18 +09:00
35 changed files with 214 additions and 1496 deletions
@@ -59,7 +59,6 @@ android {
ndkVersion = flutter.ndkVersion
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
@@ -199,7 +198,6 @@ android {
// armeabi-v7a, x86_64, x86. AGP merges these into the APK/AAB.
dependencies {
implementation("com.microsoft.onnxruntime:onnxruntime-android:1.26.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}
flutter {
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,3C8.69,3 6,5.69 6,9V13C6,16.31 8.69,19 12,19C15.31,19 18,16.31 18,13V9C18,5.69 15.31,3 12,3ZM12,5C14.21,5 16,6.79 16,9V13C16,15.21 14.21,17 12,17C9.79,17 8,15.21 8,13V9C8,6.79 9.79,5 12,5ZM11,20V22H13V20H11Z" />
</vector>
@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@drawable/ic_chanora_notification" />
@@ -34,8 +34,6 @@
<string>Chanora needs local network access to connect to your voice servers.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Chanora needs microphone access so you can talk on your voice server.</string>
<key>NSUserNotificationsUsageDescription</key>
<string>Chanora sends you a notification when another user pokes you.</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
+8 -19
View File
@@ -242,30 +242,19 @@
"clientInfoUnknown": "Unknown",
"clientInfoHidden": "Hidden",
"clientInfoNone": "None",
"pokeSettingsAction": "Poke notifications",
"pokeSettingsTitle": "Poke notifications",
"pokeSettingsEnableLabel": "Notify me about pokes",
"pokeSettingsEnableDescription": "Show local notifications for incoming pokes when this is on.",
"pokeSettingsMutedSendersHeader": "Muted senders",
"pokeSettingsMutedSendersEmpty": "No muted poke senders.",
"pokeSettingsMutedSenderLabel": "Client ID {senderId}",
"@pokeSettingsMutedSenderLabel": {
"placeholders": {
"senderId": { "type": "String" }
}
},
"pokeSettingsUnmuteSenderAction": "Unmute",
"pokeOverflowMutePrompt": "Repeated pokes from {sender} were suppressed. Mute this sender?",
"@pokeOverflowMutePrompt": {
"pokeSnackBarClearAction": "Clear",
"pokeSnackBarMoreIndicator": "...",
"pokeSnackBarIncomingNoMessage": "{sender} pokes you",
"@pokeSnackBarIncomingNoMessage": {
"placeholders": {
"sender": { "type": "String" }
}
},
"pokeOverflowMuteAction": "Mute",
"pokeMutedSenderConfirmation": "Muted pokes from {sender}",
"@pokeMutedSenderConfirmation": {
"pokeSnackBarIncomingWithMessage": "{sender} pokes you: {message}",
"@pokeSnackBarIncomingWithMessage": {
"placeholders": {
"sender": { "type": "String" }
"sender": { "type": "String" },
"message": { "type": "String" }
}
},
"pokeHistorySelfNoMessage": "<{time}> You poked \"{target}\".",
+8 -19
View File
@@ -191,30 +191,19 @@
"clientInfoUnknown": "未知",
"clientInfoHidden": "隐藏",
"clientInfoNone": "无",
"pokeSettingsAction": "戳一戳通知",
"pokeSettingsTitle": "戳一戳通知",
"pokeSettingsEnableLabel": "接收戳一戳通知",
"pokeSettingsEnableDescription": "开启后,收到戳一戳时会显示本地通知。",
"pokeSettingsMutedSendersHeader": "已静音的发送者",
"pokeSettingsMutedSendersEmpty": "没有已静音的戳一戳发送者。",
"pokeSettingsMutedSenderLabel": "用户 ID {senderId}",
"@pokeSettingsMutedSenderLabel": {
"placeholders": {
"senderId": { "type": "String" }
}
},
"pokeSettingsUnmuteSenderAction": "取消静音",
"pokeOverflowMutePrompt": "来自 {sender} 的重复戳一戳已被抑制。要静音此发送者吗?",
"@pokeOverflowMutePrompt": {
"pokeSnackBarClearAction": "清除",
"pokeSnackBarMoreIndicator": "...",
"pokeSnackBarIncomingNoMessage": "{sender} 戳了你一下",
"@pokeSnackBarIncomingNoMessage": {
"placeholders": {
"sender": { "type": "String" }
}
},
"pokeOverflowMuteAction": "静音",
"pokeMutedSenderConfirmation": "已静音来自 {sender} 的戳一戳",
"@pokeMutedSenderConfirmation": {
"pokeSnackBarIncomingWithMessage": "{sender} 戳了你一下:{message}",
"@pokeSnackBarIncomingWithMessage": {
"placeholders": {
"sender": { "type": "String" }
"sender": { "type": "String" },
"message": { "type": "String" }
}
},
"pokeHistorySelfNoMessage": "<{time}> 你戳了“{target}”一下。",
@@ -1159,71 +1159,29 @@ abstract class AppL10n {
/// **'None'**
String get clientInfoNone;
/// No description provided for @pokeSettingsAction.
/// No description provided for @pokeSnackBarClearAction.
///
/// In en, this message translates to:
/// **'Poke notifications'**
String get pokeSettingsAction;
/// **'Clear'**
String get pokeSnackBarClearAction;
/// No description provided for @pokeSettingsTitle.
/// No description provided for @pokeSnackBarMoreIndicator.
///
/// In en, this message translates to:
/// **'Poke notifications'**
String get pokeSettingsTitle;
/// **'...'**
String get pokeSnackBarMoreIndicator;
/// No description provided for @pokeSettingsEnableLabel.
/// No description provided for @pokeSnackBarIncomingNoMessage.
///
/// In en, this message translates to:
/// **'Notify me about pokes'**
String get pokeSettingsEnableLabel;
/// **'{sender} pokes you'**
String pokeSnackBarIncomingNoMessage(String sender);
/// No description provided for @pokeSettingsEnableDescription.
/// No description provided for @pokeSnackBarIncomingWithMessage.
///
/// In en, this message translates to:
/// **'Show local notifications for incoming pokes when this is on.'**
String get pokeSettingsEnableDescription;
/// No description provided for @pokeSettingsMutedSendersHeader.
///
/// In en, this message translates to:
/// **'Muted senders'**
String get pokeSettingsMutedSendersHeader;
/// No description provided for @pokeSettingsMutedSendersEmpty.
///
/// In en, this message translates to:
/// **'No muted poke senders.'**
String get pokeSettingsMutedSendersEmpty;
/// No description provided for @pokeSettingsMutedSenderLabel.
///
/// In en, this message translates to:
/// **'Client ID {senderId}'**
String pokeSettingsMutedSenderLabel(String senderId);
/// No description provided for @pokeSettingsUnmuteSenderAction.
///
/// In en, this message translates to:
/// **'Unmute'**
String get pokeSettingsUnmuteSenderAction;
/// No description provided for @pokeOverflowMutePrompt.
///
/// In en, this message translates to:
/// **'Repeated pokes from {sender} were suppressed. Mute this sender?'**
String pokeOverflowMutePrompt(String sender);
/// No description provided for @pokeOverflowMuteAction.
///
/// In en, this message translates to:
/// **'Mute'**
String get pokeOverflowMuteAction;
/// No description provided for @pokeMutedSenderConfirmation.
///
/// In en, this message translates to:
/// **'Muted pokes from {sender}'**
String pokeMutedSenderConfirmation(String sender);
/// **'{sender} pokes you: {message}'**
String pokeSnackBarIncomingWithMessage(String sender, String message);
/// No description provided for @pokeHistorySelfNoMessage.
///
@@ -590,43 +590,19 @@ class AppL10nEn extends AppL10n {
String get clientInfoNone => 'None';
@override
String get pokeSettingsAction => 'Poke notifications';
String get pokeSnackBarClearAction => 'Clear';
@override
String get pokeSettingsTitle => 'Poke notifications';
String get pokeSnackBarMoreIndicator => '...';
@override
String get pokeSettingsEnableLabel => 'Notify me about pokes';
@override
String get pokeSettingsEnableDescription =>
'Show local notifications for incoming pokes when this is on.';
@override
String get pokeSettingsMutedSendersHeader => 'Muted senders';
@override
String get pokeSettingsMutedSendersEmpty => 'No muted poke senders.';
@override
String pokeSettingsMutedSenderLabel(String senderId) {
return 'Client ID $senderId';
String pokeSnackBarIncomingNoMessage(String sender) {
return '$sender pokes you';
}
@override
String get pokeSettingsUnmuteSenderAction => 'Unmute';
@override
String pokeOverflowMutePrompt(String sender) {
return 'Repeated pokes from $sender were suppressed. Mute this sender?';
}
@override
String get pokeOverflowMuteAction => 'Mute';
@override
String pokeMutedSenderConfirmation(String sender) {
return 'Muted pokes from $sender';
String pokeSnackBarIncomingWithMessage(String sender, String message) {
return '$sender pokes you: $message';
}
@override
@@ -577,42 +577,19 @@ class AppL10nZh extends AppL10n {
String get clientInfoNone => '';
@override
String get pokeSettingsAction => '戳一戳通知';
String get pokeSnackBarClearAction => '清除';
@override
String get pokeSettingsTitle => '戳一戳通知';
String get pokeSnackBarMoreIndicator => '...';
@override
String get pokeSettingsEnableLabel => '接收戳一戳通知';
@override
String get pokeSettingsEnableDescription => '开启后,收到戳一戳时会显示本地通知。';
@override
String get pokeSettingsMutedSendersHeader => '已静音的发送者';
@override
String get pokeSettingsMutedSendersEmpty => '没有已静音的戳一戳发送者。';
@override
String pokeSettingsMutedSenderLabel(String senderId) {
return '用户 ID $senderId';
String pokeSnackBarIncomingNoMessage(String sender) {
return '$sender 戳了你一下';
}
@override
String get pokeSettingsUnmuteSenderAction => '取消静音';
@override
String pokeOverflowMutePrompt(String sender) {
return '来自 $sender 的重复戳一戳已被抑制。要静音此发送者吗?';
}
@override
String get pokeOverflowMuteAction => '静音';
@override
String pokeMutedSenderConfirmation(String sender) {
return '已静音来自 $sender 的戳一戳';
String pokeSnackBarIncomingWithMessage(String sender, String message) {
return '$sender 戳了你一下:$message';
}
@override
+146 -101
View File
@@ -28,8 +28,6 @@ import 'services/connection_phase_state.dart';
import 'services/hard_mute_owners.dart';
import 'services/ios_permissions_service.dart';
import 'services/macos_permissions_service.dart';
import 'services/poke_notification_service.dart';
import 'services/poke_preferences_service.dart';
import 'services/prefetch_debouncer.dart';
import 'services/snapshot_state_mapper.dart';
import 'services/ts3_server_link.dart';
@@ -45,7 +43,6 @@ import 'widgets/client_info_sheet.dart';
import 'widgets/connect_widgets.dart';
import 'widgets/input_dialogs.dart';
import 'widgets/permission_state_banner.dart';
import 'widgets/poke_notification_settings.dart';
import 'widgets/snapshot_view.dart';
import 'widgets/voice_platform.dart';
import 'widgets/voice_bar.dart';
@@ -164,7 +161,10 @@ class _ChanoraAppState extends State<ChanoraApp> {
supportedLocales: AppL10n.supportedLocales,
home: Stack(
children: [
_BetaHome(themeMode: _themeMode, onThemeModeChanged: _setThemeMode),
_BetaHome(
themeMode: _themeMode,
onThemeModeChanged: _setThemeMode,
),
if (_showAudioDebugOverlay) const AudioDebugStatsPanel(),
],
),
@@ -202,19 +202,6 @@ extension on ThemeMode {
}
}
bool isPokeSenderActiveChat({
required bool chatOpen,
required rust.BridgeMessageTarget? inlineChatTarget,
required BigInt senderId,
}) {
if (!chatOpen) return false;
return switch (inlineChatTarget) {
rust.BridgeMessageTarget_Poke(:final field0) ||
rust.BridgeMessageTarget_Client(:final field0) => field0 == senderId,
_ => false,
};
}
class ChanoraThemeModeMenu extends StatelessWidget {
const ChanoraThemeModeMenu({
super.key,
@@ -315,6 +302,18 @@ class _BetaHome extends StatefulWidget {
State<_BetaHome> createState() => _BetaHomeState();
}
class _ReceivedPoke {
const _ReceivedPoke({
required this.senderName,
required this.message,
required this.receivedAt,
});
final String senderName;
final String message;
final DateTime receivedAt;
}
class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
final _hostCtl = TextEditingController(text: 'cn.teamspeak.app');
final _nickCtl = TextEditingController(text: 'ChanoraBeta');
@@ -413,6 +412,11 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
/// previous conversation when the user reopens chat.
rust.BridgeMessageTarget? _lastDismissedTarget;
String _lastDismissedClientName = '';
final ValueNotifier<List<_ReceivedPoke>> _pokeSnackBarPokes = ValueNotifier(
const [],
);
bool _pokeSnackBarVisible = false;
// SDD-106 / SRS-209: Android RECORD_AUDIO runtime permission service.
// Constructed at startup so cold-launch state is captured before the
// first voice_join attempt. On non-Android hosts the service
@@ -427,9 +431,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
// MethodChannel.
final MacOSPermissionsService _macOSPermissions = MacOSPermissionsService();
final UiPreferencesService _uiPreferences = const UiPreferencesService();
final PokeNotificationService _pokeNotifications = PokeNotificationService();
final PokePreferencesService _pokePreferences = PokePreferencesService();
late final Future<void> _pokePreferencesReady;
@override
void initState() {
@@ -463,8 +464,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
_onMacOSPttCapabilityChanged,
);
_macOSPermissions.checkInitialStates();
unawaited(_pokeNotifications.init());
_pokePreferencesReady = _pokePreferences.load();
WidgetsBinding.instance.addPostFrameCallback((_) {
unawaited(_requestRecordAudioOnStartup());
});
@@ -868,11 +867,10 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
:final senderName,
:final message,
:final target,
:final pokeStrength,
):
// Skip echo of self-sent messages (already added locally).
if (senderId == _snapshot?.ownClientId) return;
final isPoke = target is rust.BridgeMessageTarget_Poke;
// Skip echo of self-sent non-poke messages (already added locally).
if (!isPoke && senderId == _snapshot?.ownClientId) return;
final receivedAt = DateTime.now();
setState(() {
_appendChatEntryUnlocked(
@@ -887,13 +885,10 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
);
});
if (isPoke) {
unawaited(
_handleIncomingPoke(
senderId: senderId,
senderName: senderName,
message: message,
strength: pokeStrength,
),
_showPokeSnackBar(
senderName: senderName,
message: message,
receivedAt: receivedAt,
);
return;
}
@@ -1093,6 +1088,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
_nickCtl.dispose();
_passwordCtl.dispose();
_chatFeedRevision.dispose();
_pokeSnackBarPokes.dispose();
_androidPermissions.recordAudioState.removeListener(
_onRecordAudioPermissionChanged,
);
@@ -1109,7 +1105,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
_androidPermissions.stop();
_iosPermissions.stop();
_macOSPermissions.stop();
_pokePreferences.dispose();
super.dispose();
}
@@ -1143,9 +1138,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
);
if (accessState == MacOSLocalNetworkState.denied) {
if (!mounted) return;
setState(() {
_phase = ConnectionPhase.idle;
});
setState(() { _phase = ConnectionPhase.idle; });
_showLocalNetworkDeniedSnackBar();
return;
}
@@ -1527,16 +1520,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
}
}
Future<void> _onOpenPokeSettings() async {
await _pokePreferences.load();
if (!mounted) return;
await showDialog<void>(
context: context,
builder: (ctx) =>
PokeNotificationSettingsDialog(preferences: _pokePreferences),
);
}
Future<String?> _askChannelPassword(AppL10n l10n) async {
// Same pattern as _onAddCurrentBookmark: route the dialog
// through a dedicated StatefulWidget so its
@@ -1821,7 +1804,9 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || _inlineChatTarget == null) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppL10n.of(context).chatPanelCollapsedHint)),
SnackBar(
content: Text(AppL10n.of(context).chatPanelCollapsedHint),
),
);
});
}
@@ -1842,68 +1827,52 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
);
}
Future<void> _handleIncomingPoke({
required BigInt senderId,
void _showPokeSnackBar({
required String senderName,
required String message,
required rust.BridgePokeStrength? strength,
}) async {
if (senderId == _snapshot?.ownClientId) return;
await _pokePreferencesReady;
if (!_pokePreferences.pokesEnabled.value) return;
if (_pokePreferences.isMuted(senderId)) return;
final pokeStrength = strength ?? rust.BridgePokeStrength.suppressed;
if (pokeStrength == rust.BridgePokeStrength.suppressedOverflow && mounted) {
_showPokeOverflowMutePrompt(senderId: senderId, senderName: senderName);
}
if (_isPokeSenderActiveChat(senderId)) return;
await _pokeNotifications.show(
senderName: senderName,
message: message,
senderId: senderId,
strength: pokeStrength,
);
}
bool _isPokeSenderActiveChat(BigInt senderId) {
return isPokeSenderActiveChat(
chatOpen: _chatOpen,
inlineChatTarget: _inlineChatTarget,
senderId: senderId,
);
}
void _showPokeOverflowMutePrompt({
required BigInt senderId,
required String senderName,
required DateTime receivedAt,
}) {
final l10n = AppL10n.of(context);
_pokeSnackBarPokes.value = [
..._pokeSnackBarPokes.value,
_ReceivedPoke(
senderName: senderName,
message: message,
receivedAt: receivedAt,
),
];
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _renderPokeSnackBar();
});
}
void _renderPokeSnackBar() {
if (_pokeSnackBarPokes.value.isEmpty || _pokeSnackBarVisible) return;
_pokeSnackBarVisible = true;
final messenger = ScaffoldMessenger.of(context);
messenger.showSnackBar(
final controller = messenger.showSnackBar(
SnackBar(
behavior: SnackBarBehavior.floating,
margin: _chatSnackBarMargin(),
duration: const Duration(seconds: 8),
content: Text(
l10n.pokeOverflowMutePrompt(senderName),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
duration: const Duration(days: 365),
dismissDirection: DismissDirection.none,
content: _PokeSnackBarContent(pokes: _pokeSnackBarPokes),
action: SnackBarAction(
label: l10n.pokeOverflowMuteAction,
label: AppL10n.of(context).pokeSnackBarClearAction,
onPressed: () {
unawaited(_pokePreferences.muteSender(senderId));
messenger.showSnackBar(
SnackBar(
behavior: SnackBarBehavior.floating,
margin: _chatSnackBarMargin(),
content: Text(l10n.pokeMutedSenderConfirmation(senderName)),
),
);
_pokeSnackBarPokes.value = const [];
_pokeSnackBarVisible = false;
},
),
),
);
controller.closed.then((_) {
if (!mounted) return;
_pokeSnackBarVisible = false;
if (_pokeSnackBarPokes.value.isEmpty) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _renderPokeSnackBar();
});
});
}
void _showChatMessageSnackBar({
@@ -1911,6 +1880,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
required String message,
required rust.BridgeMessageTarget target,
}) {
if (_pokeSnackBarPokes.value.isNotEmpty) return;
final messenger = ScaffoldMessenger.of(context);
messenger.hideCurrentSnackBar();
messenger.showSnackBar(
@@ -2407,11 +2377,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
themeMode: widget.themeMode,
onThemeModeChanged: widget.onThemeModeChanged,
),
IconButton(
tooltip: l10n.pokeSettingsAction,
icon: const Icon(Icons.notifications_outlined),
onPressed: () => unawaited(_onOpenPokeSettings()),
),
if (_phase.canOpenChatWithSnapshot(hasSnapshot: _snapshot != null)) ...[
Padding(
padding: const EdgeInsetsDirectional.only(end: 12),
@@ -2891,3 +2856,83 @@ class _LiveDiagnosticsDialogState extends State<_LiveDiagnosticsDialog> {
);
}
}
class _PokeSnackBarContent extends StatelessWidget {
const _PokeSnackBarContent({required this.pokes});
final ValueListenable<List<_ReceivedPoke>> pokes;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<List<_ReceivedPoke>>(
valueListenable: pokes,
builder: (context, entries, _) {
final l10n = AppL10n.of(context);
final visible = entries.length <= 3
? entries
: entries.sublist(entries.length - 3);
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (entries.length > 3)
Padding(
padding: const EdgeInsetsDirectional.only(bottom: 2),
child: Text(
l10n.pokeSnackBarMoreIndicator,
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
for (final poke in visible) _PokeSnackBarRow(poke: poke),
],
);
},
);
}
}
class _PokeSnackBarRow extends StatelessWidget {
const _PokeSnackBarRow({required this.poke});
final _ReceivedPoke poke;
@override
Widget build(BuildContext context) {
final l10n = AppL10n.of(context);
final message = poke.message.trim();
final text = message.isEmpty
? l10n.pokeSnackBarIncomingNoMessage(poke.senderName)
: l10n.pokeSnackBarIncomingWithMessage(poke.senderName, message);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 1),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
const SizedBox(width: 12),
Text(
pokeSnackBarTimeLabel(poke.receivedAt),
style: TextStyle(
color: Theme.of(
context,
).colorScheme.onInverseSurface.withValues(alpha: 0.72),
),
),
],
),
);
}
}
String pokeSnackBarTimeLabel(DateTime timestamp) {
String two(int value) => value.toString().padLeft(2, '0');
return '${two(timestamp.hour)}:${two(timestamp.minute)}';
}
@@ -1,179 +0,0 @@
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,
);
}
}
@@ -1,58 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
class PokePreferencesService {
static const _enabledKey = 'pokes.enabled';
static const _mutedSendersKey = 'pokes.muted_senders';
final ValueNotifier<bool> _pokesEnabled = ValueNotifier<bool>(true);
final ValueNotifier<Set<BigInt>> _mutedSenders = ValueNotifier<Set<BigInt>>(
const <BigInt>{},
);
ValueListenable<bool> get pokesEnabled => _pokesEnabled;
ValueListenable<Set<BigInt>> get mutedSenders => _mutedSenders;
Future<void> load() async {
final prefs = await SharedPreferences.getInstance();
_pokesEnabled.value = prefs.getBool(_enabledKey) ?? true;
_mutedSenders.value = (prefs.getStringList(_mutedSendersKey) ?? const [])
.map(BigInt.parse)
.toSet();
}
Future<void> setPokesEnabled(bool enabled) async {
_pokesEnabled.value = enabled;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_enabledKey, enabled);
}
Future<void> muteSender(BigInt senderId) async {
if (_mutedSenders.value.contains(senderId)) return;
_mutedSenders.value = {..._mutedSenders.value, senderId};
await _saveMutedSenders();
}
Future<void> unmuteSender(BigInt senderId) async {
if (!_mutedSenders.value.contains(senderId)) return;
_mutedSenders.value = _mutedSenders.value
.where((mutedSender) => mutedSender != senderId)
.toSet();
await _saveMutedSenders();
}
bool isMuted(BigInt senderId) => _mutedSenders.value.contains(senderId);
Future<void> _saveMutedSenders() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setStringList(
_mutedSendersKey,
_mutedSenders.value.map((senderId) => senderId.toString()).toList(),
);
}
void dispose() {
_pokesEnabled.dispose();
_mutedSenders.dispose();
}
}
+4 -88
View File
@@ -11,7 +11,7 @@ part 'api.freezed.dart';
// These functions are ignored because they are not marked as `pub`: `dispatch_platform_audio_event`, `install_panic_diagnostic_hook`, `log_file_path`, `log_sink`, `map_join_error_code`, `map_join_sync_state`, `open_log_file`, `permission_events`, `platform_audio_events`, `process`, `publish_permission_state`, `runtime`, `session`, `task_join_error`, `transmit_mode_from_u8`
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `PlatformAudioEvent`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`
// These functions are ignored (category: IgnoreBecauseExplicitAttribute): `from_kotlin_str`, `to_permission_gate`
/// Return the platform-conventional log-file path as a string, or
@@ -263,8 +263,7 @@ Future<BridgeAudioStats> audioStats() =>
/// Subscribe to real-time microphone input level at ~30 Hz.
/// Values are dBFS (-120 = silence, 0 = clipping). The stream ends
/// when the Dart subscriber cancels, the session is dropped, or
/// the session becomes persistently unavailable.
/// when the Dart subscriber cancels or the session is dropped.
Stream<double> inputLevelStream() =>
RustLib.instance.api.crateApiInputLevelStream();
@@ -1210,9 +1209,6 @@ sealed class BridgeEvent with _$BridgeEvent {
/// Target scope (server/channel/private/poke).
required BridgeMessageTarget target,
/// Poke notification strength, present only for poke messages.
BridgePokeStrength? pokeStrength,
}) = BridgeEvent_ChatMessage;
/// Human-readable server activity surfaced from protocol bookkeeping events.
@@ -1223,116 +1219,48 @@ sealed class BridgeEvent with _$BridgeEvent {
/// Audio route changed (speaker/earpiece/BT/wired).
const factory BridgeEvent.audioRouteChanged({
/// New audio output route.
required BridgeAudioRoute route,
}) = BridgeEvent_AudioRouteChanged;
/// A client moved to a different channel.
const factory BridgeEvent.clientMoved({
/// Unique client identifier.
required BigInt clientId,
/// Destination channel.
required BigInt newChannelId,
}) = BridgeEvent_ClientMoved;
/// A new client connected.
const factory BridgeEvent.clientJoined({
/// Unique client identifier.
required BigInt clientId,
/// Channel the client joined.
required BigInt channelId,
/// Display nickname.
required String name,
/// Microphone muted state.
required bool inputMuted,
/// Speaker muted state.
required bool outputMuted,
/// True for server query (bot) clients.
required bool isServerQuery,
/// Client's talk power value.
required int talkPower,
/// Whether the server granted temporary talk power.
required bool talkPowerGranted,
}) = BridgeEvent_ClientJoined;
/// A client disconnected.
const factory BridgeEvent.clientLeft({
/// Unique client identifier.
required BigInt clientId,
/// Display nickname at time of disconnect.
required String name,
}) = BridgeEvent_ClientLeft;
/// Client properties changed.
const factory BridgeEvent.clientUpdated({
/// Unique client identifier.
required BigInt clientId,
/// Microphone muted state.
required bool inputMuted,
/// Speaker muted state.
required bool outputMuted,
/// True for server query (bot) clients.
required bool isServerQuery,
/// Client's talk power value.
required int talkPower,
/// Whether the server granted temporary talk power.
required bool talkPowerGranted,
}) = BridgeEvent_ClientUpdated;
/// A new channel appeared.
const factory BridgeEvent.channelAdded({
/// Unique channel identifier.
required BigInt id,
/// Parent channel ID.
required BigInt parent,
/// Channel name.
required String name,
/// Predecessor channel ID within the same parent (TeamSpeak
/// linked-list ordering hint). Zero means first child.
required PlatformInt64 order,
/// Whether the channel requires a password.
required bool hasPassword,
/// Talk power required to speak; `None` means no restriction.
int? neededTalkPower,
}) = BridgeEvent_ChannelAdded;
/// A channel was deleted.
const factory BridgeEvent.channelRemoved({
/// Channel identifier.
required BigInt id,
}) = BridgeEvent_ChannelRemoved;
/// Channel properties changed.
const factory BridgeEvent.channelRemoved({required BigInt id}) =
BridgeEvent_ChannelRemoved;
const factory BridgeEvent.channelUpdated({
/// Unique channel identifier.
required BigInt id,
/// Channel name.
required String name,
/// Whether the channel requires a password.
required bool hasPassword,
/// Talk power required to speak; `None` means no restriction.
int? neededTalkPower,
}) = BridgeEvent_ChannelUpdated;
}
@@ -1378,18 +1306,6 @@ enum BridgeNetworkState {
offline,
}
/// Bridge poke notification strength.
enum BridgePokeStrength {
/// Poke should be surfaced at full strength.
strong,
/// Poke is rate-limited but below overflow severity.
suppressed,
/// Poke remains suppressed after repeated suppressed pokes.
suppressedOverflow,
}
/// Persisted PTT binding display state for the UI.
class BridgePttBinding {
/// Stable input category string (`""`, `"keyboard"`, or
@@ -173,7 +173,7 @@ return channelUpdated(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( String serverName)? connected,TResult Function( String reason)? lost,TResult Function( int attempt, int delaySecs)? reconnecting,TResult Function( String reason)? disconnected,TResult Function()? audioStarted,TResult Function()? audioStopped,TResult Function( String level, String backendId, String boundInputClass)? pttCapability,TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs, BigInt? currentChannelId, BigInt? pendingTargetChannelId, bool canJoin, bool canLeave, BridgeVoiceJoinSyncState joinSyncState, BridgeVoiceJoinErrorCode? joinErrorCode)? voiceState,TResult Function( bool began, bool shouldResume)? interruptionState,TResult Function( String permission, PermissionStateKind state)? permissionState,TResult Function( BigInt senderId, String senderName, String message, BridgeMessageTarget target, BridgePokeStrength? pokeStrength)? chatMessage,TResult Function( String message)? serverActivity,TResult Function( BridgeAudioRoute route)? audioRouteChanged,TResult Function( BigInt clientId, BigInt newChannelId)? clientMoved,TResult Function( BigInt clientId, BigInt channelId, String name, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted)? clientJoined,TResult Function( BigInt clientId, String name)? clientLeft,TResult Function( BigInt clientId, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted)? clientUpdated,TResult Function( BigInt id, BigInt parent, String name, PlatformInt64 order, bool hasPassword, int? neededTalkPower)? channelAdded,TResult Function( BigInt id)? channelRemoved,TResult Function( BigInt id, String name, bool hasPassword, int? neededTalkPower)? channelUpdated,required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( String serverName)? connected,TResult Function( String reason)? lost,TResult Function( int attempt, int delaySecs)? reconnecting,TResult Function( String reason)? disconnected,TResult Function()? audioStarted,TResult Function()? audioStopped,TResult Function( String level, String backendId, String boundInputClass)? pttCapability,TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs, BigInt? currentChannelId, BigInt? pendingTargetChannelId, bool canJoin, bool canLeave, BridgeVoiceJoinSyncState joinSyncState, BridgeVoiceJoinErrorCode? joinErrorCode)? voiceState,TResult Function( bool began, bool shouldResume)? interruptionState,TResult Function( String permission, PermissionStateKind state)? permissionState,TResult Function( BigInt senderId, String senderName, String message, BridgeMessageTarget target)? chatMessage,TResult Function( String message)? serverActivity,TResult Function( BridgeAudioRoute route)? audioRouteChanged,TResult Function( BigInt clientId, BigInt newChannelId)? clientMoved,TResult Function( BigInt clientId, BigInt channelId, String name, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted)? clientJoined,TResult Function( BigInt clientId, String name)? clientLeft,TResult Function( BigInt clientId, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted)? clientUpdated,TResult Function( BigInt id, BigInt parent, String name, PlatformInt64 order, bool hasPassword, int? neededTalkPower)? channelAdded,TResult Function( BigInt id)? channelRemoved,TResult Function( BigInt id, String name, bool hasPassword, int? neededTalkPower)? channelUpdated,required TResult orElse(),}) {final _that = this;
switch (_that) {
case BridgeEvent_Connected() when connected != null:
return connected(_that.serverName);case BridgeEvent_Lost() when lost != null:
@@ -186,7 +186,7 @@ return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case Bri
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs,_that.currentChannelId,_that.pendingTargetChannelId,_that.canJoin,_that.canLeave,_that.joinSyncState,_that.joinErrorCode);case BridgeEvent_InterruptionState() when interruptionState != null:
return interruptionState(_that.began,_that.shouldResume);case BridgeEvent_PermissionState() when permissionState != null:
return permissionState(_that.permission,_that.state);case BridgeEvent_ChatMessage() when chatMessage != null:
return chatMessage(_that.senderId,_that.senderName,_that.message,_that.target,_that.pokeStrength);case BridgeEvent_ServerActivity() when serverActivity != null:
return chatMessage(_that.senderId,_that.senderName,_that.message,_that.target);case BridgeEvent_ServerActivity() when serverActivity != null:
return serverActivity(_that.message);case BridgeEvent_AudioRouteChanged() when audioRouteChanged != null:
return audioRouteChanged(_that.route);case BridgeEvent_ClientMoved() when clientMoved != null:
return clientMoved(_that.clientId,_that.newChannelId);case BridgeEvent_ClientJoined() when clientJoined != null:
@@ -213,7 +213,7 @@ return channelUpdated(_that.id,_that.name,_that.hasPassword,_that.neededTalkPowe
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( String serverName) connected,required TResult Function( String reason) lost,required TResult Function( int attempt, int delaySecs) reconnecting,required TResult Function( String reason) disconnected,required TResult Function() audioStarted,required TResult Function() audioStopped,required TResult Function( String level, String backendId, String boundInputClass) pttCapability,required TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs, BigInt? currentChannelId, BigInt? pendingTargetChannelId, bool canJoin, bool canLeave, BridgeVoiceJoinSyncState joinSyncState, BridgeVoiceJoinErrorCode? joinErrorCode) voiceState,required TResult Function( bool began, bool shouldResume) interruptionState,required TResult Function( String permission, PermissionStateKind state) permissionState,required TResult Function( BigInt senderId, String senderName, String message, BridgeMessageTarget target, BridgePokeStrength? pokeStrength) chatMessage,required TResult Function( String message) serverActivity,required TResult Function( BridgeAudioRoute route) audioRouteChanged,required TResult Function( BigInt clientId, BigInt newChannelId) clientMoved,required TResult Function( BigInt clientId, BigInt channelId, String name, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted) clientJoined,required TResult Function( BigInt clientId, String name) clientLeft,required TResult Function( BigInt clientId, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted) clientUpdated,required TResult Function( BigInt id, BigInt parent, String name, PlatformInt64 order, bool hasPassword, int? neededTalkPower) channelAdded,required TResult Function( BigInt id) channelRemoved,required TResult Function( BigInt id, String name, bool hasPassword, int? neededTalkPower) channelUpdated,}) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( String serverName) connected,required TResult Function( String reason) lost,required TResult Function( int attempt, int delaySecs) reconnecting,required TResult Function( String reason) disconnected,required TResult Function() audioStarted,required TResult Function() audioStopped,required TResult Function( String level, String backendId, String boundInputClass) pttCapability,required TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs, BigInt? currentChannelId, BigInt? pendingTargetChannelId, bool canJoin, bool canLeave, BridgeVoiceJoinSyncState joinSyncState, BridgeVoiceJoinErrorCode? joinErrorCode) voiceState,required TResult Function( bool began, bool shouldResume) interruptionState,required TResult Function( String permission, PermissionStateKind state) permissionState,required TResult Function( BigInt senderId, String senderName, String message, BridgeMessageTarget target) chatMessage,required TResult Function( String message) serverActivity,required TResult Function( BridgeAudioRoute route) audioRouteChanged,required TResult Function( BigInt clientId, BigInt newChannelId) clientMoved,required TResult Function( BigInt clientId, BigInt channelId, String name, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted) clientJoined,required TResult Function( BigInt clientId, String name) clientLeft,required TResult Function( BigInt clientId, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted) clientUpdated,required TResult Function( BigInt id, BigInt parent, String name, PlatformInt64 order, bool hasPassword, int? neededTalkPower) channelAdded,required TResult Function( BigInt id) channelRemoved,required TResult Function( BigInt id, String name, bool hasPassword, int? neededTalkPower) channelUpdated,}) {final _that = this;
switch (_that) {
case BridgeEvent_Connected():
return connected(_that.serverName);case BridgeEvent_Lost():
@@ -226,7 +226,7 @@ return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case Bri
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs,_that.currentChannelId,_that.pendingTargetChannelId,_that.canJoin,_that.canLeave,_that.joinSyncState,_that.joinErrorCode);case BridgeEvent_InterruptionState():
return interruptionState(_that.began,_that.shouldResume);case BridgeEvent_PermissionState():
return permissionState(_that.permission,_that.state);case BridgeEvent_ChatMessage():
return chatMessage(_that.senderId,_that.senderName,_that.message,_that.target,_that.pokeStrength);case BridgeEvent_ServerActivity():
return chatMessage(_that.senderId,_that.senderName,_that.message,_that.target);case BridgeEvent_ServerActivity():
return serverActivity(_that.message);case BridgeEvent_AudioRouteChanged():
return audioRouteChanged(_that.route);case BridgeEvent_ClientMoved():
return clientMoved(_that.clientId,_that.newChannelId);case BridgeEvent_ClientJoined():
@@ -249,7 +249,7 @@ return channelUpdated(_that.id,_that.name,_that.hasPassword,_that.neededTalkPowe
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( String serverName)? connected,TResult? Function( String reason)? lost,TResult? Function( int attempt, int delaySecs)? reconnecting,TResult? Function( String reason)? disconnected,TResult? Function()? audioStarted,TResult? Function()? audioStopped,TResult? Function( String level, String backendId, String boundInputClass)? pttCapability,TResult? Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs, BigInt? currentChannelId, BigInt? pendingTargetChannelId, bool canJoin, bool canLeave, BridgeVoiceJoinSyncState joinSyncState, BridgeVoiceJoinErrorCode? joinErrorCode)? voiceState,TResult? Function( bool began, bool shouldResume)? interruptionState,TResult? Function( String permission, PermissionStateKind state)? permissionState,TResult? Function( BigInt senderId, String senderName, String message, BridgeMessageTarget target, BridgePokeStrength? pokeStrength)? chatMessage,TResult? Function( String message)? serverActivity,TResult? Function( BridgeAudioRoute route)? audioRouteChanged,TResult? Function( BigInt clientId, BigInt newChannelId)? clientMoved,TResult? Function( BigInt clientId, BigInt channelId, String name, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted)? clientJoined,TResult? Function( BigInt clientId, String name)? clientLeft,TResult? Function( BigInt clientId, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted)? clientUpdated,TResult? Function( BigInt id, BigInt parent, String name, PlatformInt64 order, bool hasPassword, int? neededTalkPower)? channelAdded,TResult? Function( BigInt id)? channelRemoved,TResult? Function( BigInt id, String name, bool hasPassword, int? neededTalkPower)? channelUpdated,}) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( String serverName)? connected,TResult? Function( String reason)? lost,TResult? Function( int attempt, int delaySecs)? reconnecting,TResult? Function( String reason)? disconnected,TResult? Function()? audioStarted,TResult? Function()? audioStopped,TResult? Function( String level, String backendId, String boundInputClass)? pttCapability,TResult? Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs, BigInt? currentChannelId, BigInt? pendingTargetChannelId, bool canJoin, bool canLeave, BridgeVoiceJoinSyncState joinSyncState, BridgeVoiceJoinErrorCode? joinErrorCode)? voiceState,TResult? Function( bool began, bool shouldResume)? interruptionState,TResult? Function( String permission, PermissionStateKind state)? permissionState,TResult? Function( BigInt senderId, String senderName, String message, BridgeMessageTarget target)? chatMessage,TResult? Function( String message)? serverActivity,TResult? Function( BridgeAudioRoute route)? audioRouteChanged,TResult? Function( BigInt clientId, BigInt newChannelId)? clientMoved,TResult? Function( BigInt clientId, BigInt channelId, String name, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted)? clientJoined,TResult? Function( BigInt clientId, String name)? clientLeft,TResult? Function( BigInt clientId, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted)? clientUpdated,TResult? Function( BigInt id, BigInt parent, String name, PlatformInt64 order, bool hasPassword, int? neededTalkPower)? channelAdded,TResult? Function( BigInt id)? channelRemoved,TResult? Function( BigInt id, String name, bool hasPassword, int? neededTalkPower)? channelUpdated,}) {final _that = this;
switch (_that) {
case BridgeEvent_Connected() when connected != null:
return connected(_that.serverName);case BridgeEvent_Lost() when lost != null:
@@ -262,7 +262,7 @@ return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case Bri
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs,_that.currentChannelId,_that.pendingTargetChannelId,_that.canJoin,_that.canLeave,_that.joinSyncState,_that.joinErrorCode);case BridgeEvent_InterruptionState() when interruptionState != null:
return interruptionState(_that.began,_that.shouldResume);case BridgeEvent_PermissionState() when permissionState != null:
return permissionState(_that.permission,_that.state);case BridgeEvent_ChatMessage() when chatMessage != null:
return chatMessage(_that.senderId,_that.senderName,_that.message,_that.target,_that.pokeStrength);case BridgeEvent_ServerActivity() when serverActivity != null:
return chatMessage(_that.senderId,_that.senderName,_that.message,_that.target);case BridgeEvent_ServerActivity() when serverActivity != null:
return serverActivity(_that.message);case BridgeEvent_AudioRouteChanged() when audioRouteChanged != null:
return audioRouteChanged(_that.route);case BridgeEvent_ClientMoved() when clientMoved != null:
return clientMoved(_that.clientId,_that.newChannelId);case BridgeEvent_ClientJoined() when clientJoined != null:
@@ -929,7 +929,7 @@ as PermissionStateKind,
class BridgeEvent_ChatMessage extends BridgeEvent {
const BridgeEvent_ChatMessage({required this.senderId, required this.senderName, required this.message, required this.target, this.pokeStrength}): super._();
const BridgeEvent_ChatMessage({required this.senderId, required this.senderName, required this.message, required this.target}): super._();
/// Client id of the sender.
@@ -940,8 +940,6 @@ class BridgeEvent_ChatMessage extends BridgeEvent {
final String message;
/// Target scope (server/channel/private/poke).
final BridgeMessageTarget target;
/// Poke notification strength, present only for poke messages.
final BridgePokeStrength? pokeStrength;
/// Create a copy of BridgeEvent
/// with the given fields replaced by the non-null parameter values.
@@ -953,16 +951,16 @@ $BridgeEvent_ChatMessageCopyWith<BridgeEvent_ChatMessage> get copyWith => _$Brid
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is BridgeEvent_ChatMessage&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.senderName, senderName) || other.senderName == senderName)&&(identical(other.message, message) || other.message == message)&&(identical(other.target, target) || other.target == target)&&(identical(other.pokeStrength, pokeStrength) || other.pokeStrength == pokeStrength));
return identical(this, other) || (other.runtimeType == runtimeType&&other is BridgeEvent_ChatMessage&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.senderName, senderName) || other.senderName == senderName)&&(identical(other.message, message) || other.message == message)&&(identical(other.target, target) || other.target == target));
}
@override
int get hashCode => Object.hash(runtimeType,senderId,senderName,message,target,pokeStrength);
int get hashCode => Object.hash(runtimeType,senderId,senderName,message,target);
@override
String toString() {
return 'BridgeEvent.chatMessage(senderId: $senderId, senderName: $senderName, message: $message, target: $target, pokeStrength: $pokeStrength)';
return 'BridgeEvent.chatMessage(senderId: $senderId, senderName: $senderName, message: $message, target: $target)';
}
@@ -973,7 +971,7 @@ abstract mixin class $BridgeEvent_ChatMessageCopyWith<$Res> implements $BridgeEv
factory $BridgeEvent_ChatMessageCopyWith(BridgeEvent_ChatMessage value, $Res Function(BridgeEvent_ChatMessage) _then) = _$BridgeEvent_ChatMessageCopyWithImpl;
@useResult
$Res call({
BigInt senderId, String senderName, String message, BridgeMessageTarget target, BridgePokeStrength? pokeStrength
BigInt senderId, String senderName, String message, BridgeMessageTarget target
});
@@ -990,14 +988,13 @@ class _$BridgeEvent_ChatMessageCopyWithImpl<$Res>
/// Create a copy of BridgeEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') $Res call({Object? senderId = null,Object? senderName = null,Object? message = null,Object? target = null,Object? pokeStrength = freezed,}) {
@pragma('vm:prefer-inline') $Res call({Object? senderId = null,Object? senderName = null,Object? message = null,Object? target = null,}) {
return _then(BridgeEvent_ChatMessage(
senderId: null == senderId ? _self.senderId : senderId // ignore: cast_nullable_to_non_nullable
as BigInt,senderName: null == senderName ? _self.senderName : senderName // ignore: cast_nullable_to_non_nullable
as String,message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String,target: null == target ? _self.target : target // ignore: cast_nullable_to_non_nullable
as BridgeMessageTarget,pokeStrength: freezed == pokeStrength ? _self.pokeStrength : pokeStrength // ignore: cast_nullable_to_non_nullable
as BridgePokeStrength?,
as BridgeMessageTarget,
));
}
@@ -1087,7 +1084,6 @@ class BridgeEvent_AudioRouteChanged extends BridgeEvent {
const BridgeEvent_AudioRouteChanged({required this.route}): super._();
/// New audio output route.
final BridgeAudioRoute route;
/// Create a copy of BridgeEvent
@@ -1154,9 +1150,7 @@ class BridgeEvent_ClientMoved extends BridgeEvent {
const BridgeEvent_ClientMoved({required this.clientId, required this.newChannelId}): super._();
/// Unique client identifier.
final BigInt clientId;
/// Destination channel.
final BigInt newChannelId;
/// Create a copy of BridgeEvent
@@ -1224,21 +1218,13 @@ class BridgeEvent_ClientJoined extends BridgeEvent {
const BridgeEvent_ClientJoined({required this.clientId, required this.channelId, required this.name, required this.inputMuted, required this.outputMuted, required this.isServerQuery, required this.talkPower, required this.talkPowerGranted}): super._();
/// Unique client identifier.
final BigInt clientId;
/// Channel the client joined.
final BigInt channelId;
/// Display nickname.
final String name;
/// Microphone muted state.
final bool inputMuted;
/// Speaker muted state.
final bool outputMuted;
/// True for server query (bot) clients.
final bool isServerQuery;
/// Client's talk power value.
final int talkPower;
/// Whether the server granted temporary talk power.
final bool talkPowerGranted;
/// Create a copy of BridgeEvent
@@ -1312,9 +1298,7 @@ class BridgeEvent_ClientLeft extends BridgeEvent {
const BridgeEvent_ClientLeft({required this.clientId, required this.name}): super._();
/// Unique client identifier.
final BigInt clientId;
/// Display nickname at time of disconnect.
final String name;
/// Create a copy of BridgeEvent
@@ -1382,17 +1366,11 @@ class BridgeEvent_ClientUpdated extends BridgeEvent {
const BridgeEvent_ClientUpdated({required this.clientId, required this.inputMuted, required this.outputMuted, required this.isServerQuery, required this.talkPower, required this.talkPowerGranted}): super._();
/// Unique client identifier.
final BigInt clientId;
/// Microphone muted state.
final bool inputMuted;
/// Speaker muted state.
final bool outputMuted;
/// True for server query (bot) clients.
final bool isServerQuery;
/// Client's talk power value.
final int talkPower;
/// Whether the server granted temporary talk power.
final bool talkPowerGranted;
/// Create a copy of BridgeEvent
@@ -1464,18 +1442,11 @@ class BridgeEvent_ChannelAdded extends BridgeEvent {
const BridgeEvent_ChannelAdded({required this.id, required this.parent, required this.name, required this.order, required this.hasPassword, this.neededTalkPower}): super._();
/// Unique channel identifier.
final BigInt id;
/// Parent channel ID.
final BigInt parent;
/// Channel name.
final String name;
/// Predecessor channel ID within the same parent (TeamSpeak
/// linked-list ordering hint). Zero means first child.
final PlatformInt64 order;
/// Whether the channel requires a password.
final bool hasPassword;
/// Talk power required to speak; `None` means no restriction.
final int? neededTalkPower;
/// Create a copy of BridgeEvent
@@ -1547,7 +1518,6 @@ class BridgeEvent_ChannelRemoved extends BridgeEvent {
const BridgeEvent_ChannelRemoved({required this.id}): super._();
/// Channel identifier.
final BigInt id;
/// Create a copy of BridgeEvent
@@ -1614,13 +1584,9 @@ class BridgeEvent_ChannelUpdated extends BridgeEvent {
const BridgeEvent_ChannelUpdated({required this.id, required this.name, required this.hasPassword, this.neededTalkPower}): super._();
/// Unique channel identifier.
final BigInt id;
/// Channel name.
final String name;
/// Whether the channel requires a password.
final bool hasPassword;
/// Talk power required to speak; `None` means no restriction.
final int? neededTalkPower;
/// Create a copy of BridgeEvent
@@ -1683,12 +1683,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return dco_decode_bridge_message_target(raw);
}
@protected
BridgePokeStrength dco_decode_box_autoadd_bridge_poke_strength(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return dco_decode_bridge_poke_strength(raw);
}
@protected
BridgeVoiceJoinErrorCode dco_decode_box_autoadd_bridge_voice_join_error_code(
dynamic raw,
@@ -2013,7 +2007,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
senderName: dco_decode_String(raw[2]),
message: dco_decode_String(raw[3]),
target: dco_decode_box_autoadd_bridge_message_target(raw[4]),
pokeStrength: dco_decode_opt_box_autoadd_bridge_poke_strength(raw[5]),
);
case 11:
return BridgeEvent_ServerActivity(message: dco_decode_String(raw[1]));
@@ -2105,12 +2098,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return BridgeNetworkState.values[raw as int];
}
@protected
BridgePokeStrength dco_decode_bridge_poke_strength(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return BridgePokeStrength.values[raw as int];
}
@protected
BridgePttBinding dco_decode_bridge_ptt_binding(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -2247,16 +2234,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return raw == null ? null : dco_decode_String(raw);
}
@protected
BridgePokeStrength? dco_decode_opt_box_autoadd_bridge_poke_strength(
dynamic raw,
) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return raw == null
? null
: dco_decode_box_autoadd_bridge_poke_strength(raw);
}
@protected
BridgeVoiceJoinErrorCode?
dco_decode_opt_box_autoadd_bridge_voice_join_error_code(dynamic raw) {
@@ -2381,14 +2358,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return (sse_decode_bridge_message_target(deserializer));
}
@protected
BridgePokeStrength sse_decode_box_autoadd_bridge_poke_strength(
SseDeserializer deserializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
return (sse_decode_bridge_poke_strength(deserializer));
}
@protected
BridgeVoiceJoinErrorCode sse_decode_box_autoadd_bridge_voice_join_error_code(
SseDeserializer deserializer,
@@ -2840,15 +2809,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
var var_target = sse_decode_box_autoadd_bridge_message_target(
deserializer,
);
var var_pokeStrength = sse_decode_opt_box_autoadd_bridge_poke_strength(
deserializer,
);
return BridgeEvent_ChatMessage(
senderId: var_senderId,
senderName: var_senderName,
message: var_message,
target: var_target,
pokeStrength: var_pokeStrength,
);
case 11:
var var_message = sse_decode_String(deserializer);
@@ -2976,15 +2941,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return BridgeNetworkState.values[inner];
}
@protected
BridgePokeStrength sse_decode_bridge_poke_strength(
SseDeserializer deserializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
var inner = sse_decode_i_32(deserializer);
return BridgePokeStrength.values[inner];
}
@protected
BridgePttBinding sse_decode_bridge_ptt_binding(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -3176,19 +3132,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
}
}
@protected
BridgePokeStrength? sse_decode_opt_box_autoadd_bridge_poke_strength(
SseDeserializer deserializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
if (sse_decode_bool(deserializer)) {
return (sse_decode_box_autoadd_bridge_poke_strength(deserializer));
} else {
return null;
}
}
@protected
BridgeVoiceJoinErrorCode?
sse_decode_opt_box_autoadd_bridge_voice_join_error_code(
@@ -3363,15 +3306,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_bridge_message_target(self, serializer);
}
@protected
void sse_encode_box_autoadd_bridge_poke_strength(
BridgePokeStrength self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_bridge_poke_strength(self, serializer);
}
@protected
void sse_encode_box_autoadd_bridge_voice_join_error_code(
BridgeVoiceJoinErrorCode self,
@@ -3710,17 +3644,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
senderName: final senderName,
message: final message,
target: final target,
pokeStrength: final pokeStrength,
):
sse_encode_i_32(10, serializer);
sse_encode_u_64(senderId, serializer);
sse_encode_String(senderName, serializer);
sse_encode_String(message, serializer);
sse_encode_box_autoadd_bridge_message_target(target, serializer);
sse_encode_opt_box_autoadd_bridge_poke_strength(
pokeStrength,
serializer,
);
case BridgeEvent_ServerActivity(message: final message):
sse_encode_i_32(11, serializer);
sse_encode_String(message, serializer);
@@ -3842,15 +3771,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_i_32(self.index, serializer);
}
@protected
void sse_encode_bridge_poke_strength(
BridgePokeStrength self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_i_32(self.index, serializer);
}
@protected
void sse_encode_bridge_ptt_binding(
BridgePttBinding self,
@@ -4027,19 +3947,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
}
}
@protected
void sse_encode_opt_box_autoadd_bridge_poke_strength(
BridgePokeStrength? self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_bool(self != null, serializer);
if (self != null) {
sse_encode_box_autoadd_bridge_poke_strength(self, serializer);
}
}
@protected
void sse_encode_opt_box_autoadd_bridge_voice_join_error_code(
BridgeVoiceJoinErrorCode? self,
@@ -46,9 +46,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BridgeMessageTarget dco_decode_box_autoadd_bridge_message_target(dynamic raw);
@protected
BridgePokeStrength dco_decode_box_autoadd_bridge_poke_strength(dynamic raw);
@protected
BridgeVoiceJoinErrorCode dco_decode_box_autoadd_bridge_voice_join_error_code(
dynamic raw,
@@ -123,9 +120,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BridgeNetworkState dco_decode_bridge_network_state(dynamic raw);
@protected
BridgePokeStrength dco_decode_bridge_poke_strength(dynamic raw);
@protected
BridgePttBinding dco_decode_bridge_ptt_binding(dynamic raw);
@@ -180,11 +174,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
String? dco_decode_opt_String(dynamic raw);
@protected
BridgePokeStrength? dco_decode_opt_box_autoadd_bridge_poke_strength(
dynamic raw,
);
@protected
BridgeVoiceJoinErrorCode?
dco_decode_opt_box_autoadd_bridge_voice_join_error_code(dynamic raw);
@@ -251,11 +240,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
BridgePokeStrength sse_decode_box_autoadd_bridge_poke_strength(
SseDeserializer deserializer,
);
@protected
BridgeVoiceJoinErrorCode sse_decode_box_autoadd_bridge_voice_join_error_code(
SseDeserializer deserializer,
@@ -344,11 +328,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
BridgePokeStrength sse_decode_bridge_poke_strength(
SseDeserializer deserializer,
);
@protected
BridgePttBinding sse_decode_bridge_ptt_binding(SseDeserializer deserializer);
@@ -421,11 +400,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
String? sse_decode_opt_String(SseDeserializer deserializer);
@protected
BridgePokeStrength? sse_decode_opt_box_autoadd_bridge_poke_strength(
SseDeserializer deserializer,
);
@protected
BridgeVoiceJoinErrorCode?
sse_decode_opt_box_autoadd_bridge_voice_join_error_code(
@@ -503,12 +477,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_bridge_poke_strength(
BridgePokeStrength self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_bridge_voice_join_error_code(
BridgeVoiceJoinErrorCode self,
@@ -620,12 +588,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_bridge_poke_strength(
BridgePokeStrength self,
SseSerializer serializer,
);
@protected
void sse_encode_bridge_ptt_binding(
BridgePttBinding self,
@@ -719,12 +681,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_opt_String(String? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_bridge_poke_strength(
BridgePokeStrength? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_box_autoadd_bridge_voice_join_error_code(
BridgeVoiceJoinErrorCode? self,
@@ -48,9 +48,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BridgeMessageTarget dco_decode_box_autoadd_bridge_message_target(dynamic raw);
@protected
BridgePokeStrength dco_decode_box_autoadd_bridge_poke_strength(dynamic raw);
@protected
BridgeVoiceJoinErrorCode dco_decode_box_autoadd_bridge_voice_join_error_code(
dynamic raw,
@@ -125,9 +122,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BridgeNetworkState dco_decode_bridge_network_state(dynamic raw);
@protected
BridgePokeStrength dco_decode_bridge_poke_strength(dynamic raw);
@protected
BridgePttBinding dco_decode_bridge_ptt_binding(dynamic raw);
@@ -182,11 +176,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
String? dco_decode_opt_String(dynamic raw);
@protected
BridgePokeStrength? dco_decode_opt_box_autoadd_bridge_poke_strength(
dynamic raw,
);
@protected
BridgeVoiceJoinErrorCode?
dco_decode_opt_box_autoadd_bridge_voice_join_error_code(dynamic raw);
@@ -253,11 +242,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
BridgePokeStrength sse_decode_box_autoadd_bridge_poke_strength(
SseDeserializer deserializer,
);
@protected
BridgeVoiceJoinErrorCode sse_decode_box_autoadd_bridge_voice_join_error_code(
SseDeserializer deserializer,
@@ -346,11 +330,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
BridgePokeStrength sse_decode_bridge_poke_strength(
SseDeserializer deserializer,
);
@protected
BridgePttBinding sse_decode_bridge_ptt_binding(SseDeserializer deserializer);
@@ -423,11 +402,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
String? sse_decode_opt_String(SseDeserializer deserializer);
@protected
BridgePokeStrength? sse_decode_opt_box_autoadd_bridge_poke_strength(
SseDeserializer deserializer,
);
@protected
BridgeVoiceJoinErrorCode?
sse_decode_opt_box_autoadd_bridge_voice_join_error_code(
@@ -505,12 +479,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_bridge_poke_strength(
BridgePokeStrength self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_bridge_voice_join_error_code(
BridgeVoiceJoinErrorCode self,
@@ -622,12 +590,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_bridge_poke_strength(
BridgePokeStrength self,
SseSerializer serializer,
);
@protected
void sse_encode_bridge_ptt_binding(
BridgePttBinding self,
@@ -721,12 +683,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_opt_String(String? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_bridge_poke_strength(
BridgePokeStrength? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_box_autoadd_bridge_voice_join_error_code(
BridgeVoiceJoinErrorCode? self,
@@ -1,89 +0,0 @@
import 'package:flutter/material.dart';
import '../l10n/generated/app_localizations.dart';
import '../services/poke_preferences_service.dart';
import 'voice_settings_controls.dart';
class PokeNotificationSettingsDialog extends StatelessWidget {
const PokeNotificationSettingsDialog({super.key, required this.preferences});
final PokePreferencesService preferences;
@override
Widget build(BuildContext context) {
final l10n = AppL10n.of(context);
final theme = Theme.of(context);
return AlertDialog(
title: Text(l10n.pokeSettingsTitle),
contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
content: SizedBox(
width: 400,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ValueListenableBuilder<bool>(
valueListenable: preferences.pokesEnabled,
builder: (context, enabled, _) => SwitchListTile(
dense: true,
contentPadding: EdgeInsets.zero,
title: Text(l10n.pokeSettingsEnableLabel),
subtitle: Text(l10n.pokeSettingsEnableDescription),
value: enabled,
onChanged: (value) => preferences.setPokesEnabled(value),
),
),
const Divider(height: 24),
VoiceSectionHeader(l10n.pokeSettingsMutedSendersHeader),
ValueListenableBuilder<Set<BigInt>>(
valueListenable: preferences.mutedSenders,
builder: (context, mutedSenders, _) {
final senders = mutedSenders.toList()..sort();
if (senders.isEmpty) {
return Padding(
padding: const EdgeInsets.only(top: 8, bottom: 8),
child: Text(
l10n.pokeSettingsMutedSendersEmpty,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
);
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final senderId in senders)
ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.notifications_off_outlined),
title: Text(
l10n.pokeSettingsMutedSenderLabel(
senderId.toString(),
),
),
trailing: TextButton(
onPressed: () => preferences.unmuteSender(senderId),
child: Text(l10n.pokeSettingsUnmuteSenderAction),
),
),
],
);
},
),
const SizedBox(height: 8),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(l10n.closeAction),
),
],
);
}
}
@@ -42,8 +42,6 @@
<string>Chanora uses Input Monitoring so push-to-talk keys work even when other apps are focused. Chanora never records what you type — only the key you bound for talking.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Chanora needs local network access to connect to TeamSpeak-compatible voice servers.</string>
<key>NSUserNotificationsUsageDescription</key>
<string>Chanora sends you a notification when another user pokes you.</string>
<key>NSBonjourServices</key>
<array>
<string>_ts3._tcp</string>
-48
View File
@@ -262,46 +262,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: be38e3854d2baabcda8e16966a5fe8748cebb655bb94701494da0f052c2fc352
url: "https://pub.dev"
source: hosted
version: "22.0.0"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
sha256: "9ca97e63776f29ab1b955725c09999fc2c150523269db150c39274f2a43c5a8b"
url: "https://pub.dev"
source: hosted
version: "8.0.1"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: ff0013eae795e8dc8fad4a8992a209e64d3ba2fbd8bf5e43c36bf448f95bd814
url: "https://pub.dev"
source: hosted
version: "12.0.0"
flutter_local_notifications_web:
dependency: transitive
description:
name: flutter_local_notifications_web
sha256: "516afaf97a2d1e67a036c6617321b00d205d72f7a67b6eccf936cd565f985878"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
flutter_local_notifications_windows:
dependency: transitive
description:
name: flutter_local_notifications_windows
sha256: "5aeed973a0c1480706784fad05c5c3a911335ebb561b2274b47fe80b375201e1"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
flutter_localizations:
dependency: "direct main"
description: flutter
@@ -834,14 +794,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.11"
timezone:
dependency: transitive
description:
name: timezone
sha256: "784a5e34d2eb62e1326f24d6f600aaaee452eb8ca8ef2f384a59244e292d158b"
url: "https://pub.dev"
source: hosted
version: "0.11.0"
typed_data:
dependency: transitive
description:
-1
View File
@@ -75,7 +75,6 @@ dependencies:
# DEC-003 iOS 13 floor; haptic_kit supports iOS 12+).
haptic_kit: ^1.0.0
flutter_foreground_task: ^9.2.2
flutter_local_notifications: ^22.0.0
url_launcher: ^6.3.2
shared_preferences: ^2.5.5
share_plus: ^13.1.0
@@ -1,48 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:chanora_flutter/main.dart';
import 'package:chanora_flutter/src/rust/api.dart' as rust;
void main() {
test('active poke chat suppresses same sender notification only', () {
final sender = BigInt.from(42);
expect(
isPokeSenderActiveChat(
chatOpen: true,
inlineChatTarget: rust.BridgeMessageTarget.poke(sender),
senderId: sender,
),
isTrue,
);
expect(
isPokeSenderActiveChat(
chatOpen: true,
inlineChatTarget: rust.BridgeMessageTarget.poke(BigInt.from(7)),
senderId: sender,
),
isFalse,
);
});
test('active private chat also suppresses same sender poke notification', () {
final sender = BigInt.from(42);
expect(
isPokeSenderActiveChat(
chatOpen: true,
inlineChatTarget: rust.BridgeMessageTarget.client(sender),
senderId: sender,
),
isTrue,
);
expect(
isPokeSenderActiveChat(
chatOpen: false,
inlineChatTarget: rust.BridgeMessageTarget.client(sender),
senderId: sender,
),
isFalse,
);
});
}
@@ -1,63 +0,0 @@
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');
},
);
}
@@ -1,58 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:chanora_flutter/services/poke_preferences_service.dart';
void main() {
late PokePreferencesService service;
setUp(() {
SharedPreferences.setMockInitialValues({});
service = PokePreferencesService();
});
test('loads defaults when preferences are unset', () async {
await service.load();
expect(service.pokesEnabled.value, isTrue);
expect(service.mutedSenders.value, isEmpty);
});
test('persists global enabled state', () async {
await service.load();
await service.setPokesEnabled(false);
final reloaded = PokePreferencesService();
await reloaded.load();
expect(reloaded.pokesEnabled.value, isFalse);
});
test('persists muted senders and removes them on unmute', () async {
await service.load();
final alice = BigInt.from(42);
final bob = BigInt.from(7);
await service.muteSender(alice);
await service.muteSender(bob);
await service.unmuteSender(alice);
final reloaded = PokePreferencesService();
await reloaded.load();
expect(reloaded.mutedSenders.value, {bob});
});
test('isMuted reflects in-memory changes synchronously', () async {
await service.load();
final sender = BigInt.from(99);
expect(service.isMuted(sender), isFalse);
await service.muteSender(sender);
expect(service.isMuted(sender), isTrue);
await service.unmuteSender(sender);
expect(service.isMuted(sender), isFalse);
});
}
@@ -1,37 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:chanora_flutter/l10n/generated/app_localizations.dart';
import 'package:chanora_flutter/services/poke_preferences_service.dart';
import 'package:chanora_flutter/widgets/poke_notification_settings.dart';
void main() {
testWidgets('toggles poke notifications and unmutes senders', (tester) async {
SharedPreferences.setMockInitialValues({});
final preferences = PokePreferencesService();
await preferences.load();
await preferences.muteSender(BigInt.from(42));
addTearDown(preferences.dispose);
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: PokeNotificationSettingsDialog(preferences: preferences),
),
);
expect(find.text('Poke notifications'), findsOneWidget);
expect(find.text('Client ID 42'), findsOneWidget);
await tester.tap(find.byType(Switch));
await tester.pumpAndSettle();
expect(preferences.pokesEnabled.value, isFalse);
await tester.tap(find.text('Unmute'));
await tester.pumpAndSettle();
expect(preferences.isMuted(BigInt.from(42)), isFalse);
expect(find.text('No muted poke senders.'), findsOneWidget);
});
}
@@ -9,7 +9,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
flutter_local_notifications_windows
jni
)
+1 -3
View File
@@ -1,5 +1,5 @@
use chanora_audio::{AudioRoute, PttBackendDescriptor};
use chanora_protocol::{MessageTarget, PokeStrength};
use chanora_protocol::MessageTarget;
/// Privacy-safe snapshot of the active PTT capability.
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -139,8 +139,6 @@ pub enum SessionEvent {
message: String,
/// Target scope (server/channel/private/poke).
target: MessageTarget,
/// Poke notification strength, present only for poke messages.
poke_strength: Option<PokeStrength>,
},
/// Human-readable TeamSpeak-style server activity.
ServerActivity {
+1 -2
View File
@@ -68,7 +68,7 @@ pub use chanora_diagnostics::{
};
pub use chanora_protocol::{
ChannelInfo, ChatMessage, ClientInfo, ClientProfile, ConnectConfig, DisconnectReason,
MessageTarget, PokeStrength, ProtocolError, ServerActivity, ServerSnapshot,
MessageTarget, ProtocolError, ServerActivity, ServerSnapshot,
};
pub use chanora_storage::{Bookmark, BookmarkRepository, IdentityFileStore};
pub use events::{
@@ -1674,7 +1674,6 @@ fn spawn_event_forwarders(
sender_name: msg.sender_name,
message: msg.message,
target: msg.target,
poke_strength: msg.poke_strength,
});
}
});
-25
View File
@@ -1695,8 +1695,6 @@ pub enum BridgeEvent {
message: String,
/// Target scope (server/channel/private/poke).
target: BridgeMessageTarget,
/// Poke notification strength, present only for poke messages.
poke_strength: Option<BridgePokeStrength>,
},
/// Human-readable server activity surfaced from protocol bookkeeping events.
ServerActivity {
@@ -1814,27 +1812,6 @@ impl From<chanora_core::MessageTarget> for BridgeMessageTarget {
}
}
/// Bridge poke notification strength.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BridgePokeStrength {
/// Poke should be surfaced at full strength.
Strong,
/// Poke is rate-limited but below overflow severity.
Suppressed,
/// Poke remains suppressed after repeated suppressed pokes.
SuppressedOverflow,
}
impl From<chanora_core::PokeStrength> for BridgePokeStrength {
fn from(strength: chanora_core::PokeStrength) -> Self {
match strength {
chanora_core::PokeStrength::Strong => Self::Strong,
chanora_core::PokeStrength::Suppressed => Self::Suppressed,
chanora_core::PokeStrength::SuppressedOverflow => Self::SuppressedOverflow,
}
}
}
/// Bridge mirror of core join projection sync state.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BridgeVoiceJoinSyncState {
@@ -1981,13 +1958,11 @@ impl From<chanora_core::SessionEvent> for BridgeEvent {
sender_name,
message,
target,
poke_strength,
} => BridgeEvent::ChatMessage {
sender_id,
sender_name,
message,
target: target.into(),
poke_strength: poke_strength.map(Into::into),
},
chanora_core::SessionEvent::ServerActivity { message } => {
BridgeEvent::ServerActivity { message }
@@ -2292,14 +2292,11 @@ impl SseDecode for crate::api::BridgeEvent {
let mut var_senderName = <String>::sse_decode(deserializer);
let mut var_message = <String>::sse_decode(deserializer);
let mut var_target = <crate::api::BridgeMessageTarget>::sse_decode(deserializer);
let mut var_pokeStrength =
<Option<crate::api::BridgePokeStrength>>::sse_decode(deserializer);
return crate::api::BridgeEvent::ChatMessage {
sender_id: var_senderId,
sender_name: var_senderName,
message: var_message,
target: var_target,
poke_strength: var_pokeStrength,
};
}
11 => {
@@ -2457,19 +2454,6 @@ impl SseDecode for crate::api::BridgeNetworkState {
}
}
impl SseDecode for crate::api::BridgePokeStrength {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut inner = <i32>::sse_decode(deserializer);
return match inner {
0 => crate::api::BridgePokeStrength::Strong,
1 => crate::api::BridgePokeStrength::Suppressed,
2 => crate::api::BridgePokeStrength::SuppressedOverflow,
_ => unreachable!("Invalid variant for BridgePokeStrength: {}", inner),
};
}
}
impl SseDecode for crate::api::BridgePttBinding {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@@ -2696,17 +2680,6 @@ impl SseDecode for Option<String> {
}
}
impl SseDecode for Option<crate::api::BridgePokeStrength> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
if (<bool>::sse_decode(deserializer)) {
return Some(<crate::api::BridgePokeStrength>::sse_decode(deserializer));
} else {
return None;
}
}
}
impl SseDecode for Option<crate::api::BridgeVoiceJoinErrorCode> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@@ -3326,14 +3299,12 @@ impl flutter_rust_bridge::IntoDart for crate::api::BridgeEvent {
sender_name,
message,
target,
poke_strength,
} => [
10.into_dart(),
sender_id.into_into_dart().into_dart(),
sender_name.into_into_dart().into_dart(),
message.into_into_dart().into_dart(),
target.into_into_dart().into_dart(),
poke_strength.into_into_dart().into_dart(),
]
.into_dart(),
crate::api::BridgeEvent::ServerActivity { message } => {
@@ -3513,28 +3484,6 @@ impl flutter_rust_bridge::IntoIntoDart<crate::api::BridgeNetworkState>
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::api::BridgePokeStrength {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
match self {
Self::Strong => 0.into_dart(),
Self::Suppressed => 1.into_dart(),
Self::SuppressedOverflow => 2.into_dart(),
_ => unreachable!(),
}
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
for crate::api::BridgePokeStrength
{
}
impl flutter_rust_bridge::IntoIntoDart<crate::api::BridgePokeStrength>
for crate::api::BridgePokeStrength
{
fn into_into_dart(self) -> crate::api::BridgePokeStrength {
self
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::api::BridgePttBinding {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[
@@ -4104,14 +4053,12 @@ impl SseEncode for crate::api::BridgeEvent {
sender_name,
message,
target,
poke_strength,
} => {
<i32>::sse_encode(10, serializer);
<u64>::sse_encode(sender_id, serializer);
<String>::sse_encode(sender_name, serializer);
<String>::sse_encode(message, serializer);
<crate::api::BridgeMessageTarget>::sse_encode(target, serializer);
<Option<crate::api::BridgePokeStrength>>::sse_encode(poke_strength, serializer);
}
crate::api::BridgeEvent::ServerActivity { message } => {
<i32>::sse_encode(11, serializer);
@@ -4267,23 +4214,6 @@ impl SseEncode for crate::api::BridgeNetworkState {
}
}
impl SseEncode for crate::api::BridgePokeStrength {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<i32>::sse_encode(
match self {
crate::api::BridgePokeStrength::Strong => 0,
crate::api::BridgePokeStrength::Suppressed => 1,
crate::api::BridgePokeStrength::SuppressedOverflow => 2,
_ => {
unimplemented!("");
}
},
serializer,
);
}
}
impl SseEncode for crate::api::BridgePttBinding {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@@ -4499,16 +4429,6 @@ impl SseEncode for Option<String> {
}
}
impl SseEncode for Option<crate::api::BridgePokeStrength> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<bool>::sse_encode(self.is_some(), serializer);
if let Some(value) = self {
<crate::api::BridgePokeStrength>::sse_encode(value, serializer);
}
}
}
impl SseEncode for Option<crate::api::BridgeVoiceJoinErrorCode> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
+8 -35
View File
@@ -41,7 +41,6 @@ use crate::dto::{
ChannelId, ChannelInfo, ChatMessage, ClientId, ClientInfo, ClientProfile, MessageTarget,
ProtocolDelta, ServerActivity, ServerSnapshot,
};
use crate::poke_limiter::PokeLimiter;
use crate::ProtocolError;
const SPEAKING_ACTIVITY_WINDOW: Duration = Duration::from_millis(750);
@@ -714,7 +713,6 @@ async fn connection_task(
// reply channel — at most 3 s of pending state per move.
let mut pending_moves: PendingMoves = HashMap::new();
let mut voice_activity: HashMap<u64, Instant> = HashMap::new();
let mut poke_limiter = PokeLimiter::new();
// Main loop: pump events, service requests, forward voice.
loop {
@@ -744,7 +742,6 @@ async fn connection_task(
&channels.activity,
&channels.delta,
&mut pending_moves,
&mut poke_limiter,
),
},
Ok(Some(Err(e))) => {
@@ -844,7 +841,6 @@ async fn connection_task(
&channels,
&mut pending_moves,
&mut voice_activity,
&mut poke_limiter,
)
.await;
let _ = reply.send(r);
@@ -907,7 +903,6 @@ fn handle_non_audio_stream_item(
activity_tx: &mpsc::Sender<ServerActivity>,
delta_tx: &mpsc::Sender<ProtocolDelta>,
pending_moves: &mut PendingMoves,
poke_limiter: &mut PokeLimiter,
) {
match item {
StreamItem::BookEvents(events) => {
@@ -969,25 +964,19 @@ fn handle_non_audio_stream_item(
message,
} = ev
{
let (mapped, poke_strength) = match target {
tsclientlib::MessageTarget::Server => (MessageTarget::Server, None),
tsclientlib::MessageTarget::Channel => (MessageTarget::Channel, None),
let mapped = match target {
tsclientlib::MessageTarget::Server => MessageTarget::Server,
tsclientlib::MessageTarget::Channel => MessageTarget::Channel,
tsclientlib::MessageTarget::Client(id) => {
(MessageTarget::Client(id.0 as u64), None)
}
tsclientlib::MessageTarget::Poke(id) => {
let own_client_id =
con.get_state().ok().map(|state| state.own_client.0 as u64);
let strength = poke_limiter.record(invoker.id.0 as u64, own_client_id);
(MessageTarget::Poke(id.0 as u64), Some(strength))
MessageTarget::Client(id.0 as u64)
}
tsclientlib::MessageTarget::Poke(id) => MessageTarget::Poke(id.0 as u64),
};
let _ = chat_tx.try_send(ChatMessage {
sender_id: ClientId(invoker.id.0 as u64),
sender_name: sanitize(&invoker.name),
message: sanitize(&message),
target: mapped,
poke_strength,
});
}
}
@@ -1175,7 +1164,6 @@ async fn fetch_client_profile(
channels: &EventChannels,
pending_moves: &mut PendingMoves,
voice_activity: &mut HashMap<u64, Instant>,
poke_limiter: &mut PokeLimiter,
) -> Result<ClientProfile, ProtocolError> {
let target_id = TsClientId(client_id as u16);
@@ -1221,7 +1209,6 @@ async fn fetch_client_profile(
channels,
pending_moves,
voice_activity,
poke_limiter,
)
.await;
}
@@ -1232,7 +1219,6 @@ async fn fetch_client_profile(
channels,
pending_moves,
voice_activity,
poke_limiter,
)
.await;
}
@@ -1247,7 +1233,6 @@ async fn fetch_client_profile(
channels,
pending_moves,
voice_activity,
poke_limiter,
)
.await
{
@@ -1266,7 +1251,6 @@ async fn fetch_client_profile(
channels,
pending_moves,
voice_activity,
poke_limiter,
)
.await
{
@@ -1280,16 +1264,9 @@ async fn fetch_client_profile(
}
let db_info = if refresh_plan.needs_client_db_info {
request_client_db_info(
con,
database_id,
channels,
pending_moves,
voice_activity,
poke_limiter,
)
.await
.ok()
request_client_db_info(con, database_id, channels, pending_moves, voice_activity)
.await
.ok()
} else {
None
};
@@ -1449,7 +1426,6 @@ async fn request_messages(
channels: &EventChannels,
pending_moves: &mut PendingMoves,
voice_activity: &mut HashMap<u64, Instant>,
poke_limiter: &mut PokeLimiter,
) -> Result<Vec<InMessage>, ProtocolError> {
let handle = command
.send_with_result(con)
@@ -1492,7 +1468,6 @@ async fn request_messages(
&channels.activity,
&channels.delta,
pending_moves,
poke_limiter,
),
}
}
@@ -1504,7 +1479,6 @@ async fn request_client_db_info(
channels: &EventChannels,
pending_moves: &mut PendingMoves,
voice_activity: &mut HashMap<u64, Instant>,
poke_limiter: &mut PokeLimiter,
) -> Result<InClientDbInfoPart, ProtocolError> {
let messages = request_messages(
con,
@@ -1512,7 +1486,6 @@ async fn request_client_db_info(
channels,
pending_moves,
voice_activity,
poke_limiter,
)
.await?;
for message in messages {
-4
View File
@@ -3,8 +3,6 @@
use serde::{Deserialize, Serialize};
pub use crate::poke_limiter::PokeStrength;
/// Opaque server-side channel identifier. Internal representation is
/// the upstream u64 but callers must treat it as opaque.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -56,8 +54,6 @@ pub struct ChatMessage {
pub message: String,
/// Target scope of this message.
pub target: MessageTarget,
/// Strength classification for poke notifications.
pub poke_strength: Option<PokeStrength>,
}
/// A server-activity notification derived from TeamSpeak bookkeeping events.
+1 -3
View File
@@ -35,14 +35,12 @@
mod adapter;
mod dto;
pub mod poke_limiter;
pub use adapter::{ConnectConfig, DisconnectReason, InboundVoice, ProtocolClient, SnapshotProbe};
pub use dto::{
ChannelId, ChannelInfo, ChatMessage, ClientId, ClientInfo, ClientProfile, MessageTarget,
PokeStrength, ProtocolDelta, ServerActivity, ServerSnapshot,
ProtocolDelta, ServerActivity, ServerSnapshot,
};
pub use poke_limiter::PokeLimiter;
// Re-export the upstream voice types so chanora_audio can build outbound
// voice packets without taking a direct dependency on tsclientlib /
-174
View File
@@ -1,174 +0,0 @@
//! Per-connection poke strength classification.
use std::collections::HashMap;
use std::time::{Duration, Instant};
/// Notification strength assigned to an inbound poke.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum PokeStrength {
/// Poke should be surfaced at full strength.
Strong,
/// Poke is rate-limited but below overflow severity.
Suppressed,
/// Poke remains suppressed after repeated suppressed pokes.
SuppressedOverflow,
}
/// Per-connection poke limiter.
#[derive(Debug)]
pub struct PokeLimiter {
window: Duration,
entries: HashMap<u64, PokeEntry>,
}
#[derive(Debug)]
struct PokeEntry {
tokens: u8,
last_refill: Instant,
suppressed_in_window: u8,
}
impl PokeLimiter {
const CAPACITY: u8 = 2;
const OVERFLOW_THRESHOLD: u8 = 3;
/// Create a limiter using the default five-minute refill interval.
pub fn new() -> Self {
Self {
window: Duration::from_secs(5 * 60),
entries: HashMap::new(),
}
}
/// Record a poke at the current instant.
pub fn record(&mut self, sender_id: u64, own_client_id: Option<u64>) -> PokeStrength {
self.record_at(sender_id, own_client_id, Instant::now())
}
/// Record a poke at an injected instant.
pub fn record_at(
&mut self,
sender_id: u64,
own_client_id: Option<u64>,
now: Instant,
) -> PokeStrength {
if own_client_id == Some(sender_id) {
return PokeStrength::Suppressed;
}
let entry = self.entries.entry(sender_id).or_insert(PokeEntry {
tokens: Self::CAPACITY,
last_refill: now,
suppressed_in_window: 0,
});
if now.duration_since(entry.last_refill) >= self.window {
entry.tokens = Self::CAPACITY;
entry.last_refill = now;
entry.suppressed_in_window = 0;
}
if entry.tokens > 0 {
entry.tokens -= 1;
return PokeStrength::Strong;
}
entry.suppressed_in_window = entry.suppressed_in_window.saturating_add(1);
if entry.suppressed_in_window >= Self::OVERFLOW_THRESHOLD {
PokeStrength::SuppressedOverflow
} else {
PokeStrength::Suppressed
}
}
}
impl Default for PokeLimiter {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_two_pokes_are_strong_third_is_suppressed() {
let mut limiter = PokeLimiter::new();
let now = Instant::now();
assert_eq!(limiter.record_at(7, Some(1), now), PokeStrength::Strong);
assert_eq!(
limiter.record_at(7, Some(1), now + Duration::from_secs(1)),
PokeStrength::Strong
);
assert_eq!(
limiter.record_at(7, Some(1), now + Duration::from_secs(2)),
PokeStrength::Suppressed
);
}
#[test]
fn self_poke_is_suppressed_without_consuming_token() {
let mut limiter = PokeLimiter::new();
let now = Instant::now();
assert_eq!(limiter.record_at(7, Some(7), now), PokeStrength::Suppressed);
assert_eq!(
limiter.record_at(7, Some(1), now + Duration::from_secs(1)),
PokeStrength::Strong
);
assert_eq!(
limiter.record_at(7, Some(1), now + Duration::from_secs(2)),
PokeStrength::Strong
);
}
#[test]
fn overflow_after_three_suppressed_pokes_in_five_minutes() {
let mut limiter = PokeLimiter::new();
let now = Instant::now();
assert_eq!(limiter.record_at(7, Some(1), now), PokeStrength::Strong);
assert_eq!(
limiter.record_at(7, Some(1), now + Duration::from_secs(1)),
PokeStrength::Strong
);
assert_eq!(
limiter.record_at(7, Some(1), now + Duration::from_secs(2)),
PokeStrength::Suppressed
);
assert_eq!(
limiter.record_at(7, Some(1), now + Duration::from_secs(3)),
PokeStrength::Suppressed
);
assert_eq!(
limiter.record_at(7, Some(1), now + Duration::from_secs(4)),
PokeStrength::SuppressedOverflow
);
}
#[test]
fn refill_after_interval_uses_injected_time_without_sleeping() {
let mut limiter = PokeLimiter::new();
let now = Instant::now();
assert_eq!(limiter.record_at(7, Some(1), now), PokeStrength::Strong);
assert_eq!(
limiter.record_at(7, Some(1), now + Duration::from_secs(1)),
PokeStrength::Strong
);
assert_eq!(
limiter.record_at(7, Some(1), now + Duration::from_secs(2)),
PokeStrength::Suppressed
);
assert_eq!(
limiter.record_at(7, Some(1), now + Duration::from_secs(5 * 60)),
PokeStrength::Strong
);
assert_eq!(
limiter.record_at(7, Some(1), now + Duration::from_secs(5 * 60 + 1)),
PokeStrength::Strong
);
}
}
-1
View File
@@ -615,7 +615,6 @@ mod tests {
sender_name: "Alice".into(),
message: "hello".into(),
target: MessageTarget::Channel,
poke_strength: None,
};
let disconnected = reduce(&mut state, StateEvent::ChatReceived(msg.clone()));
assert!(disconnected.deltas.is_empty());