add support for setting recent emoji

This commit is contained in:
Henry Hiles 2026-09-16 15:46:42 -04:00
commit 810a0b102b
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
4 changed files with 103 additions and 60 deletions

View file

@ -0,0 +1,44 @@
import "package:collection/collection.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/account_data.dart";
import "package:nexus/controllers/client.dart";
import "package:nexus/models/account_data.dart";
class RecentEmojiController extends Notifier<IList<RecentEmoji>> {
@override
IList<RecentEmoji> build() => ref.watch(
AccountDataController.provider.select(
(value) => value.recentEmoji.recentEmoji,
),
);
Future<void> add(String emoji) => ref
.watch(ClientController.provider.notifier)
.setAccountData(
.new(
type: AccountData.recentEmojiKey,
content: RecentEmojiData(
recentEmoji: .new([
.new(
emoji: emoji,
total:
(state
.firstWhereOrNull(
(element) => element.emoji == emoji,
)
?.total ??
0) +
1,
),
...state.whereNot((element) => element.emoji == emoji),
]),
),
),
);
static final provider =
NotifierProvider.autoDispose<RecentEmojiController, IList<RecentEmoji>>(
RecentEmojiController.new,
);
}

View file

@ -14,22 +14,9 @@ class const AccountData({
@JsonKey(name: AccountData.directKey) @JsonKey(name: AccountData.directKey)
final IMap<String, IList<String>> directMessages = const IMap.empty(), final IMap<String, IList<String>> directMessages = const IMap.empty(),
@JsonKey( @JsonKey(name: AccountData.recentEmojiKey)
name: AccountData.recentEmojiKey, final RecentEmojiData recentEmoji = const RecentEmojiData(),
readValue: AccountData.readRecentEmojiValue,
toJson: AccountData.recentEmojiToJson,
)
final IList<RecentEmoji> recentEmoji = const IList.empty(),
}) with _$AccountData { }) with _$AccountData {
static List<dynamic>? readRecentEmojiValue(
Map<dynamic, dynamic> json,
String key,
) => json[key]?["recent_emoji"];
static Map<String, List<dynamic>>? recentEmojiToJson(
IList<RecentEmoji> recentEmoji,
) => {"recent_emoji": recentEmoji.map((emoji) => emoji.toJson()).toList()};
static const invitePermissionConfigKey = "m.invite_permission_config"; static const invitePermissionConfigKey = "m.invite_permission_config";
static const directKey = "m.direct"; static const directKey = "m.direct";
static const recentEmojiKey = "m.recent_emoji"; static const recentEmojiKey = "m.recent_emoji";
@ -40,6 +27,17 @@ class const AccountData({
_$AccountDataFromJson(json); _$AccountDataFromJson(json);
} }
@Freezed(toJson: false, fromJson: false)
@JsonSerializable()
class const RecentEmojiData({
final IList<RecentEmoji> recentEmoji = const IList.empty(),
}) with _$RecentEmojiData {
Map<String, Object?> toJson() => _$RecentEmojiDataToJson(this);
factory RecentEmojiData.fromJson(Map<String, Object?> json) =>
_$RecentEmojiDataFromJson(json);
}
@Freezed(toJson: false, fromJson: false) @Freezed(toJson: false, fromJson: false)
@JsonSerializable() @JsonSerializable()
class const InvitePermissionConfig({ class const InvitePermissionConfig({

View file

@ -1,12 +1,12 @@
import "dart:async"; import "dart:async";
import "package:collection/collection.dart"; import "package:collection/collection.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:material_emoji_picker/material_emoji_picker.dart" as upstream; import "package:material_emoji_picker/material_emoji_picker.dart" as upstream;
import "package:material_ui/material_ui.dart"; import "package:material_ui/material_ui.dart";
import "package:nexus/controllers/account_data.dart"; import "package:nexus/controllers/recent_emoji.dart";
import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/main.dart";
class const EmojiPicker({ class const EmojiPicker({
required final FutureOr<void> Function(String value) onSelection, required final FutureOr<void> Function(String value) onSelection,
@ -14,13 +14,19 @@ class const EmojiPicker({
super.key, super.key,
}) extends ConsumerWidget { }) extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) => ref Widget build(BuildContext context, WidgetRef ref) {
final recentEmoji = ref.watch(RecentEmojiController.provider);
return ref
.watch(upstream.EmojiController.provider) .watch(upstream.EmojiController.provider)
.betterWhen( .betterWhen(
data: (categories) => upstream.EmojiPicker( data: (categories) => upstream.EmojiPicker(
onSelection: (value) async { onSelection: (emoji) async {
// await onSelection(emoji);
await onSelection(value); await ref
.watch(RecentEmojiController.provider.notifier)
.add(emoji)
.onError(showError);
}, },
allowFreeText: allowFreeText, allowFreeText: allowFreeText,
prependCategories: .new([ prependCategories: .new([
@ -28,20 +34,13 @@ class const EmojiPicker({
icon: Icon(Icons.history), icon: Icon(Icons.history),
name: "Recent", name: "Recent",
emojis: .new( emojis: .new(
ref recentEmoji
.watch(
AccountDataController.provider.select(
(value) => value.recentEmoji
.map((entry) => entry.emoji)
.toIList(),
),
)
.map( .map(
(emoji) => categories (recent) => categories
.map((element) => element.emojis) .map((element) => element.emojis)
.flattened .flattened
.firstWhereOrNull( .firstWhereOrNull(
(element) => element.value == emoji, (element) => element.value == recent.emoji,
), ),
) )
.nonNulls, .nonNulls,
@ -51,3 +50,4 @@ class const EmojiPicker({
), ),
); );
} }
}

View file

@ -4,12 +4,12 @@ import "package:flutter/services.dart";
import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:measure_size/measure_size.dart"; import "package:measure_size/measure_size.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/member_list_opened.dart"; import "package:nexus/controllers/member_list_opened.dart";
import "package:nexus/controllers/pinned_ids.dart"; import "package:nexus/controllers/pinned_ids.dart";
import "package:nexus/controllers/power_level.dart"; import "package:nexus/controllers/power_level.dart";
import "package:nexus/controllers/recent_emoji.dart";
import "package:nexus/controllers/rooms.dart"; import "package:nexus/controllers/rooms.dart";
import "package:nexus/controllers/room_chat.dart"; import "package:nexus/controllers/room_chat.dart";
import "package:nexus/controllers/via.dart"; import "package:nexus/controllers/via.dart";
@ -196,6 +196,8 @@ final class const RoomChat({
final danger = theme.colorScheme.error; final danger = theme.colorScheme.error;
final isSentByMe = event.sender == userId; final isSentByMe = event.sender == userId;
final recentEmoji = ref.watch(RecentEmojiController.provider);
return [ return [
if (ref.watch( if (ref.watch(
PowerLevelController.provider( PowerLevelController.provider(
@ -209,13 +211,7 @@ final class const RoomChat({
child: Row( child: Row(
children: [ children: [
...{ ...{
...ref.watch( ...recentEmoji.map((entry) => entry.emoji),
AccountDataController.provider.select(
(value) => value.recentEmoji
.map((entry) => entry.emoji)
.toIList(),
),
),
"👍", "👍",
"🤣", "🤣",
"😭", "😭",
@ -230,6 +226,11 @@ final class const RoomChat({
await notifier await notifier
.sendReaction(emoji, event) .sendReaction(emoji, event)
.onError(showError); .onError(showError);
ref
.watch(RecentEmojiController.provider.notifier)
.add(emoji)
.onError(showError);
}, },
icon: Text(emoji), icon: Text(emoji),
), ),