allow sending emoji/stickers from emote packs

This commit is contained in:
Henry Hiles 2026-10-23 18:16:45 -04:00
commit 3754bc2feb
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
3 changed files with 701 additions and 74 deletions

View file

@ -16,6 +16,7 @@ import "package:nexus/models/content/server_acl.dart";
import "package:nexus/models/content/topic.dart";
import "package:nexus/models/content/sticker.dart";
import "package:nexus/models/content/history_visibility.dart";
import "package:nexus/models/content/image_pack.dart";
class const Content({final String? parseError, final StackTrace? stackTrace}) {
factory Content.fromJson(Map<String, dynamic> json) => const Content();
@ -61,5 +62,7 @@ enum EventType(
tombstone("m.room.tombstone", Content.fromJson),
reaction("m.reaction", ReactionContent.fromJson),
pinnedEvents("m.room.pinned_events", PinnedEventsContent.fromJson),
imagePack("m.room.image_pack", ImagePackContent.fromJson),
legacyImagePack("im.ponies.room_emotes", ImagePackContent.fromJson),
message("m.room.message", MessageContent.fromJson),
}

View file

@ -0,0 +1,68 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:freezed_annotation/freezed_annotation.dart";
import "package:nexus/models/content/content.dart";
import "package:nexus/models/info/image.dart";
part "image_pack.freezed.dart";
part "image_pack.g.dart";
/// An image pack (MSC2545), from a room's state.
@freezed
sealed class ImagePackContent extends Content with _$ImagePackContent {
ImagePackContent._();
factory ImagePackContent({
@Default(IMap.empty()) IMap<String, PackImage> images,
PackInfo? pack,
}) = _ImagePackContent;
IMap<String, PackImage> imagesFor(ImageUsage usage) => images.where(
(_, image) => switch ((image.usage, pack?.usage ?? const .empty())) {
(final usages, _) when usages.isNotEmpty => usages.contains(usage),
(_, final packUsages) when packUsages.isNotEmpty => packUsages.contains(
usage,
),
_ => true,
},
);
factory ImagePackContent.fromJson(Map<String, Object?> json) =>
_$ImagePackContentFromJson(json);
}
@Freezed(toJson: false, fromJson: false)
@JsonSerializable()
class const PackImage({
required final Uri url,
final String? body,
final ImageInfo? info,
@JsonKey(fromJson: ImageUsage.setFromJson)
final ISet<ImageUsage> usage = const .empty(),
}) with _$PackImage {
Map<String, Object?> toJson() => _$PackImageToJson(this);
factory PackImage.fromJson(Map<String, Object?> json) =>
_$PackImageFromJson(json);
}
@Freezed(toJson: false, fromJson: false)
@JsonSerializable()
class const PackInfo({
final String? displayName,
final Uri? avatarUrl,
final String? attribution,
@JsonKey(fromJson: ImageUsage.setFromJson)
final ISet<ImageUsage> usage = const .empty(),
}) with _$PackInfo {
Map<String, Object?> toJson() => _$PackInfoToJson(this);
factory PackInfo.fromJson(Map<String, Object?> json) =>
_$PackInfoFromJson(json);
}
enum ImageUsage {
emoticon,
sticker;
/// Ignores usages we don't know about.
static ISet<ImageUsage> setFromJson(List<dynamic>? json) =>
ISet(values.where((value) => json?.contains(value.name) ?? false));
}

View file

@ -11,11 +11,15 @@ sealed class ReactionContent extends Content with _$ReactionContent {
factory ReactionContent({
@JsonKey(readValue: ReactionContent.keyJsonFromJson) String? key,
/// For custom emoji reactions
@JsonKey(name: "com.beeper.reaction.shortcode") String? shortcode,
}) = _ReactionContent;
@override
Map<String, dynamic> toJson() => {
"m.relates_to": {"key": key},
"com.beeper.reaction.shortcode": ?shortcode,
};
factory ReactionContent.fromJson(Map<String, Object?> json) =>