feat(chat): route pokes through notifications
This commit is contained in:
+100
-145
@@ -28,6 +28,8 @@ import 'services/connection_phase_state.dart';
|
|||||||
import 'services/hard_mute_owners.dart';
|
import 'services/hard_mute_owners.dart';
|
||||||
import 'services/ios_permissions_service.dart';
|
import 'services/ios_permissions_service.dart';
|
||||||
import 'services/macos_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/prefetch_debouncer.dart';
|
||||||
import 'services/snapshot_state_mapper.dart';
|
import 'services/snapshot_state_mapper.dart';
|
||||||
import 'services/ts3_server_link.dart';
|
import 'services/ts3_server_link.dart';
|
||||||
@@ -43,6 +45,7 @@ import 'widgets/client_info_sheet.dart';
|
|||||||
import 'widgets/connect_widgets.dart';
|
import 'widgets/connect_widgets.dart';
|
||||||
import 'widgets/input_dialogs.dart';
|
import 'widgets/input_dialogs.dart';
|
||||||
import 'widgets/permission_state_banner.dart';
|
import 'widgets/permission_state_banner.dart';
|
||||||
|
import 'widgets/poke_notification_settings.dart';
|
||||||
import 'widgets/snapshot_view.dart';
|
import 'widgets/snapshot_view.dart';
|
||||||
import 'widgets/voice_platform.dart';
|
import 'widgets/voice_platform.dart';
|
||||||
import 'widgets/voice_bar.dart';
|
import 'widgets/voice_bar.dart';
|
||||||
@@ -161,10 +164,7 @@ class _ChanoraAppState extends State<ChanoraApp> {
|
|||||||
supportedLocales: AppL10n.supportedLocales,
|
supportedLocales: AppL10n.supportedLocales,
|
||||||
home: Stack(
|
home: Stack(
|
||||||
children: [
|
children: [
|
||||||
_BetaHome(
|
_BetaHome(themeMode: _themeMode, onThemeModeChanged: _setThemeMode),
|
||||||
themeMode: _themeMode,
|
|
||||||
onThemeModeChanged: _setThemeMode,
|
|
||||||
),
|
|
||||||
if (_showAudioDebugOverlay) const AudioDebugStatsPanel(),
|
if (_showAudioDebugOverlay) const AudioDebugStatsPanel(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -202,6 +202,19 @@ 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 {
|
class ChanoraThemeModeMenu extends StatelessWidget {
|
||||||
const ChanoraThemeModeMenu({
|
const ChanoraThemeModeMenu({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -302,18 +315,6 @@ class _BetaHome extends StatefulWidget {
|
|||||||
State<_BetaHome> createState() => _BetaHomeState();
|
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 {
|
class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||||
final _hostCtl = TextEditingController(text: 'cn.teamspeak.app');
|
final _hostCtl = TextEditingController(text: 'cn.teamspeak.app');
|
||||||
final _nickCtl = TextEditingController(text: 'ChanoraBeta');
|
final _nickCtl = TextEditingController(text: 'ChanoraBeta');
|
||||||
@@ -412,11 +413,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
/// previous conversation when the user reopens chat.
|
/// previous conversation when the user reopens chat.
|
||||||
rust.BridgeMessageTarget? _lastDismissedTarget;
|
rust.BridgeMessageTarget? _lastDismissedTarget;
|
||||||
String _lastDismissedClientName = '';
|
String _lastDismissedClientName = '';
|
||||||
final ValueNotifier<List<_ReceivedPoke>> _pokeSnackBarPokes = ValueNotifier(
|
|
||||||
const [],
|
|
||||||
);
|
|
||||||
bool _pokeSnackBarVisible = false;
|
|
||||||
|
|
||||||
// SDD-106 / SRS-209: Android RECORD_AUDIO runtime permission service.
|
// SDD-106 / SRS-209: Android RECORD_AUDIO runtime permission service.
|
||||||
// Constructed at startup so cold-launch state is captured before the
|
// Constructed at startup so cold-launch state is captured before the
|
||||||
// first voice_join attempt. On non-Android hosts the service
|
// first voice_join attempt. On non-Android hosts the service
|
||||||
@@ -431,6 +427,9 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
// MethodChannel.
|
// MethodChannel.
|
||||||
final MacOSPermissionsService _macOSPermissions = MacOSPermissionsService();
|
final MacOSPermissionsService _macOSPermissions = MacOSPermissionsService();
|
||||||
final UiPreferencesService _uiPreferences = const UiPreferencesService();
|
final UiPreferencesService _uiPreferences = const UiPreferencesService();
|
||||||
|
final PokeNotificationService _pokeNotifications = PokeNotificationService();
|
||||||
|
final PokePreferencesService _pokePreferences = PokePreferencesService();
|
||||||
|
late final Future<void> _pokePreferencesReady;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -464,6 +463,8 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
_onMacOSPttCapabilityChanged,
|
_onMacOSPttCapabilityChanged,
|
||||||
);
|
);
|
||||||
_macOSPermissions.checkInitialStates();
|
_macOSPermissions.checkInitialStates();
|
||||||
|
unawaited(_pokeNotifications.init());
|
||||||
|
_pokePreferencesReady = _pokePreferences.load();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
unawaited(_requestRecordAudioOnStartup());
|
unawaited(_requestRecordAudioOnStartup());
|
||||||
});
|
});
|
||||||
@@ -867,10 +868,11 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
:final senderName,
|
:final senderName,
|
||||||
:final message,
|
:final message,
|
||||||
:final target,
|
: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;
|
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();
|
final receivedAt = DateTime.now();
|
||||||
setState(() {
|
setState(() {
|
||||||
_appendChatEntryUnlocked(
|
_appendChatEntryUnlocked(
|
||||||
@@ -885,10 +887,13 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
if (isPoke) {
|
if (isPoke) {
|
||||||
_showPokeSnackBar(
|
unawaited(
|
||||||
senderName: senderName,
|
_handleIncomingPoke(
|
||||||
message: message,
|
senderId: senderId,
|
||||||
receivedAt: receivedAt,
|
senderName: senderName,
|
||||||
|
message: message,
|
||||||
|
strength: pokeStrength,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1088,7 +1093,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
_nickCtl.dispose();
|
_nickCtl.dispose();
|
||||||
_passwordCtl.dispose();
|
_passwordCtl.dispose();
|
||||||
_chatFeedRevision.dispose();
|
_chatFeedRevision.dispose();
|
||||||
_pokeSnackBarPokes.dispose();
|
|
||||||
_androidPermissions.recordAudioState.removeListener(
|
_androidPermissions.recordAudioState.removeListener(
|
||||||
_onRecordAudioPermissionChanged,
|
_onRecordAudioPermissionChanged,
|
||||||
);
|
);
|
||||||
@@ -1105,6 +1109,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
_androidPermissions.stop();
|
_androidPermissions.stop();
|
||||||
_iosPermissions.stop();
|
_iosPermissions.stop();
|
||||||
_macOSPermissions.stop();
|
_macOSPermissions.stop();
|
||||||
|
_pokePreferences.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1138,7 +1143,9 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
);
|
);
|
||||||
if (accessState == MacOSLocalNetworkState.denied) {
|
if (accessState == MacOSLocalNetworkState.denied) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() { _phase = ConnectionPhase.idle; });
|
setState(() {
|
||||||
|
_phase = ConnectionPhase.idle;
|
||||||
|
});
|
||||||
_showLocalNetworkDeniedSnackBar();
|
_showLocalNetworkDeniedSnackBar();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1520,6 +1527,16 @@ 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 {
|
Future<String?> _askChannelPassword(AppL10n l10n) async {
|
||||||
// Same pattern as _onAddCurrentBookmark: route the dialog
|
// Same pattern as _onAddCurrentBookmark: route the dialog
|
||||||
// through a dedicated StatefulWidget so its
|
// through a dedicated StatefulWidget so its
|
||||||
@@ -1804,9 +1821,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!mounted || _inlineChatTarget == null) return;
|
if (!mounted || _inlineChatTarget == null) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(content: Text(AppL10n.of(context).chatPanelCollapsedHint)),
|
||||||
content: Text(AppL10n.of(context).chatPanelCollapsedHint),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1827,52 +1842,68 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showPokeSnackBar({
|
Future<void> _handleIncomingPoke({
|
||||||
|
required BigInt senderId,
|
||||||
required String senderName,
|
required String senderName,
|
||||||
required String message,
|
required String message,
|
||||||
required DateTime receivedAt,
|
required rust.BridgePokeStrength? strength,
|
||||||
}) {
|
}) async {
|
||||||
_pokeSnackBarPokes.value = [
|
if (senderId == _snapshot?.ownClientId) return;
|
||||||
..._pokeSnackBarPokes.value,
|
await _pokePreferencesReady;
|
||||||
_ReceivedPoke(
|
if (!_pokePreferences.pokesEnabled.value) return;
|
||||||
senderName: senderName,
|
if (_pokePreferences.isMuted(senderId)) return;
|
||||||
message: message,
|
final pokeStrength = strength ?? rust.BridgePokeStrength.suppressed;
|
||||||
receivedAt: receivedAt,
|
if (pokeStrength == rust.BridgePokeStrength.suppressedOverflow && mounted) {
|
||||||
),
|
_showPokeOverflowMutePrompt(senderId: senderId, senderName: senderName);
|
||||||
];
|
}
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
if (_isPokeSenderActiveChat(senderId)) return;
|
||||||
if (mounted) _renderPokeSnackBar();
|
await _pokeNotifications.show(
|
||||||
});
|
senderName: senderName,
|
||||||
|
message: message,
|
||||||
|
senderId: senderId,
|
||||||
|
strength: pokeStrength,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _renderPokeSnackBar() {
|
bool _isPokeSenderActiveChat(BigInt senderId) {
|
||||||
if (_pokeSnackBarPokes.value.isEmpty || _pokeSnackBarVisible) return;
|
return isPokeSenderActiveChat(
|
||||||
_pokeSnackBarVisible = true;
|
chatOpen: _chatOpen,
|
||||||
|
inlineChatTarget: _inlineChatTarget,
|
||||||
|
senderId: senderId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showPokeOverflowMutePrompt({
|
||||||
|
required BigInt senderId,
|
||||||
|
required String senderName,
|
||||||
|
}) {
|
||||||
|
final l10n = AppL10n.of(context);
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
final controller = messenger.showSnackBar(
|
messenger.showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
margin: _chatSnackBarMargin(),
|
margin: _chatSnackBarMargin(),
|
||||||
duration: const Duration(days: 365),
|
duration: const Duration(seconds: 8),
|
||||||
dismissDirection: DismissDirection.none,
|
content: Text(
|
||||||
content: _PokeSnackBarContent(pokes: _pokeSnackBarPokes),
|
l10n.pokeOverflowMutePrompt(senderName),
|
||||||
|
maxLines: 3,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
action: SnackBarAction(
|
action: SnackBarAction(
|
||||||
label: AppL10n.of(context).pokeSnackBarClearAction,
|
label: l10n.pokeOverflowMuteAction,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_pokeSnackBarPokes.value = const [];
|
unawaited(_pokePreferences.muteSender(senderId));
|
||||||
_pokeSnackBarVisible = false;
|
messenger.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
margin: _chatSnackBarMargin(),
|
||||||
|
content: Text(l10n.pokeMutedSenderConfirmation(senderName)),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
controller.closed.then((_) {
|
|
||||||
if (!mounted) return;
|
|
||||||
_pokeSnackBarVisible = false;
|
|
||||||
if (_pokeSnackBarPokes.value.isEmpty) return;
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
if (mounted) _renderPokeSnackBar();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showChatMessageSnackBar({
|
void _showChatMessageSnackBar({
|
||||||
@@ -1880,7 +1911,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
required String message,
|
required String message,
|
||||||
required rust.BridgeMessageTarget target,
|
required rust.BridgeMessageTarget target,
|
||||||
}) {
|
}) {
|
||||||
if (_pokeSnackBarPokes.value.isNotEmpty) return;
|
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
messenger.hideCurrentSnackBar();
|
messenger.hideCurrentSnackBar();
|
||||||
messenger.showSnackBar(
|
messenger.showSnackBar(
|
||||||
@@ -2377,6 +2407,11 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
themeMode: widget.themeMode,
|
themeMode: widget.themeMode,
|
||||||
onThemeModeChanged: widget.onThemeModeChanged,
|
onThemeModeChanged: widget.onThemeModeChanged,
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: l10n.pokeSettingsAction,
|
||||||
|
icon: const Icon(Icons.notifications_outlined),
|
||||||
|
onPressed: () => unawaited(_onOpenPokeSettings()),
|
||||||
|
),
|
||||||
if (_phase.canOpenChatWithSnapshot(hasSnapshot: _snapshot != null)) ...[
|
if (_phase.canOpenChatWithSnapshot(hasSnapshot: _snapshot != null)) ...[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsetsDirectional.only(end: 12),
|
padding: const EdgeInsetsDirectional.only(end: 12),
|
||||||
@@ -2856,83 +2891,3 @@ 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)}';
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user