parent
6d9a8e812b
commit
a0605a3617
3 changed files with 334 additions and 84 deletions
|
|
@ -1,15 +1,9 @@
|
|||
import "package:collection/collection.dart";
|
||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:material_adaptive_sheets/material_adaptive_sheets.dart";
|
||||
import "package:nexus/controllers/members.dart";
|
||||
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||
import "package:nexus/models/content/membership.dart";
|
||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||
import "package:nexus/widgets/settings/member_chooser.dart";
|
||||
import "package:nexus/widgets/settings/power_level_field.dart";
|
||||
import "package:nexus/widgets/settings/search_chooser.dart";
|
||||
|
||||
/// Chooses users of [roomId] to give a power level of their own.
|
||||
class const AddUsersPowerLevelSheet({
|
||||
|
|
@ -22,43 +16,16 @@ class const AddUsersPowerLevelSheet({
|
|||
required final double ownLevel,
|
||||
required final void Function(ISet<String> users, int level) onSave,
|
||||
super.key,
|
||||
}) extends HookConsumerWidget {
|
||||
}) extends HookWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
final chosen = useState(const ISet<String>.empty());
|
||||
final error = useState<String?>(null);
|
||||
final searchController = useTextEditingController();
|
||||
final levelController = useTextEditingController(text: "0");
|
||||
final level = PowerLevelField.levelFrom(
|
||||
useValueListenable(levelController).text,
|
||||
ownLevel,
|
||||
);
|
||||
|
||||
final members = IMap.fromEntries(
|
||||
(ref.watch(MembersController.provider(roomId)).value ??
|
||||
const ISet.empty())
|
||||
.map(
|
||||
(event) => switch ((event.stateKey, event.content)) {
|
||||
(final userId?, final MembershipContent content)
|
||||
when content.status == .join || content.status == .invite =>
|
||||
MapEntry(userId, content),
|
||||
_ => null,
|
||||
},
|
||||
)
|
||||
.nonNulls,
|
||||
);
|
||||
|
||||
String nameOf(String id) => members[id]?.displayName ?? id.localpart;
|
||||
|
||||
void add(String id) {
|
||||
if (existing.contains(id)) {
|
||||
error.value = "Already has a power level";
|
||||
return;
|
||||
}
|
||||
chosen.value = chosen.value.add(id);
|
||||
searchController.clear();
|
||||
}
|
||||
|
||||
final valid = chosen.value.isNotEmpty && level != null;
|
||||
void save() {
|
||||
Navigator.of(context).pop();
|
||||
|
|
@ -72,33 +39,15 @@ class const AddUsersPowerLevelSheet({
|
|||
TextButton(onPressed: valid ? save : null, child: Text("Save")),
|
||||
],
|
||||
children: [
|
||||
SearchChooser(
|
||||
controller: searchController,
|
||||
MemberChooser(
|
||||
roomId: roomId,
|
||||
chosen: chosen.value,
|
||||
options: members.keys
|
||||
.where((id) => !existing.contains(id))
|
||||
.sortedBy((id) => nameOf(id).toLowerCase())
|
||||
.toIList(),
|
||||
nameOf: nameOf,
|
||||
avatarOf: (id) =>
|
||||
AvatarOrHash(members[id]?.avatarUrl, nameOf(id), dimension: 40),
|
||||
subtitleOf: (id) => id,
|
||||
onAdded: add,
|
||||
excluded: existing,
|
||||
excludedError: "Already has a power level",
|
||||
onAdded: (id) => chosen.value = chosen.value.add(id),
|
||||
onRemoved: (id) => chosen.value = chosen.value.remove(id),
|
||||
onSubmitted: (text, topMatch) {
|
||||
if (RegExp(r"^@[^:]+:.+$").hasMatch(text)) {
|
||||
add(text);
|
||||
} else if (topMatch != null) {
|
||||
add(topMatch);
|
||||
} else {
|
||||
error.value = "Enter a user ID";
|
||||
}
|
||||
},
|
||||
onChanged: (_) => error.value = null,
|
||||
label: "Add user",
|
||||
hint: "Search members, or enter a user ID",
|
||||
removeTooltip: "Remove user",
|
||||
error: error.value,
|
||||
autofocus: true,
|
||||
),
|
||||
PowerLevelField(
|
||||
|
|
|
|||
91
lib/widgets/settings/member_chooser.dart
Normal file
91
lib/widgets/settings/member_chooser.dart
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import "package:collection/collection.dart";
|
||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
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/members.dart";
|
||||
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||
import "package:nexus/models/content/membership.dart";
|
||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||
import "package:nexus/widgets/settings/search_chooser.dart";
|
||||
|
||||
/// The [chosen] users, above a field searching [roomId]'s members, or taking a user ID, to choose more.
|
||||
class const MemberChooser({
|
||||
required final String roomId,
|
||||
required final ISet<String> chosen,
|
||||
required final void Function(String userId) onAdded,
|
||||
required final void Function(String userId) onRemoved,
|
||||
required final String label,
|
||||
required final String removeTooltip,
|
||||
|
||||
/// Users who can't be chosen, so aren't suggested.
|
||||
final ISet<String> excluded = const .empty(),
|
||||
|
||||
/// Shown when trying to choose one of the [excluded] users.
|
||||
final String excludedError = "Can't be chosen",
|
||||
final String? emptyText,
|
||||
final bool autofocus = false,
|
||||
super.key,
|
||||
}) extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final error = useState<String?>(null);
|
||||
final searchController = useTextEditingController();
|
||||
|
||||
final members = IMap.fromEntries(
|
||||
(ref.watch(MembersController.provider(roomId)).value ??
|
||||
const ISet.empty())
|
||||
.map(
|
||||
(event) => switch ((event.stateKey, event.content)) {
|
||||
(final userId?, final MembershipContent content)
|
||||
when content.status == .join || content.status == .invite =>
|
||||
MapEntry(userId, content),
|
||||
_ => null,
|
||||
},
|
||||
)
|
||||
.nonNulls,
|
||||
);
|
||||
|
||||
String nameOf(String id) => members[id]?.displayName ?? id.localpart;
|
||||
|
||||
void add(String id) {
|
||||
if (excluded.contains(id)) {
|
||||
error.value = excludedError;
|
||||
return;
|
||||
}
|
||||
onAdded(id);
|
||||
searchController.clear();
|
||||
}
|
||||
|
||||
return SearchChooser(
|
||||
controller: searchController,
|
||||
chosen: chosen,
|
||||
options: members.keys
|
||||
.where((id) => !excluded.contains(id))
|
||||
.sortedBy((id) => nameOf(id).toLowerCase())
|
||||
.toIList(),
|
||||
nameOf: nameOf,
|
||||
avatarOf: (id) =>
|
||||
AvatarOrHash(members[id]?.avatarUrl, nameOf(id), dimension: 40),
|
||||
subtitleOf: (id) => id,
|
||||
onAdded: add,
|
||||
onRemoved: onRemoved,
|
||||
onSubmitted: (text, topMatch) {
|
||||
if (RegExp(r"^@[^:]+:.+$").hasMatch(text)) {
|
||||
add(text);
|
||||
} else if (topMatch != null) {
|
||||
add(topMatch);
|
||||
} else {
|
||||
error.value = "Enter a user ID";
|
||||
}
|
||||
},
|
||||
onChanged: (_) => error.value = null,
|
||||
label: label,
|
||||
hint: "Search members, or enter a user ID",
|
||||
removeTooltip: removeTooltip,
|
||||
emptyText: emptyText,
|
||||
error: error.value,
|
||||
autofocus: autofocus,
|
||||
);
|
||||
}
|
||||
}
|
||||
100
lib/widgets/settings/upgrade_room_sheet.dart
Normal file
100
lib/widgets/settings/upgrade_room_sheet.dart
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import "package:collection/collection.dart";
|
||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:material_adaptive_sheets/material_adaptive_sheets.dart";
|
||||
import "package:nexus/controllers/capabilities.dart";
|
||||
import "package:nexus/helpers/extensions/better_when.dart";
|
||||
import "package:nexus/helpers/extensions/room_version_at_least.dart";
|
||||
import "package:nexus/widgets/settings/dialog_list_tile.dart";
|
||||
import "package:nexus/widgets/settings/member_chooser.dart";
|
||||
|
||||
/// Chooses a room version to upgrade [roomId] to, and its additional creators if the version has them.
|
||||
class const UpgradeRoomSheet({
|
||||
required final String roomId,
|
||||
required final String currentVersion,
|
||||
|
||||
/// The room's current creators, other than the user, to keep in the new room.
|
||||
required final ISet<String> creators,
|
||||
required final String? userId,
|
||||
required final void Function(String version, ISet<String> creators) onSave,
|
||||
super.key,
|
||||
}) extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final roomVersions = ref
|
||||
.watch(CapabilitiesController.provider)
|
||||
.whenData((capabilities) => capabilities.roomVersions);
|
||||
final chosen = useState<String?>(null);
|
||||
final chosenCreators = useState(creators);
|
||||
|
||||
final versions = roomVersions.value;
|
||||
final version = chosen.value ?? versions?.defaultVersion;
|
||||
|
||||
final valid = version != null;
|
||||
final withCreators = valid && version.versionAtLeast(12);
|
||||
void save() {
|
||||
Navigator.of(context).pop();
|
||||
onSave(version!, withCreators ? chosenCreators.value : const .empty());
|
||||
}
|
||||
|
||||
return SheetLayout(
|
||||
title: Text("Upgrade Room"),
|
||||
actions: [
|
||||
TextButton(onPressed: Navigator.of(context).pop, child: Text("Cancel")),
|
||||
TextButton(onPressed: valid ? save : null, child: Text("Upgrade")),
|
||||
],
|
||||
children: [
|
||||
Text(
|
||||
"Upgrading creates a new room with the chosen version and closes this one, pointing its members to the new room. Members will have to join the new room themselves.",
|
||||
),
|
||||
roomVersions.betterWhen(
|
||||
data: (versions) => versions == null
|
||||
? Text(
|
||||
"Your homeserver didn't list the room versions it supports.",
|
||||
)
|
||||
: DialogListTile<String>(
|
||||
icon: Icon(Icons.upgrade),
|
||||
title: "Room Version",
|
||||
subtitle: Text("The version the new room will have."),
|
||||
value: version,
|
||||
options: versions.available.keys.sortedBy(
|
||||
(option) => int.tryParse(option) ?? double.infinity,
|
||||
),
|
||||
getName: (option) => switch ([
|
||||
if (option == currentVersion) "current",
|
||||
if (option == versions.defaultVersion) "default",
|
||||
if (versions.available[option] == .unstable) "unstable",
|
||||
]) {
|
||||
final labels when labels.isNotEmpty =>
|
||||
"$option (${labels.join(", ")})",
|
||||
_ => option,
|
||||
},
|
||||
onChanged: (value) => chosen.value = value,
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: Durations.medium2,
|
||||
curve: Curves.easeInOut,
|
||||
alignment: .topCenter,
|
||||
child: withCreators
|
||||
? MemberChooser(
|
||||
roomId: roomId,
|
||||
chosen: chosenCreators.value,
|
||||
excluded: ISet([?userId]),
|
||||
excludedError: "You'll be a creator already",
|
||||
onAdded: (id) =>
|
||||
chosenCreators.value = chosenCreators.value.add(id),
|
||||
onRemoved: (id) =>
|
||||
chosenCreators.value = chosenCreators.value.remove(id),
|
||||
label: "Add creator",
|
||||
removeTooltip: "Remove creator",
|
||||
emptyText: "Creators outrank everyone in the new room, and can't be demoted. You'll be one, and can add others.",
|
||||
)
|
||||
: const SizedBox(width: .infinity),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue