Skip to content

Commit caebbe6

Browse files
committed
feat(settings): bind settings
1 parent ceaf3ae commit caebbe6

6 files changed

Lines changed: 247 additions & 50 deletions

File tree

lib/screens/debug/notification_test_screen.dart

Lines changed: 46 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import 'package:flutter/foundation.dart';
21
import 'package:flutter/material.dart';
32

43
import '../../l10n/app_localizations.dart';
54
import '../../models/app_notification.dart';
5+
import '../../models/settings_service.dart';
66
import '../../services/app_notification_service.dart';
77

88
class NotificationTestScreen extends StatelessWidget {
@@ -50,40 +50,49 @@ class NotificationTestScreen extends StatelessWidget {
5050
),
5151
];
5252

53-
return Scaffold(
54-
appBar: AppBar(title: Text(l10n.debugNotificationTester)),
55-
body: ListView.separated(
56-
padding: const EdgeInsets.symmetric(vertical: 8),
57-
itemCount: samples.length,
58-
separatorBuilder: (_, _) => const Divider(height: 1),
59-
itemBuilder: (context, index) {
60-
final sample = samples[index];
61-
return ListTile(
62-
contentPadding: const EdgeInsets.symmetric(
63-
horizontal: 24,
64-
vertical: 8,
65-
),
66-
leading: CircleAvatar(child: Icon(sample.icon)),
67-
title: Text(sample.title),
68-
subtitle: Text(l10n.debugNotificationTestBody),
69-
trailing: Row(
70-
mainAxisSize: MainAxisSize.min,
71-
children: [
72-
IconButton.filledTonal(
73-
icon: const Icon(Icons.web_asset_outlined),
74-
tooltip: l10n.debugNotificationTestInApp,
75-
onPressed: () => _showInApp(context, sample),
76-
),
77-
const SizedBox(width: 8),
78-
IconButton.filledTonal(
79-
icon: const Icon(Icons.desktop_windows_outlined),
80-
tooltip: l10n.debugNotificationTestSystem,
81-
onPressed: kIsWeb ? null : () => _showSystem(context, sample),
82-
),
83-
],
84-
),
85-
);
86-
},
53+
return ListenableBuilder(
54+
listenable: SettingsService.instance,
55+
builder: (context, _) => Scaffold(
56+
appBar: AppBar(title: Text(l10n.debugNotificationTester)),
57+
body: ListView.separated(
58+
padding: const EdgeInsets.symmetric(vertical: 8),
59+
itemCount: samples.length,
60+
separatorBuilder: (_, _) => const Divider(height: 1),
61+
itemBuilder: (context, index) {
62+
final sample = samples[index];
63+
final notification = _notification(context, sample);
64+
final service = AppNotificationService.instance;
65+
return ListTile(
66+
contentPadding: const EdgeInsets.symmetric(
67+
horizontal: 24,
68+
vertical: 8,
69+
),
70+
leading: CircleAvatar(child: Icon(sample.icon)),
71+
title: Text(sample.title),
72+
subtitle: Text(l10n.debugNotificationTestBody),
73+
trailing: Row(
74+
mainAxisSize: MainAxisSize.min,
75+
children: [
76+
IconButton.filledTonal(
77+
icon: const Icon(Icons.web_asset_outlined),
78+
tooltip: l10n.debugNotificationTestInApp,
79+
onPressed: service.canShowInApp(notification)
80+
? () => service.showInAppTest(notification)
81+
: null,
82+
),
83+
const SizedBox(width: 8),
84+
IconButton.filledTonal(
85+
icon: const Icon(Icons.desktop_windows_outlined),
86+
tooltip: l10n.debugNotificationTestSystem,
87+
onPressed: service.canShowSystem(notification)
88+
? () => _showSystem(context, notification)
89+
: null,
90+
),
91+
],
92+
),
93+
);
94+
},
95+
),
8796
),
8897
);
8998
}
@@ -102,18 +111,12 @@ class NotificationTestScreen extends StatelessWidget {
102111
);
103112
}
104113

