parent
b6c6ac35d9
commit
20eed13884
8 changed files with 142 additions and 116 deletions
|
|
@ -5,6 +5,7 @@ import "package:nexus/controllers/members_by_status.dart";
|
||||||
import "package:nexus/controllers/rooms.dart";
|
import "package:nexus/controllers/rooms.dart";
|
||||||
import "package:nexus/controllers/via.dart";
|
import "package:nexus/controllers/via.dart";
|
||||||
import "package:nexus/helpers/extensions/get_localpart.dart";
|
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||||
|
import "package:nexus/helpers/extensions/weighted_search.dart";
|
||||||
import "package:nexus/models/bot_argument_suggestion.dart";
|
import "package:nexus/models/bot_argument_suggestion.dart";
|
||||||
import "package:nexus/models/bot_command_draft.dart";
|
import "package:nexus/models/bot_command_draft.dart";
|
||||||
import "package:nexus/models/content/bot_command_description.dart";
|
import "package:nexus/models/content/bot_command_description.dart";
|
||||||
|
|
@ -91,13 +92,10 @@ class BotArgumentSuggestionsController(final String roomId)
|
||||||
)
|
)
|
||||||
.nonNulls,
|
.nonNulls,
|
||||||
)
|
)
|
||||||
.where(
|
.where((suggestion) => !picked.contains(suggestion.text))
|
||||||
(suggestion) =>
|
.search(
|
||||||
!picked.contains(suggestion.text) &&
|
query,
|
||||||
[
|
(suggestion) => [(suggestion.title, 2), (suggestion.subtitle, 1)],
|
||||||
suggestion.title,
|
|
||||||
suggestion.subtitle,
|
|
||||||
].any((text) => text?.toLowerCase().contains(query) == true),
|
|
||||||
)
|
)
|
||||||
.take(8)
|
.take(8)
|
||||||
.toIList();
|
.toIList();
|
||||||
|
|
|
||||||
47
lib/helpers/extensions/weighted_search.dart
Normal file
47
lib/helpers/extensions/weighted_search.dart
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import "package:collection/collection.dart";
|
||||||
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
|
|
||||||
|
extension WeightedSearch<T> on Iterable<T> {
|
||||||
|
/// The items with a field matching [query], best match first.
|
||||||
|
IList<T> search(
|
||||||
|
String query,
|
||||||
|
Iterable<(String?, int)> Function(T item) fieldsOf,
|
||||||
|
) {
|
||||||
|
final needle = query.trim().toLowerCase();
|
||||||
|
if (needle.isEmpty) return toIList();
|
||||||
|
|
||||||
|
int qualityOf(String text) {
|
||||||
|
final haystack = text.toLowerCase();
|
||||||
|
if (haystack == needle) return 4;
|
||||||
|
if (haystack.startsWith(needle)) return 3;
|
||||||
|
if (haystack
|
||||||
|
.split(RegExp(r"[\s_\-:/.@#!]+"))
|
||||||
|
.any((w) => w.startsWith(needle))) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (haystack.contains(needle)) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return .new(
|
||||||
|
map(
|
||||||
|
(item) => (
|
||||||
|
item,
|
||||||
|
fieldsOf(item)
|
||||||
|
.map(
|
||||||
|
(field) => switch (field) {
|
||||||
|
(final text?, final weight) =>
|
||||||
|
qualityOf(text) * weight,
|
||||||
|
(null, _) => 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.maxOrNull ??
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.where((scored) => scored.$2 > 0)
|
||||||
|
.sortedBy((scored) => -scored.$2)
|
||||||
|
.map((scored) => scored.$1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:material_ui/material_ui.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
import "package:nexus/controllers/all_emoji.dart";
|
import "package:nexus/controllers/all_emoji.dart";
|
||||||
import "package:nexus/helpers/extensions/better_when.dart";
|
import "package:nexus/helpers/extensions/better_when.dart";
|
||||||
|
import "package:nexus/helpers/extensions/weighted_search.dart";
|
||||||
import "package:nexus/models/content/image_pack.dart";
|
import "package:nexus/models/content/image_pack.dart";
|
||||||
import "package:nexus/models/image_pack.dart";
|
import "package:nexus/models/image_pack.dart";
|
||||||
|
|
||||||
|
|
@ -21,11 +22,13 @@ class const EmojiOverlay(
|
||||||
children: emojis
|
children: emojis
|
||||||
.map((element) => element.emojis)
|
.map((element) => element.emojis)
|
||||||
.flattened
|
.flattened
|
||||||
.where(
|
.search(
|
||||||
(emoji) =>
|
query,
|
||||||
emoji.aliases.join().contains(query) ||
|
(emoji) => [
|
||||||
emoji.description.contains(query) ||
|
...emoji.aliases.map((alias) => (alias, 3)),
|
||||||
emoji.tags.join().contains(query),
|
...emoji.tags.map((tag) => (tag, 2)),
|
||||||
|
(emoji.description, 1),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
.map(
|
.map(
|
||||||
(emoji) => ListTile(
|
(emoji) => ListTile(
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:material_ui/material_ui.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
import "package:nexus/controllers/rooms.dart";
|
import "package:nexus/controllers/rooms.dart";
|
||||||
import "package:nexus/controllers/via.dart";
|
import "package:nexus/controllers/via.dart";
|
||||||
|
import "package:nexus/helpers/extensions/weighted_search.dart";
|
||||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
|
|
||||||
class const RoomOverlay(
|
class const RoomOverlay(
|
||||||
|
|
@ -14,47 +15,46 @@ class const RoomOverlay(
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final rooms = ref.watch(RoomsController.provider);
|
final rooms = ref.watch(RoomsController.provider);
|
||||||
return ListView(
|
return ListView(
|
||||||
children:
|
children: rooms.values
|
||||||
(query.isEmpty
|
.search(
|
||||||
? rooms.values
|
query,
|
||||||
: rooms.values.where(
|
(room) => [
|
||||||
(room) => (room.metadata?.name ?? room.metadata?.id ?? "")
|
(room.metadata?.name, 3),
|
||||||
.toLowerCase()
|
(room.metadata?.canonicalAlias, 2),
|
||||||
.contains(query.toLowerCase()),
|
(room.metadata?.id, 1),
|
||||||
))
|
],
|
||||||
.map((room) {
|
)
|
||||||
final name =
|
.map((room) {
|
||||||
room.metadata?.name ??
|
final name =
|
||||||
room.metadata?.canonicalAlias ??
|
room.metadata?.name ??
|
||||||
room.metadata?.id ??
|
room.metadata?.canonicalAlias ??
|
||||||
"Unknown Room";
|
room.metadata?.id ??
|
||||||
return ListTile(
|
"Unknown Room";
|
||||||
leading: AvatarOrHash(
|
return ListTile(
|
||||||
room.metadata?.avatar,
|
leading: AvatarOrHash(
|
||||||
name,
|
room.metadata?.avatar,
|
||||||
fallback: Icon(Icons.numbers),
|
name,
|
||||||
),
|
fallback: Icon(Icons.numbers),
|
||||||
title: Text(name),
|
),
|
||||||
subtitle: room.metadata?.topic == null
|
title: Text(name),
|
||||||
? null
|
subtitle: room.metadata?.topic == null
|
||||||
: Text(room.metadata!.topic!, maxLines: 1),
|
? null
|
||||||
onTap: () {
|
: Text(room.metadata!.topic!, maxLines: 1),
|
||||||
final vias = ref
|
onTap: () {
|
||||||
.watch(ViaController.provider(room))
|
final vias = ref.watch(ViaController.provider(room)).viaQuery;
|
||||||
.viaQuery;
|
addTag(
|
||||||
addTag(
|
id: "[#$name](matrix:roomid/${room.metadata?.id.substring(1)}$vias)",
|
||||||
id: "[#$name](matrix:roomid/${room.metadata?.id.substring(1)}$vias)",
|
name:
|
||||||
name:
|
(room.metadata?.canonicalAlias ?? room.metadata?.id)
|
||||||
(room.metadata?.canonicalAlias ?? room.metadata?.id)
|
?.substring(1)
|
||||||
?.substring(1)
|
.split(":")
|
||||||
.split(":")
|
.first ??
|
||||||
.first ??
|
"",
|
||||||
"",
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
})
|
},
|
||||||
.toList(),
|
);
|
||||||
|
})
|
||||||
|
.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import "package:material_ui/material_ui.dart";
|
||||||
import "package:nexus/controllers/members_by_status.dart";
|
import "package:nexus/controllers/members_by_status.dart";
|
||||||
import "package:nexus/helpers/extensions/better_when.dart";
|
import "package:nexus/helpers/extensions/better_when.dart";
|
||||||
import "package:nexus/helpers/extensions/get_localpart.dart";
|
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||||
|
import "package:nexus/helpers/extensions/weighted_search.dart";
|
||||||
import "package:nexus/models/content/membership.dart";
|
import "package:nexus/models/content/membership.dart";
|
||||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
|
|
||||||
|
|
@ -20,45 +21,34 @@ class const UserOverlay(
|
||||||
)
|
)
|
||||||
.betterWhen(
|
.betterWhen(
|
||||||
data: (members) => ListView(
|
data: (members) => ListView(
|
||||||
children:
|
children: members
|
||||||
(query.isEmpty
|
.search(
|
||||||
? members
|
query,
|
||||||
: members.where(
|
(member) => [
|
||||||
(member) =>
|
if (member.content case MembershipContent(:final displayName))
|
||||||
member.stateKey?.toLowerCase().contains(
|
(displayName, 2),
|
||||||
query.toLowerCase(),
|
(member.stateKey, 1),
|
||||||
) ==
|
],
|
||||||
true ||
|
)
|
||||||
switch (member.content) {
|
.map(
|
||||||
MembershipContent(:final displayName) =>
|
(member) => switch (member.content) {
|
||||||
displayName?.toLowerCase().contains(
|
MembershipContent(:final displayName, :final avatarUrl) =>
|
||||||
query.toLowerCase(),
|
ListTile(
|
||||||
) ==
|
leading: AvatarOrHash(
|
||||||
true,
|
avatarUrl,
|
||||||
_ => false,
|
displayName ?? member.stateKey!.localpart,
|
||||||
},
|
),
|
||||||
))
|
title: Text(displayName ?? member.stateKey!.localpart),
|
||||||
.map(
|
subtitle: Text(member.stateKey!),
|
||||||
(member) => switch (member.content) {
|
onTap: () => addTag(
|
||||||
MembershipContent(:final displayName, :final avatarUrl) =>
|
id: "[@$displayName](matrix:u/${member.stateKey!.substring(1)})",
|
||||||
ListTile(
|
name: member.stateKey!.localpart,
|
||||||
leading: AvatarOrHash(
|
),
|
||||||
avatarUrl,
|
),
|
||||||
displayName ?? member.stateKey!.localpart,
|
_ => SizedBox.shrink(),
|
||||||
),
|
},
|
||||||
title: Text(
|
)
|
||||||
displayName ?? member.stateKey!.localpart,
|
.toList(),
|
||||||
),
|
|
||||||
subtitle: Text(member.stateKey!),
|
|
||||||
onTap: () => addTag(
|
|
||||||
id: "[@$displayName](matrix:u/${member.stateKey!.substring(1)})",
|
|
||||||
name: member.stateKey!.localpart,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_ => SizedBox.shrink(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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_hooks/flutter_hooks.dart";
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
import "package:material_ui/material_ui.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
|
import "package:nexus/helpers/extensions/weighted_search.dart";
|
||||||
|
|
||||||
/// The [chosen] items, above a field searching [options] to choose more.
|
/// The [chosen] items, above a field searching [options] to choose more.
|
||||||
class const SearchChooser({
|
class const SearchChooser({
|
||||||
|
|
@ -30,15 +31,10 @@ class const SearchChooser({
|
||||||
}) extends HookWidget {
|
}) extends HookWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final query = useValueListenable(controller).text.trim().toLowerCase();
|
final query = useValueListenable(controller).text;
|
||||||
final shown = options
|
final shown = options
|
||||||
.where(
|
.where((id) => !chosen.contains(id))
|
||||||
(id) =>
|
.search(query, (id) => [(nameOf(id), 2), (id, 1)]);
|
||||||
!chosen.contains(id) &&
|
|
||||||
(nameOf(id).toLowerCase().contains(query) ||
|
|
||||||
id.toLowerCase().contains(query)),
|
|
||||||
)
|
|
||||||
.toIList();
|
|
||||||
|
|
||||||
ListTile tileOf(String id, {Widget? trailing, VoidCallback? onTap}) =>
|
ListTile tileOf(String id, {Widget? trailing, VoidCallback? onTap}) =>
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import "package:hooks_riverpod/misc.dart";
|
||||||
import "package:m3e_card_list/m3e_card_list.dart";
|
import "package:m3e_card_list/m3e_card_list.dart";
|
||||||
import "package:navigation_rail_m3e/navigation_rail_m3e.dart";
|
import "package:navigation_rail_m3e/navigation_rail_m3e.dart";
|
||||||
import "package:nexus/helpers/extensions/better_when.dart";
|
import "package:nexus/helpers/extensions/better_when.dart";
|
||||||
|
import "package:nexus/helpers/extensions/weighted_search.dart";
|
||||||
import "package:nexus/models/settings_category.dart";
|
import "package:nexus/models/settings_category.dart";
|
||||||
import "package:nexus/widgets/pages/settings_category.dart";
|
import "package:nexus/widgets/pages/settings_category.dart";
|
||||||
import "package:nexus/widgets/divider_text.dart";
|
import "package:nexus/widgets/divider_text.dart";
|
||||||
|
|
@ -43,32 +44,22 @@ class const SettingsLayout({
|
||||||
// Only searched once shown, by which point these have loaded
|
// Only searched once shown, by which point these have loaded
|
||||||
final categories =
|
final categories =
|
||||||
ref.read(sectionsProvider).value ?? const IMap.empty();
|
ref.read(sectionsProvider).value ?? const IMap.empty();
|
||||||
final query = controller.text.toLowerCase();
|
|
||||||
|
|
||||||
final matches = categories.values.flattenedToList
|
final matches = categories.values.flattenedToList
|
||||||
.asMap()
|
.asMap()
|
||||||
.entries
|
.entries
|
||||||
.expand(
|
.expand(
|
||||||
(categoryEntry) => categoryEntry.value.settings
|
(categoryEntry) =>
|
||||||
.asMap()
|
categoryEntry.value.settings.asMap().entries.map(
|
||||||
.entries
|
|
||||||
.where(
|
|
||||||
(settingEntry) =>
|
|
||||||
settingEntry.value.title.toLowerCase().contains(
|
|
||||||
query,
|
|
||||||
) ||
|
|
||||||
settingEntry.value.description
|
|
||||||
.toLowerCase()
|
|
||||||
.contains(query),
|
|
||||||
)
|
|
||||||
.map(
|
|
||||||
(settingEntry) => (
|
(settingEntry) => (
|
||||||
(categoryEntry.key, settingEntry.key),
|
(categoryEntry.key, settingEntry.key),
|
||||||
settingEntry.value,
|
settingEntry.value,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toIList();
|
.search(
|
||||||
|
controller.text,
|
||||||
|
(match) => [(match.$2.title, 2), (match.$2.description, 1)],
|
||||||
|
);
|
||||||
|
|
||||||
return matches.map(
|
return matches.map(
|
||||||
(match) => ListTile(
|
(match) => ListTile(
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ 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/timezones.dart";
|
import "package:nexus/controllers/timezones.dart";
|
||||||
|
import "package:nexus/helpers/extensions/weighted_search.dart";
|
||||||
import "package:nexus/helpers/hooks/saving.dart";
|
import "package:nexus/helpers/hooks/saving.dart";
|
||||||
import "package:nexus/widgets/settings/inline_text_field.dart";
|
import "package:nexus/widgets/settings/inline_text_field.dart";
|
||||||
import "package:timezone/timezone.dart";
|
import "package:timezone/timezone.dart";
|
||||||
|
|
@ -84,7 +85,7 @@ class const TimezoneField({
|
||||||
onTap: () => choose(controller, null),
|
onTap: () => choose(controller, null),
|
||||||
),
|
),
|
||||||
...timezones
|
...timezones
|
||||||
.where((timezone) => timezone.toLowerCase().contains(query))
|
.search(query, (timezone) => [(timezone, 1)])
|
||||||
.map(
|
.map(
|
||||||
(timezone) => ListTile(
|
(timezone) => ListTile(
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue