diff --git a/Assets/Plugins/StreamChat/Core/IStreamChatClient.cs b/Assets/Plugins/StreamChat/Core/IStreamChatClient.cs index d0fc323b..a578e238 100644 --- a/Assets/Plugins/StreamChat/Core/IStreamChatClient.cs +++ b/Assets/Plugins/StreamChat/Core/IStreamChatClient.cs @@ -57,6 +57,18 @@ public interface IStreamChatClient : IDisposable, IStreamChatClientEventsListene /// Invite to a was rejected /// event ChannelInviteHandler ChannelInviteRejected; + + /// + /// Local user was added to a channel as a member. This event fires only for channels that are not tracked locally. + /// Use this event to get notified when the local user is added to a new channel. For tracked channels, use the event. + /// + event ChannelMemberAddedHandler AddedToChannelAsMember; + + /// + /// Local user was removed from a channel as a member. This event fires only for channels that are not tracked locally. + /// Use this event to get notified when the local user was removed from a channel. For tracked channels use event + /// + event ChannelMemberRemovedHandler RemovedFromChannelAsMember; /// /// Current connection state @@ -64,7 +76,7 @@ public interface IStreamChatClient : IDisposable, IStreamChatClientEventsListene ConnectionState ConnectionState { get; } /// - /// Is client connected. Subscribe to to get notified when connection is established + /// Is client connected. Subscribe to event to get notified when connection is established /// bool IsConnected { get; } diff --git a/Assets/Plugins/StreamChat/Core/StatefulModels/IStreamChannel.cs b/Assets/Plugins/StreamChat/Core/StatefulModels/IStreamChannel.cs index a3c1c7a2..eb155f0c 100644 --- a/Assets/Plugins/StreamChat/Core/StatefulModels/IStreamChannel.cs +++ b/Assets/Plugins/StreamChat/Core/StatefulModels/IStreamChannel.cs @@ -62,7 +62,7 @@ public interface IStreamChannel : IStreamStatefulModel /// /// Event fired when a was added, updated, or removed. /// - event StreamChannelMemberAnyChangeHandler MembersChanged; + event StreamChannelMemberAnyChangeHandler MembersChanged; //StreamTodo: typo, this should be MemberChanged /// /// Event fired when visibility of this channel changed. Check to know if channel is hidden diff --git a/Assets/Plugins/StreamChat/Core/StreamChatClient.cs b/Assets/Plugins/StreamChat/Core/StreamChatClient.cs index ba8c90fd..60bf0ea6 100644 --- a/Assets/Plugins/StreamChat/Core/StreamChatClient.cs +++ b/Assets/Plugins/StreamChat/Core/StreamChatClient.cs @@ -49,7 +49,17 @@ namespace StreamChat.Core //StreamTodo: Handle restoring state after lost connection public delegate void ChannelInviteHandler(IStreamChannel channel, IStreamUser invitee); - + + /// + /// Member added to the channel handler + /// + public delegate void ChannelMemberAddedHandler(IStreamChannel channel, IStreamChannelMember member); + + /// + /// Member removed from the channel handler + /// + public delegate void ChannelMemberRemovedHandler(IStreamChannel channel, IStreamChannelMember member); + public sealed class StreamChatClient : IStreamChatClient { public event ConnectionMadeHandler Connected; @@ -65,6 +75,9 @@ public sealed class StreamChatClient : IStreamChatClient public event ChannelInviteHandler ChannelInviteReceived; public event ChannelInviteHandler ChannelInviteAccepted; public event ChannelInviteHandler ChannelInviteRejected; + + public event ChannelMemberAddedHandler AddedToChannelAsMember; + public event ChannelMemberRemovedHandler RemovedFromChannelAsMember; public const int QueryUsersLimitMaxValue = 30; public const int QueryUsersOffsetMaxValue = 1000; @@ -197,33 +210,9 @@ public Task DisconnectUserAsync() public bool IsLocalUser(IStreamUser user) => LocalUserData.User == user; - public async Task GetOrCreateChannelWithIdAsync(ChannelType channelType, string channelId, + public Task GetOrCreateChannelWithIdAsync(ChannelType channelType, string channelId, string name = null, IDictionary optionalCustomData = null) - { - StreamAsserts.AssertChannelTypeIsValid(channelType); - StreamAsserts.AssertChannelIdLength(channelId); - - var requestBodyDto = new ChannelGetOrCreateRequestInternalDTO - { - Presence = true, - State = true, - Watch = true, - Data = new ChannelRequestInternalDTO - { - Name = name, - }, - }; - - if (optionalCustomData != null && optionalCustomData.Any()) - { - requestBodyDto.Data.AdditionalProperties = optionalCustomData?.ToDictionary(x => x.Key, x => x.Value); - } - - var channelResponseDto = await InternalLowLevelClient.InternalChannelApi.GetOrCreateChannelAsync( - channelType, - channelId, requestBodyDto); - return _cache.TryCreateOrUpdate(channelResponseDto); - } + => InternalGetOrCreateChannelWithIdAsync(channelType, channelId, name, presence: true, state: true, watch: true, optionalCustomData); public async Task GetOrCreateChannelWithMembersAsync(ChannelType channelType, IEnumerable members, IDictionary optionalCustomData = null) @@ -579,6 +568,36 @@ void IStreamChatClientEventsListener.Destroy() void IStreamChatClientEventsListener.Update() => InternalLowLevelClient.Update(_timeService.DeltaTime); internal StreamChatLowLevelClient InternalLowLevelClient { get; } + + // We probably don't want to expose the presence, state, watch params to the public API + internal async Task InternalGetOrCreateChannelWithIdAsync(ChannelType channelType, string channelId, + string name = null, bool presence = true, bool state = true, bool watch = true, + IDictionary optionalCustomData = null) + { + StreamAsserts.AssertChannelTypeIsValid(channelType); + StreamAsserts.AssertChannelIdLength(channelId); + + var requestBodyDto = new ChannelGetOrCreateRequestInternalDTO + { + Presence = presence, + State = state, + Watch = watch, + Data = new ChannelRequestInternalDTO + { + Name = name, + }, + }; + + if (optionalCustomData != null && optionalCustomData.Any()) + { + requestBodyDto.Data.AdditionalProperties = optionalCustomData?.ToDictionary(x => x.Key, x => x.Value); + } + + var channelResponseDto = await InternalLowLevelClient.InternalChannelApi.GetOrCreateChannelAsync( + channelType, + channelId, requestBodyDto); + return _cache.TryCreateOrUpdate(channelResponseDto); + } internal IStreamLocalUserData UpdateLocalUser(OwnUserInternalDTO ownUserInternalDto) { @@ -843,15 +862,23 @@ private void OnMarkReadNotification(NotificationMarkReadEventInternalDTO eventDt _localUserData.InternalHandleMarkReadNotification(eventDto); } - private void OnAddedToChannelNotification(NotificationAddedToChannelEventInternalDTO obj) + private void OnAddedToChannelNotification(NotificationAddedToChannelEventInternalDTO eventDto) { - //StreamTodo: IMPLEMENT + var channel = _cache.TryCreateOrUpdate(eventDto.Channel); + var member = _cache.TryCreateOrUpdate(eventDto.Member); + _cache.TryCreateOrUpdate(eventDto.Member.User); + + AddedToChannelAsMember?.Invoke(channel, member); } private void OnRemovedFromChannelNotification( - NotificationRemovedFromChannelEventInternalDTO obj) + NotificationRemovedFromChannelEventInternalDTO eventDto) { -//StreamTodo: IMPLEMENT + var channel = _cache.TryCreateOrUpdate(eventDto.Channel); + var member = _cache.TryCreateOrUpdate(eventDto.Member); + _cache.TryCreateOrUpdate(eventDto.Member.User); + + RemovedFromChannelAsMember?.Invoke(channel, member); } private void OnInvitedNotification(NotificationInvitedEventInternalDTO eventDto) diff --git a/Assets/Plugins/StreamChat/Samples/EventsSamples.cs b/Assets/Plugins/StreamChat/Samples/EventsSamples.cs new file mode 100644 index 00000000..7a76c645 --- /dev/null +++ b/Assets/Plugins/StreamChat/Samples/EventsSamples.cs @@ -0,0 +1,161 @@ +using System.Threading.Tasks; +using StreamChat.Core; +using StreamChat.Core.Models; +using StreamChat.Core.StatefulModels; + +namespace StreamChat.Samples +{ + internal class EventsSamples + { + public async Task QueryChannelsEvents() + { + // Get a single channel + var channel = await Client.GetOrCreateChannelWithIdAsync(ChannelType.Messaging, "my-channel-id"); + + channel.MessageReceived += OnMessageReceived; + channel.MessageUpdated += OnMessageUpdated; + channel.MessageDeleted += OnMessageDeleted; + + channel.ReactionAdded += OnReactionAdded; + channel.ReactionUpdated += OnReactionUpdated; + channel.ReactionRemoved += OnReactionRemoved; + + channel.MemberAdded += OnMemberAdded; + channel.MemberRemoved += OnMemberRemoved; + channel.MemberUpdated += OnMemberUpdated; + + channel.MembersChanged += OnMembersChanged; + channel.VisibilityChanged += OnVisibilityChanged; + channel.MuteChanged += OnMuteChanged; + channel.Truncated += OnTruncated; + channel.Updated += OnUpdated; + channel.WatcherAdded += OnWatcherAdded; + channel.WatcherRemoved += OnWatcherRemoved; + channel.UserStartedTyping += OnUserStartedTyping; + channel.UserStoppedTyping += OnUserStoppedTyping; + channel.TypingUsersChanged += OnTypingUsersChanged; + } + + private void OnMessageReceived(IStreamChannel channel, IStreamMessage message) + { + } + + private void OnMessageUpdated(IStreamChannel channel, IStreamMessage message) + { + } + + private void OnMessageDeleted(IStreamChannel channel, IStreamMessage message, bool isharddelete) + { + } + + private void OnReactionAdded(IStreamChannel channel, IStreamMessage message, StreamReaction reaction) + { + } + + private void OnReactionUpdated(IStreamChannel channel, IStreamMessage message, StreamReaction reaction) + { + } + + private void OnReactionRemoved(IStreamChannel channel, IStreamMessage message, StreamReaction reaction) + { + } + + private void OnMemberAdded(IStreamChannel channel, IStreamChannelMember member) + { + } + + private void OnMemberRemoved(IStreamChannel channel, IStreamChannelMember member) + { + } + + private void OnMemberUpdated(IStreamChannel channel, IStreamChannelMember member) + { + } + + private void OnMembersChanged(IStreamChannel channel, IStreamChannelMember member, OperationType operationType) + { + } + + private void OnVisibilityChanged(IStreamChannel channel, bool isVisible) + { + } + + private void OnMuteChanged(IStreamChannel channel, bool isMuted) + { + } + + private void OnTruncated(IStreamChannel channel) + { + } + + private void OnUpdated(IStreamChannel channel) + { + } + + private void OnWatcherAdded(IStreamChannel channel, IStreamUser user) + { + } + + private void OnWatcherRemoved(IStreamChannel channel, IStreamUser user) + { + } + + private void OnUserStartedTyping(IStreamChannel channel, IStreamUser user) + { + } + + private void OnUserStoppedTyping(IStreamChannel channel, IStreamUser user) + { + } + + private void OnTypingUsersChanged(IStreamChannel channel) + { + } + + public void ClientEvents() + { + Client.AddedToChannelAsMember += OnAddedToChannelAsMember; + Client.RemovedFromChannelAsMember += OnRemovedFromChannel; + } + + private void OnAddedToChannelAsMember(IStreamChannel channel, IStreamChannelMember member) + { + // channel - new channel to which local user was just added + // member - object containing channel membership information + } + + private void OnRemovedFromChannel(IStreamChannel channel, IStreamChannelMember member) + { + // channel - channel from which local user was removed + // member - object containing channel membership information + } + + public void ConnectionEvents() + { + Client.Connected += OnConnected; + Client.Disconnected += OnDisconnected; + Client.ConnectionStateChanged += OnConnectionStateChanged; + } + + private void OnConnectionStateChanged(ConnectionState previous, ConnectionState current) + { + } + + private void OnDisconnected() + { + } + + private void OnConnected(IStreamLocalUserData localUserData) + { + } + + public void Unsubscribe() + { + Client.Connected -= OnConnected; + Client.Disconnected -= OnDisconnected; + Client.ConnectionStateChanged -= OnConnectionStateChanged; + } + + private IStreamChatClient Client { get; } = StreamChatClient.CreateDefaultClient(); + } +} \ No newline at end of file diff --git a/Assets/Plugins/StreamChat/Samples/EventsSamples.cs.meta b/Assets/Plugins/StreamChat/Samples/EventsSamples.cs.meta new file mode 100644 index 00000000..8bdab5b4 --- /dev/null +++ b/Assets/Plugins/StreamChat/Samples/EventsSamples.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 26a2e9b9d0584abb85bb616f1be15616 +timeCreated: 1718025761 \ No newline at end of file diff --git a/Assets/Plugins/StreamChat/Tests/StatefulClient/BaseStateIntegrationTests.cs b/Assets/Plugins/StreamChat/Tests/StatefulClient/BaseStateIntegrationTests.cs index 9291c22b..4dd45c40 100644 --- a/Assets/Plugins/StreamChat/Tests/StatefulClient/BaseStateIntegrationTests.cs +++ b/Assets/Plugins/StreamChat/Tests/StatefulClient/BaseStateIntegrationTests.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using StreamChat.Core; using StreamChat.Core.Exceptions; +using StreamChat.Core.InternalDTO.Requests; using StreamChat.Core.Requests; using StreamChat.Core.StatefulModels; using StreamChat.Libs.Auth; @@ -48,11 +49,11 @@ protected static IEnumerable OtherAdminUsersCredentials /// /// Create temp channel with random id that will be removed in [TearDown] /// - protected async Task CreateUniqueTempChannelAsync(string name = null) + protected async Task CreateUniqueTempChannelAsync(string name = null, bool watch = true) { var channelId = "random-channel-11111-" + Guid.NewGuid(); - var channelState = await Client.GetOrCreateChannelWithIdAsync(ChannelType.Messaging, channelId, name); + var channelState = await Client.InternalGetOrCreateChannelWithIdAsync(ChannelType.Messaging, channelId, name, watch: watch); _tempChannels.Add(channelState); return channelState; } @@ -210,7 +211,7 @@ private static async Task ConnectAndExecuteAsync(Func test) throw new AggregateException($"Failed all attempts. Last Exception: {exceptions.Last().Message} ", exceptions); } } - + private async Task DeleteTempChannelsAsync() { if (_tempChannels.Count == 0) @@ -218,20 +219,24 @@ private async Task DeleteTempChannelsAsync() return; } - try - { - await Client.DeleteMultipleChannelsAsync(_tempChannels, isHardDelete: true); - } - catch (StreamApiException streamApiException) + for (int i = 0; i < 5; i++) { - if (streamApiException.Code == StreamApiException.RateLimitErrorHttpStatusCode) + try { - await Task.Delay(500); + await Client.DeleteMultipleChannelsAsync(_tempChannels, isHardDelete: true); } + catch (StreamApiException streamApiException) + { + if (streamApiException.Code == StreamApiException.RateLimitErrorHttpStatusCode) + { + await Task.Delay(1000); + } - Debug.Log($"Try {nameof(DeleteTempChannelsAsync)} again due to exception: " + streamApiException); + Debug.Log($"Attempt {i} failed. Try {nameof(DeleteTempChannelsAsync)} again due to exception: " + streamApiException); + continue; + } - await Client.DeleteMultipleChannelsAsync(_tempChannels, isHardDelete: true); + break; } _tempChannels.Clear(); diff --git a/Assets/Plugins/StreamChat/Tests/StatefulClient/ChannelMembersTests.cs b/Assets/Plugins/StreamChat/Tests/StatefulClient/ChannelMembersTests.cs index 3def887f..c29c145d 100644 --- a/Assets/Plugins/StreamChat/Tests/StatefulClient/ChannelMembersTests.cs +++ b/Assets/Plugins/StreamChat/Tests/StatefulClient/ChannelMembersTests.cs @@ -65,12 +65,15 @@ private async Task When_add_user_by_id_to_channel_expect_user_included_in_member await WaitWhileTrueAsync(() => channel.Members.All(m => m.User != otherUser)); Assert.NotNull(channel.Members.FirstOrDefault(member => member.User == otherUser)); } - + [UnityTest] - public IEnumerator When_add_user_to_channel_with_hide_history_and_message_expect_user_as_members_and_message_sent() - => ConnectAndExecute(When_add_user_to_channel_with_hide_history_and_message_expect_user_as_members_and_message_sent_Async); + public IEnumerator + When_add_user_to_channel_with_hide_history_and_message_expect_user_as_members_and_message_sent() + => ConnectAndExecute( + When_add_user_to_channel_with_hide_history_and_message_expect_user_as_members_and_message_sent_Async); - private async Task When_add_user_to_channel_with_hide_history_and_message_expect_user_as_members_and_message_sent_Async() + private async Task + When_add_user_to_channel_with_hide_history_and_message_expect_user_as_members_and_message_sent_Async() { var channel = await CreateUniqueTempChannelAsync(); var otherUserId = OtherAdminUsersCredentials.First().UserId; @@ -266,22 +269,22 @@ private async Task When_remove_members_expect_member_added_event_fired_Async() var receivedEvent = false; IStreamChannelMember eventMember = null; IStreamChannel eventChannel = null; - channel.MemberRemoved += (chanel, member) => + channel.MemberRemoved += (channel2, member) => { receivedEvent = true; eventMember = member; - eventChannel = chanel; + eventChannel = channel2; }; var receivedEvent2 = false; IStreamChannelMember eventMember2 = null; IStreamChannel eventChannel2 = null; OperationType? opType = default; - channel.MembersChanged += (chanel, member, op) => + channel.MembersChanged += (channel3, member, op) => { receivedEvent2 = true; eventMember2 = member; - eventChannel2 = chanel; + eventChannel2 = channel3; opType = op; }; @@ -303,6 +306,71 @@ private async Task When_remove_members_expect_member_added_event_fired_Async() Assert.AreEqual(user, eventMember2.User); Assert.AreEqual(OperationType.Removed, opType.Value); } + + [UnityTest] + public IEnumerator When_user_added_to_not_watched_channel_expect_user_receive_added_to_channel_event() + => ConnectAndExecute( + When_user_added_to_not_watched_channel_expect_user_receive_added_to_channel_event_Async); + + private async Task When_user_added_to_not_watched_channel_expect_user_receive_added_to_channel_event_Async() + { + var channel = await CreateUniqueTempChannelAsync(watch: false); + + var receivedEvent = false; + IStreamChannelMember eventMember = null; + IStreamChannel eventChannel = null; + Client.AddedToChannelAsMember += (channel2, member) => + { + receivedEvent = true; + eventMember = member; + eventChannel = channel2; + }; + + await channel.AddMembersAsync(hideHistory: default, optionalMessage: default, Client.LocalUserData.User); + await WaitWhileFalseAsync(() => receivedEvent); + + Assert.IsTrue(receivedEvent); + Assert.IsNotNull(eventChannel); + Assert.IsNotNull(eventMember); + Assert.AreEqual(channel, eventChannel); + Assert.AreEqual(Client.LocalUserData.User, eventMember.User); + } + + [UnityTest] + public IEnumerator When_user_removed_from_not_watched_channel_expect_user_removed_from_channel_event() + => ConnectAndExecute( + When_user_removed_from_not_watched_channel_expect_user_removed_from_channel_event_Async); + + private async Task When_user_removed_from_not_watched_channel_expect_user_removed_from_channel_event_Async() + { + var channel = await CreateUniqueTempChannelAsync(watch: false); + + var receivedAddedEvent = false; + var receivedRemovedEvent = false; + IStreamChannelMember eventMember = null; + IStreamChannel eventChannel = null; + + Client.AddedToChannelAsMember += (channel2, member) => { receivedAddedEvent = true; }; + + await channel.AddMembersAsync(hideHistory: default, optionalMessage: default, Client.LocalUserData.User); + await WaitWhileFalseAsync(() => receivedAddedEvent); + + Client.RemovedFromChannelAsMember += (channel3, member2) => + { + receivedRemovedEvent = true; + eventMember = member2; + eventChannel = channel3; + }; + + await channel.RemoveMembersAsync(new IStreamUser[] { Client.LocalUserData.User }); + await WaitWhileFalseAsync(() => receivedRemovedEvent); + + Assert.IsTrue(receivedRemovedEvent); + Assert.IsNotNull(eventChannel); + Assert.IsNotNull(eventMember); + Assert.AreEqual(channel, eventChannel); + Assert.AreEqual(Client.LocalUserData.User, eventMember.User); + } } } #endif \ No newline at end of file