Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .pubnub.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
---
version: v1.1.1
version: v1.2.0
changelog:
- date: 2025-11-24
version: v1.2.0
changes:
- type: feature
text: "Memberships generated from Invite methods now have Status pending and can be filtered out in GetMemberships()."
- type: feature
text: "Added new overloads for update related methods and events so that they now use ChatEntityChangeType to convey the type of update."
- type: improvement
text: "Changed SetListening(...) and AddListenerTo(...) methods to align with other Chat SDKs. New methods are use the Stream and StreamOn naming convetions."
- date: 2025-11-06
version: v1.1.1
changes:
Expand Down
29 changes: 29 additions & 0 deletions c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChannelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,35 @@ public async Task TestGetMemberships()
Assert.That(memberships.Memberships.Count, Is.GreaterThanOrEqualTo(1));
}

[Test]
public async Task TestGetInvitees()
{
var channel = TestUtils.AssertOperation(await chat.CreatePublicConversation());
await channel.Invite(user);
await Task.Delay(3500);
var invitees = TestUtils.AssertOperation(await channel.GetInvitees());
Assert.True(invitees.Memberships.Any(x => x.UserId == user.Id && x.ChannelId == channel.Id && x.MembershipData.Status == "pending"));

//Cleanup
await channel.Delete();
}

[Test]
public async Task TestInviteAndJoin()
{
var channel = TestUtils.AssertOperation(await chat.CreatePublicConversation());
await channel.Invite(user);
await Task.Delay(3500);
var invitees = TestUtils.AssertOperation(await channel.GetInvitees());
Assert.True(invitees.Memberships.Any(x => x.UserId == user.Id && x.ChannelId == channel.Id && x.MembershipData.Status == "pending"));
await channel.Join();
await Task.Delay(3500);
invitees = TestUtils.AssertOperation(await channel.GetInvitees());
Assert.False(invitees.Memberships.Any());
var members = TestUtils.AssertOperation(await channel.GetMemberships());
Assert.True(members.Memberships.Any(x => x.UserId == user.Id && x.ChannelId == channel.Id && x.MembershipData.Status != "pending"));
}

[Test]
public async Task TestStartTyping()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,13 @@ public async Task TestUpdateMemberships()
[Test]
public async Task TestInvite()
{
var testChannel = TestUtils.AssertOperation(await chat.CreateGroupConversation([user], "test_invite_group_channel")).CreatedChannel;
var testChannel = TestUtils.AssertOperation(await chat.CreateGroupConversation([user], Guid.NewGuid().ToString())).CreatedChannel;
var testUser = await chat.GetOrCreateUser("test_invite_user");
var returnedMembership = TestUtils.AssertOperation(await testChannel.Invite(testUser));
Assert.True(returnedMembership.ChannelId == testChannel.Id && returnedMembership.UserId == testUser.Id);
Assert.True(returnedMembership.ChannelId == testChannel.Id && returnedMembership.UserId == testUser.Id && returnedMembership.MembershipData.Status == "pending");

//Cleanup
await testChannel.Delete();
}

[Test]
Expand All @@ -106,6 +109,7 @@ public async Task TestInviteMultiple()
returnedMemberships.Count == 2 &&
returnedMemberships.Any(x => x.UserId == secondUser.Id && x.ChannelId == testChannel.Id) &&
returnedMemberships.Any(x => x.UserId == thirdUser.Id && x.ChannelId == testChannel.Id));
Assert.True(returnedMemberships.All(x => x.MembershipData.Status == "pending"));
}

[Test]
Expand Down
36 changes: 35 additions & 1 deletion c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1148,7 +1148,7 @@ public async Task<ChatOperationResult<List<string>>> WhoIsPresent()
/// <param name="sort">The sort parameter.</param>
/// <param name="limit">The maximum amount of the memberships received.</param>
/// <param name="page">The page object for pagination.</param>
/// <returns>A ChatOperationResult containing the list of the <c>Membership</c> objects.</returns>
/// <returns>A ChatOperationResult containing a wrapper object with the list of the <c>Membership</c> objects.</returns>
/// <example>
/// <code>
/// var channel = //...
Expand All @@ -1166,6 +1166,40 @@ public async Task<ChatOperationResult<MembersResponseWrapper>> GetMemberships(st
return await chat.GetChannelMemberships(Id, filter, sort, limit, page).ConfigureAwait(false);
}

