diff --git a/Assets/Plugins/StreamChat/Core/InternalDTO/Events/CustomEventInternalDTO.cs b/Assets/Plugins/StreamChat/Core/InternalDTO/Events/CustomEventInternalDTO.cs new file mode 100644 index 00000000..c043d749 --- /dev/null +++ b/Assets/Plugins/StreamChat/Core/InternalDTO/Events/CustomEventInternalDTO.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using StreamChat.Core.InternalDTO.Models; + +namespace StreamChat.Core.InternalDTO.Events +{ + // Hand-written (not OpenAPI-generated). As of today the OpenAPI spec CustomEvent schema is invalid + // and missing key fields required for WS dispatch (cid, user, channel_type, channel_id, parent_id). + internal sealed class CustomEventInternalDTO + { + [Newtonsoft.Json.JsonProperty("type")] + public string Type { get; set; } + + [Newtonsoft.Json.JsonProperty("cid")] + public string Cid { get; set; } + + [Newtonsoft.Json.JsonProperty("channel_type")] + public string ChannelType { get; set; } + + [Newtonsoft.Json.JsonProperty("channel_id")] + public string ChannelId { get; set; } + + [Newtonsoft.Json.JsonProperty("parent_id")] + public string ParentId { get; set; } + + [Newtonsoft.Json.JsonProperty("created_at")] + public DateTimeOffset CreatedAt { get; set; } + + [Newtonsoft.Json.JsonProperty("user")] + public UserObjectInternalDTO User { get; set; } + + [Newtonsoft.Json.JsonExtensionData] + public Dictionary AdditionalProperties { get; set; } + } +} diff --git a/Assets/Plugins/StreamChat/Core/InternalDTO/Events/CustomEventInternalDTO.cs.meta b/Assets/Plugins/StreamChat/Core/InternalDTO/Events/CustomEventInternalDTO.cs.meta new file mode 100644 index 00000000..1e84f427 --- /dev/null +++ b/Assets/Plugins/StreamChat/Core/InternalDTO/Events/CustomEventInternalDTO.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 66b38290422d97f45a54600cb30836c3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/StreamChat/Core/LowLevelClient/API/ChannelApi.cs b/Assets/Plugins/StreamChat/Core/LowLevelClient/API/ChannelApi.cs index 943cb37c..e464821a 100644 --- a/Assets/Plugins/StreamChat/Core/LowLevelClient/API/ChannelApi.cs +++ b/Assets/Plugins/StreamChat/Core/LowLevelClient/API/ChannelApi.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using StreamChat.Core.Helpers; using StreamChat.Core.InternalDTO.Responses; @@ -142,6 +143,10 @@ public Task SendTypingStartEventAsync(string channelType, string channelId) public Task SendTypingStopEventAsync(string channelType, string channelId) => _internalChannelApi.SendTypingStopEventAsync(channelType, channelId); + public Task SendCustomEventAsync(string channelType, string channelId, string eventType, + IDictionary customData = null) + => _internalChannelApi.SendCustomEventAsync(channelType, channelId, eventType, customData); + public async Task SyncAsync(SyncRequest syncRequest) { var dto = await _internalChannelApi.SyncAsync(syncRequest.TrySaveToDto()); diff --git a/Assets/Plugins/StreamChat/Core/LowLevelClient/API/IChannelApi.cs b/Assets/Plugins/StreamChat/Core/LowLevelClient/API/IChannelApi.cs index 9dcec612..54940280 100644 --- a/Assets/Plugins/StreamChat/Core/LowLevelClient/API/IChannelApi.cs +++ b/Assets/Plugins/StreamChat/Core/LowLevelClient/API/IChannelApi.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using StreamChat.Core.LowLevelClient.Models; using StreamChat.Core.LowLevelClient.Requests; @@ -151,6 +152,13 @@ Task MarkReadAsync(string channelType, string channelId, Task SendTypingStopEventAsync(string channelType, string channelId); + /// + /// Send a custom event to a channel. All members currently watching the channel receive it over the websocket. + /// + /// https://getstream.io/chat/docs/unity/event_object/?language=unity#custom-events + Task SendCustomEventAsync(string channelType, string channelId, string eventType, + IDictionary customData = null); + //StreamTodo: perhaps we can skip this declaration and use the Internal one directly Task SyncAsync(SyncRequest syncRequest); diff --git a/Assets/Plugins/StreamChat/Core/LowLevelClient/API/Internal/IInternalChannelApi.cs b/Assets/Plugins/StreamChat/Core/LowLevelClient/API/Internal/IInternalChannelApi.cs index 61a907d8..610cf627 100644 --- a/Assets/Plugins/StreamChat/Core/LowLevelClient/API/Internal/IInternalChannelApi.cs +++ b/Assets/Plugins/StreamChat/Core/LowLevelClient/API/Internal/IInternalChannelApi.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading.Tasks; using StreamChat.Core.InternalDTO.Requests; using StreamChat.Core.InternalDTO.Responses; @@ -56,6 +57,9 @@ Task MarkUnreadAsync(string channelType, string channelId, Task SendTypingStopEventAsync(string channelType, string channelId); + Task SendCustomEventAsync(string channelType, string channelId, string eventType, + IDictionary customData); + Task SyncAsync(SyncRequestInternalDTO syncRequest); Task GetUnreadCountsAsync(); diff --git a/Assets/Plugins/StreamChat/Core/LowLevelClient/API/Internal/InternalChannelApi.cs b/Assets/Plugins/StreamChat/Core/LowLevelClient/API/Internal/InternalChannelApi.cs index 82844443..dfb52a2d 100644 --- a/Assets/Plugins/StreamChat/Core/LowLevelClient/API/Internal/InternalChannelApi.cs +++ b/Assets/Plugins/StreamChat/Core/LowLevelClient/API/Internal/InternalChannelApi.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using StreamChat.Core.InternalDTO.Events; using StreamChat.Core.InternalDTO.Requests; @@ -139,6 +141,22 @@ public Task SendTypingStopEventAsync(string channelType, string channelId) Type = WSEventType.TypingStop }); + public Task SendCustomEventAsync(string channelType, string channelId, string eventType, + IDictionary customData) + { + var eventBody = new EventRequestInternalDTO + { + Type = eventType, + }; + + if (customData != null && customData.Count > 0) + { + eventBody.AdditionalProperties = customData.ToDictionary(kv => kv.Key, kv => kv.Value); + } + + return PostEventAsync(channelType, channelId, eventBody); + } + public Task SyncAsync(SyncRequestInternalDTO syncRequest) => Post($"/sync", syncRequest); diff --git a/Assets/Plugins/StreamChat/Core/LowLevelClient/Events/EventCustom.cs b/Assets/Plugins/StreamChat/Core/LowLevelClient/Events/EventCustom.cs new file mode 100644 index 00000000..75d30ca8 --- /dev/null +++ b/Assets/Plugins/StreamChat/Core/LowLevelClient/Events/EventCustom.cs @@ -0,0 +1,33 @@ +using StreamChat.Core.Helpers; +using StreamChat.Core.InternalDTO.Events; +using StreamChat.Core.InternalDTO.Models; +using StreamChat.Core.LowLevelClient.Models; + +namespace StreamChat.Core.LowLevelClient.Events +{ + public partial class EventCustom : EventBase, ILoadableFrom + { + public string ChannelId { get; set; } + + public string ChannelType { get; set; } + + public string Cid { get; set; } + + public string Type { get; set; } + + public User User { get; set; } + + EventCustom ILoadableFrom.LoadFromDto(CustomEventInternalDTO dto) + { + ChannelId = dto.ChannelId; + ChannelType = dto.ChannelType; + Cid = dto.Cid; + CreatedAt = dto.CreatedAt; + Type = dto.Type; + User = User.TryLoadFromDto(dto.User); + AdditionalProperties = dto.AdditionalProperties; + + return this; + } + } +} diff --git a/Assets/Plugins/StreamChat/Core/LowLevelClient/Events/EventCustom.cs.meta b/Assets/Plugins/StreamChat/Core/LowLevelClient/Events/EventCustom.cs.meta new file mode 100644 index 00000000..de541b8d --- /dev/null +++ b/Assets/Plugins/StreamChat/Core/LowLevelClient/Events/EventCustom.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: da6f78c93f318834bb8e4dc5f474eca1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/StreamChat/Core/LowLevelClient/IStreamRealtimeEventsProvider.cs b/Assets/Plugins/StreamChat/Core/LowLevelClient/IStreamRealtimeEventsProvider.cs index a071f99c..1f1c36b6 100644 --- a/Assets/Plugins/StreamChat/Core/LowLevelClient/IStreamRealtimeEventsProvider.cs +++ b/Assets/Plugins/StreamChat/Core/LowLevelClient/IStreamRealtimeEventsProvider.cs @@ -222,6 +222,14 @@ public interface IStreamRealtimeEventsProvider /// https://getstream.io/chat/docs/unity/event_object/?language=unity event Action TypingStopped; + /// + /// Event raised when a custom event is received on a channel. + /// + /// Use , , and to identify the channel, event type, and sender. + /// + /// https://getstream.io/chat/docs/unity/event_object/?language=unity#custom-events + event Action CustomEventReceived; + /// /// Notification Event raised when channel mutes are updated for local user. /// diff --git a/Assets/Plugins/StreamChat/Core/LowLevelClient/StreamChatLowLevelClient.cs b/Assets/Plugins/StreamChat/Core/LowLevelClient/StreamChatLowLevelClient.cs index a2bffcac..b45185e0 100644 --- a/Assets/Plugins/StreamChat/Core/LowLevelClient/StreamChatLowLevelClient.cs +++ b/Assets/Plugins/StreamChat/Core/LowLevelClient/StreamChatLowLevelClient.cs @@ -87,6 +87,8 @@ public class StreamChatLowLevelClient : IStreamChatLowLevelClient public event Action TypingStarted; public event Action TypingStopped; + public event Action CustomEventReceived; + public event Action NotificationChannelMutesUpdated; public event Action NotificationMutesUpdated; @@ -150,6 +152,8 @@ public class StreamChatLowLevelClient : IStreamChatLowLevelClient internal event Action InternalTypingStarted; internal event Action InternalTypingStopped; + internal event Action InternalCustomEventReceived; + internal event Action InternalNotificationChannelMutesUpdated; internal event Action InternalNotificationMutesUpdated; @@ -957,6 +961,11 @@ private void HandleNewWebsocketMessage(string msg) if (!_eventKeyToHandler.TryGetValue(type, out var handler)) { + if (TryHandleCustomChannelEvent(msg)) + { + return; + } + if (_config.LogLevel.IsDebugEnabled()) { _logs.Warning($"No message handler registered for `{type}`. Message not handled: " + msg); @@ -968,6 +977,32 @@ private void HandleNewWebsocketMessage(string msg) handler(msg); } + private bool TryHandleCustomChannelEvent(string serializedContent) + { + if (!_serializer.TryPeekValue(serializedContent, "cid", out var cid) + || string.IsNullOrEmpty(cid)) + { + return false; + } + + try + { + var dto = _serializer.Deserialize(serializedContent); + _lastEventReceivedAt = dto.CreatedAt; + + var evt = new EventCustom(); + ((ILoadableFrom)evt).LoadFromDto(dto); + CustomEventReceived?.Invoke(evt); + InternalCustomEventReceived?.Invoke(dto); + return true; + } + catch (Exception e) + { + _logs.Exception(e); + return false; + } + } + private void UpdateHealthCheck() { if (ConnectionState != ConnectionState.Connected) diff --git a/Assets/Plugins/StreamChat/Core/Models/IStreamCustomEvent.cs b/Assets/Plugins/StreamChat/Core/Models/IStreamCustomEvent.cs new file mode 100644 index 00000000..80359912 --- /dev/null +++ b/Assets/Plugins/StreamChat/Core/Models/IStreamCustomEvent.cs @@ -0,0 +1,25 @@ +using System; +using StreamChat.Core.StatefulModels; +using StreamChat.Core; + +namespace StreamChat.Core.Models +{ + /// + /// A custom event received on a channel. + /// + /// https://getstream.io/chat/docs/unity/event_object/?language=unity#custom-events + public interface IStreamCustomEvent + { + /// Custom event type, e.g. "friendship-request". + string Type { get; } + + /// User who sent the event (resolved from cache). + IStreamUser User { get; } + + /// Server timestamp of the event. + DateTimeOffset CreatedAt { get; } + + /// Custom payload delivered with the event (top-level custom fields). + IStreamCustomData CustomData { get; } + } +} diff --git a/Assets/Plugins/StreamChat/Core/Models/IStreamCustomEvent.cs.meta b/Assets/Plugins/StreamChat/Core/Models/IStreamCustomEvent.cs.meta new file mode 100644 index 00000000..046a5ffe --- /dev/null +++ b/Assets/Plugins/StreamChat/Core/Models/IStreamCustomEvent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: df39f85336eb82045bf71de2d4b90848 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/StreamChat/Core/Models/StreamCustomEvent.cs b/Assets/Plugins/StreamChat/Core/Models/StreamCustomEvent.cs new file mode 100644 index 00000000..7647e670 --- /dev/null +++ b/Assets/Plugins/StreamChat/Core/Models/StreamCustomEvent.cs @@ -0,0 +1,27 @@ +using System; +using StreamChat.Core; +using StreamChat.Core.State; +using StreamChat.Core.StatefulModels; + +namespace StreamChat.Core.Models +{ + internal sealed class StreamCustomEvent : IStreamCustomEvent + { + public string Type { get; } + + public IStreamUser User { get; } + + public DateTimeOffset CreatedAt { get; } + + public IStreamCustomData CustomData { get; } + + internal StreamCustomEvent(string type, IStreamUser user, DateTimeOffset createdAt, + StreamCustomData customData) + { + Type = type; + User = user; + CreatedAt = createdAt; + CustomData = customData; + } + } +} diff --git a/Assets/Plugins/StreamChat/Core/Models/StreamCustomEvent.cs.meta b/Assets/Plugins/StreamChat/Core/Models/StreamCustomEvent.cs.meta new file mode 100644 index 00000000..8f729d3b --- /dev/null +++ b/Assets/Plugins/StreamChat/Core/Models/StreamCustomEvent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7114062b9f6f0ed49b6fcb68e2c59aad +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/StreamChat/Core/State/StreamStatefulModelBase.cs b/Assets/Plugins/StreamChat/Core/State/StreamStatefulModelBase.cs index df900fca..bc7e6e67 100644 --- a/Assets/Plugins/StreamChat/Core/State/StreamStatefulModelBase.cs +++ b/Assets/Plugins/StreamChat/Core/State/StreamStatefulModelBase.cs @@ -3,6 +3,7 @@ using StreamChat.Core.LowLevelClient; using StreamChat.Core.State.Caches; using StreamChat.Libs.Logs; +using StreamChat.Libs.Serialization; namespace StreamChat.Core.State { @@ -31,6 +32,7 @@ internal StreamStatefulModelBase(string uniqueId, ICacheRepository Client.InternalLowLevelClient; protected ILogs Logs { get; } protected ICache Cache { get; } diff --git a/Assets/Plugins/StreamChat/Core/StatefulModels/IStreamChannel.cs b/Assets/Plugins/StreamChat/Core/StatefulModels/IStreamChannel.cs index 150d8776..d9b82cf3 100644 --- a/Assets/Plugins/StreamChat/Core/StatefulModels/IStreamChannel.cs +++ b/Assets/Plugins/StreamChat/Core/StatefulModels/IStreamChannel.cs @@ -107,6 +107,12 @@ public interface IStreamChannel : IStreamStatefulModel /// event StreamChannelUserChangeHandler UserStoppedTyping; + /// + /// Fired when a custom event (sent via ) is received on this channel. + /// Only fires while the channel is watched (). + /// + event StreamChannelCustomEventHandler CustomEventReceived; + /// /// Event fired when a the list of typing users has changed. /// If you want to exactly know when a users started or stopped typing subscribe to and @@ -586,6 +592,16 @@ Task TruncateAsync(DateTimeOffset? truncatedAt = default, string systemMessage = /// Task SendTypingStoppedEventAsync(); + /// + /// Send a custom event to this channel. All members currently watching the channel + /// (including the local user) receive it via . + /// Requires the `send-custom-events` capability (see ). + /// + /// Custom event type identifier, e.g. "friendship-request". + /// Optional custom key/value payload sent with the event. + /// https://getstream.io/chat/docs/unity/event_object/?language=unity#custom-events + Task SendCustomEventAsync(string eventType, IDictionary customData = null); + /// /// Joins this channel as a a member (). Only possible if local user has the `Join Own Channel` permission /// diff --git a/Assets/Plugins/StreamChat/Core/StatefulModels/StreamChannel.cs b/Assets/Plugins/StreamChat/Core/StatefulModels/StreamChannel.cs index 1bcd1419..1cf94535 100644 --- a/Assets/Plugins/StreamChat/Core/StatefulModels/StreamChannel.cs +++ b/Assets/Plugins/StreamChat/Core/StatefulModels/StreamChannel.cs @@ -35,6 +35,8 @@ public delegate void StreamChannelMemberAnyChangeHandler(IStreamChannel channel, public delegate void StreamMessageReactionHandler(IStreamChannel channel, IStreamMessage message, StreamReaction reaction); + public delegate void StreamChannelCustomEventHandler(IStreamChannel channel, IStreamCustomEvent customEvent); + internal sealed class StreamChannel : StreamStatefulModelBase, IUpdateableFrom, IUpdateableFrom2, @@ -80,6 +82,8 @@ internal sealed class StreamChannel : StreamStatefulModelBase, public event StreamChannelChangeHandler TypingUsersChanged; + public event StreamChannelCustomEventHandler CustomEventReceived; + #region Channel public bool AutoTranslationEnabled { get; private set; } @@ -691,6 +695,12 @@ public Task SendTypingStartedEventAsync() public Task SendTypingStoppedEventAsync() => LowLevelClient.InternalChannelApi.SendTypingStopEventAsync(Type, Id); + public Task SendCustomEventAsync(string eventType, IDictionary customData = null) + { + StreamAsserts.AssertNotNullOrEmpty(eventType, nameof(eventType)); + return LowLevelClient.InternalChannelApi.SendCustomEventAsync(Type, Id, eventType, customData); + } + public override string ToString() => $"Channel - Id: {Id}, Name: {Name}"; internal StreamChannel(string uniqueId, ICacheRepository repository, @@ -1148,6 +1158,30 @@ internal void InternalHandleTypingStarted(TypingStartEventInternalDTO eventDto) } } + internal void InternalHandleCustomEvent(CustomEventInternalDTO dto) + { + AssertCid(dto.Cid); + + var user = Cache.TryCreateOrUpdate(dto.User); + + var custom = new Dictionary(); + if (dto.AdditionalProperties != null) + { + foreach (var kv in dto.AdditionalProperties) + { + if (!CustomEventEnvelopeKeys.Contains(kv.Key)) + { + custom[kv.Key] = kv.Value; + } + } + } + + var customEvent = new StreamCustomEvent(dto.Type, user, dto.CreatedAt, + new StreamCustomData(custom, Serializer)); + + CustomEventReceived?.Invoke(this, customEvent); + } + internal void InternalNotifyReactionReceived(StreamMessage message, StreamReaction reaction) => ReactionAdded?.Invoke(this, message, reaction); @@ -1160,6 +1194,11 @@ public void InternalNotifyReactionDeleted(StreamMessage message, StreamReaction //StreamTodo: implement some timeout for typing users in case we dont' receive, this could be configurable private readonly List _typingUsers = new List(); + private static readonly HashSet CustomEventEnvelopeKeys = new HashSet + { + "type", "cid", "channel_type", "channel_id", "parent_id", "created_at", "user" + }; + private void HandleMessageRead(UserObjectInternalDTO userDto, DateTimeOffset createAt) { //we can only mark messages based on created_at diff --git a/Assets/Plugins/StreamChat/Core/StreamChatClient.cs b/Assets/Plugins/StreamChat/Core/StreamChatClient.cs index 96d2162a..d05928fd 100644 --- a/Assets/Plugins/StreamChat/Core/StreamChatClient.cs +++ b/Assets/Plugins/StreamChat/Core/StreamChatClient.cs @@ -1667,6 +1667,14 @@ private void OnTypingStarted(TypingStartEventInternalDTO eventDto) } } + private void OnCustomEventReceived(CustomEventInternalDTO eventDto) + { + if (_cache.Channels.TryGet(eventDto.Cid, out var streamChannel)) + { + streamChannel.InternalHandleCustomEvent(eventDto); + } + } + private void SubscribeTo(StreamChatLowLevelClient lowLevelClient) { lowLevelClient.InternalConnected += OnConnected; @@ -1704,6 +1712,8 @@ private void SubscribeTo(StreamChatLowLevelClient lowLevelClient) lowLevelClient.InternalTypingStarted += OnTypingStarted; lowLevelClient.InternalTypingStopped += OnTypingStopped; + lowLevelClient.InternalCustomEventReceived += OnCustomEventReceived; + lowLevelClient.InternalNotificationChannelMutesUpdated += OnChannelMutesUpdatedNotification; lowLevelClient.InternalNotificationMutesUpdated += OnMutesUpdatedNotification; @@ -1769,6 +1779,8 @@ private void UnsubscribeFrom(StreamChatLowLevelClient lowLevelClient) lowLevelClient.InternalTypingStarted -= OnTypingStarted; lowLevelClient.InternalTypingStopped -= OnTypingStopped; + lowLevelClient.InternalCustomEventReceived -= OnCustomEventReceived; + lowLevelClient.InternalNotificationChannelMutesUpdated -= OnChannelMutesUpdatedNotification; lowLevelClient.InternalNotificationMutesUpdated -= OnMutesUpdatedNotification; diff --git a/Assets/Plugins/StreamChat/Samples/EventsSamples.cs b/Assets/Plugins/StreamChat/Samples/EventsSamples.cs index bc03861f..fa0effa7 100644 --- a/Assets/Plugins/StreamChat/Samples/EventsSamples.cs +++ b/Assets/Plugins/StreamChat/Samples/EventsSamples.cs @@ -65,6 +65,7 @@ public async Task ListeningForEvents() channel.UserStartedTyping += OnUserStartedTyping; channel.UserStoppedTyping += OnUserStoppedTyping; channel.TypingUsersChanged += OnTypingUsersChanged; + channel.CustomEventReceived += OnCustomEventReceived; // 4. Per-message events // Reaction events fire on the specific IStreamMessage instance. @@ -227,6 +228,10 @@ private void OnTypingUsersChanged(IStreamChannel channel) { } + private void OnCustomEventReceived(IStreamChannel channel, IStreamCustomEvent customEvent) + { + } + // ---- Thread-level handlers ---- private void OnThreadUpdated(IStreamThread thread) @@ -319,9 +324,22 @@ public void Unsubscribe() /// /// https://getstream.io/chat/docs/unity/event-object/?language=unity#to-a-channel /// - public void SendCustomEventToChannel() + public async Task SendCustomEventToChannel() { - // Not yet supported in the Unity SDK + var channel = await Client.GetOrCreateChannelWithIdAsync(ChannelType.Messaging, "my-channel-id"); + + channel.CustomEventReceived += (ch, evt) => + { + if (evt.Type == "friendship-request" && evt.CustomData.TryGet("text", out var text)) + { + // handle + } + }; + + await channel.SendCustomEventAsync("friendship-request", new Dictionary + { + { "text", "Hi, let's be friends!" }, + }); } /// diff --git a/Assets/Plugins/StreamChat/Tests/LowLevelClient/Integration/ChannelApiIntegrationTests.cs b/Assets/Plugins/StreamChat/Tests/LowLevelClient/Integration/ChannelApiIntegrationTests.cs index 489b2947..b51641cf 100644 --- a/Assets/Plugins/StreamChat/Tests/LowLevelClient/Integration/ChannelApiIntegrationTests.cs +++ b/Assets/Plugins/StreamChat/Tests/LowLevelClient/Integration/ChannelApiIntegrationTests.cs @@ -850,6 +850,19 @@ public IEnumerator When_sending_typing_start_stop_events_expect_no_errors() yield return ConnectAndExecute(When_sending_typing_start_stop_events_expect_no_exceptions_Async); } + [UnityTest] + public IEnumerator When_sending_custom_channel_event_expect_no_errors() + => ConnectAndExecute(When_sending_custom_channel_event_expect_no_errors_Async); + + private async Task When_sending_custom_channel_event_expect_no_errors_Async() + { + const string channelType = "messaging"; + var tempChannel = await CreateTempUniqueChannelAsync(channelType, new ChannelGetOrCreateRequest()); + + await LowLevelClient.ChannelApi.SendCustomEventAsync(channelType, tempChannel.Channel.Id, + "friendship-request", new Dictionary { { "text", "hi" } }); + } + private async Task When_sending_typing_start_stop_events_expect_no_exceptions_Async() { const string channelType = "messaging"; diff --git a/Assets/Plugins/StreamChat/Tests/StatefulClient/ChannelsTests.cs b/Assets/Plugins/StreamChat/Tests/StatefulClient/ChannelsTests.cs index f5e3481e..9e3527d5 100644 --- a/Assets/Plugins/StreamChat/Tests/StatefulClient/ChannelsTests.cs +++ b/Assets/Plugins/StreamChat/Tests/StatefulClient/ChannelsTests.cs @@ -777,6 +777,118 @@ private async Task When_stop_watching_then_get_or_create_expect_same_instance_wa Assert.IsTrue(rewatched.IsWatched); Assert.IsTrue(Client.WatchedChannels.Any(c => c.Cid == channel.Cid)); } + + [UnityTest] + public IEnumerator When_custom_event_sent_expect_watchers_receive_it() + => ConnectAndExecute(When_custom_event_sent_expect_watchers_receive_it_Async); + + private async Task When_custom_event_sent_expect_watchers_receive_it_Async() + { + var channel = await CreateUniqueTempChannelAsync(); + + IStreamCustomEvent received = null; + var threadId = -1; + + void OnCustom(IStreamChannel ch, IStreamCustomEvent evt) + { + received = evt; + threadId = GetCurrentThreadId(); + } + + channel.CustomEventReceived += OnCustom; + + await channel.SendCustomEventAsync("friendship-request", new Dictionary + { + { "text", "hello" }, + { "score", 42 }, + }); + + await WaitWhileFalseAsync(() => received != null, + description: "custom event received on sender channel"); + + channel.CustomEventReceived -= OnCustom; + + Assert.IsNotNull(received); + Assert.AreEqual("friendship-request", received.Type); + Assert.AreEqual(Client.LocalUserData.User.Id, received.User.Id); + Assert.IsTrue(received.CustomData.TryGet("text", out var text)); + Assert.AreEqual("hello", text); + Assert.IsTrue(received.CustomData.TryGet("score", out var score)); + Assert.AreEqual(42, score); + Assert.IsFalse(received.CustomData.ContainsKey("cid")); + Assert.IsFalse(received.CustomData.ContainsKey("type")); + Assert.IsFalse(received.CustomData.ContainsKey("user")); + Assert.IsFalse(received.CustomData.ContainsKey("channel_type")); + Assert.IsFalse(received.CustomData.ContainsKey("channel_id")); + Assert.IsFalse(received.CustomData.ContainsKey("created_at")); + Assert.IsFalse(received.CustomData.ContainsKey("parent_id")); + Assert.IsTrue(received.CreatedAt > DateTimeOffset.MinValue); + Assert.AreEqual(MainThreadId, threadId); + } + + [UnityTest] + public IEnumerator When_custom_event_sent_with_empty_payload_expect_receive() + => ConnectAndExecute(When_custom_event_sent_with_empty_payload_expect_receive_Async); + + private async Task When_custom_event_sent_with_empty_payload_expect_receive_Async() + { + var channel = await CreateUniqueTempChannelAsync(); + + IStreamCustomEvent received = null; + channel.CustomEventReceived += (_, evt) => received = evt; + + await channel.SendCustomEventAsync("ping"); + + await WaitWhileFalseAsync(() => received != null, + description: "empty-payload custom event received"); + + channel.CustomEventReceived -= (_, evt) => { }; + + Assert.IsNotNull(received); + Assert.AreEqual("ping", received.Type); + Assert.AreEqual(0, received.CustomData.Count); + } + + [UnityTest] + public IEnumerator When_other_client_sends_custom_event_expect_local_watcher_receives_it() + => ConnectAndExecute(When_other_client_sends_custom_event_expect_local_watcher_receives_it_Async); + + private async Task When_other_client_sends_custom_event_expect_local_watcher_receives_it_Async() + { + var otherClient = await GetConnectedOtherClientAsync(); + var channel = await CreateUniqueTempChannelAsync(); + + var otherClientChannel = await otherClient.InternalGetOrCreateChannelWithIdAsync( + channel.Type, channel.Id, watch: true); + + IStreamCustomEvent received = null; + var threadId = -1; + + void OnCustom(IStreamChannel ch, IStreamCustomEvent evt) + { + received = evt; + threadId = GetCurrentThreadId(); + } + + channel.CustomEventReceived += OnCustom; + + await otherClientChannel.SendCustomEventAsync("game-invite", new Dictionary + { + { "level", 3 }, + }); + + await WaitWhileFalseAsync(() => received != null, + description: "custom event received from other client"); + + channel.CustomEventReceived -= OnCustom; + + Assert.IsNotNull(received); + Assert.AreEqual("game-invite", received.Type); + Assert.AreEqual(otherClient.LocalUserData.User.Id, received.User.Id); + Assert.IsTrue(received.CustomData.TryGet("level", out var level)); + Assert.AreEqual(3, level); + Assert.AreEqual(MainThreadId, threadId); + } } }