105-
void _showInApp(BuildContext context, _NotificationSample sample) {
106-
AppNotificationService.instance.showInAppTest(
107-
_notification(context, sample),
108-
);
109-
}
110-
111114
Future<void> _showSystem(
112115
BuildContext context,
113-
_NotificationSample sample,
116+
AppNotification notification,
114117
) async {
115118
final shown = await AppNotificationService.instance.showSystemTest(
116-
_notification(context, sample),
119+
notification,
117120
);
118121
if (!context.mounted || shown) return;
119122
ScaffoldMessenger.of(context).showSnackBar(

lib/services/app_notification_service.dart

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ class AppNotificationService extends ChangeNotifier
106106

107107
Future<void> present(AppNotification notification) async {
108108
final settings = SettingsService.instance;
109+
if (!_isNotificationTypeEnabled(notification)) return;
109110
if (await _shouldShowInApp()) {
110111
if (!settings.getValue<bool>('inAppNotifications', true)) return;
111112
if (settings.getValue<bool>('notificationSound', true)) {
@@ -133,15 +134,45 @@ class AppNotificationService extends ChangeNotifier
133134
return _lifecycleState == AppLifecycleState.resumed;
134135
}
135136

137+
bool canShowInApp(AppNotification notification) {
138+
return SettingsService.instance.getValue<bool>(
139+
'inAppNotifications',
140+
true,
141+
) &&
142+
_isNotificationTypeEnabled(notification);
143+
}
144+
145+
bool canShowSystem(AppNotification notification) {
146+
return !kIsWeb &&
147+
_localNotificationsReady &&
148+
SettingsService.instance.getValue<bool>('systemNotifications', true) &&
149+
_isNotificationTypeEnabled(notification);
150+
}
151+
136152
void showInAppTest(AppNotification notification) {
153+
if (!canShowInApp(notification)) return;
154+
if (SettingsService.instance.getValue<bool>('notificationSound', true)) {
155+
unawaited(SystemSound.play(SystemSoundType.alert));
156+
}
137157
add(notification);
138158
}
139159

140160
Future<bool> showSystemTest(AppNotification notification) async {
141-
if (kIsWeb || !_localNotificationsReady) return false;
161+
if (!canShowSystem(notification)) return false;
142162
return _showSystemNotification(notification);
143163
}
144164

165+
bool _isNotificationTypeEnabled(AppNotification notification) {
166+
final settings = SettingsService.instance;
167+
if (notification.topic == 'message.private') {
168+
return settings.getValue<bool>('privateChat', true);
169+
}
170+
if (notification.topic == 'message.group') {
171+
return settings.getValue<bool>('groupChat', true);
172+
}
173+
return true;
174+
}
175+
145176
void add(AppNotification notification, {Duration? duration}) {
146177
if (_items.any(
147178
(item) => item.notification.id == notification.id && !item.dismissed,

lib/services/chat_data_service.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -775,7 +775,7 @@ class ChatDataService extends ChangeNotifier {
775775
body: msg.text,
776776
avatarUrl: msg.senderAvatar ?? room.avatar,
777777
route: '/chat/$roomId',
778-
topic: 'message',
778+
topic: isGroupRoom(roomId) ? 'message.group' : 'message.private',
779779
),
780780
),
781781
);

lib/widgets/chat_input_bar.dart

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:file_picker/file_picker.dart';
77
import 'package:mime/mime.dart';
88
import '../l10n/app_localizations.dart';
99
import '../models/message_model.dart';
10+
import '../models/settings_service.dart';
1011
import 'mention_text_field.dart';
1112

1213
class ChatInputBar extends StatefulWidget {
@@ -31,19 +32,68 @@ class _ChatInputBarState extends State<ChatInputBar>
3132
with SingleTickerProviderStateMixin {
3233
bool _isExpanded = false;
3334
late final TabController _tabController;
35+
late final FocusNode _inputFocusNode;
3436

3537
@override
3638
void initState() {
3739
super.initState();
3840
_tabController = TabController(length: 3, vsync: this);
41+
_inputFocusNode = FocusNode(onKeyEvent: _handleInputKeyEvent);
3942
}
4043

4144
@override
4245
void dispose() {
4346
_tabController.dispose();
47+
_inputFocusNode.dispose();
4448
super.dispose();
4549
}
4650

51+
KeyEventResult _handleInputKeyEvent(FocusNode node, KeyEvent event) {
52+
if (event is! KeyDownEvent ||
53+
(event.logicalKey != LogicalKeyboardKey.enter &&
54+
event.logicalKey != LogicalKeyboardKey.numpadEnter)) {
55+
return KeyEventResult.ignored;
56+
}
57+
58+
final value = widget.controller.value;
59+
if (value.composing.isValid && !value.composing.isCollapsed) {
60+
return KeyEventResult.ignored;
61+
}
62+
63+
final keyboard = HardwareKeyboard.instance;
64+
final controlOrMeta = keyboard.isControlPressed || keyboard.isMetaPressed;
65+
final sendMode = SettingsService.instance.getValue<String>(
66+
'sendMode',
67+
'enter',
68+
);
69+
final shouldSend = sendMode == 'ctrlEnter'
70+
? controlOrMeta && !keyboard.isShiftPressed && !keyboard.isAltPressed
71+
: !controlOrMeta && !keyboard.isShiftPressed && !keyboard.isAltPressed;
72+
73+
if (shouldSend) {
74+
if (value.text.trim().isNotEmpty) widget.onSend();
75+
return KeyEventResult.handled;
76+
}
77+
78+
if (sendMode == 'enter' && controlOrMeta) {
79+
_insertNewline();
80+
return KeyEventResult.handled;
81+
}
82+
return KeyEventResult.ignored;
83+
}
84+
85+
void _insertNewline() {
86+
final value = widget.controller.value;
87+
final selection = value.selection;
88+
final start = selection.isValid ? selection.start : value.text.length;
89+
final end = selection.isValid ? selection.end : value.text.length;
90+
widget.controller.value = value.copyWith(
91+
text: value.text.replaceRange(start, end, '\n'),
92+
selection: TextSelection.collapsed(offset: start + 1),
93+
composing: TextRange.empty,
94+
);
95+
}
96+
4797
@override
4898
Widget build(BuildContext context) {
4999
final colorScheme = Theme.of(context).colorScheme;
@@ -141,6 +191,7 @@ class _ChatInputBarState extends State<ChatInputBar>
141191
Expanded(
142192
child: MentionTextField(
143193
controller: widget.controller,
194+
focusNode: _inputFocusNode,
144195
mentionUsers: widget.mentionUsers,
145196
maxLines: 5,
146197
minLines: 1,
@@ -155,11 +206,6 @@ class _ChatInputBarState extends State<ChatInputBar>
155206
vertical: 12,
156207
),
157208
),
158-
onSubmitted: (_) {
159-
if (widget.controller.text.trim().isNotEmpty) {
160-
widget.onSend();
161-
}
162-
},
163209
),
164210
),
165211
IconButton(

test/app_notification_test.dart

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_test/flutter_test.dart';
3+
import 'package:shared_preferences/shared_preferences.dart';
34
import 'package:touchfish_client/models/app_notification.dart';
45
import 'package:touchfish_client/models/notification_model.dart';
6+
import 'package:touchfish_client/models/settings_service.dart';
57
import 'package:touchfish_client/services/app_notification_service.dart';
68
import 'package:touchfish_client/widgets/notification_overlay.dart';
79

810
void main() {
11+
setUpAll(() async {
12+
SharedPreferences.setMockInitialValues({});
13+
await SettingsService.instance.init();
14+
});
15+
916
NotificationInfo notification({
1017
required String event,
1118
String? roomId,
@@ -117,4 +124,35 @@ void main() {
117124
AppNotificationService.instance.clear();
118125
await tester.pump();
119126
});
127+
128+
test('notification test actions honor global and chat settings', () async {
129+
const privateMessage = AppNotification(
130+
id: 'private-test',
131+
title: 'Private',
132+
body: 'Body',
133+
route: '/chat/U1',
134+
topic: 'message.private',
135+
);
136+
const groupMessage = AppNotification(
137+
id: 'group-test',
138+
title: 'Group',
139+
body: 'Body',
140+
route: '/chat/G1',
141+
topic: 'message.group',
142+
);
143+
144+
await SettingsService.instance.setValue('inAppNotifications', false);
145+
expect(
146+
AppNotificationService.instance.canShowInApp(privateMessage),
147+
isFalse,
148+
);
149+
await SettingsService.instance.setValue('inAppNotifications', true);
150+
await SettingsService.instance.setValue('privateChat', false);
151+
expect(
152+
AppNotificationService.instance.canShowInApp(privateMessage),
153+
isFalse,
154+
);
155+
expect(AppNotificationService.instance.canShowInApp(groupMessage), isTrue);
156+
await SettingsService.instance.setValue('privateChat', true);
157+
});
120158
}

0 commit comments

Comments
 (0)