parent
cb983701ca
commit
c9172e92eb
3 changed files with 596 additions and 15 deletions
|
|
@ -6,6 +6,8 @@ import "package:flutter/foundation.dart";
|
|||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:nexus/controllers/client.dart";
|
||||
import "package:nexus/models/account_data.dart";
|
||||
import "package:nexus/models/push_rules.dart";
|
||||
import "package:nexus/models/requests/update_push_rule.dart";
|
||||
|
||||
class AccountDataController extends Notifier<AccountData> {
|
||||
@override
|
||||
|
|
@ -24,24 +26,15 @@ class AccountDataController extends Notifier<AccountData> {
|
|||
}
|
||||
});
|
||||
|
||||
Future<void> setEnabledImagePacks(ImagePackRooms packs) async {
|
||||
Future<void> setEnabledImagePacks(ImagePackRooms packs) {
|
||||
ISet<(String, String)> keysOf(ImagePackRooms rooms) => rooms.rooms.entries
|
||||
.expand((entry) => entry.value.keys.map((key) => (entry.key, key)))
|
||||
.toISet();
|
||||
final keys = keysOf(packs);
|
||||
bool arrived(AccountData data) {
|
||||
final current = keysOf(data.enabledImagePacks);
|
||||
return current.length == keys.length && current.containsAll(keys);
|
||||
}
|
||||
final client = ref.read(ClientController.provider.notifier);
|
||||
|
||||
final completer = Completer<void>();
|
||||
final removeListener = listenSelf((_, data) {
|
||||
if (arrived(data) && !completer.isCompleted) completer.complete();
|
||||
});
|
||||
|
||||
try {
|
||||
final client = ref.read(ClientController.provider.notifier);
|
||||
await Future.wait([
|
||||
return _sendAndWait(
|
||||
() => .wait([
|
||||
if (state.imagePackRooms != null)
|
||||
client.setAccountData(
|
||||
.new(type: AccountData.imagePackRoomsKey, content: packs),
|
||||
|
|
@ -49,8 +42,65 @@ class AccountDataController extends Notifier<AccountData> {
|
|||
client.setAccountData(
|
||||
.new(type: AccountData.legacyImagePackRoomsKey, content: packs),
|
||||
),
|
||||
]);
|
||||
]),
|
||||
(data) {
|
||||
final current = keysOf(data.enabledImagePacks);
|
||||
return current.length == keys.length && current.containsAll(keys);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Saves [rule] of [kind], creating it if it's new, or otherwise changing only
|
||||
/// its actions and whether it's enabled.
|
||||
Future<void> savePushRule(PushRuleKind kind, PushRule rule) {
|
||||
UpdatePushRuleRequest request(UpdatePushRuleAction action) =>
|
||||
.new(kind: kind, ruleId: rule.ruleId, action: action);
|
||||
|
||||
return _updatePushRules(
|
||||
.new(switch (state.pushRules.global.find(kind, rule.ruleId)) {
|
||||
null => [request(.put).copyWith(newContent: rule)],
|
||||
final previous => [
|
||||
if (previous.actions != rule.actions)
|
||||
request(.putActions).copyWith(actions: rule.actions),
|
||||
if (previous.enabled != rule.enabled)
|
||||
request(rule.enabled ? .enable : .disable),
|
||||
],
|
||||
}),
|
||||
(rules) => rules.find(kind, rule.ruleId) == rule,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deletePushRule(PushRuleKind kind, String ruleId) =>
|
||||
_updatePushRules(
|
||||
.new([.new(kind: kind, ruleId: ruleId, action: .delete)]),
|
||||
(rules) => rules.find(kind, ruleId) == null,
|
||||
);
|
||||
|
||||
/// Sends [requests] in order, then waits until [arrived] is true for the
|
||||
/// synced push rules.
|
||||
Future<void> _updatePushRules(
|
||||
IList<UpdatePushRuleRequest> requests,
|
||||
bool Function(PushRuleset rules) arrived,
|
||||
) => _sendAndWait(
|
||||
() => .forEach(
|
||||
requests,
|
||||
ref.read(ClientController.provider.notifier).updatePushRule,
|
||||
),
|
||||
(data) => arrived(data.pushRules.global),
|
||||
);
|
||||
|
||||
/// Runs [send], then waits until [arrived] is true for the synced data.
|
||||
Future<void> _sendAndWait(
|
||||
Future<void> Function() send,
|
||||
bool Function(AccountData data) arrived,
|
||||
) async {
|
||||
final completer = Completer<void>();
|
||||
final removeListener = listenSelf((_, data) {
|
||||
if (arrived(data) && !completer.isCompleted) completer.complete();
|
||||
});
|
||||
|
||||
try {
|
||||
await send();
|
||||
if (!arrived(state)) {
|
||||
await completer.future.timeout(
|
||||
const .new(seconds: 10),
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import "package:nexus/models/requests/send_message.dart";
|
|||
import "package:nexus/models/requests/set_account_data.dart";
|
||||
import "package:nexus/models/requests/set_membership.dart";
|
||||
import "package:nexus/models/requests/set_state.dart";
|
||||
import "package:nexus/models/requests/update_push_rule.dart";
|
||||
import "package:nexus/models/requests/upload_media.dart";
|
||||
import "package:nexus/models/room.dart";
|
||||
import "package:nexus/models/room_summary.dart";
|
||||
|
|
@ -333,6 +334,9 @@ class ClientController extends AsyncNotifier<int> {
|
|||
Future<void> setAccountData(SetAccountDataRequest request) =>
|
||||
_sendCommand("set_account_data", request.toJson());
|
||||
|
||||
Future<void> updatePushRule(UpdatePushRuleRequest request) =>
|
||||
_sendCommand("update_push_rule", request.toJson());
|
||||
|
||||
Future<void> registerPusher(RegisterPusherRequest request) =>
|
||||
_sendCommand("register_homeserver_push", request.toJson());
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import "package:hooks_riverpod/hooks_riverpod.dart";
|
|||
import "package:material_ui/material_ui.dart";
|
||||
import "package:intl/intl.dart";
|
||||
import "package:m3e_buttons/m3e_buttons.dart";
|
||||
import "package:multi_state_switch/multi_state_switch.dart";
|
||||
import "package:nexus/controllers/account_data.dart";
|
||||
import "package:nexus/controllers/client.dart";
|
||||
import "package:nexus/controllers/client_state.dart";
|
||||
|
|
@ -25,15 +26,18 @@ import "package:nexus/models/account_data.dart";
|
|||
import "package:nexus/models/client_state.dart";
|
||||
import "package:nexus/models/image_pack.dart";
|
||||
import "package:nexus/models/name_color.dart";
|
||||
import "package:nexus/models/push_rule_switch.dart";
|
||||
import "package:nexus/models/room_order.dart";
|
||||
import "package:nexus/models/setting.dart";
|
||||
import "package:nexus/models/settings.dart";
|
||||
import "package:nexus/models/settings_category.dart";
|
||||
import "package:nexus/models/show_and_send.dart";
|
||||
import "package:nexus/main.dart";
|
||||
import "package:nexus/widgets/divider_text.dart";
|
||||
import "package:nexus/widgets/image_pack_avatar.dart";
|
||||
import "package:nexus/widgets/settings/dialog_list_tile.dart";
|
||||
import "package:nexus/widgets/settings/enable_image_packs_sheet.dart";
|
||||
import "package:nexus/widgets/settings/keyword_field.dart";
|
||||
import "package:nexus/widgets/user_bottom_sheet.dart";
|
||||
import "package:nexus/widgets/reset_encryption_sheet.dart";
|
||||
import "package:nexus/helpers/extensions/show_adaptive_sheet.dart";
|
||||
|
|
@ -95,6 +99,67 @@ class SettingsSectionsController
|
|||
.onError(showError),
|
||||
);
|
||||
|
||||
final pushRules = ref.watch(
|
||||
AccountDataController.provider.select((data) => data.pushRules.global),
|
||||
);
|
||||
final accountData = ref.read(AccountDataController.provider.notifier);
|
||||
|
||||
Setting header(String title, String description, IconData icon) => .new(
|
||||
title: title,
|
||||
description: description,
|
||||
icon: icon,
|
||||
builder: (title, _, _) => Padding(
|
||||
padding: .symmetric(horizontal: 16),
|
||||
child: DividerText(title),
|
||||
),
|
||||
);
|
||||
|
||||
/// A switch for push rules, which is disabled while [onChanged] saves. A
|
||||
/// null [value] is shown in the middle, for rules set up some other way.
|
||||
Setting pushSwitchSetting({
|
||||
required String title,
|
||||
required String description,
|
||||
required IconData icon,
|
||||
required bool? value,
|
||||
required Future<void> Function(bool value) onChanged,
|
||||
}) => .new(
|
||||
title: title,
|
||||
description: description,
|
||||
icon: icon,
|
||||
builder: (title, description, icon) => HookBuilder(
|
||||
builder: (context) {
|
||||
final saving = Saving.use();
|
||||
final position = switch (value) {
|
||||
false => 0,
|
||||
null => 1,
|
||||
true => 2,
|
||||
};
|
||||
void change(bool value) =>
|
||||
saving.run(() => onChanged(value)).onError(showError);
|
||||
|
||||
return ListTile(
|
||||
enabled: !saving.active,
|
||||
leading: Icon(icon),
|
||||
title: Text(title),
|
||||
subtitle: Text(description),
|
||||
trailing: MultiStateSwitch(
|
||||
states: 3,
|
||||
// As wide as a regular switch, which has 2 states 20 apart
|
||||
stepWidth: 10,
|
||||
value: position,
|
||||
semanticFormatterCallback: (position) =>
|
||||
["Off", "Custom", "On"][position],
|
||||
onChanged: saving.active
|
||||
? null
|
||||
: (newPosition) => change(newPosition > position),
|
||||
),
|
||||
// Tapping turns it off only when it's fully on
|
||||
onTap: () => change(value != true),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return .new({
|
||||
"General": .new([
|
||||
.new(
|
||||
|
|
@ -412,7 +477,7 @@ class SettingsSectionsController
|
|||
icon: Icons.notifications,
|
||||
settings: .new([
|
||||
.new(
|
||||
title: "Push notifications",
|
||||
title: "Push Notifications",
|
||||
description: "Enable push notifications using Web Push, and choose which UnifiedPush distributor to use.",
|
||||
builder: (title, description, icon) => HookConsumer(
|
||||
builder: (context, ref, _) {
|
||||
|
|
@ -509,6 +574,102 @@ class SettingsSectionsController
|
|||
),
|
||||
icon: Icons.notification_add,
|
||||
),
|
||||
if (pushRules.master case final master?)
|
||||
pushSwitchSetting(
|
||||
title: "Mute All",
|
||||
description: "Don't notify for anything, without changing your other rules.",
|
||||
icon: Icons.notifications_off,
|
||||
value: master.enabled,
|
||||
onChanged: (mute) => accountData.savePushRule(
|
||||
.override,
|
||||
master.copyWith(enabled: mute),
|
||||
),
|
||||
),
|
||||
...PushRuleSection.values.expand(
|
||||
(section) => [
|
||||
header(section.title, section.description, section.icon),
|
||||
...PushRuleSwitch.values
|
||||
.where(
|
||||
(option) =>
|
||||
option.section == section &&
|
||||
option.rulesIn(pushRules).isNotEmpty,
|
||||
)
|
||||
.map(
|
||||
(option) => pushSwitchSetting(
|
||||
title: option.title,
|
||||
description: option.description,
|
||||
icon: option.icon,
|
||||
value: option.valueIn(pushRules),
|
||||
onChanged: (value) => Future.wait(
|
||||
option
|
||||
.changesIn(pushRules, value)
|
||||
.map(
|
||||
(change) => accountData.savePushRule(
|
||||
change.kind,
|
||||
change.rule,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
header(
|
||||
"Keywords",
|
||||
"Messages containing these words, which notify with a highlight and sound.",
|
||||
Icons.text_fields,
|
||||
),
|
||||
...pushRules.keywords.map(
|
||||
(rule) => Setting(
|
||||
title: rule.pattern ?? rule.ruleId,
|
||||
description: "Messages containing this keyword.",
|
||||
icon: Icons.text_fields,
|
||||
builder: (title, description, icon) => HookBuilder(
|
||||
key: ValueKey(rule.ruleId),
|
||||
builder: (context) {
|
||||
final saving = Saving.use();
|
||||
final custom =
|
||||
!rule.enabled || rule.actions != .highlightWithSound;
|
||||
|
||||
return ListTile(
|
||||
enabled: !saving.active,
|
||||
leading: Icon(icon),
|
||||
title: Text(title),
|
||||
trailing: IconButton(
|
||||
tooltip: custom
|
||||
? "Set up differently by another client. Press to reset to Nexus defaults."
|
||||
: "Remove keyword",
|
||||
icon: Icon(custom ? Icons.warning : Icons.delete),
|
||||
onPressed: saving.active
|
||||
? null
|
||||
: () => saving
|
||||
.run(
|
||||
() => custom
|
||||
? accountData.savePushRule(
|
||||
.content,
|
||||
rule.copyWith(
|
||||
enabled: true,
|
||||
actions: .highlightWithSound,
|
||||
),
|
||||
)
|
||||
: accountData.deletePushRule(
|
||||
.content,
|
||||
rule.ruleId,
|
||||
),
|
||||
)
|
||||
.onError(showError),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
.new(
|
||||
title: "Add Keyword",
|
||||
description: "Notify for messages containing a word.",
|
||||
icon: Icons.add,
|
||||
builder: (_, _, _) => KeywordField(),
|
||||
),
|
||||
]),
|
||||
),
|
||||
.new(
|
||||
|
|
|
|||
Loading…
Reference in a new issue