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