/// <summary>
/// Gets the list of the <c>Membership</c> objects which have a Status of "pending".
/// <para>
/// Gets the list of the <c>Membership</c> objects that represent the users that are invited
/// to the channel.
/// </para>
/// </summary>
/// <param name="filter">The filter parameter. Note that it will always contain "status == \"pending\"" in the end request.</param>
/// <param name="sort">The sort parameter.</param>
/// <param name="limit">The maximum amount of the memberships received.</param>
/// <param name="page">The page object for pagination.</param>
/// <returns>A ChatOperationResult containing a wrapper object with the list of the <c>Membership</c> objects.</returns>
/// <example>
/// <code>
/// var channel = //...
/// var result = await channel.GetInvitees(limit: 10);
/// var invites = result.Result.Memberships;
/// foreach (var invited in invites) {
/// Console.WriteLine($"Invited user: {invited.UserId}");
/// }
/// </code>
/// </example>
/// <seealso cref="Membership"/>
public async Task<ChatOperationResult<MembersResponseWrapper>> GetInvitees(string filter = "", string sort = "", int limit = 0,
PNPageObject page = null)
{
var finalFilter = "status == \"pending\"";
if (!string.IsNullOrEmpty(filter))
{
finalFilter = $"{filter} && {finalFilter}";
}
return await chat.GetChannelMemberships(Id, finalFilter, sort, limit, page).ConfigureAwait(false);
}

/// <summary>
/// Asynchronously gets the <c>Message</c> object for the given timetoken sent from this <c>Channel</c>.
/// </summary>
Expand Down
9 changes: 6 additions & 3 deletions c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -357,10 +357,10 @@ public async Task<ChatOperationResult<Membership>> InviteToChannel(string channe
new()
{
Channel = channelId,
Status = "pending"
//TODO: these too here?
//TODO: again, should ChatMembershipData from Create(...)Channel also be passed here?
/*Custom = ,
Status = ,
Type = */
}
}).ExecuteAsync().ConfigureAwait(false);
Expand All @@ -380,7 +380,10 @@ public async Task<ChatOperationResult<Membership>> InviteToChannel(string channe
var inviteEventPayload = $"{{\"channelType\": \"{channel.Result.Type}\", \"channelId\": {channelId}}}";
await EmitEvent(PubnubChatEventType.Invite, userId, inviteEventPayload).ConfigureAwait(false);

var newMembership = new Membership(this, userId, channelId, new ChatMembershipData());
var newMembership = new Membership(this, userId, channelId, new ChatMembershipData()
{
Status = "pending"
});
await newMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()).ConfigureAwait(false);

result.Result = newMembership;
Expand Down Expand Up @@ -413,7 +416,7 @@ public async Task<ChatOperationResult<List<Membership>>> InviteMultipleToChannel
PNChannelMemberField.UUID_STATUS
})
//TODO: again, should ChatMembershipData from Create(...)Channel also be passed here?
.Uuids(users.Select(x => new PNChannelMember() { Custom = x.CustomData, Uuid = x.Id }).ToList())
.Uuids(users.Select(x => new PNChannelMember() { Custom = x.CustomData, Uuid = x.Id, Status = "pending"}).ToList())
.ExecuteAsync().ConfigureAwait(false);

if (result.RegisterOperation(inviteResponse))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ namespace PubnubChatApi
public class ChatMembershipData
{
public Dictionary<string, object> CustomData { get; set; } = new();
public string Status { get; set; }
public string Type { get; set; }
public string Status { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;

public static implicit operator ChatMembershipData(PNChannelMembersItemResult membersItem)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1148,7 +1148,7 @@ public async Task<ChatOperationResult<List<string>>> WhoIsPresent()
/// <param name="sort">The sort parameter.</param>
/// <param name="limit">The maximum amount of the memberships received.</param>
/// <param name="page">The page object for pagination.</param>
/// <returns>A ChatOperationResult containing the list of the <c>Membership</c> objects.</returns>
/// <returns>A ChatOperationResult containing a wrapper object with the list of the <c>Membership</c> objects.</returns>
/// <example>
/// <code>
/// var channel = //...
Expand All @@ -1166,6 +1166,40 @@ public async Task<ChatOperationResult<MembersResponseWrapper>> GetMemberships(st
return await chat.GetChannelMemberships(Id, filter, sort, limit, page).ConfigureAwait(false);
}

/// <summary>
/// Gets the list of the <c>Membership</c> objects which have a Status of "pending".
/// <para>
/// Gets the list of the <c>Membership</c> objects that represent the users that are invited
/// to the channel.
/// </para>
/// </summary>
/// <param name="filter">The filter parameter. Note that it will always contain "status == \"pending\"" in the end request.</param>
/// <param name="sort">The sort parameter.</param>
/// <param name="limit">The maximum amount of the memberships received.</param>
/// <param name="page">The page object for pagination.</param>
/// <returns>A ChatOperationResult containing a wrapper object with the list of the <c>Membership</c> objects.</returns>
/// <example>
/// <code>
/// var channel = //...
/// var result = await channel.GetInvitees(limit: 10);
/// var invites = result.Result.Memberships;
/// foreach (var invited in invites) {
/// Console.WriteLine($"Invited user: {invited.UserId}");
/// }
/// </code>
/// </example>
/// <seealso cref="Membership"/>
public async Task<ChatOperationResult<MembersResponseWrapper>> GetInvitees(string filter = "", string sort = "", int limit = 0,
PNPageObject page = null)
{
var finalFilter = "status == \"pending\"";
if (!string.IsNullOrEmpty(filter))
{
finalFilter = $"{filter} && {finalFilter}";
}
return await chat.GetChannelMemberships(Id, finalFilter, sort, limit, page).ConfigureAwait(false);
}

/// <summary>
/// Asynchronously gets the <c>Message</c> object for the given timetoken sent from this <c>Channel</c>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,10 +357,10 @@ public async Task<ChatOperationResult<Membership>> InviteToChannel(string channe
new()
{
Channel = channelId,
Status = "pending"
//TODO: these too here?
//TODO: again, should ChatMembershipData from Create(...)Channel also be passed here?
/*Custom = ,
Status = ,
Type = */
}
}).ExecuteAsync().ConfigureAwait(false);
Expand All @@ -380,7 +380,10 @@ public async Task<ChatOperationResult<Membership>> InviteToChannel(string channe
var inviteEventPayload = $"{{\"channelType\": \"{channel.Result.Type}\", \"channelId\": {channelId}}}";
await EmitEvent(PubnubChatEventType.Invite, userId, inviteEventPayload).ConfigureAwait(false);

var newMembership = new Membership(this, userId, channelId, new ChatMembershipData());
var newMembership = new Membership(this, userId, channelId, new ChatMembershipData()
{
Status = "pending"
});
await newMembership.SetLastReadMessageTimeToken(ChatUtils.TimeTokenNow()).ConfigureAwait(false);

result.Result = newMembership;
Expand Down Expand Up @@ -413,7 +416,7 @@ public async Task<ChatOperationResult<List<Membership>>> InviteMultipleToChannel
PNChannelMemberField.UUID_STATUS
})
//TODO: again, should ChatMembershipData from Create(...)Channel also be passed here?
.Uuids(users.Select(x => new PNChannelMember() { Custom = x.CustomData, Uuid = x.Id }).ToList())
.Uuids(users.Select(x => new PNChannelMember() { Custom = x.CustomData, Uuid = x.Id, Status = "pending"}).ToList())
.ExecuteAsync().ConfigureAwait(false);

if (result.RegisterOperation(inviteResponse))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ namespace PubnubChatApi
public class ChatMembershipData
{
public Dictionary<string, object> CustomData { get; set; } = new();
public string Status { get; set; }
public string Type { get; set; }
public string Status { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;

public static implicit operator ChatMembershipData(PNChannelMembersItemResult membersItem)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace PubnubChatApi
{
public class UnityChatPNSDKSource : IPNSDKSource
{
private const string build = "1.1.1";
private const string build = "1.2.0";

private string GetPlatformString()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "com.pubnub.pubnubchat",
"version": "1.1.1",
"version": "1.2.0",
"displayName": "Pubnub Chat",
"description": "PubNub Unity Chat SDK",
"unity": "2022.3",
Expand Down
22 changes: 22 additions & 0 deletions unity-chat/PubnubChatUnity/Assets/Snippets/ChannelSample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,26 @@ public static async Task EventSubscriptionExample()
}
// snippet.end
}

public static async Task GetInviteesExample()
{
// snippet.get_invitees_example
// Get or create a channel
var channelResult = await chat.GetChannel("my_channel");
if (channelResult.Error)
{
Debug.LogError($"Could not fetch channel! Error: {channelResult.Exception.Message}");
}
var channel = channelResult.Result;
var getInvitees = await channel.GetInvitees();
if (getInvitees.Error)
{
Debug.LogError($"Could not fetch invitees! Error: {getInvitees.Exception.Message}");
}
foreach (var membership in getInvitees.Result.Memberships)
{
Debug.Log($"User {membership.UserId} has is invited to channel {membership.ChannelId}");
}
// snippet.end
}
}