diff --git a/.pubnub.yml b/.pubnub.yml index 4fc5d2b..28fe44b 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,6 +1,13 @@ --- -version: v1.0.0 +version: v1.1.0 changelog: + - date: 2025-10-29 + version: v1.1.0 + changes: + - type: feature + text: "Added the option of performing reversible soft deletion on Channel and User entities." + - type: feature + text: "Added a MutedUsersManager class that allows for muting specific user IDs on client side." - date: 2025-10-02 version: v1.0.0 changes: diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs index 052b773..e5aa3ce 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs @@ -66,7 +66,7 @@ public async Task TestUpdateChannel() } [Test] - public async Task TestDeleteChannel() + public async Task TestHardDeleteChannel() { var channel = TestUtils.AssertOperation(await chat.CreatePublicConversation()); @@ -75,7 +75,7 @@ public async Task TestDeleteChannel() var channelExists = await chat.GetChannel(channel.Id); Assert.False(channelExists.Error, "Couldn't fetch created channel from chat"); - await channel.Delete(); + await channel.Delete(false); await Task.Delay(3000); @@ -83,6 +83,34 @@ public async Task TestDeleteChannel() Assert.True(channelAfterDelete.Error, "Fetched the supposedly-deleted channel from chat"); } + [Test] + public async Task TestSoftDeleteAndRestoreChannel() + { + var channel = TestUtils.AssertOperation(await chat.CreatePublicConversation()); + + await Task.Delay(3000); + + var channelExists = await chat.GetChannel(channel.Id); + Assert.False(channelExists.Error, "Couldn't fetch created channel from chat"); + + await channel.Delete(true); + + await Task.Delay(3000); + + var channelAfterDelete = await chat.GetChannel(channel.Id); + Assert.False(channelAfterDelete.Error, "Channel should still exist after soft-delete"); + Assert.True(channelAfterDelete.Result.IsDeleted, "Channel should be marked as soft-deleted"); + + await channelAfterDelete.Result.Restore(); + Assert.False(channelAfterDelete.Result.IsDeleted, "Channel should be restored"); + + await Task.Delay(3000); + + var channelAfterRestore = await chat.GetChannel(channel.Id); + Assert.False(channelAfterRestore.Error, "Channel should still exist after restore"); + Assert.False(channelAfterRestore.Result.IsDeleted, "Channel fetched from server again should be marked as not-deleted after restore"); + } + [Test] public async Task TestLeaveChannel() { diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs index ed9dd30..7721ccf 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatTests.cs @@ -107,7 +107,7 @@ public async Task TestCreateDirectConversation() directConversation.InviteesMemberships.First().UserId == convoUser.Id); //Cleanup - await directConversation.CreatedChannel.Delete(); + await directConversation.CreatedChannel.Delete(false); } [Test] @@ -126,7 +126,7 @@ public async Task TestCreateGroupConversation() x.UserId == convoUser1.Id && x.ChannelId == id)); //Cleanup - await groupConversation.CreatedChannel.Delete(); + await groupConversation.CreatedChannel.Delete(false); } [Test] @@ -172,11 +172,20 @@ public async Task TestEmitEvent() [Test] public async Task TestGetUnreadMessagesCounts() { - await channel.SendText("wololo"); + var testChannel = TestUtils.AssertOperation(await chat.CreatePublicConversation()); + await testChannel.Join(); + await testChannel.SendText("wololo"); + await testChannel.SendText("wololo1"); + await testChannel.SendText("wololo2"); + await testChannel.SendText("wololo3"); - await Task.Delay(3000); + await Task.Delay(6000); + + var unreads = + TestUtils.AssertOperation(await chat.GetUnreadMessagesCounts(filter:$"channel.id LIKE \"{testChannel.Id}\"")); + Assert.True(unreads.Any(x => x.ChannelId == testChannel.Id && x.Count == 4)); - Assert.True(TestUtils.AssertOperation(await chat.GetUnreadMessagesCounts(limit: 50)).Any(x => x.ChannelId == channel.Id && x.Count > 0)); + await testChannel.Delete(false); } [Test] @@ -200,7 +209,7 @@ public async Task TestMarkAllMessagesAsRead() var counts = TestUtils.AssertOperation(await chat.GetUnreadMessagesCounts()); markTestChannel.Leave(); - await markTestChannel.Delete(); + await markTestChannel.Delete(false); Assert.False(counts.Any(x => x.ChannelId == markTestChannel.Id && x.Count > 0)); } diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ClientSideMuteTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ClientSideMuteTests.cs new file mode 100644 index 0000000..fb77c15 --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ClientSideMuteTests.cs @@ -0,0 +1,187 @@ +using PubnubApi; +using PubnubChatApi; +using Channel = PubnubChatApi.Channel; + +namespace PubNubChatApi.Tests; + +public class ClientSideMuteTests +{ + private Chat chat1; + private User user1; + + private Chat chat2; + private User user2; + + private Channel channel1; + private Channel channel2; + + [SetUp] + public async Task Setup() + { + chat1 = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(), + new PNConfiguration(new UserId("client_side_mute_test_user_1")) + { + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + })); + chat2 = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(), + new PNConfiguration(new UserId("client_side_mute_test_user_2")) + { + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + })); + user1 = TestUtils.AssertOperation(await chat1.GetCurrentUser()); + user2 = TestUtils.AssertOperation(await chat2.GetCurrentUser()); + channel1 = TestUtils.AssertOperation(await chat1.CreatePublicConversation("mute_tests_channel")); + await Task.Delay(3000); + channel2 = TestUtils.AssertOperation(await chat2.GetChannel("mute_tests_channel")); + } + + [TearDown] + public async Task CleanUp() + { + await channel1.Leave(); + await channel2.Leave(); + await user1.DeleteUser(false); + chat1.Destroy(); + await user2.DeleteUser(false); + chat2.Destroy(); + await channel1.Delete(false); + await channel2.Delete(false); + await Task.Delay(4000); + } + + [Test] + public async Task TestMuteInMessages() + { + var messageReset = new ManualResetEvent(false); + channel1.OnMessageReceived += message => + { + messageReset.Set(); + }; + await channel1.Join(); + + await Task.Delay(3000); + + await channel2.SendText("This message should not be muted."); + var received = messageReset.WaitOne(10000); + Assert.True(received, "Didn't receive message from not-yet-muted user."); + + messageReset = new ManualResetEvent(false); + await chat1.MutedUsersManager.MuteUser(user2.Id); + await channel2.SendText("This message should be muted."); + received = messageReset.WaitOne(10000); + Assert.False(received, "Received message from muted user."); + + messageReset = new ManualResetEvent(false); + await chat1.MutedUsersManager.UnMuteUser(user2.Id); + await channel2.SendText("This message shouldn't be muted now."); + received = messageReset.WaitOne(10000); + Assert.True(received, "Didn't receive message from un-muted user."); + } + + [Test] + public async Task TestMuteInMessageHistory() + { + await channel2.SendText("One"); + await channel2.SendText("Two"); + await channel2.SendText("Three"); + + await Task.Delay(6000); + + var history = TestUtils.AssertOperation(await channel1.GetMessageHistory("99999999999999999", "00000000000000000", 3)); + Assert.True(history.Count == 3, "Didn't get message history for non-muted user"); + + await chat1.MutedUsersManager.MuteUser(user2.Id); + + history = TestUtils.AssertOperation(await channel1.GetMessageHistory("99999999999999999", "00000000000000000", 3)); + Assert.True(history.Count == 0, "Got message history for muted user"); + + await chat1.MutedUsersManager.UnMuteUser(user2.Id); + + history = TestUtils.AssertOperation(await channel1.GetMessageHistory("99999999999999999", "00000000000000000", 3)); + Assert.True(history.Count == 3, "Didn't get message history for un-muted user"); + } + + [Test] + public async Task TestMuteInEvents() + { + var eventReset = new ManualResetEvent(false); + channel1.OnCustomEvent += chatEvent => + { + if (chatEvent.Type != PubnubChatEventType.Custom) + { + return; + } + eventReset.Set(); + }; + channel1.SetListeningForCustomEvents(true); + + await Task.Delay(3000); + + await chat2.EmitEvent(PubnubChatEventType.Custom, channel2.Id, "{\"test\":\"not-muted\"}"); + var received = eventReset.WaitOne(10000); + Assert.True(received, "Didn't receive event from not-yet-muted user."); + + eventReset = new ManualResetEvent(false); + await chat1.MutedUsersManager.MuteUser(user2.Id); + await chat2.EmitEvent(PubnubChatEventType.Custom, channel2.Id, "{\"test\":\"muted\"}"); + received = eventReset.WaitOne(10000); + Assert.False(received, "Received event from muted user."); + + eventReset = new ManualResetEvent(false); + await chat1.MutedUsersManager.UnMuteUser(user2.Id); + await chat2.EmitEvent(PubnubChatEventType.Custom, channel2.Id, "{\"test\":\"un-muted\"}"); + received = eventReset.WaitOne(10000); + Assert.True(received, "Didn't receive event from un-muted user."); + } + + [Test] + public async Task TestMuteInEventsHistory() + { + await chat2.EmitEvent(PubnubChatEventType.Custom, channel2.Id, "{\"test\":\"one\"}"); + await chat2.EmitEvent(PubnubChatEventType.Custom, channel2.Id, "{\"test\":\"two\"}"); + await chat2.EmitEvent(PubnubChatEventType.Custom, channel2.Id, "{\"test\":\"three\"}"); + + var history = TestUtils.AssertOperation(await chat1.GetEventsHistory(channel1.Id,"99999999999999999", "00000000000000000", 3)); + Assert.True(history.Events.Count == 3, "Didn't get events history for non-muted user"); + + await chat1.MutedUsersManager.MuteUser(user2.Id); + + history = TestUtils.AssertOperation(await chat1.GetEventsHistory(channel1.Id,"99999999999999999", "00000000000000000", 3)); + Assert.True(history.Events.Count == 0, "Got events history for muted user"); + + await chat1.MutedUsersManager.UnMuteUser(user2.Id); + + history = TestUtils.AssertOperation(await chat1.GetEventsHistory(channel1.Id,"99999999999999999", "00000000000000000", 3)); + Assert.True(history.Events.Count == 3, "Didn't get events history for un-muted user"); + } + + [Test] + public async Task TestMuteListSyncing() + { + var userId = Guid.NewGuid().ToString(); + var chatWithSync = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(syncMutedUsers:true), + new PNConfiguration(new UserId(userId)) + { + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + })); + TestUtils.AssertOperation(await chatWithSync.MutedUsersManager.MuteUser(user1.Id)); + + chatWithSync.Destroy(); + + await Task.Delay(3000); + var chatWithSyncSecondInstance = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(syncMutedUsers:true), + new PNConfiguration(new UserId(userId)) + { + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey + })); + await Task.Delay(5000); + Assert.True(chatWithSyncSecondInstance.MutedUsersManager.MutedUsers.Contains(user1.Id), "Second instance of chat didn't have synced mute list"); + + chatWithSyncSecondInstance.Destroy(); + await chatWithSyncSecondInstance.DeleteUser(userId); + } +} \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs index 8a51705..4270320 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/MessageTests.cs @@ -160,7 +160,7 @@ public async Task TestDeleteMessage() var manualReceivedEvent = new ManualResetEvent(false); channel.OnMessageReceived += async message => { - message.Delete(true); + await message.Delete(true); await Task.Delay(2000); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/PubnubTestsParameters.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/PubnubTestsParameters.cs index 1cfc535..c8680ba 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/PubnubTestsParameters.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/PubnubTestsParameters.cs @@ -6,7 +6,7 @@ public static class PubnubTestsParameters private static readonly string EnvSubscribeKey = Environment.GetEnvironmentVariable("PN_SUB_KEY"); private static readonly string EnvSecretKey = Environment.GetEnvironmentVariable("PN_SEC_KEY"); - public static readonly string PublishKey = string.IsNullOrEmpty(EnvPublishKey) ? "pub-c-79c582a2-d7a4-4ee7-9f28-7a6f1b7fa11c" : EnvPublishKey; - public static readonly string SubscribeKey = string.IsNullOrEmpty(EnvSubscribeKey) ? "sub-c-ca0af928-f4f9-474c-b56e-d6be81bf8ed0" : EnvSubscribeKey; + public static readonly string PublishKey = string.IsNullOrEmpty(EnvPublishKey) ? "demo-36" : EnvPublishKey; + public static readonly string SubscribeKey = string.IsNullOrEmpty(EnvSubscribeKey) ? "demo-36" : EnvSubscribeKey; public static readonly string SecretKey = string.IsNullOrEmpty(EnvSecretKey) ? "demo-36" : EnvSecretKey; } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs index 46db29a..fb2bfe1 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ThreadsTests.cs @@ -32,7 +32,7 @@ public async Task CleanUp() { channel.Leave(); await Task.Delay(3000); - await channel.Delete(); + await channel.Delete(false); chat.Destroy(); await Task.Delay(3000); } diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs index 81f57ab..f538dd8 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/UserTests.cs @@ -89,17 +89,17 @@ await testUser.Update(new ChatUserData() Assert.True(updated); //Cleanup - await testUser.DeleteUser(); + await testUser.DeleteUser(false); } [Test] - public async Task TestUserDelete() + public async Task TestHardUserDelete() { var someUser = TestUtils.AssertOperation(await chat.CreateUser(Guid.NewGuid().ToString())); TestUtils.AssertOperation(await chat.GetUser(someUser.Id)); - await someUser.DeleteUser(); + await someUser.DeleteUser(false); await Task.Delay(3000); @@ -109,6 +109,34 @@ public async Task TestUserDelete() Assert.Fail("Got the freshly deleted user"); } } + + [Test] + public async Task TestSoftDeleteAndRestoreUser() + { + var testUser = TestUtils.AssertOperation(await chat.CreateUser(Guid.NewGuid().ToString())); + + await Task.Delay(3000); + + var userExists = await chat.GetUser(testUser.Id); + Assert.False(userExists.Error, "Couldn't fetch created user from chat"); + + await testUser.DeleteUser(true); + + await Task.Delay(3000); + + var userAfterDelete = await chat.GetUser(testUser.Id); + Assert.False(userAfterDelete.Error, "User should still exist after soft-delete"); + Assert.True(userAfterDelete.Result.IsDeleted, "user should be marked as soft-deleted"); + + await userAfterDelete.Result.Restore(); + Assert.False(userAfterDelete.Result.IsDeleted, "User should be restored"); + + await Task.Delay(3000); + + var userAfterRestore = await chat.GetUser(testUser.Id); + Assert.False(userAfterRestore.Error, "User should still exist after restore"); + Assert.False(userAfterRestore.Result.IsDeleted, "User fetched from server again should be marked as not-deleted after restore"); + } [Test] public async Task TestUserWherePresent() diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs index 8b340cb..4eee71d 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs @@ -66,6 +66,21 @@ public class Channel : UniqueChatEntity /// /// public string Type => channelData.Type; + + /// + /// Returns true if the Channel has been soft-deleted. + /// + public bool IsDeleted + { + get + { + if (CustomData == null || !CustomData.TryGetValue("deleted", out var deletedValue)) + { + return false; + } + return (bool)deletedValue; + } + } protected ChatChannelData channelData; @@ -564,7 +579,8 @@ public void Connect() SetListening(ref subscription, SubscriptionOptions.None, true, Id, chat.ListenerFactory.ProduceListener(messageCallback: delegate(Pubnub pn, PNMessageResult m) { - if (ChatParsers.TryParseMessageResult(chat, m, out var message)) + if (ChatParsers.TryParseMessageResult(chat, m, out var message) + && !chat.MutedUsersManager.MutedUsers.Contains(message.UserId)) { OnMessageReceived?.Invoke(message); } @@ -802,6 +818,7 @@ public async Task Update(ChatChannelData updatedData) /// Deletes the channel and removes all the messages and memberships from the channel. /// /// + /// /// Whether to perform a soft delete (true) or hard delete (false). /// A ChatOperationResult indicating the success or failure of the operation. /// /// @@ -809,9 +826,50 @@ public async Task Update(ChatChannelData updatedData) /// var result = await channel.Delete(); /// /// - public async Task Delete() + public async Task Delete(bool soft = false) + { + var result = new ChatOperationResult("Channel.Delete()", chat); + if (!result.RegisterOperation(await chat.DeleteChannel(Id, soft)) && soft) + { + channelData.CustomData ??= new Dictionary(); + channelData.CustomData["deleted"] = true; + } + return result; + } + + /// + /// Restores a previously deleted channel. + /// + /// Undoes the soft deletion of this channel. + /// This only works for channels that were soft deleted. + /// + /// + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var channel = // ...; + /// if (channel.IsDeleted) { + /// var result = await channel.Restore(); + /// if (!result.Error) { + /// // Channel has been restored + /// } + /// } + /// + /// + /// + /// + public async Task Restore() { - return await chat.DeleteChannel(Id).ConfigureAwait(false); + var result = new ChatOperationResult("Channel.Restore()", chat); + if (!IsDeleted) + { + result.Error = true; + result.Exception = new PNException("Can't restore a channel that wasn't deleted!"); + return result; + } + channelData.CustomData.Remove("deleted"); + result.RegisterOperation(await UpdateChannelData(chat, Id, channelData)); + return result; } /// diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs index 170400f..3f65ad7 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -30,6 +30,7 @@ public class Chat public event Action OnAnyEvent; public ChatAccessManager ChatAccessManager { get; } + public MutedUsersManager MutedUsersManager { get; } public PubnubChatConfig Config { get; } internal ExponentialRateLimiter RateLimiter { get; } @@ -99,6 +100,7 @@ internal Chat(PubnubChatConfig chatConfig, PNConfiguration pubnubConfig, ChatLis ListenerFactory = listenerFactory ?? new DotNetListenerFactory(); Config = chatConfig; ChatAccessManager = new ChatAccessManager(this); + MutedUsersManager = new MutedUsersManager(this); RateLimiter = new ExponentialRateLimiter(chatConfig.RateLimitFactor); } @@ -108,6 +110,7 @@ internal Chat(PubnubChatConfig chatConfig, Pubnub pubnub, ChatListenerFactory? l PubnubInstance = pubnub; ListenerFactory = listenerFactory ?? new DotNetListenerFactory(); ChatAccessManager = new ChatAccessManager(this); + MutedUsersManager = new MutedUsersManager(this); RateLimiter = new ExponentialRateLimiter(chatConfig.RateLimitFactor); } @@ -545,17 +548,34 @@ public async Task UpdateChannel(string channelId, ChatChann /// /// /// The channel ID. + /// Bool specifying the type of deletion. /// A ChatOperationResult indicating the success or failure of the operation. /// /// /// var chat = // ... - /// var result = await chat.DeleteChannel("channel_id"); + /// var result = await chat.DeleteChannel("channel_id", true); /// /// - public async Task DeleteChannel(string channelId) + public async Task DeleteChannel(string channelId, bool soft = false) { var result = new ChatOperationResult("Chat.DeleteChannel()", this); - result.RegisterOperation(await PubnubInstance.RemoveChannelMetadata().Channel(channelId).ExecuteAsync().ConfigureAwait(false)); + if (!soft) + { + result.RegisterOperation(await PubnubInstance.RemoveChannelMetadata().Channel(channelId).ExecuteAsync().ConfigureAwait(false)); + } + else + { + var data = await Channel.GetChannelData(this, channelId).ConfigureAwait(false); + if (result.RegisterOperation(data)) + { + return result; + } + var channelData = (ChatChannelData)data.Result; + channelData.CustomData ??= new Dictionary(); + channelData.CustomData["deleted"] = true; + var updateResult = await Channel.UpdateChannelData(this, channelId, channelData).ConfigureAwait(false); + result.RegisterOperation(updateResult); + } return result; } @@ -1051,6 +1071,7 @@ public async Task UpdateUser(string userId, ChatUserData up /// /// /// The user ID. + /// Bool specifying the type of deletion. /// A ChatOperationResult indicating the success or failure of the operation. /// /// @@ -1058,10 +1079,26 @@ public async Task UpdateUser(string userId, ChatUserData up /// var result = await chat.DeleteUser("user_id"); /// /// - public async Task DeleteUser(string userId) + public async Task DeleteUser(string userId, bool soft = false) { var result = new ChatOperationResult("Chat.DeleteUser()", this); - result.RegisterOperation(await PubnubInstance.RemoveUuidMetadata().Uuid(userId).ExecuteAsync().ConfigureAwait(false)); + if (!soft) + { + result.RegisterOperation(await PubnubInstance.RemoveUuidMetadata().Uuid(userId).ExecuteAsync().ConfigureAwait(false)); + } + else + { + var data = await User.GetUserData(this, userId).ConfigureAwait(false); + if (result.RegisterOperation(data)) + { + return result; + } + var userData = (ChatUserData)data.Result; + userData.CustomData ??= new Dictionary(); + userData.CustomData["deleted"] = true; + var updateResult = await User.UpdateUserData(this, userId, userData).ConfigureAwait(false); + result.RegisterOperation(updateResult); + } return result; } @@ -1585,7 +1622,8 @@ public async Task>> GetChannelMessageHistory(s var isMore = getHistory.Result.More != null; foreach (var historyItem in getHistory.Result.Messages[channelId]) { - if (ChatParsers.TryParseMessageFromHistory(this, channelId, historyItem, out var message)) + if (ChatParsers.TryParseMessageFromHistory(this, channelId, historyItem, out var message) + && !MutedUsersManager.MutedUsers.Contains(message.UserId)) { result.Result.Add(message); } @@ -1633,7 +1671,8 @@ public async Task> GetEventsHistory(st var events = new List(); foreach (var message in getHistory.Result.Messages[channelId]) { - if (ChatParsers.TryParseEventFromHistory(this, channelId, message, out var chatEvent)) + if (ChatParsers.TryParseEventFromHistory(this, channelId, message, out var chatEvent) + && !MutedUsersManager.MutedUsers.Contains(chatEvent.UserId)) { events.Add(chatEvent); } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/PubnubChatConfig.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/PubnubChatConfig.cs index 1bb27e1..7dbb7af 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/PubnubChatConfig.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/PubnubChatConfig.cs @@ -19,10 +19,11 @@ public class RateLimitPerChannel public RateLimitPerChannel RateLimitsPerChannel { get; } public bool StoreUserActivityTimestamp { get; } public int StoreUserActivityInterval { get; } + public bool SyncMutedUsers { get; } public PubnubChatConfig(int typingTimeout = 5000, int typingTimeoutDifference = 1000, int rateLimitFactor = 2, RateLimitPerChannel rateLimitPerChannel = null, bool storeUserActivityTimestamp = false, - int storeUserActivityInterval = 60000) + int storeUserActivityInterval = 60000, bool syncMutedUsers = false) { RateLimitsPerChannel = rateLimitPerChannel ?? new RateLimitPerChannel(); RateLimitFactor = rateLimitFactor; @@ -30,6 +31,7 @@ public PubnubChatConfig(int typingTimeout = 5000, int typingTimeoutDifference = StoreUserActivityInterval = storeUserActivityInterval; TypingTimeout = typingTimeout; TypingTimeoutDifference = typingTimeoutDifference; + SyncMutedUsers = syncMutedUsers; } } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs index 9a33367..5c7bef5 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs @@ -442,7 +442,7 @@ public async Task RemoveThread() return result; } MessageActions = MessageActions.Where(x => x.Type != PubnubMessageActionType.ThreadRootId).ToList(); - result.RegisterOperation(await getThread.Result.Delete().ConfigureAwait(false)); + result.RegisterOperation(await getThread.Result.Delete(false).ConfigureAwait(false)); return result; } @@ -623,7 +623,7 @@ public async Task ToggleReaction(string reactionValue) /// /// Restores a previously deleted message. /// - /// Undoes the soft deletion of this message, making it visible again to all users. + /// Undoes the soft deletion of this message. /// This only works for messages that were soft deleted. /// /// @@ -674,11 +674,17 @@ public async Task Restore() /// /// /// - public async Task Delete(bool soft) + public async Task Delete(bool soft = false) { var result = new ChatOperationResult("Message.Delete()", chat); if (soft) { + if (IsDeleted) + { + result.Error = true; + result.Exception = new PNException("Message is already soft deleted."); + return result; + } var add = await chat.PubnubInstance.AddMessageAction() .MessageTimetoken(long.Parse(TimeToken)).Action(new PNMessageAction() { @@ -706,7 +712,7 @@ public async Task Delete(bool soft) { return result; } - var deleteThread = await getThread.Result.Delete().ConfigureAwait(false); + var deleteThread = await getThread.Result.Delete(false).ConfigureAwait(false); if (result.RegisterOperation(deleteThread)) { return result; diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MutedUsersManager.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MutedUsersManager.cs new file mode 100644 index 0000000..55d60f0 --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MutedUsersManager.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PubnubApi; + +namespace PubnubChatApi +{ + public class MutedUsersManager + { + public List MutedUsers { get; private set; } = new(); + + private Chat chat; + private string userId; + private string userMuteChannelId; + + internal MutedUsersManager(Chat chat) + { + this.chat = chat; + userMuteChannelId = $"PN_PRV.{this.chat.PubnubInstance.GetCurrentUserId()}.mute1"; + if (this.chat.Config.SyncMutedUsers) + { + chat.PubnubInstance.AddListener(chat.ListenerFactory.ProduceListener( + objectEventCallback: + delegate(Pubnub pn, PNObjectEventResult eventResult) + { + var uuid = eventResult.UuidMetadata.Uuid; + var type = eventResult.Type; + if (type == "uuid" && uuid == userMuteChannelId) + { + if (eventResult.Event == "set") + { + CustomToMutedUsers(eventResult.UuidMetadata.Custom); + } + else if (eventResult.Event == "delete") + { + MutedUsers = new List(); + } + } + } + ) + ); + chat.PubnubInstance.AddListener( + chat.ListenerFactory.ProduceListener( + statusCallback: + async delegate(Pubnub pn, PNStatus status) + { + if (status.Category is PNStatusCategory.PNConnectedCategory or PNStatusCategory.PNSubscriptionChangedCategory) + { + if (!chat.PubnubInstance.GetSubscribedChannels().Contains(userMuteChannelId)) + { + // the client might have been offline for a while and missed some updates so load the list first + await LoadMutedUsers(); + this.chat.PubnubInstance.Subscribe().Channels(new []{userMuteChannelId}).Execute(); + } + } + } + ) + ); + LoadMutedUsers(); + } + } + + public async Task MuteUser(string userId) + { + var result = new ChatOperationResult("MutedUsersManager.MuteUser()", chat); + if (MutedUsers.Contains(userId)) + { + result.Error = true; + result.Exception = new PNException($"User \"{userId}\" was already muted!"); + return result; + } + MutedUsers.Add(userId); + if (chat.Config.SyncMutedUsers) + { + result.RegisterOperation(await UpdateMutedUsers()); + } + return result; + } + + public async Task UnMuteUser(string userId) + { + var result = new ChatOperationResult("MutedUsersManager.UnMuteUser()", chat); + if (!MutedUsers.Contains(userId)) + { + result.Error = true; + result.Exception = new PNException($"User \"{userId}\" was already not muted!"); + return result; + } + MutedUsers.Remove(userId); + if (chat.Config.SyncMutedUsers) + { + result.RegisterOperation(await UpdateMutedUsers()); + } + return result; + } + + private async Task UpdateMutedUsers() + { + var mutedUsersString = string.Join(',', MutedUsers); + var result = await chat.PubnubInstance.SetUuidMetadata().Uuid(userMuteChannelId).Type("pn.prv").Custom( + new Dictionary() + { + { "m", mutedUsersString } + }).ExecuteAsync(); + return result.ToChatOperationResult("MutedUsersManager.UpdateMutedUsers()", chat); + } + + private async Task LoadMutedUsers() + { + var result = new ChatOperationResult("MutedUsersManager.LoadMutedUsers()", chat); + var getResult = await chat.PubnubInstance.GetUuidMetadata().Uuid(userMuteChannelId).IncludeCustom(true) + .ExecuteAsync(); + if (result.RegisterOperation(getResult)) + { + return result; + } + result.RegisterOperation(CustomToMutedUsers(getResult.Result.Custom)); + return result; + } + + private ChatOperationResult CustomToMutedUsers(Dictionary custom) + { + var result = new ChatOperationResult("MutesUsersManager.CustomToMutedUsers()", chat); + if (custom.TryGetValue("m", out var mutedUsersObject) && mutedUsersObject != null) + { + try + { + MutedUsers = mutedUsersObject.ToString().Split(",").ToList(); + } + catch (Exception e) + { + result.Error = true; + result.Exception = new PNException($"Exception in parsing synced muted users: {e.Message}"); + } + } + return result; + } + } +} \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs index 12d4707..f3b84af 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/User.cs @@ -99,6 +99,21 @@ public string LastActiveTimeStamp } } + /// + /// Returns true if the User has been soft-deleted. + /// + public bool IsDeleted + { + get + { + if (CustomData == null || !CustomData.TryGetValue("deleted", out var deletedValue)) + { + return false; + } + return (bool)deletedValue; + } + } + /// /// Event that is triggered when the user is updated. /// @@ -319,6 +334,7 @@ public override async Task Refresh() /// It will remove the user from all the channels and delete the user's data. /// /// + /// Whether to perform a soft delete (true) or hard delete (false). /// A ChatOperationResult indicating the success or failure of the operation. /// /// @@ -326,9 +342,50 @@ public override async Task Refresh() /// await user.DeleteUser(); /// /// - public async Task DeleteUser() + public async Task DeleteUser(bool soft = false) { - return await chat.DeleteUser(Id).ConfigureAwait(false); + var result = new ChatOperationResult("User.DeleteUser()", chat); + if(!result.RegisterOperation(await chat.DeleteUser(Id, soft)) && soft) + { + userData.CustomData ??= new Dictionary(); + userData.CustomData["deleted"] = true; + } + return result; + } + + /// + /// Restores a previously deleted user. + /// + /// Undoes the soft deletion of this user. + /// This only works for users that were soft deleted. + /// + /// + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var user = // ...; + /// if (user.IsDeleted) { + /// var result = await user.Restore(); + /// if (!result.Error) { + /// // User has been restored + /// } + /// } + /// + /// + /// + /// + public async Task Restore() + { + var result = new ChatOperationResult("User.Restore()", chat); + if (!IsDeleted) + { + result.Error = true; + result.Exception = new PNException("Can't restore a user that wasn't deleted!"); + return result; + } + userData.CustomData.Remove("deleted"); + result.RegisterOperation(await UpdateUserData(chat, Id, userData).ConfigureAwait(false)); + return result; } /// diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs index 94248cf..82fb505 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs @@ -223,6 +223,11 @@ internal static bool TryParseEvent(Chat chat, PNMessageResult messageRes chatEvent = default; return false; } + if (chat.MutedUsersManager.MutedUsers.Contains(messageResult.Publisher)) + { + chatEvent = default; + return false; + } chatEvent = new ChatEvent() { TimeToken = messageResult.Timetoken.ToString(), diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Channel.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Channel.cs index 8b340cb..4eee71d 100644 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Channel.cs +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Channel.cs @@ -66,6 +66,21 @@ public class Channel : UniqueChatEntity /// /// public string Type => channelData.Type; + + /// + /// Returns true if the Channel has been soft-deleted. + /// + public bool IsDeleted + { + get + { + if (CustomData == null || !CustomData.TryGetValue("deleted", out var deletedValue)) + { + return false; + } + return (bool)deletedValue; + } + } protected ChatChannelData channelData; @@ -564,7 +579,8 @@ public void Connect() SetListening(ref subscription, SubscriptionOptions.None, true, Id, chat.ListenerFactory.ProduceListener(messageCallback: delegate(Pubnub pn, PNMessageResult m) { - if (ChatParsers.TryParseMessageResult(chat, m, out var message)) + if (ChatParsers.TryParseMessageResult(chat, m, out var message) + && !chat.MutedUsersManager.MutedUsers.Contains(message.UserId)) { OnMessageReceived?.Invoke(message); } @@ -802,6 +818,7 @@ public async Task Update(ChatChannelData updatedData) /// Deletes the channel and removes all the messages and memberships from the channel. /// /// + /// /// Whether to perform a soft delete (true) or hard delete (false). /// A ChatOperationResult indicating the success or failure of the operation. /// /// @@ -809,9 +826,50 @@ public async Task Update(ChatChannelData updatedData) /// var result = await channel.Delete(); /// /// - public async Task Delete() + public async Task Delete(bool soft = false) + { + var result = new ChatOperationResult("Channel.Delete()", chat); + if (!result.RegisterOperation(await chat.DeleteChannel(Id, soft)) && soft) + { + channelData.CustomData ??= new Dictionary(); + channelData.CustomData["deleted"] = true; + } + return result; + } + + /// + /// Restores a previously deleted channel. + /// + /// Undoes the soft deletion of this channel. + /// This only works for channels that were soft deleted. + /// + /// + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var channel = // ...; + /// if (channel.IsDeleted) { + /// var result = await channel.Restore(); + /// if (!result.Error) { + /// // Channel has been restored + /// } + /// } + /// + /// + /// + /// + public async Task Restore() { - return await chat.DeleteChannel(Id).ConfigureAwait(false); + var result = new ChatOperationResult("Channel.Restore()", chat); + if (!IsDeleted) + { + result.Error = true; + result.Exception = new PNException("Can't restore a channel that wasn't deleted!"); + return result; + } + channelData.CustomData.Remove("deleted"); + result.RegisterOperation(await UpdateChannelData(chat, Id, channelData)); + return result; } /// diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Chat.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Chat.cs index 170400f..3f65ad7 100644 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Chat.cs +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Chat.cs @@ -30,6 +30,7 @@ public class Chat public event Action OnAnyEvent; public ChatAccessManager ChatAccessManager { get; } + public MutedUsersManager MutedUsersManager { get; } public PubnubChatConfig Config { get; } internal ExponentialRateLimiter RateLimiter { get; } @@ -99,6 +100,7 @@ internal Chat(PubnubChatConfig chatConfig, PNConfiguration pubnubConfig, ChatLis ListenerFactory = listenerFactory ?? new DotNetListenerFactory(); Config = chatConfig; ChatAccessManager = new ChatAccessManager(this); + MutedUsersManager = new MutedUsersManager(this); RateLimiter = new ExponentialRateLimiter(chatConfig.RateLimitFactor); } @@ -108,6 +110,7 @@ internal Chat(PubnubChatConfig chatConfig, Pubnub pubnub, ChatListenerFactory? l PubnubInstance = pubnub; ListenerFactory = listenerFactory ?? new DotNetListenerFactory(); ChatAccessManager = new ChatAccessManager(this); + MutedUsersManager = new MutedUsersManager(this); RateLimiter = new ExponentialRateLimiter(chatConfig.RateLimitFactor); } @@ -545,17 +548,34 @@ public async Task UpdateChannel(string channelId, ChatChann /// /// /// The channel ID. + /// Bool specifying the type of deletion. /// A ChatOperationResult indicating the success or failure of the operation. /// /// /// var chat = // ... - /// var result = await chat.DeleteChannel("channel_id"); + /// var result = await chat.DeleteChannel("channel_id", true); /// /// - public async Task DeleteChannel(string channelId) + public async Task DeleteChannel(string channelId, bool soft = false) { var result = new ChatOperationResult("Chat.DeleteChannel()", this); - result.RegisterOperation(await PubnubInstance.RemoveChannelMetadata().Channel(channelId).ExecuteAsync().ConfigureAwait(false)); + if (!soft) + { + result.RegisterOperation(await PubnubInstance.RemoveChannelMetadata().Channel(channelId).ExecuteAsync().ConfigureAwait(false)); + } + else + { + var data = await Channel.GetChannelData(this, channelId).ConfigureAwait(false); + if (result.RegisterOperation(data)) + { + return result; + } + var channelData = (ChatChannelData)data.Result; + channelData.CustomData ??= new Dictionary(); + channelData.CustomData["deleted"] = true; + var updateResult = await Channel.UpdateChannelData(this, channelId, channelData).ConfigureAwait(false); + result.RegisterOperation(updateResult); + } return result; } @@ -1051,6 +1071,7 @@ public async Task UpdateUser(string userId, ChatUserData up /// /// /// The user ID. + /// Bool specifying the type of deletion. /// A ChatOperationResult indicating the success or failure of the operation. /// /// @@ -1058,10 +1079,26 @@ public async Task UpdateUser(string userId, ChatUserData up /// var result = await chat.DeleteUser("user_id"); /// /// - public async Task DeleteUser(string userId) + public async Task DeleteUser(string userId, bool soft = false) { var result = new ChatOperationResult("Chat.DeleteUser()", this); - result.RegisterOperation(await PubnubInstance.RemoveUuidMetadata().Uuid(userId).ExecuteAsync().ConfigureAwait(false)); + if (!soft) + { + result.RegisterOperation(await PubnubInstance.RemoveUuidMetadata().Uuid(userId).ExecuteAsync().ConfigureAwait(false)); + } + else + { + var data = await User.GetUserData(this, userId).ConfigureAwait(false); + if (result.RegisterOperation(data)) + { + return result; + } + var userData = (ChatUserData)data.Result; + userData.CustomData ??= new Dictionary(); + userData.CustomData["deleted"] = true; + var updateResult = await User.UpdateUserData(this, userId, userData).ConfigureAwait(false); + result.RegisterOperation(updateResult); + } return result; } @@ -1585,7 +1622,8 @@ public async Task>> GetChannelMessageHistory(s var isMore = getHistory.Result.More != null; foreach (var historyItem in getHistory.Result.Messages[channelId]) { - if (ChatParsers.TryParseMessageFromHistory(this, channelId, historyItem, out var message)) + if (ChatParsers.TryParseMessageFromHistory(this, channelId, historyItem, out var message) + && !MutedUsersManager.MutedUsers.Contains(message.UserId)) { result.Result.Add(message); } @@ -1633,7 +1671,8 @@ public async Task> GetEventsHistory(st var events = new List(); foreach (var message in getHistory.Result.Messages[channelId]) { - if (ChatParsers.TryParseEventFromHistory(this, channelId, message, out var chatEvent)) + if (ChatParsers.TryParseEventFromHistory(this, channelId, message, out var chatEvent) + && !MutedUsersManager.MutedUsers.Contains(chatEvent.UserId)) { events.Add(chatEvent); } diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/PubnubChatConfig.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/PubnubChatConfig.cs index 1bb27e1..7dbb7af 100644 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/PubnubChatConfig.cs +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/PubnubChatConfig.cs @@ -19,10 +19,11 @@ public class RateLimitPerChannel public RateLimitPerChannel RateLimitsPerChannel { get; } public bool StoreUserActivityTimestamp { get; } public int StoreUserActivityInterval { get; } + public bool SyncMutedUsers { get; } public PubnubChatConfig(int typingTimeout = 5000, int typingTimeoutDifference = 1000, int rateLimitFactor = 2, RateLimitPerChannel rateLimitPerChannel = null, bool storeUserActivityTimestamp = false, - int storeUserActivityInterval = 60000) + int storeUserActivityInterval = 60000, bool syncMutedUsers = false) { RateLimitsPerChannel = rateLimitPerChannel ?? new RateLimitPerChannel(); RateLimitFactor = rateLimitFactor; @@ -30,6 +31,7 @@ public PubnubChatConfig(int typingTimeout = 5000, int typingTimeoutDifference = StoreUserActivityInterval = storeUserActivityInterval; TypingTimeout = typingTimeout; TypingTimeoutDifference = typingTimeoutDifference; + SyncMutedUsers = syncMutedUsers; } } } \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Message.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Message.cs index 9a33367..5c7bef5 100644 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Message.cs +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Message.cs @@ -442,7 +442,7 @@ public async Task RemoveThread() return result; } MessageActions = MessageActions.Where(x => x.Type != PubnubMessageActionType.ThreadRootId).ToList(); - result.RegisterOperation(await getThread.Result.Delete().ConfigureAwait(false)); + result.RegisterOperation(await getThread.Result.Delete(false).ConfigureAwait(false)); return result; } @@ -623,7 +623,7 @@ public async Task ToggleReaction(string reactionValue) /// /// Restores a previously deleted message. /// - /// Undoes the soft deletion of this message, making it visible again to all users. + /// Undoes the soft deletion of this message. /// This only works for messages that were soft deleted. /// /// @@ -674,11 +674,17 @@ public async Task Restore() /// /// /// - public async Task Delete(bool soft) + public async Task Delete(bool soft = false) { var result = new ChatOperationResult("Message.Delete()", chat); if (soft) { + if (IsDeleted) + { + result.Error = true; + result.Exception = new PNException("Message is already soft deleted."); + return result; + } var add = await chat.PubnubInstance.AddMessageAction() .MessageTimetoken(long.Parse(TimeToken)).Action(new PNMessageAction() { @@ -706,7 +712,7 @@ public async Task Delete(bool soft) { return result; } - var deleteThread = await getThread.Result.Delete().ConfigureAwait(false); + var deleteThread = await getThread.Result.Delete(false).ConfigureAwait(false); if (result.RegisterOperation(deleteThread)) { return result; diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/MutedUsersManager.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/MutedUsersManager.cs new file mode 100644 index 0000000..55d60f0 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/MutedUsersManager.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PubnubApi; + +namespace PubnubChatApi +{ + public class MutedUsersManager + { + public List MutedUsers { get; private set; } = new(); + + private Chat chat; + private string userId; + private string userMuteChannelId; + + internal MutedUsersManager(Chat chat) + { + this.chat = chat; + userMuteChannelId = $"PN_PRV.{this.chat.PubnubInstance.GetCurrentUserId()}.mute1"; + if (this.chat.Config.SyncMutedUsers) + { + chat.PubnubInstance.AddListener(chat.ListenerFactory.ProduceListener( + objectEventCallback: + delegate(Pubnub pn, PNObjectEventResult eventResult) + { + var uuid = eventResult.UuidMetadata.Uuid; + var type = eventResult.Type; + if (type == "uuid" && uuid == userMuteChannelId) + { + if (eventResult.Event == "set") + { + CustomToMutedUsers(eventResult.UuidMetadata.Custom); + } + else if (eventResult.Event == "delete") + { + MutedUsers = new List(); + } + } + } + ) + ); + chat.PubnubInstance.AddListener( + chat.ListenerFactory.ProduceListener( + statusCallback: + async delegate(Pubnub pn, PNStatus status) + { + if (status.Category is PNStatusCategory.PNConnectedCategory or PNStatusCategory.PNSubscriptionChangedCategory) + { + if (!chat.PubnubInstance.GetSubscribedChannels().Contains(userMuteChannelId)) + { + // the client might have been offline for a while and missed some updates so load the list first + await LoadMutedUsers(); + this.chat.PubnubInstance.Subscribe().Channels(new []{userMuteChannelId}).Execute(); + } + } + } + ) + ); + LoadMutedUsers(); + } + } + + public async Task MuteUser(string userId) + { + var result = new ChatOperationResult("MutedUsersManager.MuteUser()", chat); + if (MutedUsers.Contains(userId)) + { + result.Error = true; + result.Exception = new PNException($"User \"{userId}\" was already muted!"); + return result; + } + MutedUsers.Add(userId); + if (chat.Config.SyncMutedUsers) + { + result.RegisterOperation(await UpdateMutedUsers()); + } + return result; + } + + public async Task UnMuteUser(string userId) + { + var result = new ChatOperationResult("MutedUsersManager.UnMuteUser()", chat); + if (!MutedUsers.Contains(userId)) + { + result.Error = true; + result.Exception = new PNException($"User \"{userId}\" was already not muted!"); + return result; + } + MutedUsers.Remove(userId); + if (chat.Config.SyncMutedUsers) + { + result.RegisterOperation(await UpdateMutedUsers()); + } + return result; + } + + private async Task UpdateMutedUsers() + { + var mutedUsersString = string.Join(',', MutedUsers); + var result = await chat.PubnubInstance.SetUuidMetadata().Uuid(userMuteChannelId).Type("pn.prv").Custom( + new Dictionary() + { + { "m", mutedUsersString } + }).ExecuteAsync(); + return result.ToChatOperationResult("MutedUsersManager.UpdateMutedUsers()", chat); + } + + private async Task LoadMutedUsers() + { + var result = new ChatOperationResult("MutedUsersManager.LoadMutedUsers()", chat); + var getResult = await chat.PubnubInstance.GetUuidMetadata().Uuid(userMuteChannelId).IncludeCustom(true) + .ExecuteAsync(); + if (result.RegisterOperation(getResult)) + { + return result; + } + result.RegisterOperation(CustomToMutedUsers(getResult.Result.Custom)); + return result; + } + + private ChatOperationResult CustomToMutedUsers(Dictionary custom) + { + var result = new ChatOperationResult("MutesUsersManager.CustomToMutedUsers()", chat); + if (custom.TryGetValue("m", out var mutedUsersObject) && mutedUsersObject != null) + { + try + { + MutedUsers = mutedUsersObject.ToString().Split(",").ToList(); + } + catch (Exception e) + { + result.Error = true; + result.Exception = new PNException($"Exception in parsing synced muted users: {e.Message}"); + } + } + return result; + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/MutedUsersManager.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/MutedUsersManager.cs.meta new file mode 100644 index 0000000..7b5b843 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/MutedUsersManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 72f09fb0ecc72974f9d14a489dab7d67 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/User.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/User.cs index 12d4707..f3b84af 100644 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/User.cs +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/User.cs @@ -99,6 +99,21 @@ public string LastActiveTimeStamp } } + /// + /// Returns true if the User has been soft-deleted. + /// + public bool IsDeleted + { + get + { + if (CustomData == null || !CustomData.TryGetValue("deleted", out var deletedValue)) + { + return false; + } + return (bool)deletedValue; + } + } + /// /// Event that is triggered when the user is updated. /// @@ -319,6 +334,7 @@ public override async Task Refresh() /// It will remove the user from all the channels and delete the user's data. /// /// + /// Whether to perform a soft delete (true) or hard delete (false). /// A ChatOperationResult indicating the success or failure of the operation. /// /// @@ -326,9 +342,50 @@ public override async Task Refresh() /// await user.DeleteUser(); /// /// - public async Task DeleteUser() + public async Task DeleteUser(bool soft = false) { - return await chat.DeleteUser(Id).ConfigureAwait(false); + var result = new ChatOperationResult("User.DeleteUser()", chat); + if(!result.RegisterOperation(await chat.DeleteUser(Id, soft)) && soft) + { + userData.CustomData ??= new Dictionary(); + userData.CustomData["deleted"] = true; + } + return result; + } + + /// + /// Restores a previously deleted user. + /// + /// Undoes the soft deletion of this user. + /// This only works for users that were soft deleted. + /// + /// + /// A ChatOperationResult indicating the success or failure of the operation. + /// + /// + /// var user = // ...; + /// if (user.IsDeleted) { + /// var result = await user.Restore(); + /// if (!result.Error) { + /// // User has been restored + /// } + /// } + /// + /// + /// + /// + public async Task Restore() + { + var result = new ChatOperationResult("User.Restore()", chat); + if (!IsDeleted) + { + result.Error = true; + result.Exception = new PNException("Can't restore a user that wasn't deleted!"); + return result; + } + userData.CustomData.Remove("deleted"); + result.RegisterOperation(await UpdateUserData(chat, Id, userData).ConfigureAwait(false)); + return result; } /// diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatParsers.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatParsers.cs index 94248cf..82fb505 100644 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatParsers.cs +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatParsers.cs @@ -223,6 +223,11 @@ internal static bool TryParseEvent(Chat chat, PNMessageResult messageRes chatEvent = default; return false; } + if (chat.MutedUsersManager.MutedUsers.Contains(messageResult.Publisher)) + { + chatEvent = default; + return false; + } chatEvent = new ChatEvent() { TimeToken = messageResult.Timetoken.ToString(), diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChatPNSDKSource.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChatPNSDKSource.cs index d84c933..68bdc08 100644 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChatPNSDKSource.cs +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChatPNSDKSource.cs @@ -5,7 +5,7 @@ namespace PubnubChatApi { public class UnityChatPNSDKSource : IPNSDKSource { - private const string build = "0.4.5"; + private const string build = "1.1.0"; private string GetPlatformString() { diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Samples~/PubnubChatConfigAsset/PubnubChatConfigAsset.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Samples~/PubnubChatConfigAsset/PubnubChatConfigAsset.cs index 8621e3b..31b1773 100644 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Samples~/PubnubChatConfigAsset/PubnubChatConfigAsset.cs +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Samples~/PubnubChatConfigAsset/PubnubChatConfigAsset.cs @@ -12,6 +12,7 @@ public class PubnubChatConfigAsset : ScriptableObject [field: SerializeField] public PubnubChatConfig.RateLimitPerChannel RateLimitPerChannel { get; private set; } [field: SerializeField] public bool StoreUserActivityTimestamp { get; private set; } [field: SerializeField] public int StoreUserActivityInterval { get; private set; } = 60000; + [field: SerializeField] public bool SyncMutedUsers { get; private set; } = false; public static implicit operator PubnubChatConfig(PubnubChatConfigAsset asset) { @@ -20,7 +21,8 @@ public static implicit operator PubnubChatConfig(PubnubChatConfigAsset asset) rateLimitFactor: asset.RateLimitFactor, rateLimitPerChannel: asset.RateLimitPerChannel, storeUserActivityInterval: asset.StoreUserActivityInterval, - storeUserActivityTimestamp: asset.StoreUserActivityTimestamp); + storeUserActivityTimestamp: asset.StoreUserActivityTimestamp, + syncMutedUsers: asset.SyncMutedUsers); } } } \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/package.json b/unity-chat/PubnubChatUnity/Assets/PubnubChat/package.json index 5e789d8..30e2579 100644 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/package.json +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/package.json @@ -1,6 +1,6 @@ { "name": "com.pubnub.pubnubchat", - "version": "1.0.0", + "version": "1.1.0", "displayName": "Pubnub Chat", "description": "PubNub Unity Chat SDK", "unity": "2022.3", diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteChannelSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteChannelSample.cs index 8b65df7..ad95b35 100644 --- a/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteChannelSample.cs +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteChannelSample.cs @@ -52,4 +52,50 @@ public static async Task DeleteChannelUsingChatObjectExample() await chat.DeleteChannel("support"); // snippet.end } + + public static async Task RestoreChannelSample() + { + // snippet.restore_channel_sample + var channelResult = await chat.GetChannel("support"); + if (channelResult.Error) + { + Debug.Log("Channel to restore doesn't exist."); + return; + } + var channel = channelResult.Result; + var restoreResult = await channel.Restore(); + //This could happen because the channel was not soft deleted + if (restoreResult.Error) + { + Debug.LogError($"An error has occured when trying to restore a channel: {restoreResult.Exception.Message}"); + return; + } + // snippet.end + } + + public static async Task SoftDeleteSample() + { + // snippet.channel_soft_delete + // using Channel object + var channelResult = await chat.GetChannel("support"); + if (!channelResult.Error) + { + var channel = channelResult.Result; + var softDeleteResult = await channel.Delete(soft: true); + //Could be for example because it was already soft deleted + if (softDeleteResult.Error) + { + Debug.LogError($"Error when trying to soft delete channel: {softDeleteResult.Exception.Message}"); + } + } + + // or using Chat object + var softDeleteFromChat = await chat.DeleteChannel("support", soft: true); + //Same as above, could be because it was already soft deleted + if (softDeleteFromChat.Error) + { + Debug.LogError($"Error when trying to soft delete channel: {softDeleteFromChat.Exception.Message}"); + } + // snippet.end + } } diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteUserSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteUserSample.cs index a599e3c..69487fb 100644 --- a/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteUserSample.cs +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/DeleteUserSample.cs @@ -52,4 +52,50 @@ public static async Task DeleteUserUsingChatObjectExample() await chat.DeleteUser("support_agent_15"); // snippet.end } + + public static async Task RestoreUserSample() + { + // snippet.restore_user_sample + var userResult = await chat.GetUser("support_agent_15"); + if (userResult.Error) + { + Debug.Log("User to restore doesn't exist."); + return; + } + var user = userResult.Result; + var restoreResult = await user.Restore(); + //This could happen because the user was not soft deleted + if (restoreResult.Error) + { + Debug.LogError($"An error has occured when trying to restore user: {restoreResult.Exception.Message}"); + return; + } + // snippet.end + } + + public static async Task SoftDeleteSample() + { + // snippet.user_soft_delete + // using User object + var userResult = await chat.GetUser("support-agent-15"); + if (!userResult.Error) + { + var user = userResult.Result; + var softDeleteResult = await user.DeleteUser(soft: true); + //Could be for example because it was already soft deleted + if (softDeleteResult.Error) + { + Debug.LogError($"Error when trying to soft delete user: {softDeleteResult.Exception.Message}"); + } + } + + // or using Chat object + var softDeleteFromChat = await chat.DeleteUser("support-agent-15", soft: true); + //Same as above, could be because it was already soft deleted + if (softDeleteFromChat.Error) + { + Debug.LogError($"Error when trying to soft delete user: {softDeleteFromChat.Exception.Message}"); + } + // snippet.end + } } diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/MutedUsersManagerSample.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/MutedUsersManagerSample.cs new file mode 100644 index 0000000..319711b --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/MutedUsersManagerSample.cs @@ -0,0 +1,74 @@ +// snippet.using +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +namespace Snippets +{ + public class MutedUsersManagerSample + { + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Configuration + PubnubChatConfig chatConfig = new PubnubChatConfig(); + + PNConfiguration pnConfiguration = new PNConfiguration(new UserId("myUniqueUserId")) + { + SubscribeKey = "demo", + PublishKey = "demo", + Secure = true + }; + + // Initialize Unity Chat + var chatResult = await UnityChat.CreateInstance(chatConfig, pnConfiguration); + if (!chatResult.Error) + { + chat = chatResult.Result; + } + + // snippet.end + } + + public static async Task MuteUserExample() + { + // snippet.mute_user + var mutedUsersManager = chat.MutedUsersManager; + var muteResult = await mutedUsersManager.MuteUser("some_user"); + if (muteResult.Error) + { + Debug.LogError($"Error when trying to mute user: {muteResult.Exception.Message}"); + } + // snippet.end + } + + public static async Task UnMuteUserExample() + { + // snippet.unmute_user + var mutedUsersManager = chat.MutedUsersManager; + var muteResult = await mutedUsersManager.UnMuteUser("some_user"); + if (muteResult.Error) + { + Debug.LogError($"Error when trying to unmute user: {muteResult.Exception.Message}"); + } + // snippet.end + } + + public static async Task CheckMutedExample() + { + // snippet.check_muted + var mutedUsersManager = chat.MutedUsersManager; + var mutedUsers = mutedUsersManager.MutedUsers; + foreach (var mutedUserId in mutedUsers) + { + Debug.Log($"Muted user: {mutedUserId}"); + } + // snippet.end + } + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/MutedUsersManagerSample.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/MutedUsersManagerSample.cs.meta new file mode 100644 index 0000000..597b27a --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/MutedUsersManagerSample.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 51f337821b3d4bd19dd87c42a371708f +timeCreated: 1761314720 \ No newline at end of file