parent
6d9a8e812b
commit
a0605a3617
12 changed files with 334 additions and 84 deletions
2
gomuks
2
gomuks
|
|
@ -1 +1 @@
|
||||||
Subproject commit a59201a03b0f3e14e0ecf1ce2c964e6f1115ad4f
|
Subproject commit 9e6254b703a841d1e9ee64dbaf90d5a1b798696e
|
||||||
14
lib/controllers/capabilities.dart
Normal file
14
lib/controllers/capabilities.dart
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/client.dart";
|
||||||
|
import "package:nexus/models/capabilities.dart";
|
||||||
|
|
||||||
|
class CapabilitiesController extends AsyncNotifier<Capabilities> {
|
||||||
|
@override
|
||||||
|
Future<Capabilities> build() =>
|
||||||
|
ref.read(ClientController.provider.notifier).getCapabilities();
|
||||||
|
|
||||||
|
static final provider =
|
||||||
|
AsyncNotifierProvider<CapabilitiesController, Capabilities>(
|
||||||
|
CapabilitiesController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -488,11 +488,21 @@ class ClientController(final String? accountId) extends AsyncNotifier<int> {
|
||||||
}))["visibility"] ==
|
}))["visibility"] ==
|
||||||
"public";
|
"public";
|
||||||
|
|
||||||
Future<void> setRoomPublished(String roomId, bool published) =>
|
Future<void> setRoomPublished(String roomId, bool published) => _sendCommand(
|
||||||
_sendCommand("publish_room", {
|
"publish_room",
|
||||||
"room_id": roomId,
|
{"room_id": roomId, "visibility": published ? "public" : "private"},
|
||||||
"visibility": published ? "public" : "private",
|
);
|
||||||
});
|
|
||||||
|
Future<String> upgradeRoom(
|
||||||
|
String roomId,
|
||||||
|
String version, {
|
||||||
|
ISet<String> additionalCreators = const .empty(),
|
||||||
|
}) async => (await _sendCommand("upgrade_room", {
|
||||||
|
"room_id": roomId,
|
||||||
|
"new_version": version,
|
||||||
|
if (additionalCreators.isNotEmpty)
|
||||||
|
"additional_creators": additionalCreators.toList(),
|
||||||
|
}))["replacement_room"];
|
||||||
|
|
||||||
/// The room [alias] points to, or null if it doesn't exist.
|
/// The room [alias] points to, or null if it doesn't exist.
|
||||||
Future<String?> resolveAlias(String alias) async {
|
Future<String?> resolveAlias(String alias) async {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/rooms.dart";
|
import "package:nexus/controllers/rooms.dart";
|
||||||
|
import "package:nexus/helpers/extensions/room_version_at_least.dart";
|
||||||
import "package:nexus/models/content/content.dart";
|
import "package:nexus/models/content/content.dart";
|
||||||
import "package:nexus/models/content/create.dart";
|
import "package:nexus/models/content/create.dart";
|
||||||
import "package:nexus/models/content/power_levels.dart";
|
import "package:nexus/models/content/power_levels.dart";
|
||||||
|
|
@ -26,8 +27,7 @@ class RoomPowerLevelsController(final String roomId)
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
// Creators only outrank everyone from room version 12, earlier ones just have a power level
|
// Creators only outrank everyone from room version 12, earlier ones just have a power level
|
||||||
final creatorsPrivileged =
|
final creatorsPrivileged = (create?.roomVersion).versionAtLeast(12);
|
||||||
(int.tryParse(create?.roomVersion ?? "") ?? 0) >= 12;
|
|
||||||
|
|
||||||
return .new(
|
return .new(
|
||||||
content: switch (stateEvent(.powerLevels)?.content) {
|
content: switch (stateEvent(.powerLevels)?.content) {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import "package:nexus/controllers/room_published.dart";
|
||||||
import "package:nexus/controllers/rooms.dart";
|
import "package:nexus/controllers/rooms.dart";
|
||||||
import "package:nexus/controllers/space_edges.dart";
|
import "package:nexus/controllers/space_edges.dart";
|
||||||
import "package:nexus/helpers/extensions/join_rule_name.dart";
|
import "package:nexus/helpers/extensions/join_rule_name.dart";
|
||||||
|
import "package:nexus/helpers/extensions/room_version_at_least.dart";
|
||||||
import "package:nexus/helpers/file_dialog_helper.dart";
|
import "package:nexus/helpers/file_dialog_helper.dart";
|
||||||
import "package:nexus/helpers/hooks/saving.dart";
|
import "package:nexus/helpers/hooks/saving.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
|
|
@ -29,6 +30,7 @@ import "package:nexus/models/content/membership.dart";
|
||||||
import "package:nexus/models/content/message.dart";
|
import "package:nexus/models/content/message.dart";
|
||||||
import "package:nexus/models/content/name.dart";
|
import "package:nexus/models/content/name.dart";
|
||||||
import "package:nexus/models/content/power_levels.dart";
|
import "package:nexus/models/content/power_levels.dart";
|
||||||
|
import "package:nexus/models/content/tombstone.dart";
|
||||||
import "package:nexus/models/content/topic.dart";
|
import "package:nexus/models/content/topic.dart";
|
||||||
import "package:nexus/models/join_rule.dart";
|
import "package:nexus/models/join_rule.dart";
|
||||||
import "package:nexus/models/power_level_permission.dart";
|
import "package:nexus/models/power_level_permission.dart";
|
||||||
|
|
@ -46,6 +48,7 @@ import "package:nexus/widgets/settings/local_aliases_sheet.dart";
|
||||||
import "package:nexus/widgets/settings/power_level_entry_sheet.dart";
|
import "package:nexus/widgets/settings/power_level_entry_sheet.dart";
|
||||||
import "package:nexus/widgets/settings/power_level_tile.dart";
|
import "package:nexus/widgets/settings/power_level_tile.dart";
|
||||||
import "package:nexus/widgets/settings/text_setting_tile.dart";
|
import "package:nexus/widgets/settings/text_setting_tile.dart";
|
||||||
|
import "package:nexus/widgets/settings/upgrade_room_sheet.dart";
|
||||||
|
|
||||||
class RoomSettingsSectionsController(final String roomId)
|
class RoomSettingsSectionsController(final String roomId)
|
||||||
extends AsyncNotifier<IMap<String, IList<SettingsCategory>>> {
|
extends AsyncNotifier<IMap<String, IList<SettingsCategory>>> {
|
||||||
|
|
@ -122,18 +125,18 @@ class RoomSettingsSectionsController(final String roomId)
|
||||||
?canonicalAlias.alias,
|
?canonicalAlias.alias,
|
||||||
]).toISet();
|
]).toISet();
|
||||||
|
|
||||||
final roomVersion = switch (stateContent(.create)) {
|
final versionName = switch (stateContent(.create)) {
|
||||||
CreateContent(:final roomVersion) => int.tryParse(roomVersion),
|
CreateContent(:final roomVersion) => roomVersion,
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
bool versionAtLeast(int version) =>
|
final canUpgrade =
|
||||||
roomVersion == null || roomVersion >= version;
|
canSet(.tombstone) && stateContent(.tombstone) is! TombstoneContent;
|
||||||
final joinRuleOptions = [
|
final joinRuleOptions = [
|
||||||
JoinRule.public,
|
JoinRule.public,
|
||||||
JoinRule.invite,
|
JoinRule.invite,
|
||||||
if (versionAtLeast(7)) JoinRule.knock,
|
if (versionName.versionAtLeast(7)) JoinRule.knock,
|
||||||
if (versionAtLeast(8)) JoinRule.restricted,
|
if (versionName.versionAtLeast(8)) JoinRule.restricted,
|
||||||
if (versionAtLeast(10)) JoinRule.knockRestricted,
|
if (versionName.versionAtLeast(10)) JoinRule.knockRestricted,
|
||||||
];
|
];
|
||||||
bool isRestricted(JoinRule rule) =>
|
bool isRestricted(JoinRule rule) =>
|
||||||
rule == .restricted || rule == .knockRestricted;
|
rule == .restricted || rule == .knockRestricted;
|
||||||
|
|
@ -577,9 +580,8 @@ class RoomSettingsSectionsController(final String roomId)
|
||||||
? (value) => saving.run(
|
? (value) => saving.run(
|
||||||
() => ref
|
() => ref
|
||||||
.read(
|
.read(
|
||||||
RoomPublishedController.provider(
|
RoomPublishedController.provider(roomId)
|
||||||
roomId,
|
.notifier,
|
||||||
).notifier,
|
|
||||||
)
|
)
|
||||||
.set(value)
|
.set(value)
|
||||||
.onError(showError),
|
.onError(showError),
|
||||||
|
|
@ -589,6 +591,52 @@ class RoomSettingsSectionsController(final String roomId)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
.new(
|
||||||
|
title: "Room Version",
|
||||||
|
description: "The rules the room follows, set when it's created.",
|
||||||
|
icon: Icons.upgrade,
|
||||||
|
builder: (title, description, icon) => HookBuilder(
|
||||||
|
builder: (context) {
|
||||||
|
final saving = Saving.use();
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(icon),
|
||||||
|
title: Text(title),
|
||||||
|
subtitle: Text(
|
||||||
|
versionName == null
|
||||||
|
? description
|
||||||
|
: "Version $versionName",
|
||||||
|
),
|
||||||
|
trailing: TextButton(
|
||||||
|
onPressed:
|
||||||
|
canUpgrade && versionName != null && !saving.active
|
||||||
|
? () => context.showAdaptiveSheet(
|
||||||
|
(_) => UpgradeRoomSheet(
|
||||||
|
roomId: roomId,
|
||||||
|
currentVersion: versionName,
|
||||||
|
creators: roomPowerLevels.creators.remove(
|
||||||
|
userId ?? "",
|
||||||
|
),
|
||||||
|
userId: userId,
|
||||||
|
onSave: (version, creators) => saving.run(
|
||||||
|
() => client
|
||||||
|
.upgradeRoom(
|
||||||
|
roomId,
|
||||||
|
version,
|
||||||
|
additionalCreators: creators,
|
||||||
|
)
|
||||||
|
.then<void>((_) {})
|
||||||
|
.onError(showError),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
child: Text("Upgrade"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
.new(
|
.new(
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:intl/intl.dart";
|
import "package:intl/intl.dart";
|
||||||
import "package:nexus/controllers/accounts.dart";
|
import "package:nexus/controllers/accounts.dart";
|
||||||
import "package:nexus/controllers/active_account.dart";
|
import "package:nexus/controllers/active_account.dart";
|
||||||
|
import "package:nexus/controllers/capabilities.dart";
|
||||||
import "package:nexus/controllers/selected_room.dart";
|
import "package:nexus/controllers/selected_room.dart";
|
||||||
import "package:nexus/controllers/notification.dart";
|
import "package:nexus/controllers/notification.dart";
|
||||||
import "package:nexus/controllers/client_state.dart";
|
import "package:nexus/controllers/client_state.dart";
|
||||||
|
|
@ -215,9 +216,9 @@ class UnifiedPushController extends AsyncNotifier<String?> {
|
||||||
state = .loading();
|
state = .loading();
|
||||||
try {
|
try {
|
||||||
await Future(() async {
|
await Future(() async {
|
||||||
final capabilities = await ref
|
final capabilities = await ref.read(
|
||||||
.read(ClientController.provider.notifier)
|
CapabilitiesController.provider.future,
|
||||||
.getCapabilities();
|
);
|
||||||
|
|
||||||
if (capabilities.webpush?.vapid == null) {
|
if (capabilities.webpush?.vapid == null) {
|
||||||
throw UnsupportedError(
|
throw UnsupportedError(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import "dart:io";
|
import "dart:io";
|
||||||
|
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/client.dart";
|
import "package:nexus/controllers/capabilities.dart";
|
||||||
import "package:nexus/controllers/unified_push_distributors.dart";
|
import "package:nexus/controllers/unified_push_distributors.dart";
|
||||||
|
|
||||||
class UnifiedPushAllowedController extends AsyncNotifier<String?> {
|
class UnifiedPushAllowedController extends AsyncNotifier<String?> {
|
||||||
|
|
@ -19,9 +19,9 @@ class UnifiedPushAllowedController extends AsyncNotifier<String?> {
|
||||||
: "Your platform is not currently supported by UnifiedPush"}.";
|
: "Your platform is not currently supported by UnifiedPush"}.";
|
||||||
}
|
}
|
||||||
|
|
||||||
final capabilities = await ref
|
final capabilities = await ref.watch(
|
||||||
.read(ClientController.provider.notifier)
|
CapabilitiesController.provider.future,
|
||||||
.getCapabilities();
|
);
|
||||||
|
|
||||||
if (capabilities.webpush?.vapid == null) {
|
if (capabilities.webpush?.vapid == null) {
|
||||||
return "Your homeserver does not support MSC4174 (Web Push), and therefore cannot send notifications to Nexus.";
|
return "Your homeserver does not support MSC4174 (Web Push), and therefore cannot send notifications to Nexus.";
|
||||||
|
|
|
||||||
8
lib/helpers/extensions/room_version_at_least.dart
Normal file
8
lib/helpers/extensions/room_version_at_least.dart
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
extension RoomVersionAtLeast on String? {
|
||||||
|
/// Whether this room version is [version] or newer. Unknown ones, like
|
||||||
|
/// unstable versions, are assumed to be new enough.
|
||||||
|
bool versionAtLeast(int version) => switch (int.tryParse(this ?? "")) {
|
||||||
|
final number? => number >= version,
|
||||||
|
null => true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:freezed_annotation/freezed_annotation.dart";
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
|
||||||
part "capabilities.freezed.dart";
|
part "capabilities.freezed.dart";
|
||||||
|
|
@ -7,6 +8,7 @@ part "capabilities.g.dart";
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class const Capabilities({
|
class const Capabilities({
|
||||||
@JsonKey(name: "org.matrix.msc4174.webpush") final WebPush? webpush,
|
@JsonKey(name: "org.matrix.msc4174.webpush") final WebPush? webpush,
|
||||||
|
@JsonKey(name: "m.room_versions") final RoomVersions? roomVersions,
|
||||||
}) with _$Capabilities {
|
}) with _$Capabilities {
|
||||||
Map<String, Object?> toJson() => _$CapabilitiesToJson(this);
|
Map<String, Object?> toJson() => _$CapabilitiesToJson(this);
|
||||||
|
|
||||||
|
|
@ -23,3 +25,30 @@ class const WebPush({required final bool enabled, final String? vapid})
|
||||||
factory WebPush.fromJson(Map<String, Object?> json) =>
|
factory WebPush.fromJson(Map<String, Object?> json) =>
|
||||||
_$WebPushFromJson(json);
|
_$WebPushFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Freezed(toJson: false, fromJson: false)
|
||||||
|
@JsonSerializable()
|
||||||
|
class const RoomVersions({
|
||||||
|
@JsonKey(name: "default") required final String defaultVersion,
|
||||||
|
@JsonKey(fromJson: RoomVersionStability.mapFromJson)
|
||||||
|
required final IMap<String, RoomVersionStability> available,
|
||||||
|
}) with _$RoomVersions {
|
||||||
|
Map<String, Object?> toJson() => _$RoomVersionsToJson(this);
|
||||||
|
|
||||||
|
factory RoomVersions.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$RoomVersionsFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RoomVersionStability {
|
||||||
|
stable,
|
||||||
|
unstable,
|
||||||
|
unknown;
|
||||||
|
|
||||||
|
static IMap<String, RoomVersionStability> mapFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => IMap.fromJson(
|
||||||
|
json,
|
||||||
|
(version) => version as String,
|
||||||
|
(stability) => values.asNameMap()[stability] ?? unknown,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,9 @@
|
||||||
import "package:collection/collection.dart";
|
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:flutter_hooks/flutter_hooks.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_ui/material_ui.dart";
|
||||||
import "package:material_adaptive_sheets/material_adaptive_sheets.dart";
|
import "package:material_adaptive_sheets/material_adaptive_sheets.dart";
|
||||||
import "package:nexus/controllers/members.dart";
|
import "package:nexus/widgets/settings/member_chooser.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/power_level_field.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.
|
/// Chooses users of [roomId] to give a power level of their own.
|
||||||
class const AddUsersPowerLevelSheet({
|
class const AddUsersPowerLevelSheet({
|
||||||
|
|
@ -22,43 +16,16 @@ class const AddUsersPowerLevelSheet({
|
||||||
required final double ownLevel,
|
required final double ownLevel,
|
||||||
required final void Function(ISet<String> users, int level) onSave,
|
required final void Function(ISet<String> users, int level) onSave,
|
||||||
super.key,
|
super.key,
|
||||||
}) extends HookConsumerWidget {
|
}) extends HookWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context) {
|
||||||
final chosen = useState(const ISet<String>.empty());
|
final chosen = useState(const ISet<String>.empty());
|
||||||
final error = useState<String?>(null);
|
|
||||||
final searchController = useTextEditingController();
|
|
||||||
final levelController = useTextEditingController(text: "0");
|
final levelController = useTextEditingController(text: "0");
|
||||||
final level = PowerLevelField.levelFrom(
|
final level = PowerLevelField.levelFrom(
|
||||||
useValueListenable(levelController).text,
|
useValueListenable(levelController).text,
|
||||||
ownLevel,
|
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;
|
final valid = chosen.value.isNotEmpty && level != null;
|
||||||
void save() {
|
void save() {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
|
@ -72,33 +39,15 @@ class const AddUsersPowerLevelSheet({
|
||||||
TextButton(onPressed: valid ? save : null, child: Text("Save")),
|
TextButton(onPressed: valid ? save : null, child: Text("Save")),
|
||||||
],
|
],
|
||||||
children: [
|
children: [
|
||||||
SearchChooser(
|
MemberChooser(
|
||||||
controller: searchController,
|
roomId: roomId,
|
||||||
chosen: chosen.value,
|
chosen: chosen.value,
|
||||||
options: members.keys
|
excluded: existing,
|
||||||
.where((id) => !existing.contains(id))
|
excludedError: "Already has a power level",
|
||||||
.sortedBy((id) => nameOf(id).toLowerCase())
|
onAdded: (id) => chosen.value = chosen.value.add(id),
|
||||||
.toIList(),
|
|
||||||
nameOf: nameOf,
|
|
||||||
avatarOf: (id) =>
|
|
||||||
AvatarOrHash(members[id]?.avatarUrl, nameOf(id), dimension: 40),
|
|
||||||
subtitleOf: (id) => id,
|
|
||||||
onAdded: add,
|
|
||||||
onRemoved: (id) => chosen.value = chosen.value.remove(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",
|
label: "Add user",
|
||||||
hint: "Search members, or enter a user ID",
|
|
||||||
removeTooltip: "Remove user",
|
removeTooltip: "Remove user",
|
||||||
error: error.value,
|
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
),
|
),
|
||||||
PowerLevelField(
|
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