allow sending emoji/stickers from emote packs
This commit is contained in:
parent
868023a4a1
commit
3754bc2feb
6 changed files with 701 additions and 74 deletions
|
|
@ -17,7 +17,11 @@ import "package:nexus/controllers/image_picker.dart";
|
|||
import "package:nexus/helpers/file_dialog_helper.dart";
|
||||
import "package:image_picker/image_picker.dart";
|
||||
import "package:nexus/controllers/power_level.dart";
|
||||
import "package:nexus/controllers/image_packs.dart";
|
||||
import "package:nexus/models/content/image_pack.dart";
|
||||
import "package:nexus/models/content/message.dart";
|
||||
import "package:nexus/models/image_pack.dart";
|
||||
import "package:nexus/widgets/sticker_picker.dart";
|
||||
import "package:nexus/models/relation_type.dart";
|
||||
import "package:nexus/widgets/composer/overlays/tagger_overlay.dart";
|
||||
import "package:nexus/widgets/composer/paste_files_action.dart";
|
||||
|
|
@ -38,6 +42,8 @@ class const Composer(
|
|||
required IList<Tag> tags,
|
||||
})
|
||||
onSend,
|
||||
required final Future<void> Function(String shortcode, PackImage image)
|
||||
onSendSticker,
|
||||
final FocusNode? node,
|
||||
super.key,
|
||||
}) extends HookConsumerWidget {
|
||||
|
|
@ -48,6 +54,17 @@ class const Composer(
|
|||
final triggerCharacter = useState("");
|
||||
final shouldMention = useState(true);
|
||||
final query = useState("");
|
||||
final canSendStickers =
|
||||
ref.watch(
|
||||
PowerLevelController.provider(
|
||||
.new(eventType: .sticker, roomId: roomId),
|
||||
),
|
||||
) &&
|
||||
(ref
|
||||
.watch(ImagePacksController.provider(roomId))
|
||||
.value
|
||||
?.any((pack) => pack.stickers.isNotEmpty) ??
|
||||
false);
|
||||
final relatedEvent = this.relatedEvent == null
|
||||
? null
|
||||
: ref.watch(
|
||||
|
|
@ -253,9 +270,27 @@ class const Composer(
|
|||
IconButton(
|
||||
onPressed: () => context.showAdaptiveSheet(
|
||||
(context) => EmojiPicker(
|
||||
onSelection: (value) {
|
||||
roomId: roomId,
|
||||
onSelection: (value, {shortcode}) {
|
||||
Navigator.of(context).pop();
|
||||
controller.text += value;
|
||||
if (shortcode == null) {
|
||||
controller.text += value;
|
||||
} else {
|
||||
final text = "${controller.text}:";
|
||||
controller.value = .new(
|
||||
text: text,
|
||||
selection: .collapsed(
|
||||
offset: text.length,
|
||||
),
|
||||
);
|
||||
controller.addTag(
|
||||
id: ImagePack.emojiMarkdown(
|
||||
shortcode,
|
||||
value,
|
||||
),
|
||||
name: shortcode,
|
||||
);
|
||||
}
|
||||
node?.requestFocus();
|
||||
},
|
||||
),
|
||||
|
|
@ -308,6 +343,22 @@ class const Composer(
|
|||
leading: Icon(Icons.attachment),
|
||||
),
|
||||
),
|
||||
if (canSendStickers)
|
||||
PopupMenuItem(
|
||||
onTap: () => context.showAdaptiveSheet(
|
||||
(context) => StickerPicker(
|
||||
roomId: roomId,
|
||||
onSelection: (shortcode, image) {
|
||||
Navigator.of(context).pop();
|
||||
onSendSticker(shortcode, image);
|
||||
},
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
title: Text("Sticker"),
|
||||
leading: Icon(Icons.sticky_note_2),
|
||||
),
|
||||
),
|
||||
],
|
||||
icon: Icon(Icons.add),
|
||||
),
|
||||
|
|
@ -315,6 +366,9 @@ class const Composer(
|
|||
Expanded(
|
||||
child: FlutterTagger(
|
||||
triggerStrategy: .eager,
|
||||
// Characters that continue a search, for
|
||||
// e.g. :+1, :custom_emoji or @user.name
|
||||
searchRegex: .new(r"^[\w.+-]*$"),
|
||||
overlay: TaggerOverlay(
|
||||
query.value,
|
||||
roomId: roomId,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import "package:collection/collection.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:material_emoji_picker/material_emoji_picker.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:nexus/controllers/all_emoji.dart";
|
||||
import "package:nexus/helpers/extensions/better_when.dart";
|
||||
import "package:nexus/models/image_pack.dart";
|
||||
|
||||
class const EmojiOverlay(
|
||||
final String query, {
|
||||
|
|
@ -13,7 +14,7 @@ class const EmojiOverlay(
|
|||
}) extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) => ref
|
||||
.watch(EmojiController.provider)
|
||||
.watch(AllEmojiController.provider(roomId))
|
||||
.betterWhen(
|
||||
data: (emojis) => ListView(
|
||||
children: emojis
|
||||
|
|
@ -34,8 +35,11 @@ class const EmojiOverlay(
|
|||
title: Text(emoji.aliases.first),
|
||||
subtitle: Text(emoji.description),
|
||||
onTap: () => addTag(
|
||||
id: Uri.tryParse(emoji.value)?.scheme == "mxc"
|
||||
? "" // TODO: Handle custom emotes
|
||||
id: emoji.value.startsWith("mxc://")
|
||||
? ImagePack.emojiMarkdown(
|
||||
emoji.aliases.first,
|
||||
emoji.value,
|
||||
)
|
||||
: emoji.value,
|
||||
name: emoji.aliases.first,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,15 +1,23 @@
|
|||
import "dart:async";
|
||||
|
||||
import "package:collection/collection.dart";
|
||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:material_emoji_picker/material_emoji_picker.dart" as upstream;
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:nexus/controllers/all_emoji.dart";
|
||||
import "package:nexus/controllers/image_packs.dart";
|
||||
import "package:nexus/controllers/recent_emoji.dart";
|
||||
import "package:nexus/helpers/extensions/better_when.dart";
|
||||
import "package:nexus/helpers/extensions/image_pack_emoji.dart";
|
||||
import "package:nexus/main.dart";
|
||||
import "package:nexus/models/content/image_pack.dart";
|
||||
|
||||
class const EmojiPicker({
|
||||
required final FutureOr<void> Function(String value) onSelection,
|
||||
required final String roomId,
|
||||
|
||||
required final FutureOr<void> Function(String value, {String? shortcode})
|
||||
onSelection,
|
||||
final bool allowFreeText = false,
|
||||
super.key,
|
||||
}) extends ConsumerWidget {
|
||||
|
|
@ -18,39 +26,51 @@ class const EmojiPicker({
|
|||
final recentEmoji = ref.watch(RecentEmojiController.provider);
|
||||
|
||||
return ref
|
||||
.watch(upstream.EmojiController.provider)
|
||||
.watch(AllEmojiController.provider(roomId))
|
||||
.betterWhen(
|
||||
data: (categories) => upstream.EmojiPicker(
|
||||
onSelection: (emoji, isFreeText) async {
|
||||
await onSelection(emoji);
|
||||
data: (categories) {
|
||||
final allEmoji = categories.expand((category) => category.emojis);
|
||||
final customCategories =
|
||||
ref
|
||||
.watch(ImagePacksController.provider(roomId))
|
||||
.value
|
||||
?.map((pack) => pack.emojiCategory)
|
||||
.nonNulls
|
||||
.toIList() ??
|
||||
const .empty();
|
||||
|
||||
return upstream.EmojiPicker(
|
||||
onSelection: (emoji) async {
|
||||
await onSelection(
|
||||
emoji.value,
|
||||
shortcode: emoji.data is PackImage
|
||||
? emoji.aliases.first
|
||||
: null,
|
||||
);
|
||||
|
||||
if (!isFreeText) {
|
||||
await ref
|
||||
.read(RecentEmojiController.provider.notifier)
|
||||
.add(emoji)
|
||||
.add(emoji.value)
|
||||
.onError(showError);
|
||||
}
|
||||
},
|
||||
allowFreeText: allowFreeText,
|
||||
prependCategories: .new([
|
||||
.new(
|
||||
icon: Icon(Icons.history),
|
||||
name: "Recent",
|
||||
emojis: .new(
|
||||
recentEmoji
|
||||
.map(
|
||||
(recent) => categories
|
||||
.map((element) => element.emojis)
|
||||
.flattened
|
||||
.firstWhereOrNull(
|
||||
(element) => element.value == recent.emoji,
|
||||
),
|
||||
)
|
||||
.nonNulls,
|
||||
},
|
||||
onFreeText: allowFreeText ? (text) => onSelection(text) : null,
|
||||
prependCategories: IList<upstream.EmojiCategory>([
|
||||
.new(
|
||||
icon: Icon(Icons.history),
|
||||
name: "Recent",
|
||||
emojis: .new(
|
||||
recentEmoji
|
||||
.map(
|
||||
(recent) => allEmoji.firstWhereOrNull(
|
||||
(element) => element.value == recent.emoji,
|
||||
),
|
||||
)
|
||||
.nonNulls,
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
]).addAll(customCategories),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
lib/widgets/mxc_emoji.dart
Normal file
16
lib/widgets/mxc_emoji.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import "package:material_ui/material_ui.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:nexus/helpers/mxc_image.dart";
|
||||
|
||||
/// A custom emoji or sticker from an image pack, scaled to fit its parent.
|
||||
class const MxcEmoji(final Uri url, {final String? label, super.key})
|
||||
extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) => Image(
|
||||
image: MxcImage(ref, .new(mxc: url)),
|
||||
semanticLabel: label,
|
||||
fit: .contain,
|
||||
errorBuilder: (context, _, _) =>
|
||||
Icon(Icons.broken_image, color: Theme.of(context).colorScheme.error),
|
||||
);
|
||||
}
|
||||
|
|
@ -323,6 +323,10 @@ final class const RoomChat({
|
|||
relatedEvent.value,
|
||||
)
|
||||
.onError(showError),
|
||||
onSendSticker: (shortcode, image) =>
|
||||
notifier
|
||||
.sendSticker(shortcode, image)
|
||||
.onError(showError),
|
||||
relationType: relationType.value,
|
||||
relatedEvent: relatedEvent.value,
|
||||
onDismiss: () {
|
||||
|
|
|
|||
47
lib/widgets/sticker_picker.dart
Normal file
47
lib/widgets/sticker_picker.dart
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import "dart:async";
|
||||
|
||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:material_emoji_picker/material_emoji_picker.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:nexus/controllers/image_packs.dart";
|
||||
import "package:nexus/helpers/extensions/better_when.dart";
|
||||
import "package:nexus/helpers/extensions/image_pack_emoji.dart";
|
||||
import "package:nexus/models/content/image_pack.dart";
|
||||
|
||||
/// Stickers from the image packs available in [roomId], grouped by pack.
|
||||
class const StickerPicker({
|
||||
required final String roomId,
|
||||
required final FutureOr<void> Function(String shortcode, PackImage image)
|
||||
onSelection,
|
||||
super.key,
|
||||
}) extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) => ref
|
||||
.watch(ImagePacksController.provider(roomId))
|
||||
.betterWhen(
|
||||
data: (packs) {
|
||||
final categories = packs
|
||||
.map((pack) => pack.stickerCategory)
|
||||
.nonNulls
|
||||
.toIList();
|
||||
|
||||
return categories.isEmpty
|
||||
? Padding(
|
||||
padding: .all(16),
|
||||
child: Center(child: Text("No sticker packs available")),
|
||||
)
|
||||
: EmojiPicker(
|
||||
prependCategories: categories,
|
||||
includeDefaultEmoji: false,
|
||||
itemSize: 96,
|
||||
searchHint: "Search stickers",
|
||||
onSelection: (sticker) {
|
||||
if (sticker.data case final PackImage image) {
|
||||
return onSelection(sticker.aliases.first, image);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue