wip new emoji picker

has perf issues currently
This commit is contained in:
Henry Hiles 2026-09-15 21:34:16 -04:00
commit a0fa30e036
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
4 changed files with 142 additions and 15 deletions

View file

@ -4,7 +4,6 @@ import "package:collection/collection.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter/services.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:material_ui/material_ui.dart";
import "package:nexus/models/emoji.dart";
import "package:nexus/models/emoji_category.dart";
@ -25,7 +24,7 @@ class EmojiController extends AsyncNotifier<IList<EmojiCategory>> {
return .new(
grouped.entries.map(
(entry) => .new(
icon: Text(entry.value.first.emoji.emoji),
icon: entry.value.first.emoji.widget,
name: entry.key,
emojis: .new(entry.value.map((e) => e.emoji)),
),

View file

@ -1,17 +1,18 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:freezed_annotation/freezed_annotation.dart";
import "package:flutter/widgets.dart";
part "emoji.freezed.dart";
part "emoji.g.dart";
@Freezed(toJson: false, fromJson: false)
@JsonSerializable()
@JsonSerializable(createToJson: false)
class Emoji({
required final String emoji,
@JsonKey(fromJson: Emoji.widgetFromJson, name: "emoji")
required final Widget widget,
required final IList<String> aliases,
required final String description,
required final IList<String> tags,
}) with _$Emoji {
Map<String, Object?> toJson() => _$EmojiToJson(this);
static Widget widgetFromJson(String emoji) => Text(emoji);
factory Emoji.fromJson(Map<String, Object?> json) => _$EmojiFromJson(json);
}

View file

@ -1,8 +1,131 @@
import "package:flutter/widgets.dart";
class EmojiPicker extends StatelessWidget {
const new({super.key});
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/emoji.dart";
import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/models/emoji_category.dart";
import "package:nexus/widgets/divider_text.dart";
import "package:super_sliver_list/super_sliver_list.dart";
final class const EmojiPicker({
final IList<EmojiCategory> extraCategories = const IList.empty(),
super.key,
}) extends HookConsumerWidget {
@override
Widget build(BuildContext context) => Placeholder(); // TODO: Emoji picker
Widget build(BuildContext context, WidgetRef ref) => Container(
padding: .symmetric(horizontal: 16),
constraints: .loose(.new(500, 700)),
child: ref
.watch(EmojiController.provider)
.betterWhen(
data: (categories) {
final combined = categories.addAll(extraCategories);
return HookBuilder(
builder: (context) {
final selectedCategory = useState<int?>(null);
final scrollController = useScrollController();
final listController = useMemoized(ListController.new);
useEffect(() {
void onChange() {
final range = listController.visibleRange;
if (range != null) {
final firstVisible = range.$1;
if (selectedCategory.value != firstVisible) {
WidgetsBinding.instance.addPostFrameCallback((_) {
selectedCategory.value = firstVisible;
});
}
}
}
listController.addListener(onChange);
return () => listController.removeListener(onChange);
}, [listController]);
return Column(
spacing: 4,
children: [
SearchAnchor.bar(
barHintText: "Search emoji",
suggestionsBuilder: (context, controller) {
return [];
},
),
SizedBox(
height: 48,
child: ListView(
scrollDirection: .horizontal,
children: combined
.mapIndexed(
(index, category) => Padding(
padding: .symmetric(horizontal: 4),
child: FilterChip(
label: Text(category.name),
avatar: category.icon,
selected: selectedCategory.value == index,
showCheckmark: false,
onSelected: (_) {
selectedCategory.value = index;
listController.animateToItem(
index: index,
scrollController: scrollController,
curve: (_) => Curves.easeInOut,
duration: (_) =>
Duration(milliseconds: 300),
alignment: 0,
);
},
),
),
)
.toList(),
),
),
Expanded(
child: SuperListView.builder(
listController: listController,
controller: scrollController,
itemCount: combined.length,
itemBuilder: (context, index) {
final category = combined[index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DividerText(category.name),
DefaultTextStyle(
style: TextStyle(fontSize: 24),
child: GridView(
gridDelegate:
const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 52,
mainAxisExtent: 52,
),
children: category.emojis
.map(
(emoji) => IconButton(
onPressed: () {},
icon: emoji.widget,
),
)
.toList(),
),
),
],
);
},
),
),
],
);
},
);
},
),
);
}

View file

@ -17,6 +17,7 @@ import "package:nexus/models/content/message.dart";
import "package:nexus/models/event.dart";
import "package:nexus/models/relation_type.dart";
import "package:nexus/widgets/composer/composer.dart";
import "package:nexus/widgets/emoji_picker.dart";
import "package:nexus/widgets/pinned_events_drawer.dart";
import "package:nexus/widgets/renderers/event.dart";
import "package:nexus/widgets/member_list.dart";
@ -237,11 +238,14 @@ final class const RoomChat({
icon: Icon(Icons.emoji_emotions),
onPressed: () {
// TODO: More adaptive
Navigator.of(context).pop();
// TODO: Push
// onSelection: (emoji) =>
// notifier.sendReaction(emoji, event).onError(showError),
showModalBottomSheet(
showDragHandle: true,
isScrollControlled: true,
context: context,
builder: (context) => EmojiPicker(),
);
},
),
],