diff --git a/packages/moxxmpp/lib/moxxmpp.dart b/packages/moxxmpp/lib/moxxmpp.dart index 9096101..3794614 100644 --- a/packages/moxxmpp/lib/moxxmpp.dart +++ b/packages/moxxmpp/lib/moxxmpp.dart @@ -75,6 +75,7 @@ export 'package:moxxmpp/src/xeps/xep_0384/xep_0384.dart'; export 'package:moxxmpp/src/xeps/xep_0385.dart'; export 'package:moxxmpp/src/xeps/xep_0414.dart'; export 'package:moxxmpp/src/xeps/xep_0424.dart'; +export 'package:moxxmpp/src/xeps/xep_0444.dart'; export 'package:moxxmpp/src/xeps/xep_0446.dart'; export 'package:moxxmpp/src/xeps/xep_0447.dart'; export 'package:moxxmpp/src/xeps/xep_0448.dart'; diff --git a/packages/moxxmpp/lib/src/events.dart b/packages/moxxmpp/lib/src/events.dart index 8213794..44f18f6 100644 --- a/packages/moxxmpp/lib/src/events.dart +++ b/packages/moxxmpp/lib/src/events.dart @@ -9,6 +9,7 @@ import 'package:moxxmpp/src/xeps/xep_0085.dart'; import 'package:moxxmpp/src/xeps/xep_0359.dart'; import 'package:moxxmpp/src/xeps/xep_0385.dart'; import 'package:moxxmpp/src/xeps/xep_0424.dart'; +import 'package:moxxmpp/src/xeps/xep_0444.dart'; import 'package:moxxmpp/src/xeps/xep_0446.dart'; import 'package:moxxmpp/src/xeps/xep_0447.dart'; import 'package:moxxmpp/src/xeps/xep_0461.dart'; @@ -75,6 +76,7 @@ class MessageEvent extends XmppEvent { this.funCancellation, this.messageRetraction, this.messageCorrectionId, + this.messageReactions, }); final StanzaError? error; final String body; @@ -97,6 +99,7 @@ class MessageEvent extends XmppEvent { final bool encrypted; final MessageRetractionData? messageRetraction; final String? messageCorrectionId; + final MessageReactions? messageReactions; final Map other; } diff --git a/packages/moxxmpp/lib/src/managers/data.dart b/packages/moxxmpp/lib/src/managers/data.dart index d4ffdbf..e97d7e1 100644 --- a/packages/moxxmpp/lib/src/managers/data.dart +++ b/packages/moxxmpp/lib/src/managers/data.dart @@ -7,6 +7,7 @@ import 'package:moxxmpp/src/xeps/xep_0359.dart'; import 'package:moxxmpp/src/xeps/xep_0380.dart'; import 'package:moxxmpp/src/xeps/xep_0385.dart'; import 'package:moxxmpp/src/xeps/xep_0424.dart'; +import 'package:moxxmpp/src/xeps/xep_0444.dart'; import 'package:moxxmpp/src/xeps/xep_0446.dart'; import 'package:moxxmpp/src/xeps/xep_0447.dart'; import 'package:moxxmpp/src/xeps/xep_0461.dart'; @@ -61,6 +62,8 @@ class StanzaHandlerData with _$StanzaHandlerData { MessageRetractionData? messageRetraction, // If non-null, then the message is a correction for the specified stanza Id String? lastMessageCorrectionSid, + // Reactions data + MessageReactions? messageReactions, } ) = _StanzaHandlerData; } diff --git a/packages/moxxmpp/lib/src/managers/data.freezed.dart b/packages/moxxmpp/lib/src/managers/data.freezed.dart index abbc16a..7a61819 100644 --- a/packages/moxxmpp/lib/src/managers/data.freezed.dart +++ b/packages/moxxmpp/lib/src/managers/data.freezed.dart @@ -59,7 +59,9 @@ mixin _$StanzaHandlerData { // retracted MessageRetractionData? get messageRetraction => throw _privateConstructorUsedError; // If non-null, then the message is a correction for the specified stanza Id - String? get lastMessageCorrectionSid => throw _privateConstructorUsedError; + String? get lastMessageCorrectionSid => + throw _privateConstructorUsedError; // Reactions data + MessageReactions? get messageReactions => throw _privateConstructorUsedError; @JsonKey(ignore: true) $StanzaHandlerDataCopyWith get copyWith => @@ -94,7 +96,8 @@ abstract class $StanzaHandlerDataCopyWith<$Res> { DelayedDelivery? delayedDelivery, Map other, MessageRetractionData? messageRetraction, - String? lastMessageCorrectionSid}); + String? lastMessageCorrectionSid, + MessageReactions? messageReactions}); } /// @nodoc @@ -131,6 +134,7 @@ class _$StanzaHandlerDataCopyWithImpl<$Res> Object? other = freezed, Object? messageRetraction = freezed, Object? lastMessageCorrectionSid = freezed, + Object? messageReactions = freezed, }) { return _then(_value.copyWith( done: done == freezed @@ -225,6 +229,10 @@ class _$StanzaHandlerDataCopyWithImpl<$Res> ? _value.lastMessageCorrectionSid : lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable as String?, + messageReactions: messageReactions == freezed + ? _value.messageReactions + : messageReactions // ignore: cast_nullable_to_non_nullable + as MessageReactions?, )); } } @@ -259,7 +267,8 @@ abstract class _$$_StanzaHandlerDataCopyWith<$Res> DelayedDelivery? delayedDelivery, Map other, MessageRetractionData? messageRetraction, - String? lastMessageCorrectionSid}); + String? lastMessageCorrectionSid, + MessageReactions? messageReactions}); } /// @nodoc @@ -298,6 +307,7 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res> Object? other = freezed, Object? messageRetraction = freezed, Object? lastMessageCorrectionSid = freezed, + Object? messageReactions = freezed, }) { return _then(_$_StanzaHandlerData( done == freezed @@ -392,6 +402,10 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res> ? _value.lastMessageCorrectionSid : lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable as String?, + messageReactions: messageReactions == freezed + ? _value.messageReactions + : messageReactions // ignore: cast_nullable_to_non_nullable + as MessageReactions?, )); } } @@ -418,7 +432,8 @@ class _$_StanzaHandlerData implements _StanzaHandlerData { this.delayedDelivery, final Map other = const {}, this.messageRetraction, - this.lastMessageCorrectionSid}) + this.lastMessageCorrectionSid, + this.messageReactions}) : _other = other; // Indicates to the runner that processing is now done. This means that all @@ -501,10 +516,13 @@ class _$_StanzaHandlerData implements _StanzaHandlerData { // If non-null, then the message is a correction for the specified stanza Id @override final String? lastMessageCorrectionSid; +// Reactions data + @override + final MessageReactions? messageReactions; @override String toString() { - return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, stableId: $stableId, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other, messageRetraction: $messageRetraction, lastMessageCorrectionSid: $lastMessageCorrectionSid)'; + return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, stableId: $stableId, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other, messageRetraction: $messageRetraction, lastMessageCorrectionSid: $lastMessageCorrectionSid, messageReactions: $messageReactions)'; } @override @@ -544,7 +562,9 @@ class _$_StanzaHandlerData implements _StanzaHandlerData { const DeepCollectionEquality() .equals(other.messageRetraction, messageRetraction) && const DeepCollectionEquality().equals( - other.lastMessageCorrectionSid, lastMessageCorrectionSid)); + other.lastMessageCorrectionSid, lastMessageCorrectionSid) && + const DeepCollectionEquality() + .equals(other.messageReactions, messageReactions)); } @override @@ -572,7 +592,8 @@ class _$_StanzaHandlerData implements _StanzaHandlerData { const DeepCollectionEquality().hash(delayedDelivery), const DeepCollectionEquality().hash(_other), const DeepCollectionEquality().hash(messageRetraction), - const DeepCollectionEquality().hash(lastMessageCorrectionSid) + const DeepCollectionEquality().hash(lastMessageCorrectionSid), + const DeepCollectionEquality().hash(messageReactions) ]); @JsonKey(ignore: true) @@ -603,7 +624,8 @@ abstract class _StanzaHandlerData implements StanzaHandlerData { final DelayedDelivery? delayedDelivery, final Map other, final MessageRetractionData? messageRetraction, - final String? lastMessageCorrectionSid}) = _$_StanzaHandlerData; + final String? lastMessageCorrectionSid, + final MessageReactions? messageReactions}) = _$_StanzaHandlerData; @override // Indicates to the runner that processing is now done. This means that all // pre-processing is done and no other handlers should be consulted. @@ -658,6 +680,8 @@ abstract class _StanzaHandlerData implements StanzaHandlerData { MessageRetractionData? get messageRetraction; @override // If non-null, then the message is a correction for the specified stanza Id String? get lastMessageCorrectionSid; + @override // Reactions data + MessageReactions? get messageReactions; @override @JsonKey(ignore: true) _$$_StanzaHandlerDataCopyWith<_$_StanzaHandlerData> get copyWith => diff --git a/packages/moxxmpp/lib/src/managers/namespaces.dart b/packages/moxxmpp/lib/src/managers/namespaces.dart index d340848..aeb61ed 100644 --- a/packages/moxxmpp/lib/src/managers/namespaces.dart +++ b/packages/moxxmpp/lib/src/managers/namespaces.dart @@ -26,3 +26,4 @@ const cryptographicHashManager = 'org.moxxmpp.cryptographichashmanager'; const delayedDeliveryManager = 'org.moxxmpp.delayeddeliverymanager'; const messageRetractionManager = 'org.moxxmpp.messageretractionmanager'; const lastMessageCorrectionManager = 'org.moxxmpp.lastmessagecorrectionmanager'; +const messageReactionsManager = 'org.moxxmpp.messagereactionsmanager'; diff --git a/packages/moxxmpp/lib/src/message.dart b/packages/moxxmpp/lib/src/message.dart index 6e157e9..b858ee3 100644 --- a/packages/moxxmpp/lib/src/message.dart +++ b/packages/moxxmpp/lib/src/message.dart @@ -15,6 +15,7 @@ import 'package:moxxmpp/src/xeps/xep_0308.dart'; import 'package:moxxmpp/src/xeps/xep_0333.dart'; import 'package:moxxmpp/src/xeps/xep_0359.dart'; import 'package:moxxmpp/src/xeps/xep_0424.dart'; +import 'package:moxxmpp/src/xeps/xep_0444.dart'; import 'package:moxxmpp/src/xeps/xep_0446.dart'; import 'package:moxxmpp/src/xeps/xep_0447.dart'; import 'package:moxxmpp/src/xeps/xep_0448.dart'; @@ -38,6 +39,7 @@ class MessageDetails { this.shouldEncrypt = false, this.messageRetraction, this.lastMessageCorrectionId, + this.messageReactions, }); final String to; final String? body; @@ -56,6 +58,7 @@ class MessageDetails { final bool shouldEncrypt; final MessageRetractionData? messageRetraction; final String? lastMessageCorrectionId; + final MessageReactions? messageReactions; } class MessageManager extends XmppManagerBase { @@ -102,6 +105,7 @@ class MessageManager extends XmppManagerBase { encrypted: state.encrypted, messageRetraction: state.messageRetraction, messageCorrectionId: state.lastMessageCorrectionSid, + messageReactions: state.messageReactions, other: state.other, error: StanzaError.fromStanza(message), ),); @@ -261,6 +265,10 @@ class MessageManager extends XmppManagerBase { ), ); } + + if (details.messageReactions != null) { + stanza.addChild(details.messageReactions!.toXml()); + } getAttributes().sendStanza(stanza, awaitable: false); } diff --git a/packages/moxxmpp/lib/src/namespaces.dart b/packages/moxxmpp/lib/src/namespaces.dart index 5b95a2d..5204bb0 100644 --- a/packages/moxxmpp/lib/src/namespaces.dart +++ b/packages/moxxmpp/lib/src/namespaces.dart @@ -123,9 +123,12 @@ const fasteningXmlns = 'urn:xmpp:fasten:0'; // XEP-0424 const messageRetractionXmlns = 'urn:xmpp:message-retract:0'; -// XEp-0428 +// XEP-0428 const fallbackIndicationXmlns = 'urn:xmpp:fallback:0'; +// XEP-0444 +const messageReactionsXmlns = 'urn:xmpp:reactions:0'; + // XEP-0446 const fileMetadataXmlns = 'urn:xmpp:file:metadata:0'; diff --git a/packages/moxxmpp/lib/src/xeps/xep_0444.dart b/packages/moxxmpp/lib/src/xeps/xep_0444.dart new file mode 100644 index 0000000..ac28832 --- /dev/null +++ b/packages/moxxmpp/lib/src/xeps/xep_0444.dart @@ -0,0 +1,68 @@ +import 'package:moxxmpp/src/managers/base.dart'; +import 'package:moxxmpp/src/managers/data.dart'; +import 'package:moxxmpp/src/managers/handlers.dart'; +import 'package:moxxmpp/src/managers/namespaces.dart'; +import 'package:moxxmpp/src/namespaces.dart'; +import 'package:moxxmpp/src/stanza.dart'; +import 'package:moxxmpp/src/stringxml.dart'; + +class MessageReactions { + const MessageReactions(this.messageId, this.emojis); + final String messageId; + final List emojis; + + XMLNode toXml() { + return XMLNode.xmlns( + tag: 'reactions', + xmlns: messageReactionsXmlns, + attributes: { + 'id': messageId, + }, + children: emojis.map((emoji) { + return XMLNode( + tag: 'reaction', + text: emoji, + ); + }).toList(), + ); + } +} + +class MessageReactionsManager extends XmppManagerBase { + @override + List getDiscoFeatures() => [ messageReactionsXmlns ]; + + @override + String getName() => 'MessageReactionsManager'; + + @override + String getId() => messageReactionsManager; + + @override + List getIncomingStanzaHandlers() => [ + StanzaHandler( + stanzaTag: 'message', + tagName: 'reactions', + tagXmlns: messageReactionsXmlns, + callback: _onReactionsReceived, + // Before the message handler + priority: -99, + ), + ]; + + @override + Future isSupported() async => true; + + Future _onReactionsReceived(Stanza message, StanzaHandlerData state) async { + final reactionsElement = message.firstTag('reactions', xmlns: messageReactionsXmlns)!; + return state.copyWith( + messageReactions: MessageReactions( + reactionsElement.attributes['id']! as String, + reactionsElement.children + .where((c) => c.tag == 'reaction') + .map((c) => c.innerText()) + .toList(), + ), + ); + } +}