parent
b55a6e0e83
commit
979068fc3a
5 changed files with 228 additions and 0 deletions
|
|
@ -50,6 +50,22 @@ class AccountDataController extends Notifier<AccountData> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setIgnored(String userId, {required bool ignored}) =>
|
||||||
|
_sendAndWait(
|
||||||
|
() => ref
|
||||||
|
.read(ClientController.provider.notifier)
|
||||||
|
.setAccountData(
|
||||||
|
.new(
|
||||||
|
type: AccountData.ignoredUserListKey,
|
||||||
|
content: state.ignoredUserList.withUser(
|
||||||
|
userId,
|
||||||
|
ignored: ignored,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(data) => data.ignoredUserList.contains(userId) == ignored,
|
||||||
|
);
|
||||||
|
|
||||||
/// Saves [rule] of [kind], creating it if it's new, or otherwise changing only
|
/// Saves [rule] of [kind], creating it if it's new, or otherwise changing only
|
||||||
/// its actions and whether it's enabled.
|
/// its actions and whether it's enabled.
|
||||||
Future<void> savePushRule(PushRuleKind kind, PushRule rule) {
|
Future<void> savePushRule(PushRuleKind kind, PushRule rule) {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import "package:nexus/controllers/client_state.dart";
|
||||||
import "package:nexus/controllers/enabled_image_packs.dart";
|
import "package:nexus/controllers/enabled_image_packs.dart";
|
||||||
import "package:nexus/controllers/init_complete.dart";
|
import "package:nexus/controllers/init_complete.dart";
|
||||||
import "package:nexus/controllers/notification.dart";
|
import "package:nexus/controllers/notification.dart";
|
||||||
|
import "package:nexus/controllers/profile.dart";
|
||||||
import "package:nexus/controllers/rooms.dart";
|
import "package:nexus/controllers/rooms.dart";
|
||||||
import "package:nexus/controllers/settings.dart";
|
import "package:nexus/controllers/settings.dart";
|
||||||
import "package:nexus/controllers/unified_push.dart";
|
import "package:nexus/controllers/unified_push.dart";
|
||||||
|
|
@ -20,6 +21,7 @@ import "package:nexus/controllers/spec_versions.dart";
|
||||||
import "package:nexus/controllers/unified_push_allowed.dart";
|
import "package:nexus/controllers/unified_push_allowed.dart";
|
||||||
import "package:nexus/controllers/unified_push_distributors.dart";
|
import "package:nexus/controllers/unified_push_distributors.dart";
|
||||||
import "package:nexus/helpers/extensions/distributor_name.dart";
|
import "package:nexus/helpers/extensions/distributor_name.dart";
|
||||||
|
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||||
import "package:nexus/helpers/extensions/room_image_packs.dart";
|
import "package:nexus/helpers/extensions/room_image_packs.dart";
|
||||||
import "package:nexus/helpers/hooks/saving.dart";
|
import "package:nexus/helpers/hooks/saving.dart";
|
||||||
import "package:nexus/models/account_data.dart";
|
import "package:nexus/models/account_data.dart";
|
||||||
|
|
@ -33,8 +35,10 @@ import "package:nexus/models/settings.dart";
|
||||||
import "package:nexus/models/settings_category.dart";
|
import "package:nexus/models/settings_category.dart";
|
||||||
import "package:nexus/models/show_and_send.dart";
|
import "package:nexus/models/show_and_send.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
import "package:nexus/widgets/divider_text.dart";
|
import "package:nexus/widgets/divider_text.dart";
|
||||||
import "package:nexus/widgets/image_pack_avatar.dart";
|
import "package:nexus/widgets/image_pack_avatar.dart";
|
||||||
|
import "package:nexus/widgets/settings/block_user_field.dart";
|
||||||
import "package:nexus/widgets/settings/dialog_list_tile.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/enable_image_packs_sheet.dart";
|
||||||
import "package:nexus/widgets/settings/keyword_field.dart";
|
import "package:nexus/widgets/settings/keyword_field.dart";
|
||||||
|
|
@ -102,6 +106,11 @@ class SettingsSectionsController
|
||||||
final pushRules = ref.watch(
|
final pushRules = ref.watch(
|
||||||
AccountDataController.provider.select((data) => data.pushRules.global),
|
AccountDataController.provider.select((data) => data.pushRules.global),
|
||||||
);
|
);
|
||||||
|
final ignoredUsers = ref.watch(
|
||||||
|
AccountDataController.provider.select(
|
||||||
|
(data) => data.ignoredUserList.ignoredUsers.keys.toIList().sort(),
|
||||||
|
),
|
||||||
|
);
|
||||||
final accountData = ref.read(AccountDataController.provider.notifier);
|
final accountData = ref.read(AccountDataController.provider.notifier);
|
||||||
|
|
||||||
Setting header(String title, String description, IconData icon) => .new(
|
Setting header(String title, String description, IconData icon) => .new(
|
||||||
|
|
@ -726,6 +735,59 @@ class SettingsSectionsController
|
||||||
),
|
),
|
||||||
icon: Icons.person_off,
|
icon: Icons.person_off,
|
||||||
),
|
),
|
||||||
|
header(
|
||||||
|
"Blocked Users",
|
||||||
|
"Users whose messages and invites are hidden. They aren't told they're blocked.",
|
||||||
|
Icons.block,
|
||||||
|
),
|
||||||
|
...ignoredUsers.map(
|
||||||
|
(userId) => Setting(
|
||||||
|
title: userId,
|
||||||
|
description: "A blocked user.",
|
||||||
|
icon: Icons.person,
|
||||||
|
builder: (title, description, icon) => HookConsumer(
|
||||||
|
key: ValueKey(userId),
|
||||||
|
builder: (context, ref, _) {
|
||||||
|
final saving = Saving.use();
|
||||||
|
final profile = ref
|
||||||
|
.watch(ProfileController.provider(userId))
|
||||||
|
.value
|
||||||
|
?.profile;
|
||||||
|
final name = profile?.displayName ?? userId.localpart;
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
enabled: !saving.active,
|
||||||
|
leading: AvatarOrHash(profile?.avatarUrl, name),
|
||||||
|
title: Text(name, overflow: .ellipsis),
|
||||||
|
subtitle: Text(userId, overflow: .ellipsis),
|
||||||
|
onTap: () => context.showAdaptiveSheet(
|
||||||
|
(_) => UserBottomSheet(userId),
|
||||||
|
),
|
||||||
|
trailing: IconButton(
|
||||||
|
tooltip: "Unblock",
|
||||||
|
icon: Icon(Icons.lock_open),
|
||||||
|
onPressed: saving.active
|
||||||
|
? null
|
||||||
|
: () => saving
|
||||||
|
.run(
|
||||||
|
() => accountData.setIgnored(
|
||||||
|
userId,
|
||||||
|
ignored: false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.onError(showError),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
.new(
|
||||||
|
title: "Block User",
|
||||||
|
description: "Block a user by their ID.",
|
||||||
|
icon: Icons.block,
|
||||||
|
builder: (_, _, _) => BlockUserField(),
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
.new(
|
.new(
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ class const AccountData({
|
||||||
final ImagePackRooms? legacyImagePackRooms,
|
final ImagePackRooms? legacyImagePackRooms,
|
||||||
@JsonKey(name: AccountData.pushRulesKey)
|
@JsonKey(name: AccountData.pushRulesKey)
|
||||||
final PushRules pushRules = const .new(),
|
final PushRules pushRules = const .new(),
|
||||||
|
@JsonKey(name: AccountData.ignoredUserListKey)
|
||||||
|
final IgnoredUserList ignoredUserList = const .new(),
|
||||||
}) with _$AccountData {
|
}) with _$AccountData {
|
||||||
static const invitePermissionConfigKey = "m.invite_permission_config";
|
static const invitePermissionConfigKey = "m.invite_permission_config";
|
||||||
static const directKey = "m.direct";
|
static const directKey = "m.direct";
|
||||||
|
|
@ -32,6 +34,7 @@ class const AccountData({
|
||||||
static const imagePackRoomsKey = "m.image_pack.rooms";
|
static const imagePackRoomsKey = "m.image_pack.rooms";
|
||||||
static const legacyImagePackRoomsKey = "im.ponies.emote_rooms";
|
static const legacyImagePackRoomsKey = "im.ponies.emote_rooms";
|
||||||
static const pushRulesKey = "m.push_rules";
|
static const pushRulesKey = "m.push_rules";
|
||||||
|
static const ignoredUserListKey = "m.ignored_user_list";
|
||||||
|
|
||||||
ImagePackRooms get enabledImagePacks =>
|
ImagePackRooms get enabledImagePacks =>
|
||||||
imagePackRooms ?? legacyImagePackRooms ?? const .new();
|
imagePackRooms ?? legacyImagePackRooms ?? const .new();
|
||||||
|
|
@ -89,6 +92,27 @@ class const ImagePackRooms({
|
||||||
_$ImagePackRoomsFromJson(json);
|
_$ImagePackRoomsFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Freezed(toJson: false, fromJson: false)
|
||||||
|
@JsonSerializable()
|
||||||
|
class const IgnoredUserList({
|
||||||
|
/// Blocked user IDs, each to a (currently empty) object
|
||||||
|
@JsonKey(name: "ignored_users")
|
||||||
|
final IMap<String, Map<String, dynamic>> ignoredUsers = const .empty(),
|
||||||
|
}) with _$IgnoredUserList {
|
||||||
|
bool contains(String userId) => ignoredUsers.containsKey(userId);
|
||||||
|
|
||||||
|
IgnoredUserList withUser(String userId, {required bool ignored}) => copyWith(
|
||||||
|
ignoredUsers: ignored
|
||||||
|
? ignoredUsers.add(userId, const {})
|
||||||
|
: ignoredUsers.remove(userId),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, Object?> toJson() => _$IgnoredUserListToJson(this);
|
||||||
|
|
||||||
|
factory IgnoredUserList.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$IgnoredUserListFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
@Freezed(toJson: false, fromJson: false)
|
@Freezed(toJson: false, fromJson: false)
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class const InvitePermissionConfig({
|
class const InvitePermissionConfig({
|
||||||
|
|
|
||||||
66
lib/widgets/settings/block_user_field.dart
Normal file
66
lib/widgets/settings/block_user_field.dart
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
import "package:material_ui/material_ui.dart";
|
||||||
|
import "package:nexus/controllers/account_data.dart";
|
||||||
|
import "package:nexus/controllers/client_state.dart";
|
||||||
|
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||||
|
import "package:nexus/helpers/hooks/saving.dart";
|
||||||
|
import "package:nexus/main.dart";
|
||||||
|
|
||||||
|
/// Blocks a user by their ID, hiding their messages and invites.
|
||||||
|
class const BlockUserField({super.key}) extends HookConsumerWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final saving = Saving.use();
|
||||||
|
final controller = useTextEditingController();
|
||||||
|
final userId = useValueListenable(controller).text.trim();
|
||||||
|
final blocked = ref.watch(
|
||||||
|
AccountDataController.provider.select(
|
||||||
|
(data) => data.ignoredUserList.contains(userId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final ownId = ref.watch(
|
||||||
|
ClientStateController.provider.select((state) => state?.userId),
|
||||||
|
);
|
||||||
|
|
||||||
|
final error = userId.isEmpty || userId.isValidUserId
|
||||||
|
? userId == ownId
|
||||||
|
? "You can't block yourself"
|
||||||
|
: blocked
|
||||||
|
? "Already blocked"
|
||||||
|
: null
|
||||||
|
: "Enter a user ID, like @user:example.com";
|
||||||
|
final canBlock = userId.isNotEmpty && error == null && !saving.active;
|
||||||
|
|
||||||
|
void block() {
|
||||||
|
if (!canBlock) return;
|
||||||
|
saving
|
||||||
|
.run(() async {
|
||||||
|
await ref
|
||||||
|
.read(AccountDataController.provider.notifier)
|
||||||
|
.setIgnored(userId, ignored: true);
|
||||||
|
if (context.mounted) controller.clear();
|
||||||
|
})
|
||||||
|
.onError(showError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8).copyWith(top: 8),
|
||||||
|
child: TextField(
|
||||||
|
controller: controller,
|
||||||
|
enabled: !saving.active,
|
||||||
|
onSubmitted: (_) => block(),
|
||||||
|
decoration: .new(
|
||||||
|
labelText: "User ID",
|
||||||
|
hintText: "@user:example.com",
|
||||||
|
errorText: error,
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
tooltip: "Block user",
|
||||||
|
icon: Icon(Icons.block),
|
||||||
|
onPressed: canBlock ? block : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:intl/intl.dart";
|
import "package:intl/intl.dart";
|
||||||
import "package:m3e_buttons/m3e_buttons.dart";
|
import "package:m3e_buttons/m3e_buttons.dart";
|
||||||
|
import "package:nexus/controllers/account_data.dart";
|
||||||
import "package:nexus/controllers/client.dart";
|
import "package:nexus/controllers/client.dart";
|
||||||
import "package:nexus/controllers/client_state.dart";
|
import "package:nexus/controllers/client_state.dart";
|
||||||
import "package:nexus/controllers/mutual_rooms.dart";
|
import "package:nexus/controllers/mutual_rooms.dart";
|
||||||
|
|
@ -17,6 +18,7 @@ import "package:nexus/helpers/extensions/better_when.dart";
|
||||||
import "package:nexus/helpers/extensions/create_room.dart";
|
import "package:nexus/helpers/extensions/create_room.dart";
|
||||||
import "package:nexus/helpers/extensions/show_adaptive_sheet.dart";
|
import "package:nexus/helpers/extensions/show_adaptive_sheet.dart";
|
||||||
import "package:nexus/helpers/extensions/success_scheme.dart";
|
import "package:nexus/helpers/extensions/success_scheme.dart";
|
||||||
|
import "package:nexus/helpers/extensions/with_progress_snackbar.dart";
|
||||||
import "package:nexus/helpers/extensions/get_localpart.dart";
|
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||||
import "package:nexus/widgets/error_dialog.dart";
|
import "package:nexus/widgets/error_dialog.dart";
|
||||||
import "package:nexus/widgets/loading.dart";
|
import "package:nexus/widgets/loading.dart";
|
||||||
|
|
@ -145,7 +147,65 @@ final class const UserBottomSheet(
|
||||||
if (!editable) ...[
|
if (!editable) ...[
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: .end,
|
mainAxisAlignment: .end,
|
||||||
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
|
if (userId !=
|
||||||
|
ref.watch(ClientStateController.provider)?.userId)
|
||||||
|
Consumer(
|
||||||
|
builder: (context, ref, _) {
|
||||||
|
final ignored = ref.watch(
|
||||||
|
AccountDataController.provider.select(
|
||||||
|
(data) => data.ignoredUserList.contains(userId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final name = member.displayName ?? userId.localpart;
|
||||||
|
Future<void> setIgnored(
|
||||||
|
bool ignored,
|
||||||
|
) => context.withProgressSnackbar(
|
||||||
|
"${ignored ? "Blocking" : "Unblocking"} $name...",
|
||||||
|
() => ref
|
||||||
|
.read(AccountDataController.provider.notifier)
|
||||||
|
.setIgnored(userId, ignored: ignored),
|
||||||
|
);
|
||||||
|
|
||||||
|
return M3EButton(
|
||||||
|
tooltip: ignored ? "Unblock" : "Block",
|
||||||
|
onPressed: ignored
|
||||||
|
? () => setIgnored(false)
|
||||||
|
: () => context.showAdaptiveSheet(
|
||||||
|
(context) => SheetLayout(
|
||||||
|
title: Text("Block $name"),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: Navigator.of(context).pop,
|
||||||
|
child: Text("Cancel"),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
setIgnored(true);
|
||||||
|
},
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor:
|
||||||
|
theme.colorScheme.error,
|
||||||
|
),
|
||||||
|
child: Text("Block"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Are you sure you want to block $userId? You won't see their new messages or invites.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: .tonal,
|
||||||
|
child: Icon(
|
||||||
|
ignored ? Icons.lock_open : Icons.block,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
M3EButton(
|
M3EButton(
|
||||||
onPressed: Navigator.of(context).pop,
|
onPressed: Navigator.of(context).pop,
|
||||||
child: Icon(Icons.close),
|
child: Icon(Icons.close),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue