diff --git a/.pubnub.yml b/.pubnub.yml index be0f2bc..a725069 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,6 +1,13 @@ --- -version: v1.2.0 +version: v1.3.0 changelog: + - date: 2025-12-08 + version: v1.3.0 + changes: + - type: feature + text: "Integrated Pubnub files support into Chat functionality - messages sent via MessageDraft or directly from Channel.SendText() can now have file attachments." + - type: feature + text: "Integrated Pubnub push notifications support into Chat - PubnubChatConfig now has a PushNotifications section and Channel entities can be registered to receive push notifications." - date: 2025-11-24 version: v1.2.0 changes: diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs index e003f8e..d23a55d 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/ChatEventTests.cs @@ -28,7 +28,7 @@ public async Task Setup() [TearDown] public async Task CleanUp() { - channel.Leave(); + await channel.Leave(); await Task.Delay(3000); chat.Destroy(); await Task.Delay(3000); diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/FilesTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/FilesTests.cs new file mode 100644 index 0000000..303f669 --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/FilesTests.cs @@ -0,0 +1,203 @@ +using PubnubApi; +using PubnubChatApi; +using Channel = PubnubChatApi.Channel; + +namespace PubNubChatApi.Tests; + +[TestFixture] +public class FilesTests +{ + private const string FILE_NAME = "fileupload.txt"; + private const string FILE_LOCATION = @"fileupload.txt"; + private const string LARGE_FILE_NAME = "file_large.png"; + private const string LARGE_FILE_LOCATION = @"file_large.png"; + + private Chat chat; + private Channel channel; + private User user; + + [SetUp] + public async Task Setup() + { + chat = TestUtils.AssertOperation(await Chat.CreateInstance(new PubnubChatConfig(), + new PNConfiguration(new UserId("file_tests_user")) + { + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey, + })); + channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("file_tests_channel")); + user = TestUtils.AssertOperation(await chat.GetCurrentUser()); + await channel.Join(); + await Task.Delay(3500); + } + + [TearDown] + public async Task CleanUp() + { + await ClearChannelOfFiles(); + await channel.Leave(); + await Task.Delay(3000); + chat.Destroy(); + await Task.Delay(3000); + } + + private async Task ClearChannelOfFiles() + { + var files = await channel.GetFiles(); + if (files.Error) + { + Assert.Fail($"Error in files cleanup: {files.Exception.Message}"); + return; + } + + foreach (var file in files.Result.Files) + { + var result = await channel.DeleteFile(file.Id, file.Name); + if (result.Error) + { + Assert.Fail($"Error in files cleanup: {files.Exception.Message}"); + } + } + } + + [Test] + public async Task TestFileUploadInMessage() + { + //Cleanup: delete files from channel + await ClearChannelOfFiles(); + + await channel.Join(); + await Task.Delay(250); + + var receivedMessageReset = new ManualResetEvent(false); + Message receivedMessage = null; + channel.OnMessageReceived += message => + { + if (message.MessageText == "FILE") + { + receivedMessage = message; + receivedMessageReset.Set(); + } + }; + + //Add file to SendTextParams and send message + TestUtils.AssertOperation(await channel.SendText("FILE", new SendTextParams() + { + Files = + [ + new ChatInputFile() + { + Name = FILE_NAME, + Type = "text", + Source = FILE_LOCATION + } + ] + })); + + //Receive message and check message.Files + var received = receivedMessageReset.WaitOne(10000); + Assert.True(received, "Did not receive message with file at all!"); + Assert.True(receivedMessage != null, "receivedMessage was null!"); + Assert.True(receivedMessage.Files != null, "receivedMessage.Files was null!"); + Assert.True(receivedMessage.Files.Count == 1, + $"receivedMessage.Files.Count was {receivedMessage.Files.Count} instead of 1!"); + var receivedFile = receivedMessage.Files[0]; + Assert.True(receivedFile.Name == FILE_NAME, + $"Expected file name \"the_file\" but got \"{receivedFile.Name}\""); + Assert.True(receivedFile.Type == "text", $"Expected file type \"text\" but got \"{receivedFile.Type}\""); + Assert.True(!string.IsNullOrEmpty(receivedFile.Id), "File ID is empty"); + Assert.True(!string.IsNullOrEmpty(receivedFile.Url), "File URL is empty"); + + //Check channel.GetFiles() for the file and check if data matches with the one from message.Files + var channelFiles = TestUtils.AssertOperation(await channel.GetFiles()); + Assert.True( + channelFiles.Files.Any(x => + x.Id == receivedFile.Id && x.Name == receivedFile.Name && x.Url == receivedFile.Url), + "Did not find message file in channel.GetFiles()!"); + } + + [Test] + public async Task TestFileUploadInMessageDraft() + { + //Cleanup: delete files from channel + await ClearChannelOfFiles(); + + await channel.Join(); + await Task.Delay(250); + + var receivedMessageReset = new ManualResetEvent(false); + Message receivedMessage = null; + channel.OnMessageReceived += message => + { + if (message.MessageText == "FILE") + { + receivedMessage = message; + receivedMessageReset.Set(); + } + }; + + //Add file to SendTextParams in a MessageDraft and send message + var messageDraft = channel.CreateMessageDraft(); + messageDraft.InsertText(0, "FILE"); + messageDraft.Files.Add(new ChatInputFile() + { + Name = FILE_NAME, + Type = "text", + Source = FILE_LOCATION + }); + + TestUtils.AssertOperation(await messageDraft.Send()); + + //Receive message and check message.Files + var received = receivedMessageReset.WaitOne(10000); + Assert.True(received, "Did not receive message with file at all!"); + Assert.True(receivedMessage != null, "receivedMessage was null!"); + Assert.True(receivedMessage.Files != null, "receivedMessage.Files was null!"); + Assert.True(receivedMessage.Files.Count == 1, + $"receivedMessage.Files.Count was {receivedMessage.Files.Count} instead of 1!"); + var receivedFile = receivedMessage.Files[0]; + Assert.True(receivedFile.Name == FILE_NAME, + $"Expected file name \"the_file\" but got \"{receivedFile.Name}\""); + Assert.True(receivedFile.Type == "text", $"Expected file type \"text\" but got \"{receivedFile.Type}\""); + Assert.True(!string.IsNullOrEmpty(receivedFile.Id), "File ID is empty"); + Assert.True(!string.IsNullOrEmpty(receivedFile.Url), "File URL is empty"); + + //Check channel.GetFiles() for the file and check if data matches with the one from message.Files + var channelFiles = TestUtils.AssertOperation(await channel.GetFiles()); + Assert.True( + channelFiles.Files.Any(x => + x.Id == receivedFile.Id && x.Name == receivedFile.Name && x.Url == receivedFile.Url), + "Did not find message file in channel.GetFiles()!"); + } + + [Test] + public async Task TestFileUploadErrorHandling() + { + var receivedMessageReset = new ManualResetEvent(false); + channel.OnMessageReceived += message => + { + if (message.MessageText == "FILE TOO BIG") + { + receivedMessageReset.Set(); + } + }; + + var sendResult = await channel.SendText("FILE TOO BIG", new SendTextParams() + { + Files = + [ + new ChatInputFile() + { + Name = LARGE_FILE_NAME, + Type = "image", + Source = LARGE_FILE_LOCATION + } + ] + }); + Assert.True(sendResult.Error, "sendResult.Error should be true for file over size limit"); + Assert.True(sendResult.Exception.Message.Contains("Your proposed upload exceeds the maximum allowed size"), "Error message should contain info about file size"); + + var received = receivedMessageReset.WaitOne(5000); + Assert.False(received, "SendText should abort and not send message in case of file upload error"); + } +} \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/PubNubChatApi.Tests.csproj b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/PubNubChatApi.Tests.csproj index 4da4cae..5389c8f 100644 --- a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/PubNubChatApi.Tests.csproj +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/PubNubChatApi.Tests.csproj @@ -25,4 +25,13 @@ + + + Always + + + Always + + + diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/PushTests.cs b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/PushTests.cs new file mode 100644 index 0000000..dd38c9a --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/PushTests.cs @@ -0,0 +1,80 @@ +using PubnubApi; +using PubnubChatApi; +using Channel = PubnubChatApi.Channel; + +namespace PubNubChatApi.Tests; + +[TestFixture] +public class PushTests +{ + private Chat chat; + private Channel channel; + private User user; + + [SetUp] + public async Task Setup() + { + chat = TestUtils.AssertOperation(await Chat.CreateInstance( + new PubnubChatConfig(pushNotifications: new PubnubChatConfig.PushNotificationsConfig() + { + APNSEnvironment = PushEnvironment.Development, + APNSTopic = "someTopic", + DeviceGateway = PNPushType.FCM, + DeviceToken = "sometoken", + SendPushes = true + }), + new PNConfiguration(new UserId("push_tests_user")) + { + PublishKey = PubnubTestsParameters.PublishKey, + SubscribeKey = PubnubTestsParameters.SubscribeKey, + })); + channel = TestUtils.AssertOperation(await chat.CreatePublicConversation("push_tests_channel", + new ChatChannelData() { Name = "Push Channels Name" })); + user = TestUtils.AssertOperation(await chat.GetCurrentUser()); + await channel.Join(); + await Task.Delay(3500); + } + + [TearDown] + public async Task CleanUp() + { + await channel.Leave(); + await Task.Delay(3000); + chat.Destroy(); + await Task.Delay(3000); + } + + [Test] + public async Task TestAddAndRemovePushChannel() + { + var res = await channel.RegisterForPush(); + TestUtils.AssertOperation(res); + await Task.Delay(2000); + var pushChannels = TestUtils.AssertOperation(await chat.GetPushChannels()); + Assert.True(pushChannels.Contains(channel.Id), "Push channels don't contain registered channel ID"); + TestUtils.AssertOperation(await channel.UnRegisterFromPush()); + await Task.Delay(2000); + pushChannels = TestUtils.AssertOperation(await chat.GetPushChannels()); + Assert.False(pushChannels.Contains(channel.Id), "Push channels contain unregistered channel ID"); + } + + [Test] + public async Task TestRemoveAllPushChannel() + { + TestUtils.AssertOperation(await channel.RegisterForPush()); + await Task.Delay(2000); + var pushChannels = TestUtils.AssertOperation(await chat.GetPushChannels()); + Assert.True(pushChannels.Contains(channel.Id), "Push channels don't contain registered channel ID"); + TestUtils.AssertOperation(await chat.UnRegisterAllPushChannels()); + pushChannels = TestUtils.AssertOperation(await chat.GetPushChannels()); + Assert.False(pushChannels.Contains(channel.Id), "Push channels contain unregistered channel ID"); + } + + [Test] + public async Task TestPublishWithPushData() + { + TestUtils.AssertOperation(await channel.SendText("some_message", + new SendTextParams() + { CustomPushData = new Dictionary() { { "some_key", "some_value" } } })); + } +} \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/file_large.png b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/file_large.png new file mode 100644 index 0000000..9dd4414 Binary files /dev/null and b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/file_large.png differ diff --git a/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/fileupload.txt b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/fileupload.txt new file mode 100644 index 0000000..61a4a0b --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubNubChatApi.Tests/fileupload.txt @@ -0,0 +1 @@ +This is the file to be uploaded for testing. \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs index 966370e..0fd42b5 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Channel.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using System.Timers; using PubnubApi; +using Environment = PubnubApi.Environment; namespace PubnubChatApi { @@ -800,6 +801,32 @@ public async Task SendText(string message) return await SendText(message, new SendTextParams()).ConfigureAwait(false); } + private async Task> SendFileForPublish(ChatInputFile inputFile) + { + var result = new ChatOperationResult("Channel.SendFileForPublish()", chat); + var send = await chat.PubnubInstance.SendFile().Channel(Id).File(inputFile.Source).FileName(inputFile.Name) + .ShouldStore(false) + .ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(send)) + { + return result; + } + var getUrl = await chat.PubnubInstance.GetFileUrl().Channel(Id).FileId(send.Result.FileId) + .FileName(send.Result.FileName).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(getUrl)) + { + return result; + } + result.Result = new ChatFile() + { + Id = send.Result.FileId, + Name = send.Result.FileName, + Type = inputFile.Type, + Url = getUrl.Result.Url + }; + return result; + } + /// /// Sends the text message with additional parameters. /// @@ -812,6 +839,7 @@ public async Task SendText(string message) public virtual async Task SendText(string message, SendTextParams sendTextParams) { var result = new ChatOperationResult("Channel.SendText()", chat); + var jsonLibrary = chat.PubnubInstance.JsonPluggableLibrary; var baseInterval = Type switch { @@ -829,6 +857,35 @@ public virtual async Task SendText(string message, SendText {"text", message}, {"type", "text"} }; + if (chat.Config.PushNotifications is { SendPushes : true}) + { + var pushPayload = await GetPushPayload(message, sendTextParams.CustomPushData ?? new Dictionary()); + foreach (var kvp in pushPayload) + { + var pushJson = jsonLibrary.SerializeToJsonString(kvp.Value); + messageDict[kvp.Key] = pushJson; + } + } + if (sendTextParams.Files.Any()) + { + var fileTasks = sendTextParams.Files.Select(SendFileForPublish); + var fileResults = await Task.WhenAll(fileTasks).ConfigureAwait(false); + foreach (var fileResult in fileResults) + { + result.RegisterOperation(fileResult); + } + var failedUploads = fileResults.Where(x => x.Error).ToList(); + if (failedUploads.Any()) + { + var combinedException = string.Join("\n",failedUploads.Select(x => x.Exception.Message)); + result.Exception = new PNException($"Message publishing aborted: {failedUploads.Count} out of {fileResults.Length} " + + $"file uploads failed. Exceptions from file uploads: {combinedException}"); + result.Error = true; + return result; + } + var chatFilesAsDictionaries = fileResults.Select(x => x.Result.ToDictionary()).ToList(); + messageDict["files"] = jsonLibrary.SerializeToJsonString(chatFilesAsDictionaries); + } var meta = sendTextParams.Meta ?? new Dictionary(); if (sendTextParams.QuotedMessage != null) { @@ -851,7 +908,7 @@ public virtual async Task SendText(string message, SendText .Channel(Id) .ShouldStore(sendTextParams.StoreInHistory) .UsePOST(sendTextParams.SendByPost) - .Message(chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(messageDict)) + .Message(jsonLibrary.SerializeToJsonString(messageDict)) .Meta(meta) .ExecuteAsync().ConfigureAwait(false); if (result.RegisterOperation(publishResult)) @@ -874,6 +931,8 @@ public virtual async Task SendText(string message, SendText }, exception => { chat.Logger.Error($"Error occured when trying to SendText(): {exception.Message}"); + result.Error = true; + result.Exception = new PNException($"Encountered exception in SendText(): {exception.Message}"); completionSource.SetResult(true); }); @@ -1108,6 +1167,8 @@ public async Task> IsUserPresent(string userId) /// Gets all the users that are present in the channel. /// /// + /// Limit number of users details to be returned. Default and max value is 1000. + /// Use this parameter to provide starting position of results for pagination purpose. Default value is 0. /// A ChatOperationResult containing the list of users present in the channel. /// /// @@ -1120,11 +1181,15 @@ public async Task> IsUserPresent(string userId) /// /// /// - public async Task>> WhoIsPresent() + public async Task>> WhoIsPresent(int limit = 1000, int offset = 0) { + if (limit > 1000) + { + limit = 1000; + } var result = new ChatOperationResult>("Channel.WhoIsPresent()", chat) { Result = new List() }; var response = await chat.PubnubInstance.HereNow().Channels(new[] { Id }).IncludeState(true) - .IncludeUUIDs(true).ExecuteAsync().ConfigureAwait(false); + .IncludeUUIDs(true).Limit(limit).Offset(offset).ExecuteAsync().ConfigureAwait(false); if (result.RegisterOperation(response)) { return result; @@ -1242,5 +1307,136 @@ public async Task>> InviteMultiple(List + /// Retrieves a wrapper object with a of files that were sent on this channel. + /// + /// Optional - number of files to return. + /// Optional - String token to get the next batch of files. + public async Task> GetFiles(int limit = 0, string next = "") + { + var result = new ChatOperationResult("Channel.GetFiles()", chat); + + var listFilesOperation = chat.PubnubInstance.ListFiles().Channel(Id); + if (limit > 0) + { + listFilesOperation = listFilesOperation.Limit(limit); + } + if (!string.IsNullOrEmpty(next)) + { + listFilesOperation = listFilesOperation.Next(next); + } + var listFiles = await listFilesOperation.ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(listFiles)) + { + return result; + } + + var files = new List(); + if (listFiles.Result.FilesList != null && listFiles.Result.FilesList.Any()) + { + var getUrlsTasks = listFiles.Result.FilesList.Select(x => + chat.PubnubInstance.GetFileUrl().Channel(Id).FileId(x.Id).FileName(x.Name).ExecuteAsync()); + var getUrls = await Task.WhenAll(getUrlsTasks).ConfigureAwait(false); + foreach (var pnResult in getUrls) + { + if (result.RegisterOperation(pnResult)) + { + return result; + } + } + for (int i = 0; i < listFiles.Result.FilesList.Count; i++) + { + files.Add(new ChatFile() + { + Id = listFiles.Result.FilesList[i].Id, + Name = listFiles.Result.FilesList[i].Name, + Url = getUrls[i].Result.Url, + }); + } + } + + result.Result = new ChatFilesResult() + { + Files = files, + Next = listFiles.Result.Next, + Total = listFiles.Result.Count + }; + return result; + } + + /// + /// Deletes a file with a specified Id and Name from this channel. + /// + public async Task DeleteFile(string id, string name) + { + return (await chat.PubnubInstance.DeleteFile().Channel(Id).FileId(id).FileName(name).ExecuteAsync()) + .ToChatOperationResult("Channel.DeleteFile()", chat); + } + + private async Task> GetPushPayload(string text, Dictionary customPushData) + { + var pushConfig = chat.Config.PushNotifications; + if (pushConfig == null || pushConfig.SendPushes == false) + { + return new Dictionary(); + } + var title = chat.PubnubInstance.GetCurrentUserId().ToString(); + var currentUser = await chat.GetCurrentUser(); + if (!currentUser.Error && !string.IsNullOrEmpty(currentUser.Result.UserName)) + { + title = currentUser.Result.UserName; + } + var customData = new Dictionary(); + foreach (var entry in customPushData) + { + customData.Add(entry.Key, entry.Value); + } + if (!string.IsNullOrEmpty(Name)) + { + customData["subtitle"] = Name; + } + + var finalCustom = new Dictionary>() + { + { PNPushType.FCM, customData } + }; + var pushBuilder = new MobilePushHelper().PushTypeSupport(new[] { PNPushType.APNS2, PNPushType.FCM }) + .Title(title).Sound("default").Body(text); + + var apnsTopic = pushConfig.APNSTopic; + if (!string.IsNullOrEmpty(apnsTopic)) + { + var apnsEnv = pushConfig.APNSEnvironment; + pushBuilder.Apns2Data(new List() + { + new Apns2Data() + { + targets = new List() + { new PushTarget() { topic = apnsTopic, environment = (Environment)apnsEnv } }, + } + }); + finalCustom.Add(PNPushType.APNS2, customData); + } + pushBuilder.Custom(finalCustom); + + return pushBuilder.GetPayload(); + } + + /// + /// Registers this channel to receive push notifications. + /// + public async Task RegisterForPush() + { + return await chat.RegisterPushChannels(new List() { Id }).ConfigureAwait(false); + } + + /// + /// Un-Registers this channel from receiving push notifications. + /// + public async Task UnRegisterFromPush() + { + return await chat.UnRegisterPushChannels(new List() { Id }).ConfigureAwait(false); + } } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs index 70e1341..030e6f2 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Chat.cs @@ -1716,6 +1716,88 @@ public async Task EmitEvent(PubnubChatEventType type, strin #endregion + #region Push + + /// + /// Retrieves the Push Notifications config from the main Chat config. + /// Alternatively you can also use Config.PushNotifications + /// + public PubnubChatConfig.PushNotificationsConfig GetCommonPushOptions => Config.PushNotifications; + + /// + /// Registers a list of channels to receive push notifications. + /// + public async Task RegisterPushChannels(List channelIds) + { + var pushSettings = GetCommonPushOptions; + return (await PubnubInstance.AddPushNotificationsOnChannels() + .Channels(channelIds.ToArray()) + .PushType(pushSettings.DeviceGateway) + .DeviceId(pushSettings.DeviceToken) + .Topic(pushSettings.APNSTopic) + .Environment(pushSettings.APNSEnvironment) + .ExecuteAsync() + .ConfigureAwait(false)) + .ToChatOperationResult("Chat.RegisterPushChannels()", this); + } + + /// + /// Un-registers a list of channels from receiving push notifications. + /// + public async Task UnRegisterPushChannels(List channelIds) + { + var pushSettings = GetCommonPushOptions; + return (await PubnubInstance.RemovePushNotificationsFromChannels() + .Channels(channelIds.ToArray()) + .PushType(pushSettings.DeviceGateway) + .DeviceId(pushSettings.DeviceToken) + .Topic(pushSettings.APNSTopic) + .Environment(pushSettings.APNSEnvironment) + .ExecuteAsync() + .ConfigureAwait(false)) + .ToChatOperationResult("Chat.RegisterPushChannels()", this); + } + + /// + /// Un-registers all channels from receiving push notifications. + /// + public async Task UnRegisterAllPushChannels() + { + var pushSettings = GetCommonPushOptions; + return (await PubnubInstance.RemoveAllPushNotificationsFromDeviceWithPushToken() + .PushType(pushSettings.DeviceGateway) + .DeviceId(pushSettings.DeviceToken) + .Topic(pushSettings.APNSTopic) + .Environment(pushSettings.APNSEnvironment) + .ExecuteAsync() + .ConfigureAwait(false)) + .ToChatOperationResult("Chat.RegisterPushChannels()", this); + } + + /// + /// Returns the IDs of all currently registered push channels. + /// + public async Task>> GetPushChannels() + { + var result = new ChatOperationResult>("Chat.GetPushChannels()", this); + var pushSettings = GetCommonPushOptions; + var audit = await PubnubInstance.AuditPushChannelProvisions() + .PushType(pushSettings.DeviceGateway) + .DeviceId(pushSettings.DeviceToken) + .Topic(pushSettings.APNSTopic) + .Environment(pushSettings.APNSEnvironment) + .ExecuteAsync() + .ConfigureAwait(false); + if (result.RegisterOperation(audit)) + { + return result; + } + result.Result = audit.Result.Channels; + return result; + } + + #endregion + /// /// Destroys the chat instance and cleans up resources. /// diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatFile.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatFile.cs new file mode 100644 index 0000000..420b4a9 --- /dev/null +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/ChatFile.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using PubnubApi; + +namespace PubnubChatApi +{ + public class ChatFilesResult + { + public List Files; + public string Next; + public int Total; + } + + public class ChatFile + { + public string Name; + public string Id; + public string Url; + public string Type; + + internal Dictionary ToDictionary() + { + return new Dictionary() + { + { "name", Name }, + { "id", Id }, + { "url", Url }, + { "type", Type } + }; + } + } + + public struct ChatInputFile + { + public string Name; + public string Type; + public string Source; + } +} \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/PubnubChatConfig.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/PubnubChatConfig.cs index 7dbb7af..c0d6843 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/PubnubChatConfig.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/PubnubChatConfig.cs @@ -4,6 +4,16 @@ namespace PubnubChatApi { public class PubnubChatConfig { + [System.Serializable] + public class PushNotificationsConfig + { + public bool SendPushes; + public string DeviceToken; + public PNPushType DeviceGateway = PNPushType.FCM; + public string APNSTopic; + public PushEnvironment APNSEnvironment = PushEnvironment.Development; + } + [System.Serializable] public class RateLimitPerChannel { @@ -20,10 +30,11 @@ public class RateLimitPerChannel public bool StoreUserActivityTimestamp { get; } public int StoreUserActivityInterval { get; } public bool SyncMutedUsers { get; } + public PushNotificationsConfig PushNotifications { get; } public PubnubChatConfig(int typingTimeout = 5000, int typingTimeoutDifference = 1000, int rateLimitFactor = 2, RateLimitPerChannel rateLimitPerChannel = null, bool storeUserActivityTimestamp = false, - int storeUserActivityInterval = 60000, bool syncMutedUsers = false) + int storeUserActivityInterval = 60000, bool syncMutedUsers = false, PushNotificationsConfig pushNotifications = null) { RateLimitsPerChannel = rateLimitPerChannel ?? new RateLimitPerChannel(); RateLimitFactor = rateLimitFactor; @@ -32,6 +43,7 @@ public PubnubChatConfig(int typingTimeout = 5000, int typingTimeoutDifference = TypingTimeout = typingTimeout; TypingTimeoutDifference = typingTimeoutDifference; SyncMutedUsers = syncMutedUsers; + PushNotifications = pushNotifications ?? new PushNotificationsConfig(); } } } \ No newline at end of file diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/SendTextParams.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/SendTextParams.cs index abf4951..20b8551 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/SendTextParams.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Data/SendTextParams.cs @@ -9,5 +9,7 @@ public class SendTextParams public Dictionary Meta = new(); public Dictionary MentionedUsers = new(); public Message QuotedMessage = null; + public List Files = new(); + public Dictionary CustomPushData = new(); } } \ 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 a2984ec..855e84f 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/Message.cs @@ -211,6 +211,10 @@ public List TextLinks { /// public PubnubChatMessageType Type { get; internal set; } + /// + /// Files sent within this message. + /// + public List Files { get; } /// /// Event that is triggered when the message is updated. @@ -234,7 +238,7 @@ public List TextLinks { protected override string UpdateChannelId => ChannelId; - internal Message(Chat chat, string timeToken,string originalMessageText, string channelId, string userId, PubnubChatMessageType type, Dictionary meta, List messageActions) : base(chat, timeToken) + internal Message(Chat chat, string timeToken,string originalMessageText, string channelId, string userId, PubnubChatMessageType type, Dictionary meta, List messageActions, List files) : base(chat, timeToken) { TimeToken = timeToken; OriginalMessageText = originalMessageText; @@ -243,6 +247,7 @@ internal Message(Chat chat, string timeToken,string originalMessageText, string Type = type; Meta = meta; MessageActions = messageActions; + Files = files; } protected override SubscribeCallback CreateUpdateListener() diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs index 7e33207..b24ba9d 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/MessageDraft.cs @@ -105,8 +105,16 @@ private class DraftCallbackDataHelper /// public List MessageElements => GetMessageElements(); + /// + /// Defines whether this MessageDraft should search for suggestions after it's content is updated. + /// public bool ShouldSearchForSuggestions { get; set; } + /// + /// Can be used to attach files to send with this MessageDraft. + /// + public List Files { get; set; } = new(); + private Channel channel; private Chat chat; @@ -579,6 +587,7 @@ public async Task Send(SendTextParams sendTextParams) } } sendTextParams.MentionedUsers = mentions; + sendTextParams.Files = Files; return await channel.SendText(Render(), sendTextParams).ConfigureAwait(false); } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs index cca5b1a..e5a666b 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadChannel.cs @@ -92,7 +92,7 @@ public async Task>> GetThreadHistory(str { result.Result.Add(new ThreadMessage(chat, message.TimeToken, message.OriginalMessageText, message.ChannelId, ParentChannelId, message.UserId, PubnubChatMessageType.Text, message.Meta, - message.MessageActions)); + message.MessageActions, message.Files)); } return result; diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs index 781e99f..970aa9e 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Entities/ThreadMessage.cs @@ -13,8 +13,8 @@ public class ThreadMessage : Message internal ThreadMessage(Chat chat, string timeToken, string originalMessageText, string channelId, string parentChannelId, string userId, PubnubChatMessageType type, Dictionary meta, - List messageActions) : base(chat, timeToken, originalMessageText, channelId, userId, type, - meta, messageActions) + List messageActions, List files) : base(chat, timeToken, originalMessageText, channelId, userId, type, + meta, messageActions, files) { ParentChannelId = parentChannelId; } diff --git a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs index 081260f..28ee87c 100644 --- a/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs +++ b/c-sharp-chat/PubnubChatApi/PubnubChatApi/Utilities/ChatParsers.cs @@ -7,7 +7,8 @@ namespace PubnubChatApi { internal static class ChatParsers { - internal static bool TryParseMessageResult(Chat chat, PNMessageResult messageResult, out Message message) + internal static bool TryParseMessageResult(Chat chat, PNMessageResult messageResult, + out Message message) { try { @@ -25,24 +26,46 @@ internal static bool TryParseMessageResult(Chat chat, PNMessageResult me var type = PubnubChatMessageType.Text; var text = messageDict["text"].ToString(); var meta = messageResult.UserMetadata ?? new Dictionary(); - - message = new Message(chat, messageResult.Timetoken.ToString(), text, messageResult.Channel, messageResult.Publisher, type, meta, new List()); + + var messageFiles = new List(); + if (messageDict.TryGetValue("files", out var filesRawString)) + { + var files = chat.PubnubInstance.JsonPluggableLibrary.DeserializeToListOfObject(filesRawString.ToString()); + foreach (var file in files) + { + var fileDict = + chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(file.ToString()); + messageFiles.Add(new ChatFile() + { + Name = fileDict["name"].ToString(), + Id = fileDict["id"].ToString(), + Url = fileDict["url"].ToString(), + Type = fileDict["type"].ToString(), + }); + } + } + + message = new Message(chat, messageResult.Timetoken.ToString(), text, messageResult.Channel, + messageResult.Publisher, type, meta, new List(), messageFiles); return true; } catch (Exception e) { - chat.Logger.Debug($"Failed to parse PNMessageResult with payload: {messageResult.Message} into chat Message entity. Exception was: {e.Message}"); + chat.Logger.Debug( + $"Failed to parse PNMessageResult with payload: {messageResult.Message} into chat Message entity. Exception was: {e.Message}"); message = null; return false; } } - - internal static bool TryParseMessageFromHistory(Chat chat, string channelId, PNHistoryItemResult historyItem, out Message message) + + internal static bool TryParseMessageFromHistory(Chat chat, string channelId, PNHistoryItemResult historyItem, + out Message message) { try { var messageDict = - chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(historyItem.Entry.ToString()); + chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject( + historyItem.Entry.ToString()); if (!messageDict.TryGetValue("type", out var typeValue) || typeValue.ToString() != "text") { @@ -53,7 +76,25 @@ internal static bool TryParseMessageFromHistory(Chat chat, string channelId, PNH //TODO: later more types I guess? var type = PubnubChatMessageType.Text; var text = messageDict["text"].ToString(); - + + var messageFiles = new List(); + if (messageDict.TryGetValue("files", out var filesRawString)) + { + var files = chat.PubnubInstance.JsonPluggableLibrary.DeserializeToListOfObject(filesRawString.ToString()); + foreach (var file in files) + { + var fileDict = + chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(file.ToString()); + messageFiles.Add(new ChatFile() + { + Name = fileDict["name"].ToString(), + Id = fileDict["id"].ToString(), + Url = fileDict["url"].ToString(), + Type = fileDict["type"].ToString(), + }); + } + } + var actions = new List(); if (historyItem.ActionItems != null) { @@ -72,18 +113,22 @@ internal static bool TryParseMessageFromHistory(Chat chat, string channelId, PNH } } } - message = new Message(chat, historyItem.Timetoken.ToString(), text, channelId, historyItem.Uuid, type, historyItem.Meta, actions); + + message = new Message(chat, historyItem.Timetoken.ToString(), text, channelId, historyItem.Uuid, type, + historyItem.Meta, actions, messageFiles); return true; } catch (Exception e) { - chat.Logger.Debug($"Failed to parse PNHistoryItemResult with payload: {historyItem.Entry} into chat Message entity. Exception was: {e.Message}"); + chat.Logger.Debug( + $"Failed to parse PNHistoryItemResult with payload: {historyItem.Entry} into chat Message entity. Exception was: {e.Message}"); message = null; return false; } } - internal static bool TryParseMembershipUpdate(Chat chat, Membership membership, PNObjectEventResult objectEvent, out ChatMembershipData updatedData, out ChatEntityChangeType changeType) + internal static bool TryParseMembershipUpdate(Chat chat, Membership membership, PNObjectEventResult objectEvent, + out ChatMembershipData updatedData, out ChatEntityChangeType changeType) { try { @@ -114,14 +159,16 @@ internal static bool TryParseMembershipUpdate(Chat chat, Membership membership, } catch (Exception e) { - chat.Logger.Debug($"Failed to parse PNObjectEventResult of type: {objectEvent.Event} into Membership update. Exception was: {e.Message}"); + chat.Logger.Debug( + $"Failed to parse PNObjectEventResult of type: {objectEvent.Event} into Membership update. Exception was: {e.Message}"); updatedData = null; changeType = default; return false; } } - - internal static bool TryParseUserUpdate(Chat chat, User user, PNObjectEventResult objectEvent, out ChatUserData updatedData, out ChatEntityChangeType changeType) + + internal static bool TryParseUserUpdate(Chat chat, User user, PNObjectEventResult objectEvent, + out ChatUserData updatedData, out ChatEntityChangeType changeType) { try { @@ -146,14 +193,16 @@ internal static bool TryParseUserUpdate(Chat chat, User user, PNObjectEventResul } catch (Exception e) { - chat.Logger.Debug($"Failed to parse PNObjectEventResult of type: {objectEvent.Event} into User update. Exception was: {e.Message}"); + chat.Logger.Debug( + $"Failed to parse PNObjectEventResult of type: {objectEvent.Event} into User update. Exception was: {e.Message}"); updatedData = null; changeType = default; return false; } } - - internal static bool TryParseChannelUpdate(Chat chat, Channel channel, PNObjectEventResult objectEvent, out ChatChannelData updatedData, out ChatEntityChangeType changeType) + + internal static bool TryParseChannelUpdate(Chat chat, Channel channel, PNObjectEventResult objectEvent, + out ChatChannelData updatedData, out ChatEntityChangeType changeType) { try { @@ -178,18 +227,20 @@ internal static bool TryParseChannelUpdate(Chat chat, Channel channel, PNObjectE } catch (Exception e) { - chat.Logger.Debug($"Failed to parse PNObjectEventResult of type: {objectEvent.Event} into Channel update. Exception was: {e.Message}"); + chat.Logger.Debug( + $"Failed to parse PNObjectEventResult of type: {objectEvent.Event} into Channel update. Exception was: {e.Message}"); updatedData = null; changeType = default; return false; } } - + internal static bool TryParseMessageUpdate(Chat chat, Message message, PNMessageActionEventResult actionEvent) { try { - if (actionEvent.MessageTimetoken.ToString() == message.TimeToken && actionEvent.Uuid == message.UserId && actionEvent.Channel == message.ChannelId) + if (actionEvent.MessageTimetoken.ToString() == message.TimeToken && + actionEvent.Uuid == message.UserId && actionEvent.Channel == message.ChannelId) { if (actionEvent.Event != "removed") { @@ -198,6 +249,7 @@ internal static bool TryParseMessageUpdate(Chat chat, Message message, PNMessage { return true; } + message.MessageActions.Add(new MessageAction() { TimeToken = actionEvent.ActionTimetoken.ToString(), @@ -212,6 +264,7 @@ internal static bool TryParseMessageUpdate(Chat chat, Message message, PNMessage dict.Remove(actionEvent.ActionTimetoken.ToString()); message.MessageActions = dict.Values.ToList(); } + return true; } else @@ -221,12 +274,14 @@ internal static bool TryParseMessageUpdate(Chat chat, Message message, PNMessage } catch (Exception e) { - chat.Logger.Debug($"Failed to parse PNMessageActionEventResult into Message update. Exception was: {e.Message}"); + chat.Logger.Debug( + $"Failed to parse PNMessageActionEventResult into Message update. Exception was: {e.Message}"); return false; } } - internal static bool TryParseEvent(Chat chat, PNMessageResult messageResult, PubnubChatEventType eventType, out ChatEvent chatEvent) + internal static bool TryParseEvent(Chat chat, PNMessageResult messageResult, + PubnubChatEventType eventType, out ChatEvent chatEvent) { try { @@ -238,17 +293,20 @@ internal static bool TryParseEvent(Chat chat, PNMessageResult messageRes chatEvent = default; return false; } + var receivedEventType = ChatEnumConverters.StringToEventType(typeString.ToString()); if (receivedEventType != eventType) { chatEvent = default; return false; } + if (chat.MutedUsersManager.MutedUsers.Contains(messageResult.Publisher)) { chatEvent = default; return false; } + chatEvent = new ChatEvent() { TimeToken = messageResult.Timetoken.ToString(), @@ -261,23 +319,27 @@ internal static bool TryParseEvent(Chat chat, PNMessageResult messageRes } catch (Exception e) { - chat.Logger.Debug($"Failed to parse PNMessageResult into ChatEvent of type \"{eventType}\". Exception was: {e.Message}"); + chat.Logger.Debug( + $"Failed to parse PNMessageResult into ChatEvent of type \"{eventType}\". Exception was: {e.Message}"); chatEvent = default; return false; } } - - internal static bool TryParseEventFromHistory(Chat chat, string channelId, PNHistoryItemResult historyItem, out ChatEvent chatEvent) + + internal static bool TryParseEventFromHistory(Chat chat, string channelId, PNHistoryItemResult historyItem, + out ChatEvent chatEvent) { try { var jsonDict = - chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(historyItem.Entry.ToString()); + chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject( + historyItem.Entry.ToString()); if (!jsonDict.TryGetValue("type", out var typeString)) { chatEvent = default; return false; } + var receivedEventType = ChatEnumConverters.StringToEventType(typeString.ToString()); chatEvent = new ChatEvent() { 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 966370e..0fd42b5 100644 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Channel.cs +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Channel.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using System.Timers; using PubnubApi; +using Environment = PubnubApi.Environment; namespace PubnubChatApi { @@ -800,6 +801,32 @@ public async Task SendText(string message) return await SendText(message, new SendTextParams()).ConfigureAwait(false); } + private async Task> SendFileForPublish(ChatInputFile inputFile) + { + var result = new ChatOperationResult("Channel.SendFileForPublish()", chat); + var send = await chat.PubnubInstance.SendFile().Channel(Id).File(inputFile.Source).FileName(inputFile.Name) + .ShouldStore(false) + .ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(send)) + { + return result; + } + var getUrl = await chat.PubnubInstance.GetFileUrl().Channel(Id).FileId(send.Result.FileId) + .FileName(send.Result.FileName).ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(getUrl)) + { + return result; + } + result.Result = new ChatFile() + { + Id = send.Result.FileId, + Name = send.Result.FileName, + Type = inputFile.Type, + Url = getUrl.Result.Url + }; + return result; + } + /// /// Sends the text message with additional parameters. /// @@ -812,6 +839,7 @@ public async Task SendText(string message) public virtual async Task SendText(string message, SendTextParams sendTextParams) { var result = new ChatOperationResult("Channel.SendText()", chat); + var jsonLibrary = chat.PubnubInstance.JsonPluggableLibrary; var baseInterval = Type switch { @@ -829,6 +857,35 @@ public virtual async Task SendText(string message, SendText {"text", message}, {"type", "text"} }; + if (chat.Config.PushNotifications is { SendPushes : true}) + { + var pushPayload = await GetPushPayload(message, sendTextParams.CustomPushData ?? new Dictionary()); + foreach (var kvp in pushPayload) + { + var pushJson = jsonLibrary.SerializeToJsonString(kvp.Value); + messageDict[kvp.Key] = pushJson; + } + } + if (sendTextParams.Files.Any()) + { + var fileTasks = sendTextParams.Files.Select(SendFileForPublish); + var fileResults = await Task.WhenAll(fileTasks).ConfigureAwait(false); + foreach (var fileResult in fileResults) + { + result.RegisterOperation(fileResult); + } + var failedUploads = fileResults.Where(x => x.Error).ToList(); + if (failedUploads.Any()) + { + var combinedException = string.Join("\n",failedUploads.Select(x => x.Exception.Message)); + result.Exception = new PNException($"Message publishing aborted: {failedUploads.Count} out of {fileResults.Length} " + + $"file uploads failed. Exceptions from file uploads: {combinedException}"); + result.Error = true; + return result; + } + var chatFilesAsDictionaries = fileResults.Select(x => x.Result.ToDictionary()).ToList(); + messageDict["files"] = jsonLibrary.SerializeToJsonString(chatFilesAsDictionaries); + } var meta = sendTextParams.Meta ?? new Dictionary(); if (sendTextParams.QuotedMessage != null) { @@ -851,7 +908,7 @@ public virtual async Task SendText(string message, SendText .Channel(Id) .ShouldStore(sendTextParams.StoreInHistory) .UsePOST(sendTextParams.SendByPost) - .Message(chat.PubnubInstance.JsonPluggableLibrary.SerializeToJsonString(messageDict)) + .Message(jsonLibrary.SerializeToJsonString(messageDict)) .Meta(meta) .ExecuteAsync().ConfigureAwait(false); if (result.RegisterOperation(publishResult)) @@ -874,6 +931,8 @@ public virtual async Task SendText(string message, SendText }, exception => { chat.Logger.Error($"Error occured when trying to SendText(): {exception.Message}"); + result.Error = true; + result.Exception = new PNException($"Encountered exception in SendText(): {exception.Message}"); completionSource.SetResult(true); }); @@ -1108,6 +1167,8 @@ public async Task> IsUserPresent(string userId) /// Gets all the users that are present in the channel. /// /// + /// Limit number of users details to be returned. Default and max value is 1000. + /// Use this parameter to provide starting position of results for pagination purpose. Default value is 0. /// A ChatOperationResult containing the list of users present in the channel. /// /// @@ -1120,11 +1181,15 @@ public async Task> IsUserPresent(string userId) /// /// /// - public async Task>> WhoIsPresent() + public async Task>> WhoIsPresent(int limit = 1000, int offset = 0) { + if (limit > 1000) + { + limit = 1000; + } var result = new ChatOperationResult>("Channel.WhoIsPresent()", chat) { Result = new List() }; var response = await chat.PubnubInstance.HereNow().Channels(new[] { Id }).IncludeState(true) - .IncludeUUIDs(true).ExecuteAsync().ConfigureAwait(false); + .IncludeUUIDs(true).Limit(limit).Offset(offset).ExecuteAsync().ConfigureAwait(false); if (result.RegisterOperation(response)) { return result; @@ -1242,5 +1307,136 @@ public async Task>> InviteMultiple(List + /// Retrieves a wrapper object with a of files that were sent on this channel. + /// + /// Optional - number of files to return. + /// Optional - String token to get the next batch of files. + public async Task> GetFiles(int limit = 0, string next = "") + { + var result = new ChatOperationResult("Channel.GetFiles()", chat); + + var listFilesOperation = chat.PubnubInstance.ListFiles().Channel(Id); + if (limit > 0) + { + listFilesOperation = listFilesOperation.Limit(limit); + } + if (!string.IsNullOrEmpty(next)) + { + listFilesOperation = listFilesOperation.Next(next); + } + var listFiles = await listFilesOperation.ExecuteAsync().ConfigureAwait(false); + if (result.RegisterOperation(listFiles)) + { + return result; + } + + var files = new List(); + if (listFiles.Result.FilesList != null && listFiles.Result.FilesList.Any()) + { + var getUrlsTasks = listFiles.Result.FilesList.Select(x => + chat.PubnubInstance.GetFileUrl().Channel(Id).FileId(x.Id).FileName(x.Name).ExecuteAsync()); + var getUrls = await Task.WhenAll(getUrlsTasks).ConfigureAwait(false); + foreach (var pnResult in getUrls) + { + if (result.RegisterOperation(pnResult)) + { + return result; + } + } + for (int i = 0; i < listFiles.Result.FilesList.Count; i++) + { + files.Add(new ChatFile() + { + Id = listFiles.Result.FilesList[i].Id, + Name = listFiles.Result.FilesList[i].Name, + Url = getUrls[i].Result.Url, + }); + } + } + + result.Result = new ChatFilesResult() + { + Files = files, + Next = listFiles.Result.Next, + Total = listFiles.Result.Count + }; + return result; + } + + /// + /// Deletes a file with a specified Id and Name from this channel. + /// + public async Task DeleteFile(string id, string name) + { + return (await chat.PubnubInstance.DeleteFile().Channel(Id).FileId(id).FileName(name).ExecuteAsync()) + .ToChatOperationResult("Channel.DeleteFile()", chat); + } + + private async Task> GetPushPayload(string text, Dictionary customPushData) + { + var pushConfig = chat.Config.PushNotifications; + if (pushConfig == null || pushConfig.SendPushes == false) + { + return new Dictionary(); + } + var title = chat.PubnubInstance.GetCurrentUserId().ToString(); + var currentUser = await chat.GetCurrentUser(); + if (!currentUser.Error && !string.IsNullOrEmpty(currentUser.Result.UserName)) + { + title = currentUser.Result.UserName; + } + var customData = new Dictionary(); + foreach (var entry in customPushData) + { + customData.Add(entry.Key, entry.Value); + } + if (!string.IsNullOrEmpty(Name)) + { + customData["subtitle"] = Name; + } + + var finalCustom = new Dictionary>() + { + { PNPushType.FCM, customData } + }; + var pushBuilder = new MobilePushHelper().PushTypeSupport(new[] { PNPushType.APNS2, PNPushType.FCM }) + .Title(title).Sound("default").Body(text); + + var apnsTopic = pushConfig.APNSTopic; + if (!string.IsNullOrEmpty(apnsTopic)) + { + var apnsEnv = pushConfig.APNSEnvironment; + pushBuilder.Apns2Data(new List() + { + new Apns2Data() + { + targets = new List() + { new PushTarget() { topic = apnsTopic, environment = (Environment)apnsEnv } }, + } + }); + finalCustom.Add(PNPushType.APNS2, customData); + } + pushBuilder.Custom(finalCustom); + + return pushBuilder.GetPayload(); + } + + /// + /// Registers this channel to receive push notifications. + /// + public async Task RegisterForPush() + { + return await chat.RegisterPushChannels(new List() { Id }).ConfigureAwait(false); + } + + /// + /// Un-Registers this channel from receiving push notifications. + /// + public async Task UnRegisterFromPush() + { + return await chat.UnRegisterPushChannels(new List() { Id }).ConfigureAwait(false); + } } } \ No newline at end of file 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 70e1341..030e6f2 100644 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Chat.cs +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Chat.cs @@ -1716,6 +1716,88 @@ public async Task EmitEvent(PubnubChatEventType type, strin #endregion + #region Push + + /// + /// Retrieves the Push Notifications config from the main Chat config. + /// Alternatively you can also use Config.PushNotifications + /// + public PubnubChatConfig.PushNotificationsConfig GetCommonPushOptions => Config.PushNotifications; + + /// + /// Registers a list of channels to receive push notifications. + /// + public async Task RegisterPushChannels(List channelIds) + { + var pushSettings = GetCommonPushOptions; + return (await PubnubInstance.AddPushNotificationsOnChannels() + .Channels(channelIds.ToArray()) + .PushType(pushSettings.DeviceGateway) + .DeviceId(pushSettings.DeviceToken) + .Topic(pushSettings.APNSTopic) + .Environment(pushSettings.APNSEnvironment) + .ExecuteAsync() + .ConfigureAwait(false)) + .ToChatOperationResult("Chat.RegisterPushChannels()", this); + } + + /// + /// Un-registers a list of channels from receiving push notifications. + /// + public async Task UnRegisterPushChannels(List channelIds) + { + var pushSettings = GetCommonPushOptions; + return (await PubnubInstance.RemovePushNotificationsFromChannels() + .Channels(channelIds.ToArray()) + .PushType(pushSettings.DeviceGateway) + .DeviceId(pushSettings.DeviceToken) + .Topic(pushSettings.APNSTopic) + .Environment(pushSettings.APNSEnvironment) + .ExecuteAsync() + .ConfigureAwait(false)) + .ToChatOperationResult("Chat.RegisterPushChannels()", this); + } + + /// + /// Un-registers all channels from receiving push notifications. + /// + public async Task UnRegisterAllPushChannels() + { + var pushSettings = GetCommonPushOptions; + return (await PubnubInstance.RemoveAllPushNotificationsFromDeviceWithPushToken() + .PushType(pushSettings.DeviceGateway) + .DeviceId(pushSettings.DeviceToken) + .Topic(pushSettings.APNSTopic) + .Environment(pushSettings.APNSEnvironment) + .ExecuteAsync() + .ConfigureAwait(false)) + .ToChatOperationResult("Chat.RegisterPushChannels()", this); + } + + /// + /// Returns the IDs of all currently registered push channels. + /// + public async Task>> GetPushChannels() + { + var result = new ChatOperationResult>("Chat.GetPushChannels()", this); + var pushSettings = GetCommonPushOptions; + var audit = await PubnubInstance.AuditPushChannelProvisions() + .PushType(pushSettings.DeviceGateway) + .DeviceId(pushSettings.DeviceToken) + .Topic(pushSettings.APNSTopic) + .Environment(pushSettings.APNSEnvironment) + .ExecuteAsync() + .ConfigureAwait(false); + if (result.RegisterOperation(audit)) + { + return result; + } + result.Result = audit.Result.Channels; + return result; + } + + #endregion + /// /// Destroys the chat instance and cleans up resources. /// diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatFile.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatFile.cs new file mode 100644 index 0000000..420b4a9 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatFile.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using PubnubApi; + +namespace PubnubChatApi +{ + public class ChatFilesResult + { + public List Files; + public string Next; + public int Total; + } + + public class ChatFile + { + public string Name; + public string Id; + public string Url; + public string Type; + + internal Dictionary ToDictionary() + { + return new Dictionary() + { + { "name", Name }, + { "id", Id }, + { "url", Url }, + { "type", Type } + }; + } + } + + public struct ChatInputFile + { + public string Name; + public string Type; + public string Source; + } +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatFile.cs.meta b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatFile.cs.meta new file mode 100644 index 0000000..f05618f --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/ChatFile.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 38227f13c1d32ea41956ef171393c4b8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: 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 7dbb7af..c0d6843 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 @@ -4,6 +4,16 @@ namespace PubnubChatApi { public class PubnubChatConfig { + [System.Serializable] + public class PushNotificationsConfig + { + public bool SendPushes; + public string DeviceToken; + public PNPushType DeviceGateway = PNPushType.FCM; + public string APNSTopic; + public PushEnvironment APNSEnvironment = PushEnvironment.Development; + } + [System.Serializable] public class RateLimitPerChannel { @@ -20,10 +30,11 @@ public class RateLimitPerChannel public bool StoreUserActivityTimestamp { get; } public int StoreUserActivityInterval { get; } public bool SyncMutedUsers { get; } + public PushNotificationsConfig PushNotifications { get; } public PubnubChatConfig(int typingTimeout = 5000, int typingTimeoutDifference = 1000, int rateLimitFactor = 2, RateLimitPerChannel rateLimitPerChannel = null, bool storeUserActivityTimestamp = false, - int storeUserActivityInterval = 60000, bool syncMutedUsers = false) + int storeUserActivityInterval = 60000, bool syncMutedUsers = false, PushNotificationsConfig pushNotifications = null) { RateLimitsPerChannel = rateLimitPerChannel ?? new RateLimitPerChannel(); RateLimitFactor = rateLimitFactor; @@ -32,6 +43,7 @@ public PubnubChatConfig(int typingTimeout = 5000, int typingTimeoutDifference = TypingTimeout = typingTimeout; TypingTimeoutDifference = typingTimeoutDifference; SyncMutedUsers = syncMutedUsers; + PushNotifications = pushNotifications ?? new PushNotificationsConfig(); } } } \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/SendTextParams.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/SendTextParams.cs index abf4951..20b8551 100644 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/SendTextParams.cs +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Data/SendTextParams.cs @@ -9,5 +9,7 @@ public class SendTextParams public Dictionary Meta = new(); public Dictionary MentionedUsers = new(); public Message QuotedMessage = null; + public List Files = new(); + public Dictionary CustomPushData = new(); } } \ 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 a2984ec..855e84f 100644 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Message.cs +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/Message.cs @@ -211,6 +211,10 @@ public List TextLinks { /// public PubnubChatMessageType Type { get; internal set; } + /// + /// Files sent within this message. + /// + public List Files { get; } /// /// Event that is triggered when the message is updated. @@ -234,7 +238,7 @@ public List TextLinks { protected override string UpdateChannelId => ChannelId; - internal Message(Chat chat, string timeToken,string originalMessageText, string channelId, string userId, PubnubChatMessageType type, Dictionary meta, List messageActions) : base(chat, timeToken) + internal Message(Chat chat, string timeToken,string originalMessageText, string channelId, string userId, PubnubChatMessageType type, Dictionary meta, List messageActions, List files) : base(chat, timeToken) { TimeToken = timeToken; OriginalMessageText = originalMessageText; @@ -243,6 +247,7 @@ internal Message(Chat chat, string timeToken,string originalMessageText, string Type = type; Meta = meta; MessageActions = messageActions; + Files = files; } protected override SubscribeCallback CreateUpdateListener() diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/MessageDraft.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/MessageDraft.cs index 7e33207..b24ba9d 100644 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/MessageDraft.cs +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/MessageDraft.cs @@ -105,8 +105,16 @@ private class DraftCallbackDataHelper /// public List MessageElements => GetMessageElements(); + /// + /// Defines whether this MessageDraft should search for suggestions after it's content is updated. + /// public bool ShouldSearchForSuggestions { get; set; } + /// + /// Can be used to attach files to send with this MessageDraft. + /// + public List Files { get; set; } = new(); + private Channel channel; private Chat chat; @@ -579,6 +587,7 @@ public async Task Send(SendTextParams sendTextParams) } } sendTextParams.MentionedUsers = mentions; + sendTextParams.Files = Files; return await channel.SendText(Render(), sendTextParams).ConfigureAwait(false); } diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadChannel.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadChannel.cs index cca5b1a..e5a666b 100644 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadChannel.cs +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadChannel.cs @@ -92,7 +92,7 @@ public async Task>> GetThreadHistory(str { result.Result.Add(new ThreadMessage(chat, message.TimeToken, message.OriginalMessageText, message.ChannelId, ParentChannelId, message.UserId, PubnubChatMessageType.Text, message.Meta, - message.MessageActions)); + message.MessageActions, message.Files)); } return result; diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadMessage.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadMessage.cs index 781e99f..970aa9e 100644 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadMessage.cs +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Entities/ThreadMessage.cs @@ -13,8 +13,8 @@ public class ThreadMessage : Message internal ThreadMessage(Chat chat, string timeToken, string originalMessageText, string channelId, string parentChannelId, string userId, PubnubChatMessageType type, Dictionary meta, - List messageActions) : base(chat, timeToken, originalMessageText, channelId, userId, type, - meta, messageActions) + List messageActions, List files) : base(chat, timeToken, originalMessageText, channelId, userId, type, + meta, messageActions, files) { ParentChannelId = parentChannelId; } 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 081260f..28ee87c 100644 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatParsers.cs +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/PubnubChatApi/Utilities/ChatParsers.cs @@ -7,7 +7,8 @@ namespace PubnubChatApi { internal static class ChatParsers { - internal static bool TryParseMessageResult(Chat chat, PNMessageResult messageResult, out Message message) + internal static bool TryParseMessageResult(Chat chat, PNMessageResult messageResult, + out Message message) { try { @@ -25,24 +26,46 @@ internal static bool TryParseMessageResult(Chat chat, PNMessageResult me var type = PubnubChatMessageType.Text; var text = messageDict["text"].ToString(); var meta = messageResult.UserMetadata ?? new Dictionary(); - - message = new Message(chat, messageResult.Timetoken.ToString(), text, messageResult.Channel, messageResult.Publisher, type, meta, new List()); + + var messageFiles = new List(); + if (messageDict.TryGetValue("files", out var filesRawString)) + { + var files = chat.PubnubInstance.JsonPluggableLibrary.DeserializeToListOfObject(filesRawString.ToString()); + foreach (var file in files) + { + var fileDict = + chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(file.ToString()); + messageFiles.Add(new ChatFile() + { + Name = fileDict["name"].ToString(), + Id = fileDict["id"].ToString(), + Url = fileDict["url"].ToString(), + Type = fileDict["type"].ToString(), + }); + } + } + + message = new Message(chat, messageResult.Timetoken.ToString(), text, messageResult.Channel, + messageResult.Publisher, type, meta, new List(), messageFiles); return true; } catch (Exception e) { - chat.Logger.Debug($"Failed to parse PNMessageResult with payload: {messageResult.Message} into chat Message entity. Exception was: {e.Message}"); + chat.Logger.Debug( + $"Failed to parse PNMessageResult with payload: {messageResult.Message} into chat Message entity. Exception was: {e.Message}"); message = null; return false; } } - - internal static bool TryParseMessageFromHistory(Chat chat, string channelId, PNHistoryItemResult historyItem, out Message message) + + internal static bool TryParseMessageFromHistory(Chat chat, string channelId, PNHistoryItemResult historyItem, + out Message message) { try { var messageDict = - chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(historyItem.Entry.ToString()); + chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject( + historyItem.Entry.ToString()); if (!messageDict.TryGetValue("type", out var typeValue) || typeValue.ToString() != "text") { @@ -53,7 +76,25 @@ internal static bool TryParseMessageFromHistory(Chat chat, string channelId, PNH //TODO: later more types I guess? var type = PubnubChatMessageType.Text; var text = messageDict["text"].ToString(); - + + var messageFiles = new List(); + if (messageDict.TryGetValue("files", out var filesRawString)) + { + var files = chat.PubnubInstance.JsonPluggableLibrary.DeserializeToListOfObject(filesRawString.ToString()); + foreach (var file in files) + { + var fileDict = + chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(file.ToString()); + messageFiles.Add(new ChatFile() + { + Name = fileDict["name"].ToString(), + Id = fileDict["id"].ToString(), + Url = fileDict["url"].ToString(), + Type = fileDict["type"].ToString(), + }); + } + } + var actions = new List(); if (historyItem.ActionItems != null) { @@ -72,18 +113,22 @@ internal static bool TryParseMessageFromHistory(Chat chat, string channelId, PNH } } } - message = new Message(chat, historyItem.Timetoken.ToString(), text, channelId, historyItem.Uuid, type, historyItem.Meta, actions); + + message = new Message(chat, historyItem.Timetoken.ToString(), text, channelId, historyItem.Uuid, type, + historyItem.Meta, actions, messageFiles); return true; } catch (Exception e) { - chat.Logger.Debug($"Failed to parse PNHistoryItemResult with payload: {historyItem.Entry} into chat Message entity. Exception was: {e.Message}"); + chat.Logger.Debug( + $"Failed to parse PNHistoryItemResult with payload: {historyItem.Entry} into chat Message entity. Exception was: {e.Message}"); message = null; return false; } } - internal static bool TryParseMembershipUpdate(Chat chat, Membership membership, PNObjectEventResult objectEvent, out ChatMembershipData updatedData, out ChatEntityChangeType changeType) + internal static bool TryParseMembershipUpdate(Chat chat, Membership membership, PNObjectEventResult objectEvent, + out ChatMembershipData updatedData, out ChatEntityChangeType changeType) { try { @@ -114,14 +159,16 @@ internal static bool TryParseMembershipUpdate(Chat chat, Membership membership, } catch (Exception e) { - chat.Logger.Debug($"Failed to parse PNObjectEventResult of type: {objectEvent.Event} into Membership update. Exception was: {e.Message}"); + chat.Logger.Debug( + $"Failed to parse PNObjectEventResult of type: {objectEvent.Event} into Membership update. Exception was: {e.Message}"); updatedData = null; changeType = default; return false; } } - - internal static bool TryParseUserUpdate(Chat chat, User user, PNObjectEventResult objectEvent, out ChatUserData updatedData, out ChatEntityChangeType changeType) + + internal static bool TryParseUserUpdate(Chat chat, User user, PNObjectEventResult objectEvent, + out ChatUserData updatedData, out ChatEntityChangeType changeType) { try { @@ -146,14 +193,16 @@ internal static bool TryParseUserUpdate(Chat chat, User user, PNObjectEventResul } catch (Exception e) { - chat.Logger.Debug($"Failed to parse PNObjectEventResult of type: {objectEvent.Event} into User update. Exception was: {e.Message}"); + chat.Logger.Debug( + $"Failed to parse PNObjectEventResult of type: {objectEvent.Event} into User update. Exception was: {e.Message}"); updatedData = null; changeType = default; return false; } } - - internal static bool TryParseChannelUpdate(Chat chat, Channel channel, PNObjectEventResult objectEvent, out ChatChannelData updatedData, out ChatEntityChangeType changeType) + + internal static bool TryParseChannelUpdate(Chat chat, Channel channel, PNObjectEventResult objectEvent, + out ChatChannelData updatedData, out ChatEntityChangeType changeType) { try { @@ -178,18 +227,20 @@ internal static bool TryParseChannelUpdate(Chat chat, Channel channel, PNObjectE } catch (Exception e) { - chat.Logger.Debug($"Failed to parse PNObjectEventResult of type: {objectEvent.Event} into Channel update. Exception was: {e.Message}"); + chat.Logger.Debug( + $"Failed to parse PNObjectEventResult of type: {objectEvent.Event} into Channel update. Exception was: {e.Message}"); updatedData = null; changeType = default; return false; } } - + internal static bool TryParseMessageUpdate(Chat chat, Message message, PNMessageActionEventResult actionEvent) { try { - if (actionEvent.MessageTimetoken.ToString() == message.TimeToken && actionEvent.Uuid == message.UserId && actionEvent.Channel == message.ChannelId) + if (actionEvent.MessageTimetoken.ToString() == message.TimeToken && + actionEvent.Uuid == message.UserId && actionEvent.Channel == message.ChannelId) { if (actionEvent.Event != "removed") { @@ -198,6 +249,7 @@ internal static bool TryParseMessageUpdate(Chat chat, Message message, PNMessage { return true; } + message.MessageActions.Add(new MessageAction() { TimeToken = actionEvent.ActionTimetoken.ToString(), @@ -212,6 +264,7 @@ internal static bool TryParseMessageUpdate(Chat chat, Message message, PNMessage dict.Remove(actionEvent.ActionTimetoken.ToString()); message.MessageActions = dict.Values.ToList(); } + return true; } else @@ -221,12 +274,14 @@ internal static bool TryParseMessageUpdate(Chat chat, Message message, PNMessage } catch (Exception e) { - chat.Logger.Debug($"Failed to parse PNMessageActionEventResult into Message update. Exception was: {e.Message}"); + chat.Logger.Debug( + $"Failed to parse PNMessageActionEventResult into Message update. Exception was: {e.Message}"); return false; } } - internal static bool TryParseEvent(Chat chat, PNMessageResult messageResult, PubnubChatEventType eventType, out ChatEvent chatEvent) + internal static bool TryParseEvent(Chat chat, PNMessageResult messageResult, + PubnubChatEventType eventType, out ChatEvent chatEvent) { try { @@ -238,17 +293,20 @@ internal static bool TryParseEvent(Chat chat, PNMessageResult messageRes chatEvent = default; return false; } + var receivedEventType = ChatEnumConverters.StringToEventType(typeString.ToString()); if (receivedEventType != eventType) { chatEvent = default; return false; } + if (chat.MutedUsersManager.MutedUsers.Contains(messageResult.Publisher)) { chatEvent = default; return false; } + chatEvent = new ChatEvent() { TimeToken = messageResult.Timetoken.ToString(), @@ -261,23 +319,27 @@ internal static bool TryParseEvent(Chat chat, PNMessageResult messageRes } catch (Exception e) { - chat.Logger.Debug($"Failed to parse PNMessageResult into ChatEvent of type \"{eventType}\". Exception was: {e.Message}"); + chat.Logger.Debug( + $"Failed to parse PNMessageResult into ChatEvent of type \"{eventType}\". Exception was: {e.Message}"); chatEvent = default; return false; } } - - internal static bool TryParseEventFromHistory(Chat chat, string channelId, PNHistoryItemResult historyItem, out ChatEvent chatEvent) + + internal static bool TryParseEventFromHistory(Chat chat, string channelId, PNHistoryItemResult historyItem, + out ChatEvent chatEvent) { try { var jsonDict = - chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject(historyItem.Entry.ToString()); + chat.PubnubInstance.JsonPluggableLibrary.DeserializeToDictionaryOfObject( + historyItem.Entry.ToString()); if (!jsonDict.TryGetValue("type", out var typeString)) { chatEvent = default; return false; } + var receivedEventType = ChatEnumConverters.StringToEventType(typeString.ToString()); chatEvent = new ChatEvent() { diff --git a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChatPNSDKSource.cs b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Runtime/UnityChatPNSDKSource.cs index 7bf9e40..364ad7a 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 = "1.2.0"; + private const string build = "1.3.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 31b1773..3d1ec33 100644 --- a/unity-chat/PubnubChatUnity/Assets/PubnubChat/Samples~/PubnubChatConfigAsset/PubnubChatConfigAsset.cs +++ b/unity-chat/PubnubChatUnity/Assets/PubnubChat/Samples~/PubnubChatConfigAsset/PubnubChatConfigAsset.cs @@ -13,6 +13,7 @@ public class PubnubChatConfigAsset : ScriptableObject [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; + [field: SerializeField] public PubnubChatConfig.PushNotificationsConfig PushNotifications { get; private set; } = new (); public static implicit operator PubnubChatConfig(PubnubChatConfigAsset asset) { @@ -22,7 +23,8 @@ public static implicit operator PubnubChatConfig(PubnubChatConfigAsset asset) rateLimitPerChannel: asset.RateLimitPerChannel, storeUserActivityInterval: asset.StoreUserActivityInterval, storeUserActivityTimestamp: asset.StoreUserActivityTimestamp, - syncMutedUsers: asset.SyncMutedUsers); + syncMutedUsers: asset.SyncMutedUsers, + pushNotifications: asset.PushNotifications); } } } \ 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 8ea9ca6..fc68347 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.2.0", + "version": "1.3.0", "displayName": "Pubnub Chat", "description": "PubNub Unity Chat SDK", "unity": "2022.3", diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/FileSamples.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/FileSamples.cs new file mode 100644 index 0000000..f5199af --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/FileSamples.cs @@ -0,0 +1,163 @@ +// snippet.using + +using System.Collections.Generic; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class FileSamples +{ + 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 SendFile() + { + // snippet.send_file + var getChannel = await chat.GetChannel("some_channel"); + if (getChannel.Error) + { + Debug.LogError($"Could not get channel! Error: {getChannel.Exception.Message}"); + return; + } + var channel = getChannel.Result; + + var sendResult = await channel.SendText("some message", new SendTextParams() + { + Files = new List() + { + new ChatInputFile() + { + Name = "some_file.txt", + //Same as above because assuming it's in the same directory as the script + Source = "some_file.txt", + Type = "text" + } + } + }); + + //Checking sendResult in case there was an issue with the file e.g. it didn't exist, was too large, + //or the keyset didn't have Files functionality enabled + if (sendResult.Error) + { + Debug.LogError($"Error when sending message with file: {sendResult.Exception.Message}"); + } + // snippet.end + } + + public static async Task SendFileFromDraft() + { + // snippet.send_file_from_draft + var getChannel = await chat.GetChannel("some_channel"); + if (getChannel.Error) + { + Debug.LogError($"Could not get channel! Error: {getChannel.Exception.Message}"); + return; + } + var channel = getChannel.Result; + + var messageDraft = channel.CreateMessageDraft(); + messageDraft.InsertText(0, "some text"); + messageDraft.Files.Add(new ChatInputFile() + { + Name = "some_file.txt", + //Same as above because assuming it's in the same directory as the script + Source = "some_file.txt", + Type = "text" + }); + + var sendResult = await messageDraft.Send(); + + //Checking sendResult in case there was an issue with the file e.g. it didn't exist, was too large, + //or the keyset didn't have Files functionality enabled + if (sendResult.Error) + { + Debug.LogError($"Error when sending message with file: {sendResult.Exception.Message}"); + } + // snippet.end + } + + public static async Task GetFilesFromMessage() + { + // snippet.get_files_from_message + + //Also works with messages from OnMessageReceived callback + var message = await chat.GetMessage("some_channel", "12345678912345678"); + if (message.Error) + { + Debug.LogError($"Could not get message! Error: {message.Exception.Message}"); + return; + } + + var files = message.Result.Files; + foreach (var chatFile in files) + { + Debug.Log($"Message file with ID: {chatFile.Id}, Name: {chatFile.Name}, URL: {chatFile.Url}, and Type: {chatFile.Type}"); + } + // snippet.end + } + + public static async Task GetFiles() + { + // snippet.get_files + var getChannel = await chat.GetChannel("some_channel"); + if (getChannel.Error) + { + Debug.LogError($"Could not get channel! Error: {getChannel.Exception.Message}"); + return; + } + var channel = getChannel.Result; + + var files = await channel.GetFiles(); + if (files.Error) + { + Debug.LogError($"Error when trying to get files: {files.Exception.Message}"); + return; + } + + foreach (var file in files.Result.Files) + { + Debug.Log($"File ID: {file.Id}, file Name: {file.Name}, file URL: {file.Url}"); + } + // snippet.end + } + + public static async Task DeleteFile() + { + // snippet.delete_file + var getChannel = await chat.GetChannel("some_channel"); + if (getChannel.Error) + { + Debug.LogError($"Could not get channel! Error: {getChannel.Exception.Message}"); + return; + } + var channel = getChannel.Result; + + //ID and Name should be from either Message.Files or channel.GetFiles() + var delete = await channel.DeleteFile("file_id", "file_name"); + // snippet.end + } +} diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/FileSamples.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/FileSamples.cs.meta new file mode 100644 index 0000000..91b2a68 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/FileSamples.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b40952a58bd5100439dd0794cbe5cc5e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/PushSamples.cs b/unity-chat/PubnubChatUnity/Assets/Snippets/PushSamples.cs new file mode 100644 index 0000000..51fb3d8 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/PushSamples.cs @@ -0,0 +1,141 @@ +// snippet.using +using System.Collections.Generic; +using System.Threading.Tasks; +using PubnubApi; +using PubnubChatApi; +using UnityEngine; + +// snippet.end + +public class PushSamples +{ + private static Chat chat; + + static async Task Init() + { + // snippet.init + // Chat configuration with FCM push settings (APNS2 is also supported) + PubnubChatConfig chatConfig = new PubnubChatConfig() + { + PushNotifications = + { + SendPushes = true, + DeviceGateway = PNPushType.FCM, + DeviceToken = "some_device" + } + }; + + 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 GetPushConfig() + { + // snippet.push_config + var pushConfig1 = chat.Config.PushNotifications; + //or: + var pushConfig2 = chat.GetCommonPushOptions; + // snippet.end + } + + public static async Task RegisterPushChannel() + { + // snippet.channel_register_push + var getChannel = await chat.GetChannel("some_channel"); + if (getChannel.Error) + { + Debug.LogError($"Could not get channel! Error: {getChannel.Exception.Message}"); + return; + } + var channel = getChannel.Result; + + var result = await channel.RegisterForPush(); + if (result.Error) + { + Debug.LogError($"Error when trying to register channel for push: {result.Exception.Message}"); + } + + //Alternatively you can also use: + await chat.RegisterPushChannels(new List() { channel.Id }); + // snippet.end + } + + public static async Task UnRegisterPushChannel() + { + // snippet.channel_un_register_push + var getChannel = await chat.GetChannel("some_channel"); + if (getChannel.Error) + { + Debug.LogError($"Could not get channel! Error: {getChannel.Exception.Message}"); + return; + } + var channel = getChannel.Result; + + var result = await channel.UnRegisterFromPush(); + if (result.Error) + { + Debug.LogError($"Error when trying to unregister channel from push: {result.Exception.Message}"); + } + + //Alternatively you can also use: + await chat.UnRegisterPushChannels(new List() { channel.Id }); + // snippet.end + } + + public static async Task UnRegisterAllPushChannels() + { + // snippet.push_un_register_all + var result = await chat.UnRegisterAllPushChannels(); + if (result.Error) + { + Debug.LogError($"Error when trying to unregister all push channels: {result.Exception.Message}"); + } + // snippet.end + } + + public static async Task GetPushChannels() + { + // snippet.get_all_push_channels + var getPushChannels = await chat.GetPushChannels(); + if (getPushChannels.Error) + { + Debug.LogError($"Error when trying to get all push channels: {getPushChannels.Exception.Message}"); + } + foreach (var channelId in getPushChannels.Result) + { + Debug.Log($"Found push channel with ID: {channelId}"); + } + // snippet.end + } + + public static async Task SendTextCustomPushData() + { + // snippet.send_text_push + var getChannel = await chat.GetChannel("some_channel"); + if (getChannel.Error) + { + Debug.LogError($"Could not get channel! Error: {getChannel.Exception.Message}"); + return; + } + var channel = getChannel.Result; + + await channel.SendText("some message", + new SendTextParams() + { CustomPushData = new Dictionary() { { "some_key", "some_value" } } }); + // snippet.end + } + + +} \ No newline at end of file diff --git a/unity-chat/PubnubChatUnity/Assets/Snippets/PushSamples.cs.meta b/unity-chat/PubnubChatUnity/Assets/Snippets/PushSamples.cs.meta new file mode 100644 index 0000000..0f4b133 --- /dev/null +++ b/unity-chat/PubnubChatUnity/Assets/Snippets/PushSamples.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4ec70ee37ed2e1449808847d9741d5af +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: