diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc148bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,92 @@ +## Custom .plist +Tests/PubNubSwiftChatSDKTests.plist + +## macOS +.DS_Store + +## User settings +xcuserdata/ +PubNubSwiftChatSDK.xcworkspace/xcuserdata/ +PubNubSwiftChatSDK.xcodeproj/xcuserdata/ + +## Build generated +build/ +DerivedData/ + +## IDE +.idea/ + +## Tests +Results/ + +## Config files containing secret keys +*.debug.xcconfig +*.release.xcconfig + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xccheckout +*.xcscmblueprint +keysset.plist + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +Packages/ +Package.pins +Package.resolved +.swiftpm +.build + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: + +# https://guides.cocoapods.org/using/using-cocoapod.html#should-i-check-the-pods-directory-into-source-control + +Pods/ + +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/test_output + +# GitHub Actions # +################## +.github/.release diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..54b4c8f --- /dev/null +++ b/.swiftformat @@ -0,0 +1,24 @@ +# Format options +--indent 2 +--commas inline + +--disable wrapMultilineStatementBraces +--wraparguments before-first +--wrapparameters before-first +--maxwidth 150 +--disable wrapSingleLineComments +--voidtype void +--redundanttype inferred +--self remove + +--enable spaceAroundBrackets,spaceAroundComments,spaceAroundGenerics,spaceAroundParens,spaceInsideBraces +--enable spaceInsideBrackets,spaceInsideComments,spaceInsideGenerics,spaceInsideParens,trailingclosures,trailingCommas +--enable wrapConditionalBodies, wrapEnumCases, wrapLoopBodies +--enable redundantLetError,redundantParens,redundantSelf,redundantReturn,redundantBreak +--enable duplicateImports + +# File options +--exclude .build + +# Swift Version +--swiftversion 5.9 diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..efff30a --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,29 @@ +line_length: + warning: 150 + ignores_comments: true +disabled_rules: + - identifier_name + - type_name + - function_body_length + - function_parameter_count +excluded: + - .build + - bundle + - .bundle + - fastlane + - Tests + - Pods +opt_in_rules: + - force_unwrapping + - overridden_super_call + - prohibited_super_call + - first_where + - closure_spacing + - unneeded_parentheses_in_closure_argument + - redundant_nil_coalescing + - pattern_matching_keywords + - explicit_init + - contains_over_first_not_nil + - empty_parentheses_with_trailing_closure + - empty_string + - sorted_first_last diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5e1ef18 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +PubNub Software Development Kit License Agreement +Copyright © 2023 PubNub Inc. All rights reserved. + +Subject to the terms and conditions of the license, you are hereby granted +a non-exclusive, worldwide, royalty-free license to (a) copy and modify +the software in source code or binary form for use with the software services +and interfaces provided by PubNub, and (b) redistribute unmodified copies +of the software to third parties. The software may not be incorporated in +or used to provide any product or service competitive with the products +and services of PubNub. + +The above copyright notice and this license shall be included +in or with all copies or substantial portions of the software. + +This license does not grant you permission to use the trade names, trademarks, +service marks, or product names of PubNub, except as required for reasonable +and customary use in describing the origin of the software and reproducing +the content of this license. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL PUBNUB OR THE AUTHORS OR COPYRIGHT HOLDERS OF THE SOFTWARE BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +https://www.pubnub.com/ +https://www.pubnub.com/terms diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..e3f0632 --- /dev/null +++ b/Package.swift @@ -0,0 +1,35 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "PubNubSwiftChatSDK", + platforms: [.iOS(.v14)], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "PubNubSwiftChatSDK", + targets: ["PubNubSwiftChatSDK"] + ) + ], + dependencies: [ + .package(url: "https://github.com/pubnub/kmp-chat", revision: "910c9bd122ec408c577b78e96aff106c459926b9"), + .package(url: "https://github.com/pubnub/swift", exact: "8.0.0") + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "PubNubSwiftChatSDK", + dependencies: [ + .product(name: "PubNubSDK", package: "swift"), + .product(name: "PubNubChat", package: "kmp-chat") + ] + ), + .testTarget( + name: "PubNubSwiftChatSDKTests", + dependencies: ["PubNubSwiftChatSDK"] + ) + ] +) diff --git a/PubNubSwiftChatSDK.xcodeproj/project.pbxproj b/PubNubSwiftChatSDK.xcodeproj/project.pbxproj new file mode 100644 index 0000000..bb56e36 --- /dev/null +++ b/PubNubSwiftChatSDK.xcodeproj/project.pbxproj @@ -0,0 +1,876 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 3D2CA2362C5B9320008D2284 /* PubNubChat+Transform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2CA2352C5B9320008D2284 /* PubNubChat+Transform.swift */; }; + 3D2CA2382C5B9ACA008D2284 /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2CA2372C5B9ACA008D2284 /* File.swift */; }; + 3D2CA23A2C5B9CF6008D2284 /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2CA2392C5B9CF6008D2284 /* Dictionary.swift */; }; + 3D2CA23C2C5B9E51008D2284 /* Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2CA23B2C5B9E51008D2284 /* Action.swift */; }; + 3D2CA23E2C5BBCDB008D2284 /* PubNubChat.PNPushType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2CA23D2C5BBCDB008D2284 /* PubNubChat.PNPushType.swift */; }; + 3D2CA2402C5BBDB6008D2284 /* PubNubChat.PNPushEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2CA23F2C5BBDB6008D2284 /* PubNubChat.PNPushEnvironment.swift */; }; + 3D2CA2442C5CFBE7008D2284 /* FutureResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2CA2432C5CFBE7008D2284 /* FutureResult.swift */; }; + 3D2CA2482C621876008D2284 /* MessageActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2CA2472C621876008D2284 /* MessageActionType.swift */; }; + 3D334BD02C8EE9E500F8793C /* PubNub.PushService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D334BCF2C8EE9E500F8793C /* PubNub.PushService.swift */; }; + 3D334BD22C8EEAA800F8793C /* PubNub.PushEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D334BD12C8EEAA800F8793C /* PubNub.PushEnvironment.swift */; }; + 3D7BBF6F2C8893D400FBA623 /* ChatAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D7BBF6E2C8893D400FBA623 /* ChatAdapter.swift */; }; + 3D842D242C9DC0AA005C0B55 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D842D232C9DC0AA005C0B55 /* Constants.swift */; }; + 3DA530C62C87455200DDE763 /* ThreadChannelIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DA530C52C87455200DDE763 /* ThreadChannelIntegrationTests.swift */; }; + 3DA530C82C882F2500DDE763 /* ChatIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DA530C72C882F2500DDE763 /* ChatIntegrationTests.swift */; }; + 3DB2A8AF2C7F54A400167058 /* ChannelIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB2A8AE2C7F54A400167058 /* ChannelIntegrationTests.swift */; }; + 3DB2A8B32C807DDC00167058 /* UserImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB2A8B22C807DDC00167058 /* UserImpl.swift */; }; + 3DB2A8B52C807E2A00167058 /* MembershipImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB2A8B42C807E2A00167058 /* MembershipImpl.swift */; }; + 3DB2A8B92C80993B00167058 /* ChannelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB2A8B82C80993B00167058 /* ChannelType.swift */; }; + 3DB2A8BB2C8099F300167058 /* EventContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB2A8BA2C8099F300167058 /* EventContent.swift */; }; + 3DB2A8BD2C809A1400167058 /* ChatError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB2A8BC2C809A1400167058 /* ChatError.swift */; }; + 3DB2A8BF2C809B1F00167058 /* EmitEventMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB2A8BE2C809B1F00167058 /* EmitEventMethod.swift */; }; + 3DB49E1B2C75F573006356ED /* PubNubSwiftChatSDKTests.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3DB49E1A2C75F573006356ED /* PubNubSwiftChatSDKTests.plist */; }; + 3DB49E1D2C75F70C006356ED /* UserIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB49E1C2C75F70C006356ED /* UserIntegrationTests.swift */; }; + 3DB49E212C761BD1006356ED /* AutoCloseable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB49E202C761BD1006356ED /* AutoCloseable.swift */; }; + 3DB49E282C777ECD006356ED /* MembershipIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB49E272C777ECD006356ED /* MembershipIntegrationTests.swift */; }; + 3DB49E2A2C788529006356ED /* MessageIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB49E292C788529006356ED /* MessageIntegrationTests.swift */; }; + 3DB551D12C7C652300634BDC /* PubNubSwiftChatSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DB73A072C4FE13C007FE249 /* PubNubSwiftChatSDK.framework */; }; + 3DB551DB2C7C983000634BDC /* ThreadMessageIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB551DA2C7C983000634BDC /* ThreadMessageIntegrationTests.swift */; }; + 3DB73A0C2C4FE13C007FE249 /* PubNubSwiftChatSDK.docc in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A0B2C4FE13C007FE249 /* PubNubSwiftChatSDK.docc */; }; + 3DB73A172C4FE13C007FE249 /* PubNubSwiftChatSDKIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A162C4FE13C007FE249 /* PubNubSwiftChatSDKIntegrationTests.swift */; }; + 3DB73A182C4FE13C007FE249 /* PubNubSwiftChatSDK.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DB73A0A2C4FE13C007FE249 /* PubNubSwiftChatSDK.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3DB73A262C4FE1F6007FE249 /* ChatConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A242C4FE1F6007FE249 /* ChatConfiguration.swift */; }; + 3DB73A272C4FE1F6007FE249 /* Chat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A252C4FE1F6007FE249 /* Chat.swift */; }; + 3DB73A352C502688007FE249 /* Int.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A342C502688007FE249 /* Int.swift */; }; + 3DB73A372C5026B4007FE249 /* PubNubHashedPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A362C5026B4007FE249 /* PubNubHashedPage.swift */; }; + 3DB73A392C5026E5007FE249 /* PubNub.MembershipSortField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A382C5026E5007FE249 /* PubNub.MembershipSortField.swift */; }; + 3DB73A3C2C50F42B007FE249 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A3B2C50F42B007FE249 /* User.swift */; }; + 3DB73A3E2C50F5F3007FE249 /* Membership.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A3D2C50F5F3007FE249 /* Membership.swift */; }; + 3DB73A402C51051C007FE249 /* Channel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A3F2C51051C007FE249 /* Channel.swift */; }; + 3DB73A422C511900007FE249 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A412C511900007FE249 /* Message.swift */; }; + 3DB73A452C511D52007FE249 /* Restriction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A442C511D52007FE249 /* Restriction.swift */; }; + 3DB73A472C51353B007FE249 /* MessageMentionedUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A462C51353B007FE249 /* MessageMentionedUser.swift */; }; + 3DB73A492C513578007FE249 /* MessageReferencedChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A482C513578007FE249 /* MessageReferencedChannel.swift */; }; + 3DB73A4B2C5135C3007FE249 /* QuotedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A4A2C5135C3007FE249 /* QuotedMessage.swift */; }; + 3DB73A4D2C513646007FE249 /* TextLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A4C2C513646007FE249 /* TextLink.swift */; }; + 3DB73A4F2C52881A007FE249 /* PubNubChat.ChannelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A4E2C52881A007FE249 /* PubNubChat.ChannelType.swift */; }; + 3DB73A512C539421007FE249 /* InputFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A502C539421007FE249 /* InputFile.swift */; }; + 3DB73A532C53A20C007FE249 /* MessageImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A522C53A20C007FE249 /* MessageImpl.swift */; }; + 3DB73A552C53A22F007FE249 /* ThreadMessageImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A542C53A22F007FE249 /* ThreadMessageImpl.swift */; }; + 3DB73A592C53BF18007FE249 /* GetFileItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A582C53BF18007FE249 /* GetFileItem.swift */; }; + 3DB73A5B2C53CAB1007FE249 /* BaseChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A5A2C53CAB1007FE249 /* BaseChannel.swift */; }; + 3DB73A5D2C53CC0A007FE249 /* ThreadChannelImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A5C2C53CC0A007FE249 /* ThreadChannelImpl.swift */; }; + 3DB73A632C579938007FE249 /* PubNub.ObjectSortField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A622C579938007FE249 /* PubNub.ObjectSortField.swift */; }; + 3DB73A652C57A782007FE249 /* CreateDirectConversationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A642C57A782007FE249 /* CreateDirectConversationResult.swift */; }; + 3DB73A672C57A8C5007FE249 /* CreateGroupConversationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A662C57A8C5007FE249 /* CreateGroupConversationResult.swift */; }; + 3DB73A692C57AA1B007FE249 /* PubNubChat.KotlinArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A682C57AA1B007FE249 /* PubNubChat.KotlinArray.swift */; }; + 3DB73A6B2C57B9D8007FE249 /* GetUnreadMessagesCount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A6A2C57B9D8007FE249 /* GetUnreadMessagesCount.swift */; }; + 3DB73A6D2C57BB35007FE249 /* BaseMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A6C2C57BB35007FE249 /* BaseMessage.swift */; }; + 3DB73A6F2C57BB97007FE249 /* ChannelImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A6E2C57BB97007FE249 /* ChannelImpl.swift */; }; + 3DB73A712C57C034007FE249 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A702C57C034007FE249 /* Event.swift */; }; + 3DB73A732C57CE9E007FE249 /* PubNubChat.RestrictionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A722C57CE9E007FE249 /* PubNubChat.RestrictionType.swift */; }; + 3DB73A752C57D073007FE249 /* PubNubChat.Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A742C57D073007FE249 /* PubNubChat.Event.swift */; }; + 3DB73A7B2C57EA29007FE249 /* ChatImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A7A2C57EA29007FE249 /* ChatImpl.swift */; }; + 3DB73A7D2C57EA94007FE249 /* Timetoken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A7C2C57EA94007FE249 /* Timetoken.swift */; }; + 3DB73A7F2C58CCAE007FE249 /* GetCurrentUserMentionsResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB73A7E2C58CCAE007FE249 /* GetCurrentUserMentionsResult.swift */; }; + 3DD5DBE52CA301C1008954FF /* PubNubSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 3DD5DBE42CA301C1008954FF /* PubNubSDK */; }; + 3DD5DBE82CA3034C008954FF /* PubNubChat in Frameworks */ = {isa = PBXBuildFile; productRef = 3DD5DBE72CA3034C008954FF /* PubNubChat */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 3DB551D32C7C652300634BDC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3DB739FE2C4FE13C007FE249 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3DB73A062C4FE13C007FE249; + remoteInfo = PubNubChatSDK; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 3DB551D92C7C654600634BDC /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 3D1C44A52C918A2200E68446 /* PubNubSwiftChatSDK_Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = PubNubSwiftChatSDK_Info.plist; sourceTree = ""; }; + 3D2CA2352C5B9320008D2284 /* PubNubChat+Transform.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PubNubChat+Transform.swift"; sourceTree = ""; }; + 3D2CA2372C5B9ACA008D2284 /* File.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = ""; }; + 3D2CA2392C5B9CF6008D2284 /* Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dictionary.swift; sourceTree = ""; }; + 3D2CA23B2C5B9E51008D2284 /* Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Action.swift; sourceTree = ""; }; + 3D2CA23D2C5BBCDB008D2284 /* PubNubChat.PNPushType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubNubChat.PNPushType.swift; sourceTree = ""; }; + 3D2CA23F2C5BBDB6008D2284 /* PubNubChat.PNPushEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubNubChat.PNPushEnvironment.swift; sourceTree = ""; }; + 3D2CA2432C5CFBE7008D2284 /* FutureResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FutureResult.swift; sourceTree = ""; }; + 3D2CA2472C621876008D2284 /* MessageActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActionType.swift; sourceTree = ""; }; + 3D334BCF2C8EE9E500F8793C /* PubNub.PushService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubNub.PushService.swift; sourceTree = ""; }; + 3D334BD12C8EEAA800F8793C /* PubNub.PushEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubNub.PushEnvironment.swift; sourceTree = ""; }; + 3D7BBF6E2C8893D400FBA623 /* ChatAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatAdapter.swift; sourceTree = ""; }; + 3D842D232C9DC0AA005C0B55 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + 3D842D2B2C9DD7EB005C0B55 /* PubNubSwiftChatSDKTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = PubNubSwiftChatSDKTests.xctestplan; sourceTree = ""; }; + 3DA530C52C87455200DDE763 /* ThreadChannelIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadChannelIntegrationTests.swift; sourceTree = ""; }; + 3DA530C72C882F2500DDE763 /* ChatIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatIntegrationTests.swift; sourceTree = ""; }; + 3DADCAED2C9896AF001B3DE2 /* Package.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; + 3DB2A8AE2C7F54A400167058 /* ChannelIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelIntegrationTests.swift; sourceTree = ""; }; + 3DB2A8B22C807DDC00167058 /* UserImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserImpl.swift; sourceTree = ""; }; + 3DB2A8B42C807E2A00167058 /* MembershipImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MembershipImpl.swift; sourceTree = ""; }; + 3DB2A8B82C80993B00167058 /* ChannelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelType.swift; sourceTree = ""; }; + 3DB2A8BA2C8099F300167058 /* EventContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventContent.swift; sourceTree = ""; }; + 3DB2A8BC2C809A1400167058 /* ChatError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatError.swift; sourceTree = ""; }; + 3DB2A8BE2C809B1F00167058 /* EmitEventMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmitEventMethod.swift; sourceTree = ""; }; + 3DB49E1A2C75F573006356ED /* PubNubSwiftChatSDKTests.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = PubNubSwiftChatSDKTests.plist; sourceTree = ""; }; + 3DB49E1C2C75F70C006356ED /* UserIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIntegrationTests.swift; sourceTree = ""; }; + 3DB49E202C761BD1006356ED /* AutoCloseable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCloseable.swift; sourceTree = ""; }; + 3DB49E272C777ECD006356ED /* MembershipIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MembershipIntegrationTests.swift; sourceTree = ""; }; + 3DB49E292C788529006356ED /* MessageIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageIntegrationTests.swift; sourceTree = ""; }; + 3DB551DA2C7C983000634BDC /* ThreadMessageIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMessageIntegrationTests.swift; sourceTree = ""; }; + 3DB73A072C4FE13C007FE249 /* PubNubSwiftChatSDK.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PubNubSwiftChatSDK.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3DB73A0A2C4FE13C007FE249 /* PubNubSwiftChatSDK.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PubNubSwiftChatSDK.h; sourceTree = ""; }; + 3DB73A0B2C4FE13C007FE249 /* PubNubSwiftChatSDK.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = PubNubSwiftChatSDK.docc; sourceTree = ""; }; + 3DB73A112C4FE13C007FE249 /* PubNubSwiftChatSDKTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PubNubSwiftChatSDKTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3DB73A162C4FE13C007FE249 /* PubNubSwiftChatSDKIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubNubSwiftChatSDKIntegrationTests.swift; sourceTree = ""; }; + 3DB73A242C4FE1F6007FE249 /* ChatConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatConfiguration.swift; sourceTree = ""; }; + 3DB73A252C4FE1F6007FE249 /* Chat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Chat.swift; sourceTree = ""; }; + 3DB73A342C502688007FE249 /* Int.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Int.swift; sourceTree = ""; }; + 3DB73A362C5026B4007FE249 /* PubNubHashedPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubNubHashedPage.swift; sourceTree = ""; }; + 3DB73A382C5026E5007FE249 /* PubNub.MembershipSortField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubNub.MembershipSortField.swift; sourceTree = ""; }; + 3DB73A3B2C50F42B007FE249 /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + 3DB73A3D2C50F5F3007FE249 /* Membership.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Membership.swift; sourceTree = ""; }; + 3DB73A3F2C51051C007FE249 /* Channel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channel.swift; sourceTree = ""; }; + 3DB73A412C511900007FE249 /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; + 3DB73A442C511D52007FE249 /* Restriction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Restriction.swift; sourceTree = ""; }; + 3DB73A462C51353B007FE249 /* MessageMentionedUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageMentionedUser.swift; sourceTree = ""; }; + 3DB73A482C513578007FE249 /* MessageReferencedChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReferencedChannel.swift; sourceTree = ""; }; + 3DB73A4A2C5135C3007FE249 /* QuotedMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotedMessage.swift; sourceTree = ""; }; + 3DB73A4C2C513646007FE249 /* TextLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextLink.swift; sourceTree = ""; }; + 3DB73A4E2C52881A007FE249 /* PubNubChat.ChannelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubNubChat.ChannelType.swift; sourceTree = ""; }; + 3DB73A502C539421007FE249 /* InputFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputFile.swift; sourceTree = ""; }; + 3DB73A522C53A20C007FE249 /* MessageImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageImpl.swift; sourceTree = ""; }; + 3DB73A542C53A22F007FE249 /* ThreadMessageImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMessageImpl.swift; sourceTree = ""; }; + 3DB73A582C53BF18007FE249 /* GetFileItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetFileItem.swift; sourceTree = ""; }; + 3DB73A5A2C53CAB1007FE249 /* BaseChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseChannel.swift; sourceTree = ""; }; + 3DB73A5C2C53CC0A007FE249 /* ThreadChannelImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadChannelImpl.swift; sourceTree = ""; }; + 3DB73A622C579938007FE249 /* PubNub.ObjectSortField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubNub.ObjectSortField.swift; sourceTree = ""; }; + 3DB73A642C57A782007FE249 /* CreateDirectConversationResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateDirectConversationResult.swift; sourceTree = ""; }; + 3DB73A662C57A8C5007FE249 /* CreateGroupConversationResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateGroupConversationResult.swift; sourceTree = ""; }; + 3DB73A682C57AA1B007FE249 /* PubNubChat.KotlinArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubNubChat.KotlinArray.swift; sourceTree = ""; }; + 3DB73A6A2C57B9D8007FE249 /* GetUnreadMessagesCount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetUnreadMessagesCount.swift; sourceTree = ""; }; + 3DB73A6C2C57BB35007FE249 /* BaseMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseMessage.swift; sourceTree = ""; }; + 3DB73A6E2C57BB97007FE249 /* ChannelImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelImpl.swift; sourceTree = ""; }; + 3DB73A702C57C034007FE249 /* Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; + 3DB73A722C57CE9E007FE249 /* PubNubChat.RestrictionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubNubChat.RestrictionType.swift; sourceTree = ""; }; + 3DB73A742C57D073007FE249 /* PubNubChat.Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubNubChat.Event.swift; sourceTree = ""; }; + 3DB73A7A2C57EA29007FE249 /* ChatImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatImpl.swift; sourceTree = ""; }; + 3DB73A7C2C57EA94007FE249 /* Timetoken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timetoken.swift; sourceTree = ""; }; + 3DB73A7E2C58CCAE007FE249 /* GetCurrentUserMentionsResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetCurrentUserMentionsResult.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3DB73A042C4FE13C007FE249 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3DD5DBE82CA3034C008954FF /* PubNubChat in Frameworks */, + 3DD5DBE52CA301C1008954FF /* PubNubSDK in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3DB73A0E2C4FE13C007FE249 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3DB551D12C7C652300634BDC /* PubNubSwiftChatSDK.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3DB49DE92C75E0C3006356ED /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 3DB739FD2C4FE13B007FE249 = { + isa = PBXGroup; + children = ( + 3DADCAED2C9896AF001B3DE2 /* Package.swift */, + 3DB73A232C4FE1E9007FE249 /* Sources */, + 3DB73A092C4FE13C007FE249 /* PubNubSwiftChatSDK */, + 3DB73A152C4FE13C007FE249 /* Tests */, + 3DB73A082C4FE13C007FE249 /* Products */, + 3DB49DE92C75E0C3006356ED /* Frameworks */, + ); + sourceTree = ""; + }; + 3DB73A082C4FE13C007FE249 /* Products */ = { + isa = PBXGroup; + children = ( + 3DB73A072C4FE13C007FE249 /* PubNubSwiftChatSDK.framework */, + 3DB73A112C4FE13C007FE249 /* PubNubSwiftChatSDKTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 3DB73A092C4FE13C007FE249 /* PubNubSwiftChatSDK */ = { + isa = PBXGroup; + children = ( + 3DB73A0A2C4FE13C007FE249 /* PubNubSwiftChatSDK.h */, + 3DB73A0B2C4FE13C007FE249 /* PubNubSwiftChatSDK.docc */, + ); + path = PubNubSwiftChatSDK; + sourceTree = ""; + }; + 3DB73A152C4FE13C007FE249 /* Tests */ = { + isa = PBXGroup; + children = ( + 3DB49E1A2C75F573006356ED /* PubNubSwiftChatSDKTests.plist */, + 3D842D2B2C9DD7EB005C0B55 /* PubNubSwiftChatSDKTests.xctestplan */, + 3DB73A162C4FE13C007FE249 /* PubNubSwiftChatSDKIntegrationTests.swift */, + 3DB49E1C2C75F70C006356ED /* UserIntegrationTests.swift */, + 3DB49E272C777ECD006356ED /* MembershipIntegrationTests.swift */, + 3DB49E292C788529006356ED /* MessageIntegrationTests.swift */, + 3DB551DA2C7C983000634BDC /* ThreadMessageIntegrationTests.swift */, + 3DB2A8AE2C7F54A400167058 /* ChannelIntegrationTests.swift */, + 3DA530C52C87455200DDE763 /* ThreadChannelIntegrationTests.swift */, + 3DA530C72C882F2500DDE763 /* ChatIntegrationTests.swift */, + ); + path = Tests; + sourceTree = ""; + }; + 3DB73A232C4FE1E9007FE249 /* Sources */ = { + isa = PBXGroup; + children = ( + 3D1C44A52C918A2200E68446 /* PubNubSwiftChatSDK_Info.plist */, + 3DB73A252C4FE1F6007FE249 /* Chat.swift */, + 3DB73A7A2C57EA29007FE249 /* ChatImpl.swift */, + 3DB73A242C4FE1F6007FE249 /* ChatConfiguration.swift */, + 3DB73A432C511D36007FE249 /* Models */, + 3DB73A3A2C50F415007FE249 /* Entities */, + 3DB73A312C502670007FE249 /* Extensions */, + 3DB73A2C2C501790007FE249 /* Miscellaneous */, + ); + path = Sources; + sourceTree = ""; + }; + 3DB73A2C2C501790007FE249 /* Miscellaneous */ = { + isa = PBXGroup; + children = ( + 3D2CA2432C5CFBE7008D2284 /* FutureResult.swift */, + 3DB49E202C761BD1006356ED /* AutoCloseable.swift */, + 3D7BBF6E2C8893D400FBA623 /* ChatAdapter.swift */, + 3D842D232C9DC0AA005C0B55 /* Constants.swift */, + ); + path = Miscellaneous; + sourceTree = ""; + }; + 3DB73A312C502670007FE249 /* Extensions */ = { + isa = PBXGroup; + children = ( + 3DB73A342C502688007FE249 /* Int.swift */, + 3D2CA2392C5B9CF6008D2284 /* Dictionary.swift */, + 3DB73A7C2C57EA94007FE249 /* Timetoken.swift */, + 3DB73A362C5026B4007FE249 /* PubNubHashedPage.swift */, + 3DB73A382C5026E5007FE249 /* PubNub.MembershipSortField.swift */, + 3DB73A622C579938007FE249 /* PubNub.ObjectSortField.swift */, + 3D334BCF2C8EE9E500F8793C /* PubNub.PushService.swift */, + 3D334BD12C8EEAA800F8793C /* PubNub.PushEnvironment.swift */, + 3D2CA2352C5B9320008D2284 /* PubNubChat+Transform.swift */, + 3DB73A4E2C52881A007FE249 /* PubNubChat.ChannelType.swift */, + 3DB73A682C57AA1B007FE249 /* PubNubChat.KotlinArray.swift */, + 3DB73A722C57CE9E007FE249 /* PubNubChat.RestrictionType.swift */, + 3D2CA23D2C5BBCDB008D2284 /* PubNubChat.PNPushType.swift */, + 3D2CA23F2C5BBDB6008D2284 /* PubNubChat.PNPushEnvironment.swift */, + 3DB73A742C57D073007FE249 /* PubNubChat.Event.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 3DB73A3A2C50F415007FE249 /* Entities */ = { + isa = PBXGroup; + children = ( + 3DB73A3B2C50F42B007FE249 /* User.swift */, + 3DB2A8B22C807DDC00167058 /* UserImpl.swift */, + 3DB73A3F2C51051C007FE249 /* Channel.swift */, + 3DB73A3D2C50F5F3007FE249 /* Membership.swift */, + 3DB2A8B42C807E2A00167058 /* MembershipImpl.swift */, + 3DB73A5A2C53CAB1007FE249 /* BaseChannel.swift */, + 3DB73A6E2C57BB97007FE249 /* ChannelImpl.swift */, + 3DB73A5C2C53CC0A007FE249 /* ThreadChannelImpl.swift */, + 3DB73A412C511900007FE249 /* Message.swift */, + 3DB73A6C2C57BB35007FE249 /* BaseMessage.swift */, + 3DB73A522C53A20C007FE249 /* MessageImpl.swift */, + 3DB73A542C53A22F007FE249 /* ThreadMessageImpl.swift */, + ); + path = Entities; + sourceTree = ""; + }; + 3DB73A432C511D36007FE249 /* Models */ = { + isa = PBXGroup; + children = ( + 3D2CA23B2C5B9E51008D2284 /* Action.swift */, + 3DB2A8BA2C8099F300167058 /* EventContent.swift */, + 3DB2A8BC2C809A1400167058 /* ChatError.swift */, + 3DB73A442C511D52007FE249 /* Restriction.swift */, + 3DB73A462C51353B007FE249 /* MessageMentionedUser.swift */, + 3DB73A482C513578007FE249 /* MessageReferencedChannel.swift */, + 3DB73A4A2C5135C3007FE249 /* QuotedMessage.swift */, + 3DB73A4C2C513646007FE249 /* TextLink.swift */, + 3DB73A502C539421007FE249 /* InputFile.swift */, + 3DB73A582C53BF18007FE249 /* GetFileItem.swift */, + 3DB73A642C57A782007FE249 /* CreateDirectConversationResult.swift */, + 3DB73A662C57A8C5007FE249 /* CreateGroupConversationResult.swift */, + 3DB73A6A2C57B9D8007FE249 /* GetUnreadMessagesCount.swift */, + 3DB73A702C57C034007FE249 /* Event.swift */, + 3DB73A7E2C58CCAE007FE249 /* GetCurrentUserMentionsResult.swift */, + 3D2CA2372C5B9ACA008D2284 /* File.swift */, + 3D2CA2472C621876008D2284 /* MessageActionType.swift */, + 3DB2A8B82C80993B00167058 /* ChannelType.swift */, + 3DB2A8BE2C809B1F00167058 /* EmitEventMethod.swift */, + ); + path = Models; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 3DB73A022C4FE13C007FE249 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 3DB73A182C4FE13C007FE249 /* PubNubSwiftChatSDK.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 3DB73A062C4FE13C007FE249 /* PubNubSwiftChatSDK */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3DB73A1B2C4FE13C007FE249 /* Build configuration list for PBXNativeTarget "PubNubSwiftChatSDK" */; + buildPhases = ( + 3DB73A022C4FE13C007FE249 /* Headers */, + 3DB73A032C4FE13C007FE249 /* Sources */, + 3DB73A042C4FE13C007FE249 /* Frameworks */, + 3DB73A052C4FE13C007FE249 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PubNubSwiftChatSDK; + packageProductDependencies = ( + 3DD5DBE42CA301C1008954FF /* PubNubSDK */, + 3DD5DBE72CA3034C008954FF /* PubNubChat */, + ); + productName = PubNubChatSDK; + productReference = 3DB73A072C4FE13C007FE249 /* PubNubSwiftChatSDK.framework */; + productType = "com.apple.product-type.framework"; + }; + 3DB73A102C4FE13C007FE249 /* PubNubSwiftChatSDKTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3DB73A1E2C4FE13C007FE249 /* Build configuration list for PBXNativeTarget "PubNubSwiftChatSDKTests" */; + buildPhases = ( + 3DB73A0D2C4FE13C007FE249 /* Sources */, + 3DB73A0E2C4FE13C007FE249 /* Frameworks */, + 3DB73A0F2C4FE13C007FE249 /* Resources */, + 3DB551D92C7C654600634BDC /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 3DB551D42C7C652300634BDC /* PBXTargetDependency */, + ); + name = PubNubSwiftChatSDKTests; + productName = PubNubChatSDKTests; + productReference = 3DB73A112C4FE13C007FE249 /* PubNubSwiftChatSDKTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 3DB739FE2C4FE13C007FE249 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1530; + LastUpgradeCheck = 1530; + TargetAttributes = { + 3DB73A062C4FE13C007FE249 = { + CreatedOnToolsVersion = 15.3; + }; + 3DB73A102C4FE13C007FE249 = { + CreatedOnToolsVersion = 15.3; + }; + }; + }; + buildConfigurationList = 3DB73A012C4FE13C007FE249 /* Build configuration list for PBXProject "PubNubSwiftChatSDK" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 3DB739FD2C4FE13B007FE249; + packageReferences = ( + 3DD5DBE32CA301C1008954FF /* XCRemoteSwiftPackageReference "swift" */, + 3DD5DBE62CA3034C008954FF /* XCRemoteSwiftPackageReference "kmp-chat" */, + ); + productRefGroup = 3DB73A082C4FE13C007FE249 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 3DB73A062C4FE13C007FE249 /* PubNubSwiftChatSDK */, + 3DB73A102C4FE13C007FE249 /* PubNubSwiftChatSDKTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3DB73A052C4FE13C007FE249 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3DB73A0F2C4FE13C007FE249 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3DB49E1B2C75F573006356ED /* PubNubSwiftChatSDKTests.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3DB73A032C4FE13C007FE249 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3DB73A392C5026E5007FE249 /* PubNub.MembershipSortField.swift in Sources */, + 3D7BBF6F2C8893D400FBA623 /* ChatAdapter.swift in Sources */, + 3DB73A0C2C4FE13C007FE249 /* PubNubSwiftChatSDK.docc in Sources */, + 3D2CA2402C5BBDB6008D2284 /* PubNubChat.PNPushEnvironment.swift in Sources */, + 3DB73A4D2C513646007FE249 /* TextLink.swift in Sources */, + 3DB73A692C57AA1B007FE249 /* PubNubChat.KotlinArray.swift in Sources */, + 3D2CA2442C5CFBE7008D2284 /* FutureResult.swift in Sources */, + 3DB73A372C5026B4007FE249 /* PubNubHashedPage.swift in Sources */, + 3DB73A6F2C57BB97007FE249 /* ChannelImpl.swift in Sources */, + 3DB73A402C51051C007FE249 /* Channel.swift in Sources */, + 3D2CA23C2C5B9E51008D2284 /* Action.swift in Sources */, + 3DB2A8B32C807DDC00167058 /* UserImpl.swift in Sources */, + 3DB73A452C511D52007FE249 /* Restriction.swift in Sources */, + 3DB73A552C53A22F007FE249 /* ThreadMessageImpl.swift in Sources */, + 3D2CA2382C5B9ACA008D2284 /* File.swift in Sources */, + 3DB73A752C57D073007FE249 /* PubNubChat.Event.swift in Sources */, + 3D2CA2362C5B9320008D2284 /* PubNubChat+Transform.swift in Sources */, + 3DB73A492C513578007FE249 /* MessageReferencedChannel.swift in Sources */, + 3DB73A352C502688007FE249 /* Int.swift in Sources */, + 3DB73A632C579938007FE249 /* PubNub.ObjectSortField.swift in Sources */, + 3DB73A732C57CE9E007FE249 /* PubNubChat.RestrictionType.swift in Sources */, + 3DB73A262C4FE1F6007FE249 /* ChatConfiguration.swift in Sources */, + 3D2CA2482C621876008D2284 /* MessageActionType.swift in Sources */, + 3DB73A272C4FE1F6007FE249 /* Chat.swift in Sources */, + 3DB73A472C51353B007FE249 /* MessageMentionedUser.swift in Sources */, + 3DB73A7D2C57EA94007FE249 /* Timetoken.swift in Sources */, + 3DB73A712C57C034007FE249 /* Event.swift in Sources */, + 3DB2A8BD2C809A1400167058 /* ChatError.swift in Sources */, + 3DB73A3C2C50F42B007FE249 /* User.swift in Sources */, + 3DB73A652C57A782007FE249 /* CreateDirectConversationResult.swift in Sources */, + 3DB73A5D2C53CC0A007FE249 /* ThreadChannelImpl.swift in Sources */, + 3D334BD22C8EEAA800F8793C /* PubNub.PushEnvironment.swift in Sources */, + 3DB73A4F2C52881A007FE249 /* PubNubChat.ChannelType.swift in Sources */, + 3DB73A6B2C57B9D8007FE249 /* GetUnreadMessagesCount.swift in Sources */, + 3DB49E212C761BD1006356ED /* AutoCloseable.swift in Sources */, + 3DB73A532C53A20C007FE249 /* MessageImpl.swift in Sources */, + 3DB73A672C57A8C5007FE249 /* CreateGroupConversationResult.swift in Sources */, + 3DB2A8B52C807E2A00167058 /* MembershipImpl.swift in Sources */, + 3DB2A8B92C80993B00167058 /* ChannelType.swift in Sources */, + 3DB73A592C53BF18007FE249 /* GetFileItem.swift in Sources */, + 3D2CA23E2C5BBCDB008D2284 /* PubNubChat.PNPushType.swift in Sources */, + 3DB2A8BF2C809B1F00167058 /* EmitEventMethod.swift in Sources */, + 3DB73A6D2C57BB35007FE249 /* BaseMessage.swift in Sources */, + 3DB73A5B2C53CAB1007FE249 /* BaseChannel.swift in Sources */, + 3DB73A7F2C58CCAE007FE249 /* GetCurrentUserMentionsResult.swift in Sources */, + 3DB73A3E2C50F5F3007FE249 /* Membership.swift in Sources */, + 3DB73A7B2C57EA29007FE249 /* ChatImpl.swift in Sources */, + 3D2CA23A2C5B9CF6008D2284 /* Dictionary.swift in Sources */, + 3DB73A422C511900007FE249 /* Message.swift in Sources */, + 3DB73A4B2C5135C3007FE249 /* QuotedMessage.swift in Sources */, + 3DB2A8BB2C8099F300167058 /* EventContent.swift in Sources */, + 3D334BD02C8EE9E500F8793C /* PubNub.PushService.swift in Sources */, + 3DB73A512C539421007FE249 /* InputFile.swift in Sources */, + 3D842D242C9DC0AA005C0B55 /* Constants.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3DB73A0D2C4FE13C007FE249 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3DB49E1D2C75F70C006356ED /* UserIntegrationTests.swift in Sources */, + 3DA530C62C87455200DDE763 /* ThreadChannelIntegrationTests.swift in Sources */, + 3DB2A8AF2C7F54A400167058 /* ChannelIntegrationTests.swift in Sources */, + 3DB49E2A2C788529006356ED /* MessageIntegrationTests.swift in Sources */, + 3DB49E282C777ECD006356ED /* MembershipIntegrationTests.swift in Sources */, + 3DA530C82C882F2500DDE763 /* ChatIntegrationTests.swift in Sources */, + 3DB551DB2C7C983000634BDC /* ThreadMessageIntegrationTests.swift in Sources */, + 3DB73A172C4FE13C007FE249 /* PubNubSwiftChatSDKIntegrationTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 3DB551D42C7C652300634BDC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3DB73A062C4FE13C007FE249 /* PubNubSwiftChatSDK */; + targetProxy = 3DB551D32C7C652300634BDC /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 3DB73A192C4FE13C007FE249 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 3DB73A1A2C4FE13C007FE249 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 3DB73A1C2C4FE13C007FE249 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Sources/PubNubSwiftChatSDK_Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.0; + MARKETING_VERSION = 0.8.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS"; + PRODUCT_BUNDLE_IDENTIFIER = com.pubnub.SwiftChatSDK; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,3,4,6,7"; + TVOS_DEPLOYMENT_TARGET = 14.0; + WATCHOS_DEPLOYMENT_TARGET = 8; + XROS_DEPLOYMENT_TARGET = 1.1; + }; + name = Debug; + }; + 3DB73A1D2C4FE13C007FE249 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Sources/PubNubSwiftChatSDK_Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.0; + MARKETING_VERSION = 0.8.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS"; + PRODUCT_BUNDLE_IDENTIFIER = com.pubnub.SwiftChatSDK; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,3,4,6,7"; + TVOS_DEPLOYMENT_TARGET = 14.0; + WATCHOS_DEPLOYMENT_TARGET = 8; + XROS_DEPLOYMENT_TARGET = 1.1; + }; + name = Release; + }; + 3DB73A1F2C4FE13C007FE249 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.pubnub.SwiftChatSDKTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3DB73A202C4FE13C007FE249 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.pubnub.SwiftChatSDKTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3DB73A012C4FE13C007FE249 /* Build configuration list for PBXProject "PubNubSwiftChatSDK" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3DB73A192C4FE13C007FE249 /* Debug */, + 3DB73A1A2C4FE13C007FE249 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3DB73A1B2C4FE13C007FE249 /* Build configuration list for PBXNativeTarget "PubNubSwiftChatSDK" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3DB73A1C2C4FE13C007FE249 /* Debug */, + 3DB73A1D2C4FE13C007FE249 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3DB73A1E2C4FE13C007FE249 /* Build configuration list for PBXNativeTarget "PubNubSwiftChatSDKTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3DB73A1F2C4FE13C007FE249 /* Debug */, + 3DB73A202C4FE13C007FE249 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 3DD5DBE32CA301C1008954FF /* XCRemoteSwiftPackageReference "swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pubnub/swift"; + requirement = { + kind = exactVersion; + version = 8.0.0; + }; + }; + 3DD5DBE62CA3034C008954FF /* XCRemoteSwiftPackageReference "kmp-chat" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pubnub/kmp-chat"; + requirement = { + kind = revision; + revision = 910c9bd122ec408c577b78e96aff106c459926b9; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 3DD5DBE42CA301C1008954FF /* PubNubSDK */ = { + isa = XCSwiftPackageProductDependency; + package = 3DD5DBE32CA301C1008954FF /* XCRemoteSwiftPackageReference "swift" */; + productName = PubNubSDK; + }; + 3DD5DBE72CA3034C008954FF /* PubNubChat */ = { + isa = XCSwiftPackageProductDependency; + package = 3DD5DBE62CA3034C008954FF /* XCRemoteSwiftPackageReference "kmp-chat" */; + productName = PubNubChat; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 3DB739FE2C4FE13C007FE249 /* Project object */; +} diff --git a/PubNubSwiftChatSDK.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/PubNubSwiftChatSDK.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/PubNubSwiftChatSDK.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/PubNubSwiftChatSDK.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/PubNubSwiftChatSDK.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/PubNubSwiftChatSDK.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/PubNubSwiftChatSDK.xcodeproj/xcshareddata/xcschemes/PubNubSwiftChatSDK.xcscheme b/PubNubSwiftChatSDK.xcodeproj/xcshareddata/xcschemes/PubNubSwiftChatSDK.xcscheme new file mode 100644 index 0000000..a25c52c --- /dev/null +++ b/PubNubSwiftChatSDK.xcodeproj/xcshareddata/xcschemes/PubNubSwiftChatSDK.xcscheme @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PubNubSwiftChatSDK.xcodeproj/xcshareddata/xcschemes/PubNubSwiftChatSDKTests.xcscheme b/PubNubSwiftChatSDK.xcodeproj/xcshareddata/xcschemes/PubNubSwiftChatSDKTests.xcscheme new file mode 100644 index 0000000..56b9165 --- /dev/null +++ b/PubNubSwiftChatSDK.xcodeproj/xcshareddata/xcschemes/PubNubSwiftChatSDKTests.xcscheme @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PubNubSwiftChatSDK.xcworkspace/contents.xcworkspacedata b/PubNubSwiftChatSDK.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..3241bad --- /dev/null +++ b/PubNubSwiftChatSDK.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/PubNubSwiftChatSDK.xcworkspace/xcshareddata/IDETemplateMacros.plist b/PubNubSwiftChatSDK.xcworkspace/xcshareddata/IDETemplateMacros.plist new file mode 100644 index 0000000..b5aaf1c --- /dev/null +++ b/PubNubSwiftChatSDK.xcworkspace/xcshareddata/IDETemplateMacros.plist @@ -0,0 +1,16 @@ + + + + + FILEHEADER + +// ___FILENAME___ +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + + diff --git a/PubNubSwiftChatSDK.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/PubNubSwiftChatSDK.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/PubNubSwiftChatSDK.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/PubNubSwiftChatSDK.xcworkspace/xcshareddata/swiftpm/Package.resolved b/PubNubSwiftChatSDK.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..7be2f71 --- /dev/null +++ b/PubNubSwiftChatSDK.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,23 @@ +{ + "originHash" : "160b44fbed91415c985f7829e32c01b952bd76451638cfaf3ec3add33864981b", + "pins" : [ + { + "identity" : "kmp-chat", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pubnub/kmp-chat", + "state" : { + "revision" : "910c9bd122ec408c577b78e96aff106c459926b9" + } + }, + { + "identity" : "swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pubnub/swift", + "state" : { + "revision" : "7ec97085f008532fde807568409941badbc1e737", + "version" : "8.0.0" + } + } + ], + "version" : 3 +} diff --git a/PubNubSwiftChatSDK/PubNubSwiftChatSDK.docc/SwiftChatSDK.md b/PubNubSwiftChatSDK/PubNubSwiftChatSDK.docc/SwiftChatSDK.md new file mode 100755 index 0000000..49e3c21 --- /dev/null +++ b/PubNubSwiftChatSDK/PubNubSwiftChatSDK.docc/SwiftChatSDK.md @@ -0,0 +1,13 @@ +# ``SwiftChatSDK`` + +Summary + +## Overview + +Text + +## Topics + +### Group + +- ``Symbol`` diff --git a/PubNubSwiftChatSDK/PubNubSwiftChatSDK.h b/PubNubSwiftChatSDK/PubNubSwiftChatSDK.h new file mode 100644 index 0000000..36b4039 --- /dev/null +++ b/PubNubSwiftChatSDK/PubNubSwiftChatSDK.h @@ -0,0 +1,22 @@ +// +// SwiftChatSDK.h +// SwiftChatSDK +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +#import + +//! Project version number for PubNubSwiftChatSDK. +FOUNDATION_EXPORT double PubNubSwiftChatSDKVersionNumber; + +//! Project version string for PubNubSwiftChatSDK. +FOUNDATION_EXPORT const unsigned char PubNubSwiftChatSDKVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/README.md b/README.md index 3f24d47..22ae3e0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,94 @@ -# swift-chat-sdk -PubNub Swift Chat SDK +# PubNub Swift Chat SDK + +PubNub takes care of the infrastructure and APIs needed for the realtime communication layer of your application. Work on your app's logic and let PubNub handle sending and receiving data across the world in less than 100ms. + +This SDK offers a set of handy methods to create your own feature-rich chat or add a chat to your existing application. + +It exposes various PubNub APIs with twists: + +* Tailored specifically to the chat use case by offering easy-to-use methods that let you do exactly what you want, like startTyping() (a message) or join() (a channel). +* Meant to be easy & intuitive to use as it focuses on features you would most likely build in your chat app, not PubNub APIs and all the technicalities behind them. +* Offers new chat options, like quotes, threads, or read receipts, that let you build a full-fledged app quickly. + +## Table Of Contents + +* [Requirements](#requirements) +* [Get keys](#get-keys) +* [Set up your project](#set-up-your-project) +* [Configure](#configure) +* [Documentation](#documentation) +* [Support](#support) +* [License](#license) + +## Requirements + +* iOS 14.0+ (support for other platforms is coming in future releases) +* Xcode 15+ +* Swift 5+ + +## Get keys + +You will need the publish and subscribe keys to authenticate your app. Get your keys from the [Admin Portal](https://dashboard.pubnub.com/). + +## Set up your project + +### [Swift Package Manager](https://github.com/apple/swift-package-manager) + +1. Create or open your project inside of Xcode +2. Navigate to File > Add Package Dependencies +3. Search for `https://github.com/pubnub/swift-chat-sdk` and hit the Add Package button +4. Use the `Up to Next Major Version` rule spanning from `1.0.0` < `2.0.0`, and hit the Next button + +For more information see Apple's guide on [Adding Package Dependencies to Your App](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app) + +## Configure + +1. Import the module named `PubNubSwiftChatSDK` and `PubNubSDK` inside any of your Swift source file: + + ```swift + import PubNubSDK + import PubNubSwiftChatSDK // <- Here is our PubNubSwiftChatSDK module import. + ``` + +1. Create `PubNubConfiguration` object and `ChatImpl` object: + + ```swift + let pubNubConfiguration = PubNubConfiguration( + publishKey: "myPublishKey", + subscribeKey: "mySubscribeKey", + userId: "myUniqueUserId" + // Fill in the necessary parameters for PubNubConfiguration if needed + ) + let chat = ChatImpl( + // Fill in the necessary parameters for ChatConfiguration if needed + chatConfiguration: ChatConfiguration(), + pubNubConfiguration: pubNubConfiguration + ) + ``` + +2. Initialize a `chat` object: + + ```swift + chat.initialize() { + switch $0 { + case .success(_): + print("Chat object initialized") + case let .failure(error): + print("Unable to initialize due to error \(error)") + } + } + ``` + +## Documentation + +* [API reference for PubNubSwiftChatSDK](https://www.pubnub.com/docs/chat/swift-chat-sdk/overview) + +## Support + +If you **need help** or have a **general question**, contact [support@pubnub.com](mailto:support@pubnub.com). + +## License + +The PubNub Swift Chat SDK is released under the `PubNub Software Development Kit License`. + +[See LICENSE](https://github.com/pubnub/swift-chat-sdk/blob/master/LICENSE) for details. diff --git a/Sources/Chat.swift b/Sources/Chat.swift new file mode 100644 index 0000000..8e7c8fd --- /dev/null +++ b/Sources/Chat.swift @@ -0,0 +1,253 @@ +// +// Chat.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubSDK + +public protocol Chat: AnyObject { + associatedtype ChatUserType: User + associatedtype ChatChannelType: Channel + associatedtype ChatThreadChannelType: ThreadChannel + associatedtype ChatMembershipType: Membership + associatedtype ChatMessageType: Message + associatedtype ChatThreadMessageType: ThreadMessage + + var config: ChatConfiguration { get } + var pubNub: PubNub { get } + var currentUser: ChatUserType { get } + var editMessageActionName: String { get } + var deleteMessageActionName: String { get } + + func initialize( + completion: ((Swift.Result) -> Void)? + ) + + func createUser( + user: ChatUserType, + completion: ((Swift.Result) -> Void)? + ) + + func createUser( + id: String, + name: String?, + externalId: String?, + profileUrl: String?, + email: String?, + custom: [String: JSONCodableScalar]?, + status: String?, + type: String?, + completion: ((Swift.Result) -> Void)? + ) + + func getUser( + userId: String, + completion: ((Swift.Result) -> Void)? + ) + + func getUsers( + filter: String?, + sort: [PubNub.ObjectSortField], + limit: Int?, + page: PubNubHashedPage?, + completion: ((Swift.Result<(users: [ChatUserType], page: PubNubHashedPage?), Error>) -> Void)? + ) + + func updateUser( + id: String, + name: String?, + externalId: String?, + profileUrl: String?, + email: String?, + custom: [String: JSONCodableScalar]?, + status: String?, + type: String?, + completion: ((Swift.Result) -> Void)? + ) + + func deleteUser( + id: String, + soft: Bool, + completion: ((Swift.Result) -> Void)? + ) + + func wherePresent( + userId: String, + completion: ((Swift.Result<[String], Error>) -> Void)? + ) + + func isPresent( + userId: String, + channelId: String, + completion: ((Swift.Result) -> Void)? + ) + + func createChannel( + id: String, + name: String?, + description: String?, + custom: [String: JSONCodableScalar]?, + type: ChannelType?, + status: String?, + completion: ((Swift.Result) -> Void)? + ) + + func getChannel( + channelId: String, + completion: ((Swift.Result) -> Void)? + ) + + func getChannels( + filter: String?, + sort: [PubNub.ObjectSortField], + limit: Int?, + page: PubNubHashedPage?, + completion: ((Swift.Result<(channels: [ChatChannelType], page: PubNubHashedPage?), Error>) -> Void)? + ) + + func updateChannel( + id: String, + name: String?, + custom: [String: JSONCodableScalar]?, + description: String?, + status: String?, + type: ChannelType?, + completion: ((Swift.Result) -> Void)? + ) + + func deleteChannel( + id: String, + soft: Bool, + completion: ((Swift.Result) -> Void)? + ) + + func forwardMessage( + message: ChatMessageType, + channelId: String, + completion: ((Swift.Result) -> Void)? + ) + + func whoIsPresent( + channelId: String, + completion: ((Swift.Result<[String], Error>) -> Void)? + ) + + func emitEvent( + channelId: String, + payload: T, + mergePayloadWith otherPayload: [String: JSONCodable]?, + completion: ((Swift.Result) -> Void)? + ) + + func createPublicConversation( + channelId: String?, + channelName: String?, + channelDescription: String?, + channelCustom: [String: JSONCodableScalar]?, + channelStatus: String?, + completion: ((Swift.Result) -> Void)? + ) + + func createDirectConversation( + invitedUser: UserImpl, + channelId: String?, + channelName: String?, + channelDescription: String?, + channelCustom: [String: JSONCodableScalar]?, + channelStatus: String?, + membershipCustom: [String: JSONCodableScalar]?, + completion: ((Swift.Result, Error>) -> Void)? + ) + + func createGroupConversation( + invitedUsers: [UserImpl], + channelId: String?, + channelName: String?, + channelDescription: String?, + channelCustom: [String: JSONCodableScalar]?, + channelStatus: String?, + membershipCustom: [String: JSONCodableScalar]?, + completion: ((Swift.Result, Error>) -> Void)? + ) + + func listenForEvents( + type: T.Type, + channelId: String, + customMethod: EmitEventMethod, + callback: @escaping ((EventWrapper) -> Void) + ) -> AutoCloseable + + func registerPushChannels( + channels: [String], + completion: ((Swift.Result) -> Void)? + ) + + func unregisterPushChannels( + channels: [String], + completion: ((Swift.Result) -> Void)? + ) + + func unregisterAllPushChannels( + completion: ((Swift.Result) -> Void)? + ) + + func getThreadChannel( + message: ChatMessageType, + completion: ((Swift.Result) -> Void)? + ) + + func getUnreadMessagesCount( + limit: Int?, + page: PubNubHashedPage?, + filter: String?, + sort: [PubNub.MembershipSortField], + completion: ((Swift.Result<[GetUnreadMessagesCount], Error>) -> Void)? + ) + + func markAllMessagesAsRead( + limit: Int?, + page: PubNubHashedPage?, + filter: String?, + sort: [PubNub.MembershipSortField], + completion: ((Swift.Result<(memberships: [ChatMembershipType], page: PubNubHashedPage?), Error>) -> Void)? + ) + + func getChannelSuggestions( + text: String, + limit: Int, + completion: ((Swift.Result<[ChatChannelType], Error>) -> Void)? + ) + + func getUserSuggestions( + text: String, + limit: Int, + completion: ((Swift.Result<[ChatUserType], Error>) -> Void)? + ) + + func getPushChannels( + completion: ((Swift.Result<[String], Error>) -> Void)? + ) + + func getEventsHistory( + channelId: String, + startTimetoken: Timetoken?, + endTimetoken: Timetoken?, + count: Int, + completion: ((Swift.Result<(events: [EventWrapper], isMore: Bool), Error>) -> Void)? + ) + + func getCurrentUserMentions( + startTimetoken: Timetoken?, + endTimetoken: Timetoken?, + count: Int, + completion: ((Swift.Result<(mentions: [UserMentionDataWrapper], isMore: Bool), Error>) -> Void)? + ) + + func destroy() +} diff --git a/Sources/ChatConfiguration.swift b/Sources/ChatConfiguration.swift new file mode 100644 index 0000000..e75046d --- /dev/null +++ b/Sources/ChatConfiguration.swift @@ -0,0 +1,203 @@ +// +// ChatConfiguration.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +// MARK: - CustomPayloads + +public typealias GetMessagePublishBody = (EventContent.TextMessageContent, String, DefaultGetMessagePublishBody) -> [String: Any] +public typealias DefaultGetMessagePublishBody = (EventContent.TextMessageContent) -> [String: Any] + +public typealias GetMessageResponseBody = ((JSONCodable, String, DefaultGetMessageResponseBody) -> EventContent.TextMessageContent?) +public typealias DefaultGetMessageResponseBody = (JSONCodable) -> EventContent.TextMessageContent? + +typealias KotlinGetMessagePublishBody = (PubNubChat.EventContent.TextMessageContent, String, KotlinDefaultGetMessagePublishBody) -> [String: Any] +typealias KotlinDefaultGetMessagePublishBody = (PubNubChat.EventContent.TextMessageContent) -> [String: Any] +typealias KotlinGetMessageResponseBody = (JsonElement, String, KotlinDefaultGetMessageResponseBody) -> PubNubChat.EventContent.TextMessageContent? +typealias KotlinDefaultGetMessageResponseBody = (JsonElement) -> PubNubChat.EventContent.TextMessageContent? + +public class CustomPayloads { + var getMessagePublishBody: GetMessagePublishBody? + var getMessageResponseBody: GetMessageResponseBody? + var editMessageActionName: String? + var deleteMessageActionName: String? + + public init( + getMessagePublishBody: GetMessagePublishBody? = nil, + getMessageResponseBody: GetMessageResponseBody? = nil, + editMessageActionName: String? = nil, + deleteMessageActionName: String? = nil + ) { + self.getMessagePublishBody = getMessagePublishBody + self.getMessageResponseBody = getMessageResponseBody + self.editMessageActionName = editMessageActionName + self.deleteMessageActionName = deleteMessageActionName + } + + func transform() -> PubNubChat.CustomPayloads { + var kmpGetMessagePublishBody: KotlinGetMessagePublishBody? + var kmpGetMessageResponseBody: KotlinGetMessageResponseBody? + + if let thisGetMessagePublishBody = getMessagePublishBody { + kmpGetMessagePublishBody = { textContent, channelId, _ in + let thisDefaultBodyHandler: DefaultGetMessagePublishBody = { + ConstantsKt.defaultGetMessagePublishBody(m: $0.transform()) + } + let result = thisGetMessagePublishBody( + textContent.transform(), + channelId, + thisDefaultBodyHandler + ) + return result + } + } + + if let thisGetMessageResponseBody = getMessageResponseBody { + kmpGetMessageResponseBody = { element, channelId, _ in + let thisDefaultHandler: DefaultGetMessageResponseBody = { _ in + ConstantsKt.defaultGetMessageResponseBody(message: element)?.transform() + } + if let value = element.value as? KMPAnyJSON { + return thisGetMessageResponseBody( + value.value, + channelId, + thisDefaultHandler + )?.transform() + } + return nil + } + } + + return PubNubChat.CustomPayloads( + getMessagePublishBody: kmpGetMessagePublishBody, + getMessageResponseBody: kmpGetMessageResponseBody, + editMessageActionName: editMessageActionName, + deleteMessageActionName: deleteMessageActionName + ) + } +} + +// MARK: - LogLevel + +public enum LogLevel { + case off + case error + case warn + case info + case debug + case verbose + + func transform() -> PubNubChat.LogLevel { + switch self { + case .off: + .off + case .error: + .error + case .warn: + .warn + case .info: + .info + case .debug: + .debug + case .verbose: + .verbose + } + } +} + +// MARK: - ChatConfiguration + +public struct ChatConfiguration { + public var logLevel: LogLevel + public var typingTimeout: Int + public var storeUserActivityInterval: Int + public var storeUserActivityTimestamps: Bool + public var pushNotificationsConfig: PushNotificationsConfig + public var rateLimitFactor: Int + public var rateLimitPerChannel: [ChannelType: Int64] + public var customPayloads: CustomPayloads? + + public init( + logLevel: LogLevel = .off, + typingTimeout: Int = 5, + storeUserActivityInterval: Int = 600, + storeUserActivityTimestamps: Bool = false, + pushNotificationsConfig: PushNotificationsConfig = .init(), + rateLimitFactor: Int = 2, + rateLimitPerChannel: [ChannelType: Int64] = ChannelType.allCases.reduce(into: [ChannelType: Int64]()) { res, type in res[type] = 0 }, + customPayloads: CustomPayloads? = nil + ) { + self.logLevel = logLevel + self.typingTimeout = typingTimeout + self.storeUserActivityInterval = storeUserActivityInterval + self.storeUserActivityTimestamps = storeUserActivityTimestamps + self.pushNotificationsConfig = pushNotificationsConfig + self.rateLimitFactor = rateLimitFactor + self.rateLimitPerChannel = rateLimitPerChannel + self.customPayloads = customPayloads + } + + func transform() -> any PubNubChat.ChatConfiguration { + ChatConfigurationKt.ChatConfiguration( + logLevel: logLevel.transform(), + typingTimeout: KotlinDurationUtils.companion.toSeconds(interval: Int32(typingTimeout)), + storeUserActivityInterval: KotlinDurationUtils.companion.toSeconds( + interval: Int32(storeUserActivityInterval) + ), + storeUserActivityTimestamps: storeUserActivityTimestamps, + pushNotifications: PubNubChat.PushNotificationsConfig( + sendPushes: pushNotificationsConfig.sendPushes, + deviceToken: pushNotificationsConfig.deviceToken, + deviceGateway: pushNotificationsConfig.deviceGateway.transform(), + apnsTopic: pushNotificationsConfig.apnsTopic, + apnsEnvironment: pushNotificationsConfig.apnsEnvironment.transform() + ), + rateLimitFactor: Int32(rateLimitFactor), + rateLimitPerChannel: rateLimitPerChannel.transform(), + customPayloads: customPayloads?.transform() + ) + } +} + +// MARK: - PushNotificationsConfig + +public struct PushNotificationsConfig { + public var sendPushes: Bool + public var deviceToken: String? + public var deviceGateway: PubNub.PushService + public var apnsTopic: String? + public var apnsEnvironment: PubNub.PushEnvironment + + public init( + sendPushes: Bool = false, + deviceToken: String? = nil, + deviceGateway: PubNub.PushService = .fcm, + apnsTopic: String? = nil, + apnsEnvironment: PubNub.PushEnvironment = .development + ) { + self.sendPushes = sendPushes + self.deviceToken = deviceToken + self.deviceGateway = deviceGateway + self.apnsTopic = apnsTopic + self.apnsEnvironment = apnsEnvironment + } + + func transform() -> PubNubChat.PushNotificationsConfig { + PubNubChat.PushNotificationsConfig( + sendPushes: sendPushes, + deviceToken: deviceToken, + deviceGateway: deviceGateway.transform(), + apnsTopic: apnsTopic, + apnsEnvironment: apnsEnvironment.transform() + ) + } +} diff --git a/Sources/ChatImpl.swift b/Sources/ChatImpl.swift new file mode 100644 index 0000000..8995191 --- /dev/null +++ b/Sources/ChatImpl.swift @@ -0,0 +1,836 @@ +// +// ChatImpl.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +public final class ChatImpl { + public let pubNub: PubNub + public let config: ChatConfiguration + + let chat: PubNubChat.ChatImpl + + public init(chatConfiguration: ChatConfiguration, pubNubConfiguration: PubNubConfiguration) { + pubNub = PubNub(configuration: pubNubConfiguration) + config = chatConfiguration + chat = ChatImpl.createKMPChat(from: pubNub, config: chatConfiguration) + + // Provide a mechanism for reading a version number from a .plist file. + pubNub.setConsumer(identifier: "chat-sdk", value: "CA-SWIFT/0.8.0") + + ChatAdapter.associate( + chat: self, + rawChat: chat + ) + } + + init(pubNub: PubNub, configuration: ChatConfiguration) { + self.pubNub = pubNub + config = configuration + chat = ChatImpl.createKMPChat(from: pubNub, config: configuration) + + // Provide a mechanism for reading a version number from a .plist file. + pubNub.setConsumer(identifier: "chat-sdk", value: "CA-SWIFT/0.8.0") + + ChatAdapter.associate( + chat: self, + rawChat: chat + ) + } + + deinit { + destroy() + ChatAdapter.clean() + } +} + +extension ChatImpl { + static func createKMPChat( + from pubnub: PubNub, + config: ChatConfiguration + ) -> PubNubChat.ChatImpl { + PubNubChat.ChatImpl( + config: config.transform(), + pubNub: PubNubImpl.Companion.shared.create(kmpPubNub: KMPPubNub(pubnub: pubnub)), + editMessageActionName: config.customPayloads?.editMessageActionName ?? MessageActionType.edited.rawValue, + deleteMessageActionName: config.customPayloads?.deleteMessageActionName ?? MessageActionType.deleted.rawValue, + timerManager: TimerManagerImpl() + ) + } +} + +extension ChatImpl: Chat { + public typealias ChatUserType = UserImpl + public typealias ChatMessageType = MessageImpl + public typealias ChatThreadMessageType = ThreadMessageImpl + public typealias ChatMembershipType = MembershipImpl + public typealias ChatChannelType = ChannelImpl + public typealias ChatThreadChannelType = ThreadChannelImpl + + public var currentUser: UserImpl { UserImpl(user: chat.currentUser) } + public var editMessageActionName: String { chat.editMessageActionName } + public var deleteMessageActionName: String { chat.deleteMessageActionName } + + public func initialize(completion: ((Swift.Result) -> Void)? = nil) { + chat.initialize().weakAsync(caller: self) { (result: WeakFutureResult) in + switch result.result { + case .success: + if let caller = result.caller { + completion?(.success(caller)) + } else { + completion?(.failure(ChatError(message: objectNoLongerExists))) + } + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func createUser( + user: UserImpl, + completion: ((Swift.Result) -> Void)? + ) { + chat.createUser( + user: user.user + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(createdUser): + completion?(.success(UserImpl(user: createdUser))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func createUser( + id: String, + name: String? = nil, + externalId: String? = nil, + profileUrl: String? = nil, + email: String? = nil, + custom: [String: JSONCodableScalar]? = nil, + status: String? = nil, + type: String? = nil, + completion: ((Swift.Result) -> Void)? = nil + ) { + chat.createUser( + id: id, + name: name, + externalId: externalId, + profileUrl: profileUrl, + email: email, + custom: custom?.asCustomObject(), + status: status, + type: type + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(user): + completion?(.success(UserImpl(user: user))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func getUser( + userId: String, + completion: ((Swift.Result) -> Void)? = nil + ) { + chat.getUser( + userId: userId + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(user): + completion?(.success(UserImpl(user: user))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func getUsers( + filter: String? = nil, + sort: [PubNub.ObjectSortField] = [], + limit: Int?, + page: PubNubHashedPage? = nil, + completion: ((Swift.Result<(users: [UserImpl], page: PubNubHashedPage?), Error>) -> Void)? = nil + ) { + chat.getUsers( + filter: filter, + sort: sort.compactMap { $0.transform() }, + limit: limit?.asKotlinInt, + page: page?.transform() + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(getUserResponse): + completion?( + .success(( + users: getUserResponse.users.compactMap { + $0 as? PubNubChat.User + }.map { + UserImpl(user: $0) + }, + page: PubNubHashedPageBase( + start: getUserResponse.next?.pageHash, + end: getUserResponse.prev?.pageHash, + totalCount: Int(getUserResponse.total) + ) + )) + ) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func updateUser( + id: String, + name: String? = nil, + externalId: String? = nil, + profileUrl: String? = nil, + email: String? = nil, + custom: [String: JSONCodableScalar]? = nil, + status: String? = nil, + type: String? = nil, + completion: ((Swift.Result) -> Void)? = nil + ) { + chat.updateUser( + id: id, + name: name, + externalId: externalId, + profileUrl: profileUrl, + email: email, + custom: custom?.asCustomObject(), + status: status, + type: type + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(user): + completion?(.success(UserImpl(user: user))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func deleteUser( + id: String, + soft: Bool = false, + completion: ((Swift.Result) -> Void)? = nil + ) { + chat.deleteUser( + id: id, + soft: soft + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(user): + completion?(.success(UserImpl(user: user))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func wherePresent( + userId: String, + completion: ((Swift.Result<[String], Error>) -> Void)? = nil + ) { + chat.wherePresent( + userId: userId + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(channelIds): + completion?(.success(channelIds)) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func isPresent( + userId: String, + channelId: String, + completion: ((Swift.Result) -> Void)? = nil + ) { + chat.isPresent( + userId: userId, + channelId: channelId + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(isPresent): + completion?(.success(isPresent)) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func createChannel( + id: String, + name: String? = nil, + description: String? = nil, + custom: [String: JSONCodableScalar]? = nil, + type: ChannelType? = nil, + status: String? = nil, + completion: ((Swift.Result) -> Void)? = nil + ) { + chat.createChannel( + id: id, + name: name, + description: description, + custom: custom?.asCustomObject(), + type: type?.transform(), + status: status + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(channel): + completion?(.success(ChannelImpl(channel: channel))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func getChannel( + channelId: String, + completion: ((Swift.Result) -> Void)? = nil + ) { + chat.getChannel( + channelId: channelId + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(channel): + completion?(.success(ChannelImpl(channel: channel))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func getChannels( + filter: String? = nil, + sort: [PubNub.ObjectSortField] = [], + limit: Int? = nil, + page: PubNubHashedPage? = nil, + completion: ((Swift.Result<(channels: [ChannelImpl], page: PubNubHashedPage?), Error>) -> Void)? = nil + ) { + chat.getChannels( + filter: filter, + sort: sort.compactMap { $0.transform() }, + limit: limit?.asKotlinInt, + page: page?.transform() + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(getChannelsResponse): + completion?( + .success(( + channels: getChannelsResponse.channels.compactMap { + $0 as? PubNubChat.Channel_ + }.map { + ChannelImpl(channel: $0) + }, + page: PubNubHashedPageBase( + start: getChannelsResponse.next?.pageHash, + end: getChannelsResponse.prev?.pageHash, + totalCount: Int(getChannelsResponse.total) + ) + )) + ) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func updateChannel( + id: String, + name: String? = nil, + custom: [String: JSONCodableScalar]? = nil, + description: String? = nil, + status: String? = nil, + type: ChannelType? = nil, + completion: ((Swift.Result) -> Void)? = nil + ) { + chat.updateChannel( + id: id, + name: name, + custom: custom?.asCustomObject(), + description: description, + status: status, + type: type?.transform() + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(channel): + completion?(.success(ChannelImpl(channel: channel))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func deleteChannel( + id: String, + soft: Bool = false, + completion: ((Swift.Result) -> Void)? = nil + ) { + chat.deleteChannel( + id: id, + soft: soft + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(channel): + completion?(.success(ChannelImpl(channel: channel))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func forwardMessage( + message: MessageImpl, + channelId: String, + completion: ((Swift.Result) -> Void)? = nil + ) { + chat.forwardMessage( + message: message.target.message, channelId: channelId + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(publishResult): + completion?(.success(Timetoken(publishResult.timetoken))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func whoIsPresent( + channelId: String, + completion: ((Swift.Result<[String], Error>) -> Void)? = nil + ) { + chat.whoIsPresent( + channelId: channelId + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(ids): + completion?(.success(ids)) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func emitEvent( + channelId: String, + payload: some EventContent, + mergePayloadWith otherPayload: [String: JSONCodable]? = nil, + completion: ((Swift.Result) -> Void)? = nil + ) { + chat.emitEvent( + channelId: channelId, + payload: EventContent.transform(content: payload), + mergePayloadWith: otherPayload?.compactMapValues { $0.rawValue } + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(result): + completion?(.success(Timetoken(result.timetoken))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func createPublicConversation( + channelId: String? = nil, + channelName: String? = nil, + channelDescription: String? = nil, + channelCustom: [String: JSONCodableScalar]? = nil, + channelStatus: String? = nil, + completion: ((Swift.Result) -> Void)? = nil + ) { + chat.createPublicConversation( + channelId: channelId, + channelName: channelName, + channelDescription: channelDescription, + channelCustom: channelCustom?.asCustomObject(), + channelStatus: channelStatus + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(channel): + completion?(.success(ChannelImpl(channel: channel))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func createDirectConversation( + invitedUser: UserImpl, + channelId: String? = nil, + channelName: String? = nil, + channelDescription: String? = nil, + channelCustom: [String: JSONCodableScalar]? = nil, + channelStatus: String? = nil, + membershipCustom: [String: JSONCodableScalar]? = nil, + completion: ((Swift.Result, Error>) -> Void)? = nil + ) { + chat.createDirectConversation( + invitedUser: invitedUser.user, + channelId: channelId, + channelName: channelName, + channelDescription: channelDescription, + channelCustom: channelCustom?.asCustomObject(), + channelStatus: channelStatus, + membershipCustom: membershipCustom?.asCustomObject() + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(response): + completion?(.success(CreateDirectConversationResult( + channel: ChannelImpl(channel: response.channel), + hostMembership: MembershipImpl(membership: response.hostMembership), + inviteeMembership: MembershipImpl(membership: response.inviteeMembership) + ))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func createGroupConversation( + invitedUsers: [UserImpl], + channelId: String? = nil, + channelName: String? = nil, + channelDescription: String? = nil, + channelCustom: [String: JSONCodableScalar]? = nil, + channelStatus: String? = nil, + membershipCustom: [String: JSONCodableScalar]? = nil, + completion: ((Swift.Result, Error>) -> Void)? = nil + ) { + chat.createGroupConversation( + invitedUsers: invitedUsers.compactMap { $0.user }, + channelId: channelId, + channelName: channelName, + channelDescription: channelDescription, + channelCustom: channelCustom?.asCustomObject(), + channelStatus: channelStatus, + membershipCustom: membershipCustom?.asCustomObject() + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(response): + completion?(.success(CreateGroupConversationResult( + channel: ChannelImpl(channel: response.channel), + hostMembership: MembershipImpl(membership: response.hostMembership), + inviteeMemberships: transformKotlinArray(response.inviteeMemberships) { MembershipImpl(membership: $0) } + ))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func listenForEvents( + type: T.Type, + channelId: String, + customMethod: EmitEventMethod = .publish, + callback: @escaping ((EventWrapper) -> Void) + ) -> AutoCloseable { + AutoCloseableImpl( + chat.listenForEvents( + type: T.classIdentifier(type: type), + channelId: channelId, + customMethod: customMethod == .publish ? .publish : .signal, + callback: { [weak self] in + if let selfRef = self, let payload = $0.payload.map() as? T { + callback( + EventWrapper( + event: EventImpl( + chat: selfRef, + timetoken: Timetoken($0.timetoken_), + payload: payload, + channelId: $0.channelId, + userId: $0.userId + ) + ) + ) + } + } + ) + ) + } + + public func registerPushChannels( + channels: [String], + completion: ((Swift.Result) -> Void)? = nil + ) { + chat.registerPushChannels( + channels: channels + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case .success: + completion?(.success(())) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func unregisterPushChannels( + channels: [String], + completion: ((Swift.Result) -> Void)? = nil + ) { + chat.unregisterPushChannels( + channels: channels + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case .success: + completion?(.success(())) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func unregisterAllPushChannels(completion: ((Swift.Result) -> Void)? = nil) { + chat.unregisterAllPushChannels().async(caller: self) { (result: FutureResult) in + switch result.result { + case .success: + completion?(.success(())) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func getThreadChannel( + message: MessageImpl, + completion: ((Swift.Result) -> Void)? = nil + ) { + chat.getThreadChannel( + message: message.target.message + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(threadChannel): + completion?(.success(ThreadChannelImpl(channel: threadChannel))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func getUnreadMessagesCount( + limit: Int? = nil, + page: PubNubHashedPage? = nil, + filter: String? = nil, + sort: [PubNub.MembershipSortField] = [], + completion: ((Swift.Result<[GetUnreadMessagesCount], Error>) -> Void)? = nil + ) { + chat.getUnreadMessagesCounts( + limit: limit?.asKotlinInt, + page: page?.transform(), + filter: filter, + sort: sort.compactMap { $0.transform() } + ).async(caller: self) { (result: FutureResult>) in + switch result.result { + case let .success(response): + completion?(.success(response.map { + GetUnreadMessagesCount( + channel: ChannelImpl(channel: $0.channel), + membership: MembershipImpl(membership: $0.membership), + count: UInt64($0.count) + ) + })) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func markAllMessagesAsRead( + limit: Int? = nil, + page: PubNubHashedPage? = nil, + filter: String? = nil, + sort: [PubNub.MembershipSortField] = [], + completion: ((Swift.Result<(memberships: [MembershipImpl], page: PubNubHashedPage?), Error>) -> Void)? = nil + ) { + chat.markAllMessagesAsRead( + limit: limit?.asKotlinInt, + page: page?.transform(), + filter: filter, + sort: sort.compactMap { $0.transform() } + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(response): + completion?(.success(( + memberships: response.memberships.compactMap { $0 as? PubNubChat.Membership }.map { + MembershipImpl(membership: $0) + }, + page: PubNubHashedPageBase( + start: response.next?.pageHash, + end: response.prev?.pageHash, + totalCount: Int(response.total) + )) + )) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func getChannelSuggestions( + text: String, + limit: Int = 10, + completion: ((Swift.Result<[ChannelImpl], Error>) -> Void)? = nil + ) { + chat.getChannelSuggestions( + text: text, + limit: Int32(limit) + ).async(caller: self) { (result: FutureResult>) in + switch result.result { + case let .success(channels): + completion?(.success(channels.compactMap { $0 as? PubNubChat.Channel_ }.map { + ChannelImpl(channel: $0) + })) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func getUserSuggestions( + text: String, + limit: Int = 10, + completion: ((Swift.Result<[UserImpl], Error>) -> Void)? = nil + ) { + chat.getUserSuggestions( + text: text, + limit: Int32(limit) + ).async(caller: self) { (result: FutureResult>) in + switch result.result { + case let .success(users): + completion?(.success(users.compactMap { + $0 as? PubNubChat.User + }.map { + UserImpl(user: $0) + })) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func getPushChannels(completion: ((Swift.Result<[String], Error>) -> Void)? = nil) { + chat.getPushChannels().async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(channelIds): + completion?(.success(channelIds)) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func getEventsHistory( + channelId: String, + startTimetoken: Timetoken? = nil, + endTimetoken: Timetoken? = nil, + count: Int = 100, + completion: ((Swift.Result<(events: [EventWrapper], isMore: Bool), Error>) -> Void)? = nil + ) { + chat.getEventsHistory( + channelId: channelId, + startTimetoken: startTimetoken?.asKotlinLong(), + endTimetoken: endTimetoken?.asKotlinLong(), + count: Int32(count) + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(response): + let eventImplArray = response.events.compactMap { + $0 as? PubNubChat.Event + }.compactMap { (event: PubNubChat.Event) -> EventWrapper? in + EventWrapper(event: EventImpl( + chat: result.caller, + timetoken: Timetoken(event.timetoken_), + payload: EventContent.from(rawValue: event.payload), + channelId: event.channelId, + userId: event.userId + )) + } + completion?(.success(( + events: eventImplArray, + isMore: response.isMore + ))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func getCurrentUserMentions( + startTimetoken: Timetoken? = nil, + endTimetoken: Timetoken? = nil, + count: Int = 100, + completion: ((Swift.Result<(mentions: [UserMentionDataWrapper], isMore: Bool), Error>) -> Void)? + ) { + chat.getCurrentUserMentions( + startTimetoken: startTimetoken?.asKotlinLong(), + endTimetoken: endTimetoken?.asKotlinLong(), + count: Int32(count) + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(response): + let userMentions = response.enhancedMentionsData.compactMap { mention -> (UserMentionDataWrapper?) in + if let payload = mention.event.payload.map() as? EventContent.Mention { + let mentionEvent = EventContent.Mention( + messageTimetoken: Timetoken(payload.messageTimetoken), + channel: payload.channel, + parentChannel: payload.parentChannel + ) + if let mentionData = mention as? PubNubChat.ChannelMentionData { + return UserMentionDataWrapper( + userMentionData: ChannelMentionData( + event: mentionEvent, + message: MessageImpl(message: mentionData.message), + userId: mentionData.userId, + channelId: mentionData.channelId + ) + ) + } else if let mentionData = mention as? PubNubChat.ThreadMentionData { + return UserMentionDataWrapper( + userMentionData: ThreadMentionData( + event: mentionEvent, + message: MessageImpl(message: mentionData.message), + userId: mentionData.userId, + parentChannelId: mentionData.parentChannelId, + threadChannelId: mentionData.threadChannelId + ) + ) + } else { + return nil + } + } else { + return nil + } + } + completion?(.success(( + mentions: userMentions, + isMore: response.isMore + ))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func destroy() { + chat.destroy() + } + + // swiftlint:disable:next file_length +} diff --git a/Sources/Entities/BaseChannel.swift b/Sources/Entities/BaseChannel.swift new file mode 100644 index 0000000..c5ced1a --- /dev/null +++ b/Sources/Entities/BaseChannel.swift @@ -0,0 +1,580 @@ +// +// BaseChannel.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +// swiftlint:disable:next type_body_length +final class BaseChannel: Channel { + var chat: ChatImpl { ChatAdapter.map(chat: channel.chat).chat } + var id: String { channel.id } + var name: String? { channel.name } + var custom: [String: JSONCodableScalar]? { channel.custom?.mapToScalars() } + var description: String? { channel.description_ } + var updated: String? { channel.updated } + var status: String? { channel.status } + var type: ChannelType? { channel.type?.transform() } + var rawChannel: PubNubChat.Channel_ { channel } + + let channel: C + + init(channel: C) { + self.channel = channel + } + + static func streamUpdatesOn( + channels: [BaseChannel], + callback: @escaping (([BaseChannel]) -> Void) + ) -> AutoCloseable { + AutoCloseableImpl( + PubNubChat.BaseChannelCompanion.shared.streamUpdatesOn(channels: channels.map(\.channel)) { + if let channels = $0 as? [C] { + callback(channels.map { + BaseChannel(channel: $0) + }) + } + } + ) + } + + func update( + name: String?, + custom: [String: JSONCodableScalar]?, + description: String?, + status: String?, + type: ChannelType?, + completion: ((Swift.Result) -> Void)? + ) { + channel.update( + name: name, + custom: custom?.asCustomObject(), + description: description, + status: status, + type: type?.transform() + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(channel): + completion?(.success(ChannelImpl(channel: channel))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func delete(soft: Bool, completion: ((Swift.Result) -> Void)?) { + channel.delete( + soft: soft + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(channel): + completion?(.success(ChannelImpl(channel: channel))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func forward(message: ChatType.ChatMessageType, completion: ((Swift.Result) -> Void)?) { + channel.forwardMessage( + message: message.target.message + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(res): + completion?(.success(Timetoken(res.timetoken))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func startTyping(completion: ((Swift.Result) -> Void)?) { + channel.startTyping().async(caller: self) { (result: FutureResult) in + switch result.result { + case .success: + completion?(.success(())) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func stopTyping(completion: ((Swift.Result) -> Void)?) { + channel.stopTyping().async(caller: self) { (result: FutureResult) in + switch result.result { + case .success: + completion?(.success(())) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func getTyping(callback: @escaping (([String]) -> Void)) -> AutoCloseable { + AutoCloseableImpl( + channel.getTyping { [weak self] in + if let typingUserIdentifiers = $0 as? [String], self != nil { + callback(typingUserIdentifiers) + } + } + ) + } + + func whoIsPresent(completion: ((Swift.Result<[String], Error>) -> Void)?) { + channel.whoIsPresent().async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(userIdentifiers): + completion?(.success(userIdentifiers)) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func isPresent(userId: String, completion: ((Swift.Result) -> Void)?) { + channel.isPresent( + userId: userId + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(isPresent): + completion?(.success(isPresent)) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func getHistory( + startTimetoken: Timetoken?, + endTimetoken: Timetoken?, + count: Int, + completion: ((Swift.Result<(messages: [BaseMessage], isMore: Bool), Error>) -> Void)? + ) { + channel.getHistory( + startTimetoken: startTimetoken?.asKotlinLong(), + endTimetoken: endTimetoken?.asKotlinLong(), + count: Int32(count) + ).async(caller: self) { (result: FutureResult>) in + switch result.result { + case let .success(response): + completion?(.success(( + messages: (response.messages.map { BaseMessage(message: $0) }), + isMore: response.isMore + ))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func sendText( + text: String, + meta: [String: JSONCodable]?, + shouldStore: Bool, + usePost: Bool, + ttl: Int?, + mentionedUsers: MessageMentionedUsers?, + referencedChannels: MessageReferencedChannels?, + textLinks: [TextLink]?, + quotedMessage: ChatType.ChatMessageType?, + files: [InputFile]?, + completion: ((Swift.Result) -> Void)? + ) { + channel.sendText( + text: text, + meta: meta?.compactMapValues { $0.rawValue }, + shouldStore: shouldStore, + usePost: usePost, + ttl: ttl?.asKotlinInt, + mentionedUsers: mentionedUsers?.transform(), + referencedChannels: referencedChannels?.transform(), + textLinks: textLinks?.compactMap { $0.transform() }, + quotedMessage: quotedMessage?.target.message, + files: files?.compactMap { $0.transform() } + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(response): + completion?(.success(Timetoken(response.timetoken))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func sendText( + text: String, + meta: [String: JSONCodable]?, + shouldStore: Bool, + usePost: Bool, + ttl: Int?, + quotedMessage: MessageImpl?, + files: [InputFile]?, + completion: ((Swift.Result) -> Void)? + ) { + channel.sendText( + text: text, + meta: meta?.compactMapValues { $0.rawValue }, + shouldStore: shouldStore, + usePost: usePost, + ttl: ttl?.asKotlinInt, + quotedMessage: quotedMessage?.target.message, + files: files?.compactMap { $0.transform() } + ) + } + + func invite(user: ChatType.ChatUserType, completion: ((Swift.Result) -> Void)?) { + channel.invite( + user: user.user + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(membership): + completion?(.success(MembershipImpl(membership: membership))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func inviteMultiple(users: [ChatType.ChatUserType], completion: ((Swift.Result<[ChatType.ChatMembershipType], Error>) -> Void)?) { + channel.inviteMultiple( + users: users.compactMap { $0.user } + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(memberships): + completion?(.success(memberships.compactMap { MembershipImpl(membership: $0) })) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func getMembers( + limit: Int?, + page: PubNubHashedPage?, + filter: String?, + sort: [PubNub.MembershipSortField], + completion: ((Swift.Result<(memberships: [ChatType.ChatMembershipType], page: PubNubHashedPage?), Error>) -> Void)? + ) { + channel.getMembers( + limit: limit?.asKotlinInt, + page: page?.transform(), + filter: filter, + sort: sort.compactMap { $0.transform() } + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(response): + completion?(.success( + ( + memberships: response.members.compactMap { + $0 as? PubNubChat.Membership + }.map { + MembershipImpl(membership: $0) + }, + page: PubNubHashedPageBase( + start: response.next?.pageHash, + end: response.prev?.pageHash, + totalCount: Int(response.total) + ) + ) + )) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func connect(callback: @escaping (ChatType.ChatMessageType) -> Void) -> AutoCloseable { + AutoCloseableImpl(channel.connect { [weak self] in + if self != nil, let message = $0 as? M { + callback(MessageImpl(message: message)) + } + }) + } + + func join( + custom: [String: JSONCodableScalar]?, + callback: ((ChatType.ChatMessageType) -> Void)? = nil, + completion: ((Swift.Result<(membership: MembershipImpl, disconnect: AutoCloseable?), Error>) -> Void)? + ) { + channel.join(custom: custom?.asCustomObject(), callback: { [weak self] in + if self != nil { + callback?(MessageImpl(message: $0)) + } + }).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(joinRes): + completion?(.success(( + membership: MembershipImpl(membership: joinRes.membership), + disconnect: AutoCloseableImpl(joinRes.disconnect) + ))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func leave(completion: ((Swift.Result) -> Void)?) { + channel.leave().async(caller: self) { (result: FutureResult) in + switch result.result { + case .success: + completion?(.success(())) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func getPinnedMessage(completion: ((Swift.Result<(ChatType.ChatMessageType)?, Error>) -> Void)?) { + channel.getPinnedMessage().async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(message): + if let message { + completion?(.success(MessageImpl(message: message))) + } else { + completion?(.success(nil)) + } + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func getMessage( + timetoken: Timetoken, + completion: ((Swift.Result) -> Void)? = nil + ) { + channel.getMessage( + timetoken: Int64(timetoken) + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(message): + if let message { + completion?(.success(MessageImpl(message: message))) + } else { + completion?(.success(nil)) + } + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func registerForPush(completion: ((Swift.Result) -> Void)?) { + channel.registerForPush().async( + caller: self + ) { (result: FutureResult) in + switch result.result { + case .success: + completion?(.success(())) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func unregisterFromPush(completion: ((Swift.Result) -> Void)?) { + channel.unregisterFromPush().async( + caller: self + ) { (result: FutureResult) in + switch result.result { + case .success: + completion?(.success(())) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func pinMessage(message: MessageImpl, completion: ((Swift.Result, Error>) -> Void)?) { + channel.pinMessage( + message: message.target.message + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(channel): + completion?(.success(BaseChannel(channel: channel))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func unpinMessage(completion: ((Swift.Result, Error>) -> Void)?) { + channel.unpinMessage().async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(channel): + completion?(.success(BaseChannel(channel: channel))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func streamUpdates(callback: @escaping ((ChatType.ChatChannelType)?) -> Void) -> AutoCloseable { + AutoCloseableImpl( + channel.streamUpdates { [weak self] in + if self != nil { + if let channel = $0 { + callback(ChannelImpl(channel: channel)) + } else { + callback(nil) + } + } + } + ) + } + + func streamReadReceipts(callback: @escaping (([Timetoken: [String]]) -> Void)) -> AutoCloseable { + AutoCloseableImpl( + channel.streamReadReceipts { [weak self] in + if self != nil { + callback( + $0.reduce(into: [Timetoken: [String]]()) { res, currentItem in + res[Timetoken(currentItem.key.uint64Value)] = currentItem.value + } + ) + } + } + ) + } + + func getFiles( + limit: Int, + next: String?, + completion: ((Swift.Result<(files: [GetFileItem], page: PubNubHashedPage?), Error>) -> Void)? + ) { + channel.getFiles( + limit: Int32(limit), + next: next + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(response): + completion?( + .success(( + files: (response.files as? [PubNubChat.GetFileItem] ?? []).compactMap { + GetFileItem( + name: $0.name, + id: $0.id, + url: $0.url + ) + }, + page: PubNubHashedPageBase( + start: response.next, + end: nil, + totalCount: Int(response.total) + ) + )) + ) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func deleteFile(id: String, name: String, completion: ((Swift.Result) -> Void)?) { + channel.deleteFile( + id: id, + name: name + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case .success: + completion?(.success(())) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func streamPresence(callback: @escaping (Set) -> Void) -> AutoCloseable { + AutoCloseableImpl( + channel.streamPresence { [weak self] in + if let userIds = $0 as? Set, self != nil { + callback(userIds) + } + } + ) + } + + func getUserSuggestions( + text: String, + limit: Int, + completion: ((Swift.Result<[ChatType.ChatMembershipType], Error>) -> Void)? + ) { + channel.getUserSuggestions( + text: text, + limit: Int32(limit) + ).async(caller: self) { (result: FutureResult>) in + switch result.result { + case let .success(userIds): + completion?(.success(userIds.compactMap { + $0 as? PubNubChat.Membership + }.map { + MembershipImpl(membership: $0) + })) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func getMessageReportsHistory( + startTimetoken: Timetoken? = nil, + endTimetoken: Timetoken? = nil, + count: Int = 25, + completion: ((Swift.Result<(events: [EventWrapper], isMore: Bool), Error>) -> Void)? + ) { + channel.getMessageReportsHistory( + startTimetoken: startTimetoken?.asKotlinLong(), + endTimetoken: endTimetoken?.asKotlinLong(), + count: Int32(count) + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(response): + completion?(.success(( + events: response.events.compactMap { + $0 as? PubNubChat.Event + }.compactMap { (event: PubNubChat.Event) -> EventWrapper? in + EventWrapper( + event: EventImpl( + chat: result.caller.chat, + timetoken: Timetoken(event.timetoken_), + payload: event.payload.map(), + channelId: event.channelId, + userId: event.userId + ) + ) + }, + isMore: response.isMore + ))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + func streamMessageReports(callback: @escaping (any Event) -> Void) -> AutoCloseable { + AutoCloseableImpl( + channel.streamMessageReports { [weak self] in + if let selfRef = self, let payload = $0.payload.map() as? EventContent.Report { + callback( + EventImpl( + chat: selfRef.chat, + timetoken: Timetoken($0.timetoken_), + payload: payload, + channelId: $0.channelId, + userId: $0.userId + ) + ) + } + } + ) + } + + // swiftlint:disable:next file_length +} diff --git a/Sources/Entities/BaseMessage.swift b/Sources/Entities/BaseMessage.swift new file mode 100644 index 0000000..799df20 --- /dev/null +++ b/Sources/Entities/BaseMessage.swift @@ -0,0 +1,220 @@ +// +// BaseMessage.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +final class BaseMessage { + let message: M + + init(message: M) { + self.message = message + } + + convenience init?(message: M?) { + if let message { + self.init(message: message) + } else { + return nil + } + } +} + +extension BaseMessage: Message { + public var chat: ChatImpl { ChatAdapter.map(chat: message.chat).chat } + public var timetoken: Timetoken { Timetoken(message.timetoken_) } + public var content: EventContent.TextMessageContent { message.content.transform() } + public var channelId: String { message.channelId } + public var userId: String { message.userId } + public var actions: [String: [String: [Action]]]? { message.actions?.transform() } + public var meta: [String: JSONCodable]? { message.meta?.compactMapValues { AnyJSON($0) } } + public var mentionedUsers: MessageMentionedUsers? { message.mentionedUsers?.transform() } + public var referencedChannels: MessageReferencedChannels? { message.referencedChannels?.transform() } + public var quotedMessage: QuotedMessage? { message.quotedMessage?.transform() } + public var text: String { message.text } + public var deleted: Bool { message.deleted } + public var hasThread: Bool { message.hasThread } + public var type: String { message.type } + public var files: [File] { message.files.transform() } + public var reactions: [String: [Action]] { message.reactions.transform() } + public var textLinks: [TextLink]? { message.textLinks?.transform() } + + static func streamUpdatesOn( + messages: [BaseMessage], + callback: @escaping (([BaseMessage]) -> Void) + ) -> AutoCloseable { + AutoCloseableImpl( + PubNubChat.BaseMessageCompanion.shared.streamUpdatesOn(messages: messages.map(\.message)) { + if let messages = $0 as? [M] { + callback(messages.map { + BaseMessage(message: $0) + }) + } + } + ) + } + + public func hasUserReaction(reaction: String) -> Bool { + message.hasUserReaction( + reaction: reaction + ) + } + + public func editText( + newText: String, + completion: ((Swift.Result) -> Void)? + ) { + message.editText( + newText: newText + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(message): + completion?(.success(MessageImpl(message: message))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func delete( + soft: Bool = false, + preserveFiles: Bool = false, + completion: ((Swift.Result) -> Void)? = nil + ) { + message.delete( + soft: soft, + preserveFiles: preserveFiles + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(message): + completion?(.success(MessageImpl(message: message))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func getThread(completion: ((Swift.Result) -> Void)? = nil) { + message.getThread().async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(threadChannel): + completion?(.success(ThreadChannelImpl(channel: threadChannel))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func forward( + channelId: String, + completion: ((Swift.Result) -> Void)? + ) { + message.forward( + channelId: channelId + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(result): + completion?(.success(Timetoken(result.timetoken))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func pin(completion: ((Swift.Result) -> Void)? = nil) { + message.pin().async(caller: self) { (result: FutureResult, PubNubChat.Channel_>) in + switch result.result { + case let .success(channel): + completion?(.success(ChannelImpl(channel: channel))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func report( + reason: String, + completion: ((Swift.Result) -> Void)? = nil + ) { + message.report( + reason: reason + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(result): + completion?(.success(Timetoken(result.timetoken))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func createThread(completion: ((Swift.Result) -> Void)? = nil) { + message.createThread().async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(threadChannel): + completion?(.success(ThreadChannelImpl(channel: threadChannel))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func removeThread(completion: ((Swift.Result) -> Void)? = nil) { + message.removeThread().async( + caller: self + ) { (result: FutureResult>) in + switch result.result { + case let .success(pair): + // swiftlint:disable:next force_unwrapping + completion?(.success(ChannelImpl(channel: pair.second!))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func toggleReaction( + reaction: String, + completion: ((Swift.Result, Error>) -> Void)? = nil + ) { + message.toggleReaction( + reaction: reaction + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(message): + completion?(.success(BaseMessage(message: message))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func streamUpdates(completion: @escaping ((BaseMessage) -> Void)) -> AutoCloseable { + AutoCloseableImpl( + message.streamUpdates { [weak self] in + if let message = $0 as? M, self != nil { + completion(BaseMessage(message: message)) + } + } + ) + } + + func restore(completion: ((Swift.Result, Error>) -> Void)? = nil) { + message.restore().async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(message): + completion?(.success(BaseMessage(message: message))) + case let .failure(error): + completion?(.failure(error)) + } + } + } +} diff --git a/Sources/Entities/Channel.swift b/Sources/Entities/Channel.swift new file mode 100644 index 0000000..2cbbaa9 --- /dev/null +++ b/Sources/Entities/Channel.swift @@ -0,0 +1,219 @@ +// +// Channel.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubSDK + +public protocol Channel { + associatedtype ChatType: Chat + associatedtype MessageType: Message + + var chat: ChatType { get } + var id: String { get } + var name: String? { get } + var custom: [String: JSONCodableScalar]? { get } + var description: String? { get } + var updated: String? { get } + var status: String? { get } + var type: ChannelType? { get } + + static func streamUpdatesOn( + channels: [Self], + callback: @escaping (([Self]) -> Void) + ) -> AutoCloseable + + func update( + name: String?, + custom: [String: JSONCodableScalar]?, + description: String?, + status: String?, + type: ChannelType?, + completion: ((Swift.Result) -> Void)? + ) + + func delete( + soft: Bool, + completion: ((Swift.Result) -> Void)? + ) + + func forward( + message: ChatType.ChatMessageType, + completion: ((Swift.Result) -> Void)? + ) + + func startTyping( + completion: ((Swift.Result) -> Void)? + ) + + func stopTyping( + completion: ((Swift.Result) -> Void)? + ) + + func getTyping( + callback: @escaping (([String]) -> Void) + ) -> AutoCloseable + + func whoIsPresent( + completion: ((Swift.Result<[String], Error>) -> Void)? + ) + + func isPresent( + userId: String, + completion: ((Swift.Result) -> Void)? + ) + + func getHistory( + startTimetoken: Timetoken?, + endTimetoken: Timetoken?, + count: Int, + completion: ((Swift.Result<(messages: [MessageType], isMore: Bool), Error>) -> Void)? + ) + + @available(*, deprecated, message: "Will be removed from SDK in the future") + func sendText( + text: String, + meta: [String: JSONCodable]?, + shouldStore: Bool, + usePost: Bool, + ttl: Int?, + mentionedUsers: MessageMentionedUsers?, + referencedChannels: MessageReferencedChannels?, + textLinks: [TextLink]?, + quotedMessage: ChatType.ChatMessageType?, + files: [InputFile]?, + completion: ((Swift.Result) -> Void)? + ) + + func sendText( + text: String, + meta: [String: JSONCodable]?, + shouldStore: Bool, + usePost: Bool, + ttl: Int?, + quotedMessage: ChatType.ChatMessageType?, + files: [InputFile]?, + completion: ((Swift.Result) -> Void)? + ) + + func invite( + user: ChatType.ChatUserType, + completion: ((Swift.Result) -> Void)? + ) + + func inviteMultiple( + users: [ChatType.ChatUserType], + completion: ((Swift.Result<[ChatType.ChatMembershipType], Error>) -> Void)? + ) + + func getMembers( + limit: Int?, + page: PubNubHashedPage?, + filter: String?, + sort: [PubNub.MembershipSortField], + completion: ((Swift.Result<(memberships: [ChatType.ChatMembershipType], page: PubNubHashedPage?), Error>) -> Void)? + ) + + func connect( + callback: @escaping (ChatType.ChatMessageType) -> Void + ) -> AutoCloseable + + func join( + custom: [String: JSONCodableScalar]?, + callback: ((ChatType.ChatMessageType) -> Void)?, + completion: ((Swift.Result<(membership: ChatType.ChatMembershipType, disconnect: AutoCloseable?), Error>) -> Void)? + ) + + func leave( + completion: ((Swift.Result) -> Void)? + ) + + func getPinnedMessage( + completion: ((Swift.Result<(ChatType.ChatMessageType)?, Error>) -> Void)? + ) + + func getMessage( + timetoken: Timetoken, + completion: ((Swift.Result<(ChatType.ChatMessageType)?, Error>) -> Void)? + ) + + func registerForPush( + completion: ((Swift.Result) -> Void)? + ) + + func unregisterFromPush( + completion: ((Swift.Result) -> Void)? + ) + + func pinMessage( + message: ChatType.ChatMessageType, + completion: ((Swift.Result) -> Void)? + ) + + func unpinMessage( + completion: ((Swift.Result) -> Void)? + ) + + func streamUpdates( + callback: @escaping ((ChatType.ChatChannelType)?) -> Void + ) -> AutoCloseable + + func streamReadReceipts( + callback: @escaping (([Timetoken: [String]]) -> Void) + ) -> AutoCloseable + + func getFiles( + limit: Int, + next: String?, + completion: ((Swift.Result<(files: [GetFileItem], page: PubNubHashedPage?), Error>) -> Void)? + ) + + func deleteFile( + id: String, + name: String, + completion: ((Swift.Result) -> Void)? + ) + + func streamPresence( + callback: @escaping (Set) -> Void + ) -> AutoCloseable + + func getUserSuggestions( + text: String, + limit: Int, + completion: ((Swift.Result<[ChatType.ChatMembershipType], Error>) -> Void)? + ) + + func getMessageReportsHistory( + startTimetoken: Timetoken?, + endTimetoken: Timetoken?, + count: Int, + completion: ((Swift.Result<(events: [EventWrapper], isMore: Bool), Error>) -> Void)? + ) + + func streamMessageReports( + callback: @escaping (any Event) -> Void + ) -> AutoCloseable +} + +// MARK: - ThreadChannel + +public protocol ThreadChannel: Channel { + var parentChannelId: String { get } + var parentMessage: ChatType.ChatMessageType { get } + + func pinMessageToParentChannel( + message: ChatType.ChatThreadMessageType, + completion: ((Swift.Result) -> Void)? + ) + + func unpinMessageFromParentChannel( + completion: ((Swift.Result) -> Void)? + ) +} diff --git a/Sources/Entities/ChannelImpl.swift b/Sources/Entities/ChannelImpl.swift new file mode 100644 index 0000000..d5f15bf --- /dev/null +++ b/Sources/Entities/ChannelImpl.swift @@ -0,0 +1,388 @@ +// +// ChannelImpl.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +public final class ChannelImpl { + let target: BaseChannel + + public convenience init( + chat: ChatImpl, + id: String, + name: String? = nil, + custom: [String: JSONCodableScalar]? = nil, + description: String? = nil, + updated: String? = nil, + status: String? = nil, + type: ChannelType? = nil + ) { + let underlyingChannel = PubNubChat.ChannelImpl( + chat: chat.chat, + clock: ClockSystem(), + id: id, + name: name, + custom: custom?.compactMapValues { $0.rawValue as? JSONCodableScalar }, + description: description, + updated: updated, + status: status, + type: type?.transform() + ) + self.init( + channel: underlyingChannel + ) + } + + convenience init?(channel: PubNubChat.Channel_?) { + if let channel { + self.init(channel: channel) + } else { + return nil + } + } + + init(channel: PubNubChat.Channel_) { + target = BaseChannel(channel: channel) + } +} + +extension ChannelImpl: Channel { + public var chat: ChatImpl { target.chat } + public var id: String { target.id } + public var name: String? { target.name } + public var custom: [String: JSONCodableScalar]? { target.custom } + public var description: String? { target.description } + public var updated: String? { target.updated } + public var status: String? { target.status } + public var type: ChannelType? { target.type } + + public static func streamUpdatesOn( + channels: [ChannelImpl], + callback: @escaping (([ChannelImpl]) -> Void) + ) -> AutoCloseable { + AutoCloseableImpl( + PubNubChat.ThreadChannelCompanion.shared.streamUpdatesOn(channels: channels.map(\.target.channel)) { + callback(($0 as? [PubNubChat.Channel_] ?? []).map { + ChannelImpl(channel: $0) + }) + } + ) + } + + public func update( + name: String? = nil, + custom: [String: JSONCodableScalar]? = nil, + description: String? = nil, + status: String? = nil, + type: ChannelType? = nil, + completion: ((Swift.Result) -> Void)? = nil + ) { + target.update( + name: name, + custom: custom, + description: description, + status: status, + type: type, + completion: completion + ) + } + + public func delete( + soft: Bool = false, + completion: ((Swift.Result) -> Void)? = nil + ) { + target.delete( + soft: soft, + completion: completion + ) + } + + public func forward( + message: MessageImpl, + completion: ((Swift.Result) -> Void)? = nil + ) { + target.forward( + message: message, + completion: completion + ) + } + + public func startTyping(completion: ((Swift.Result) -> Void)? = nil) { + target.startTyping( + completion: completion + ) + } + + public func stopTyping(completion: ((Swift.Result) -> Void)? = nil) { + target.stopTyping( + completion: completion + ) + } + + public func getTyping(callback: @escaping (([String]) -> Void)) -> AutoCloseable { + target.getTyping( + callback: callback + ) + } + + public func whoIsPresent(completion: ((Swift.Result<[String], Error>) -> Void)? = nil) { + target.whoIsPresent( + completion: completion + ) + } + + public func isPresent(userId: String, completion: ((Swift.Result) -> Void)? = nil) { + target.isPresent( + userId: userId, + completion: completion + ) + } + + public func getHistory( + startTimetoken: Timetoken? = nil, + endTimetoken: Timetoken? = nil, + count: Int = 25, + completion: ((Swift.Result<(messages: [MessageImpl], isMore: Bool), Error>) -> Void)? + ) { + target.getHistory(startTimetoken: startTimetoken, endTimetoken: endTimetoken, count: count) { + switch $0 { + case let .success(res): + completion?(.success(( + messages: res.messages.map { MessageImpl(message: $0.message) }, + isMore: res.isMore + ))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func sendText( + text: String, + meta: [String: JSONCodable]? = nil, + shouldStore: Bool = true, + usePost: Bool = false, + ttl: Int? = nil, + mentionedUsers: MessageMentionedUsers? = nil, + referencedChannels: MessageReferencedChannels? = nil, + textLinks: [TextLink]? = nil, + quotedMessage: MessageImpl? = nil, + files: [InputFile]? = nil, + completion: ((Swift.Result) -> Void)? = nil + ) { + target.sendText( + text: text, + meta: meta, + shouldStore: shouldStore, + usePost: usePost, + ttl: ttl, + mentionedUsers: mentionedUsers, + referencedChannels: referencedChannels, + textLinks: textLinks, + quotedMessage: quotedMessage, + files: files, + completion: completion + ) + } + + public func sendText( + text: String, + meta: [String: JSONCodable]? = nil, + shouldStore: Bool = true, + usePost: Bool = false, + ttl: Int? = nil, + quotedMessage: MessageImpl? = nil, + files: [InputFile]?, + completion: ((Swift.Result) -> Void)? = nil + ) { + sendText( + text: text, + shouldStore: shouldStore, + usePost: usePost, + ttl: ttl, + mentionedUsers: nil, + referencedChannels: nil, + textLinks: nil, + quotedMessage: quotedMessage, + files: files, + completion: completion + ) + } + + public func invite(user: UserImpl, completion: ((Swift.Result) -> Void)? = nil) { + target.invite( + user: user, + completion: completion + ) + } + + public func inviteMultiple(users: [UserImpl], completion: ((Swift.Result<[MembershipImpl], Error>) -> Void)? = nil) { + target.inviteMultiple( + users: users, + completion: completion + ) + } + + public func getMembers( + limit: Int? = nil, + page: PubNubHashedPage? = nil, + filter: String? = nil, + sort: [PubNub.MembershipSortField] = [], + completion: ((Swift.Result<(memberships: [MembershipImpl], page: PubNubHashedPage?), Error>) -> Void)? = nil + ) { + target.getMembers( + limit: limit, + page: page, + filter: filter, + sort: sort, + completion: completion + ) + } + + public func connect(callback: @escaping (MessageImpl) -> Void) -> AutoCloseable { + target.connect(callback: callback) + } + + public func join( + custom: [String: JSONCodableScalar]? = nil, + callback: ((MessageImpl) -> Void)? = nil, + completion: ((Swift.Result<(membership: MembershipImpl, disconnect: AutoCloseable?), Error>) -> Void)? = nil + ) { + target.join( + custom: custom, + callback: callback, + completion: completion + ) + } + + public func leave(completion: ((Swift.Result) -> Void)? = nil) { + target.leave( + completion: completion + ) + } + + public func getPinnedMessage(completion: ((Swift.Result) -> Void)? = nil) { + target.getPinnedMessage( + completion: completion + ) + } + + public func getMessage(timetoken: Timetoken, completion: ((Swift.Result) -> Void)? = nil) { + target.getMessage( + timetoken: timetoken, + completion: completion + ) + } + + public func registerForPush(completion: ((Swift.Result) -> Void)? = nil) { + target.registerForPush( + completion: completion + ) + } + + public func unregisterFromPush(completion: ((Swift.Result) -> Void)? = nil) { + target.unregisterFromPush( + completion: completion + ) + } + + public func pinMessage(message: MessageImpl, completion: ((Swift.Result) -> Void)? = nil) { + target.pinMessage(message: message) { + switch $0 { + case let .success(channel): + completion?(.success(ChannelImpl(channel: channel.channel))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func unpinMessage(completion: ((Swift.Result) -> Void)? = nil) { + target.unpinMessage { + switch $0 { + case let .success(channel): + completion?(.success(ChannelImpl(channel: channel.channel))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func streamUpdates(callback: @escaping (ChannelImpl?) -> Void) -> AutoCloseable { + target.streamUpdates( + callback: callback + ) + } + + public func streamReadReceipts(callback: @escaping (([Timetoken: [String]]) -> Void)) -> AutoCloseable { + target.streamReadReceipts( + callback: callback + ) + } + + public func getFiles( + limit: Int = 100, + next: String? = nil, + completion: ((Swift.Result<(files: [GetFileItem], page: PubNubHashedPage?), Error>) -> Void)? = nil + ) { + target.getFiles( + limit: limit, + next: next, + completion: completion + ) + } + + public func deleteFile(id: String, name: String, completion: ((Swift.Result) -> Void)? = nil) { + target.deleteFile( + id: id, + name: name, + completion: completion + ) + } + + public func streamPresence(callback: @escaping (Set) -> Void) -> AutoCloseable { + target.streamPresence( + callback: callback + ) + } + + public func getUserSuggestions( + text: String, + limit: Int = 10, + completion: ((Swift.Result<[MembershipImpl], Error>) -> Void)? = nil + ) { + target.getUserSuggestions( + text: text, + limit: limit, + completion: completion + ) + } + + public func getMessageReportsHistory( + startTimetoken: Timetoken? = nil, + endTimetoken: Timetoken? = nil, + count: Int = 25, + completion: ((Swift.Result<(events: [EventWrapper], isMore: Bool), Error>) -> Void)? + ) { + target.getMessageReportsHistory( + startTimetoken: startTimetoken, + endTimetoken: endTimetoken, + count: count, + completion: completion + ) + } + + public func streamMessageReports(callback: @escaping (any Event) -> Void) -> AutoCloseable { + target.streamMessageReports( + callback: callback + ) + } +} diff --git a/Sources/Entities/Membership.swift b/Sources/Entities/Membership.swift new file mode 100644 index 0000000..808bcfc --- /dev/null +++ b/Sources/Entities/Membership.swift @@ -0,0 +1,52 @@ +// +// Membership.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubSDK + +public protocol Membership { + associatedtype ChatType: Chat + + var chat: ChatType { get } + var channel: ChatType.ChatChannelType { get } + var user: ChatType.ChatUserType { get } + var custom: [String: JSONCodableScalar]? { get } + var eTag: String? { get } + var updated: String? { get } + var lastReadMessageTimetoken: Timetoken? { get } + + static func streamUpdatesOn( + memberships: [Self], + callback: @escaping (([Self]) -> Void) + ) -> AutoCloseable + + func setLastReadMessage( + message: ChatType.ChatMessageType, + completion: ((Swift.Result) -> Void)? + ) + + func update( + custom: [String: JSONCodableScalar], + completion: ((Swift.Result) -> Void)? + ) + + func setLastReadMessageTimetoken( + _ timetoken: Timetoken, + completion: ((Swift.Result) -> Void)? + ) + + func getUnreadMessagesCount( + completion: ((Swift.Result) -> Void)? + ) + + func streamUpdates( + callback: @escaping ((ChatType.ChatMembershipType?) -> Void) + ) -> AutoCloseable +} diff --git a/Sources/Entities/MembershipImpl.swift b/Sources/Entities/MembershipImpl.swift new file mode 100644 index 0000000..960cb3f --- /dev/null +++ b/Sources/Entities/MembershipImpl.swift @@ -0,0 +1,137 @@ +// +// MembershipImpl.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +public final class MembershipImpl { + let membership: PubNubChat.Membership + + public convenience init( + chat: ChatImpl, + channel: ChannelImpl, + user: UserImpl, + custom: [String: JSONCodableScalar]? = nil, + updated: String? = nil, + eTag: String? = nil + ) { + let underlyingMembership = PubNubChat.MembershipImpl( + chat: chat.chat, + channel: channel.target.channel, + user: user.user, + custom: custom?.compactMapValues { $0.rawValue }, + updated: updated, + eTag: eTag + ) + self.init( + membership: underlyingMembership + ) + } + + init(membership: PubNubChat.Membership) { + self.membership = membership + } +} + +extension MembershipImpl: Membership { + public var chat: ChatImpl { ChatAdapter.map(chat: membership.chat).chat } + public var channel: ChannelImpl { ChannelImpl(channel: membership.channel) } + public var user: UserImpl { UserImpl(user: membership.user) } + public var custom: [String: JSONCodableScalar]? { membership.custom?.mapToScalars() } + public var eTag: String? { membership.eTag } + public var updated: String? { membership.updated } + public var lastReadMessageTimetoken: Timetoken? { membership.lastReadMessageTimetoken?.uint64Value } + + public static func streamUpdatesOn( + memberships: [MembershipImpl], + callback: @escaping (([MembershipImpl]) -> Void) + ) -> AutoCloseable { + AutoCloseableImpl( + PubNubChat.MembershipImpl.Companion.shared.streamUpdatesOn(memberships: memberships.compactMap { $0.membership }) { + if let memberships = $0 as? [PubNubChat.Membership] { + callback(memberships.map { + MembershipImpl(membership: $0) + }) + } + } + ) + } + + public func setLastReadMessage( + message: MessageImpl, + completion: ((Swift.Result) -> Void)? = nil + ) { + membership.setLastReadMessage( + message: message.target.message + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(membership): + completion?(.success(MembershipImpl(membership: membership))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func update( + custom: [String: JSONCodableScalar], + completion: ((Swift.Result) -> Void)? = nil + ) { + membership.update( + custom: CustomObject(value: custom) + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(membership): + completion?(.success(MembershipImpl(membership: membership))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func setLastReadMessageTimetoken(_ timetoken: Timetoken, completion: ((Swift.Result) -> Void)? = nil) { + membership.setLastReadMessageTimetoken( + timetoken: Int64(timetoken) + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(membership): + completion?(.success(MembershipImpl(membership: membership))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func getUnreadMessagesCount(completion: ((Swift.Result) -> Void)? = nil) { + membership.getUnreadMessagesCount().async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(messagesCount): + completion?(.success(messagesCount)) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func streamUpdates(callback: @escaping ((MembershipImpl?) -> Void)) -> AutoCloseable { + AutoCloseableImpl( + membership.streamUpdates { [weak self] in + if self != nil { + if let membership = $0 { + callback(MembershipImpl(membership: membership)) + } else { + callback(nil) + } + } + } + ) + } +} diff --git a/Sources/Entities/Message.swift b/Sources/Entities/Message.swift new file mode 100644 index 0000000..f17391b --- /dev/null +++ b/Sources/Entities/Message.swift @@ -0,0 +1,105 @@ +// +// Message.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubSDK + +public protocol Message { + associatedtype ChatType: Chat + + var chat: ChatType { get } + var timetoken: Timetoken { get } + var content: EventContent.TextMessageContent { get } + var channelId: String { get } + var userId: String { get } + var actions: [String: [String: [Action]]]? { get } + var meta: [String: JSONCodable]? { get } + var mentionedUsers: MessageMentionedUsers? { get } + var referencedChannels: MessageReferencedChannels? { get } + var quotedMessage: QuotedMessage? { get } + var text: String { get } + var deleted: Bool { get } + var hasThread: Bool { get } + var type: String { get } + var files: [File] { get } + var reactions: [String: [Action]] { get } + var textLinks: [TextLink]? { get } + + static func streamUpdatesOn( + messages: [Self], + callback: @escaping (([Self]) -> Void) + ) -> AutoCloseable + + func hasUserReaction( + reaction: String + ) -> Bool + + func editText( + newText: String, + completion: ((Swift.Result) -> Void)? + ) + + func delete( + soft: Bool, + preserveFiles: Bool, + completion: ((Swift.Result<(ChatType.ChatMessageType)?, Error>) -> Void)? + ) + + func getThread( + completion: ((Swift.Result) -> Void)? + ) + + func forward( + channelId: String, + completion: ((Swift.Result) -> Void)? + ) + + func pin( + completion: ((Swift.Result) -> Void)? + ) + + func report( + reason: String, + completion: ((Swift.Result) -> Void)? + ) + + func createThread( + completion: ((Swift.Result) -> Void)? + ) + + func removeThread( + completion: ((Swift.Result) -> Void)? + ) + + func toggleReaction( + reaction: String, + completion: ((Swift.Result) -> Void)? + ) + + func streamUpdates( + completion: @escaping ((Self) -> Void) + ) -> AutoCloseable + + func restore( + completion: ((Swift.Result) -> Void)? + ) +} + +public protocol ThreadMessage: Message { + var parentChannelId: String { get } + + func pinToParentChannel( + completion: ((Swift.Result) -> Void)? + ) + + func unpinFromParentChannel( + completion: ((Swift.Result) -> Void)? + ) +} diff --git a/Sources/Entities/MessageImpl.swift b/Sources/Entities/MessageImpl.swift new file mode 100644 index 0000000..492ebd6 --- /dev/null +++ b/Sources/Entities/MessageImpl.swift @@ -0,0 +1,195 @@ +// +// MessageImpl.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +public final class MessageImpl { + let target: BaseMessage + + public convenience init( + chat: ChatImpl, + timetoken: Timetoken, + content: EventContent.TextMessageContent, + channelId: String, + userId: String, + actions: [String: [String: [Action]]]? = nil, + meta: [String: JSONCodable]? = nil, + mentionedUsers: MessageMentionedUsers? = nil, + referencedChannels: MessageReferencedChannels? = nil, + quotedMessage: QuotedMessage? = nil + ) { + let underlyingMessage = PubNubChat.MessageImpl( + chat: chat.chat, + timetoken: Int64(timetoken), + content: content.transform(), + channelId: channelId, + userId: userId, + actions: actions?.transform(), + meta: meta?.compactMapValues { $0.rawValue }, + mentionedUsers: mentionedUsers?.transform(), + referencedChannels: referencedChannels?.transform(), + quotedMessage: quotedMessage?.transform() + ) + self.init( + message: underlyingMessage + ) + } + + convenience init?(message: PubNubChat.Message?) { + if let message { + self.init(message: message) + } else { + return nil + } + } + + init(message: PubNubChat.Message) { + target = BaseMessage(message: message) + } +} + +extension MessageImpl: Message { + public var chat: ChatImpl { target.chat } + public var timetoken: Timetoken { target.timetoken } + public var content: EventContent.TextMessageContent { target.content } + public var channelId: String { target.channelId } + public var userId: String { target.userId } + public var actions: [String: [String: [Action]]]? { target.actions } + public var meta: [String: JSONCodable]? { target.meta } + public var mentionedUsers: MessageMentionedUsers? { target.mentionedUsers } + public var referencedChannels: MessageReferencedChannels? { target.referencedChannels } + public var quotedMessage: QuotedMessage? { target.quotedMessage } + public var text: String { target.text } + public var deleted: Bool { target.deleted } + public var hasThread: Bool { target.hasThread } + public var type: String { target.type } + public var files: [File] { target.files } + public var reactions: [String: [Action]] { target.reactions } + public var textLinks: [TextLink]? { target.textLinks } + + public static func streamUpdatesOn( + messages: [MessageImpl], + callback: @escaping (([MessageImpl]) -> Void) + ) -> AutoCloseable { + AutoCloseableImpl( + PubNubChat.MessageCompanion.shared.streamUpdatesOn(messages: messages.map(\.target.message)) { + if let messages = $0 as? [PubNubChat.Message] { + callback(messages.map { + MessageImpl(message: $0) + }) + } + } + ) + } + + public func hasUserReaction(reaction: String) -> Bool { + target.hasUserReaction( + reaction: reaction + ) + } + + public func editText( + newText: String, + completion: ((Swift.Result) -> Void)? = nil + ) { + target.editText( + newText: newText, + completion: completion + ) + } + + public func delete( + soft: Bool = false, + preserveFiles: Bool = false, + completion: ((Swift.Result) -> Void)? = nil + ) { + target.delete( + soft: soft, + preserveFiles: preserveFiles, + completion: completion + ) + } + + public func getThread(completion: ((Swift.Result) -> Void)? = nil) { + target.getThread( + completion: completion + ) + } + + public func forward(channelId: String, completion: ((Swift.Result) -> Void)?) { + target.forward( + channelId: channelId, + completion: completion + ) + } + + public func pin(completion: ((Swift.Result) -> Void)? = nil) { + target.pin( + completion: completion + ) + } + + public func report( + reason: String, + completion: ((Swift.Result) -> Void)? = nil + ) { + target.report( + reason: reason, + completion: completion + ) + } + + public func createThread(completion: ((Swift.Result) -> Void)? = nil) { + target.createThread( + completion: completion + ) + } + + public func removeThread(completion: ((Swift.Result) -> Void)? = nil) { + target.removeThread( + completion: completion + ) + } + + public func toggleReaction( + reaction: String, + completion: ((Swift.Result) -> Void)? = nil + ) { + target.toggleReaction(reaction: reaction) { + switch $0 { + case let .success(message): + completion?(.success(MessageImpl(message: message.message))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func streamUpdates(completion: @escaping ((MessageImpl) -> Void)) -> AutoCloseable { + target.streamUpdates { [weak self] in + if self != nil { + completion(MessageImpl(message: $0.message)) + } + } + } + + public func restore(completion: ((Swift.Result) -> Void)? = nil) { + target.restore { + switch $0 { + case let .success(message): + completion?(.success(MessageImpl(message: message.message))) + case let .failure(error): + completion?(.failure(error)) + } + } + } +} diff --git a/Sources/Entities/ThreadChannelImpl.swift b/Sources/Entities/ThreadChannelImpl.swift new file mode 100644 index 0000000..d8b166f --- /dev/null +++ b/Sources/Entities/ThreadChannelImpl.swift @@ -0,0 +1,428 @@ +// +// ThreadChannelImpl.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +public final class ThreadChannelImpl { + let target: BaseChannel + + public convenience init( + parentMessage: MessageImpl, + chat: ChatImpl, + id: String, + name: String? = nil, + custom: [String: JSONCodableScalar], + description: String? = nil, + updated: String? = nil, + status: String? = nil, + type: ChannelType? = nil, + threadCreated: Bool = true + ) { + let underlyingThreadChannel = PubNubChat.ThreadChannelImpl( + parentMessage: parentMessage.target.message, + chat: chat.chat, + clock: PubNubChat.ClockSystem(), + id: id, + name: name, + custom: custom.compactMapValues { $0.rawValue }, + description: description, + updated: updated, + status: status, + type: type?.transform(), + threadCreated: threadCreated + ) + self.init( + channel: underlyingThreadChannel + ) + } + + init(channel: PubNubChat.ThreadChannel) { + target = BaseChannel(channel: channel) + } + + convenience init?(channel: PubNubChat.ThreadChannel?) { + if let channel { + self.init(channel: channel) + } else { + return nil + } + } +} + +extension ThreadChannelImpl: ThreadChannel { + public var chat: ChatImpl { target.chat } + public var id: String { target.id } + public var name: String? { target.name } + public var custom: [String: JSONCodableScalar]? { target.custom } + public var description: String? { target.description } + public var updated: String? { target.updated } + public var status: String? { target.status } + public var type: ChannelType? { target.type } + public var parentMessage: MessageImpl { MessageImpl(message: target.channel.parentMessage) } + public var parentChannelId: String { target.channel.parentChannelId } + + public static func streamUpdatesOn( + channels: [ThreadChannelImpl], + callback: @escaping (([ThreadChannelImpl]) -> Void) + ) -> AutoCloseable { + AutoCloseableImpl( + PubNubChat.ThreadChannelCompanion.shared.streamUpdatesOn(channels: channels.map(\.target.channel)) { + callback(($0 as? [PubNubChat.ThreadChannel] ?? []).map { + ThreadChannelImpl(channel: $0) + }) + } + ) + } + + public func pinMessageToParentChannel( + message: ThreadMessageImpl, + completion: ((Swift.Result) -> Void)? = nil + ) { + target.channel.pinMessageToParentChannel( + message: message.target.message + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(channel): + completion?(.success(ChannelImpl(channel: channel))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func unpinMessageFromParentChannel(completion: ((Swift.Result) -> Void)? = nil) { + target.channel.unpinMessageFromParentChannel().async( + caller: self + ) { (result: FutureResult) in + switch result.result { + case let .success(channel): + completion?(.success(ChannelImpl(channel: channel))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func update( + name: String? = nil, + custom: [String: JSONCodableScalar]? = nil, + description: String? = nil, + status: String? = nil, + type: ChannelType? = nil, + completion: ((Swift.Result) -> Void)? = nil + ) { + target.update( + name: name, + custom: custom, + description: description, + status: status, + type: type, + completion: completion + ) + } + + public func delete(soft: Bool = false, completion: ((Swift.Result) -> Void)? = nil) { + target.delete( + soft: soft, + completion: completion + ) + } + + public func forward(message: MessageImpl, completion: ((Swift.Result) -> Void)? = nil) { + target.forward( + message: message, + completion: completion + ) + } + + public func startTyping(completion: ((Swift.Result) -> Void)? = nil) { + target.startTyping( + completion: completion + ) + } + + public func stopTyping(completion: ((Swift.Result) -> Void)? = nil) { + target.stopTyping( + completion: completion + ) + } + + public func getTyping(callback: @escaping (([String]) -> Void)) -> AutoCloseable { + target.getTyping( + callback: callback + ) + } + + public func whoIsPresent(completion: ((Swift.Result<[String], Error>) -> Void)? = nil) { + target.whoIsPresent( + completion: completion + ) + } + + public func isPresent(userId: String, completion: ((Swift.Result) -> Void)? = nil) { + target.isPresent( + userId: userId, + completion: completion + ) + } + + public func getHistory( + startTimetoken: Timetoken? = nil, + endTimetoken: Timetoken? = nil, + count: Int = 25, + completion: ((Swift.Result<(messages: [ThreadMessageImpl], isMore: Bool), Error>) -> Void)? + ) { + target.getHistory( + startTimetoken: startTimetoken, + endTimetoken: endTimetoken, + count: count + ) { + switch $0 { + case let .success(res): + completion?(.success(( + messages: res.messages.map { ThreadMessageImpl(message: $0.message) }, + isMore: res.isMore + ))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func sendText( + text: String, + meta: [String: JSONCodable]? = nil, + shouldStore: Bool = true, + usePost: Bool = false, + ttl: Int? = nil, + mentionedUsers: MessageMentionedUsers? = nil, + referencedChannels: MessageReferencedChannels? = nil, + textLinks: [TextLink]? = nil, + quotedMessage: MessageImpl? = nil, + files: [InputFile]? = nil, + completion: ((Swift.Result) -> Void)? = nil + ) { + target.sendText( + text: text, + meta: meta, + shouldStore: shouldStore, + usePost: usePost, + ttl: ttl, + mentionedUsers: mentionedUsers, + referencedChannels: referencedChannels, + textLinks: textLinks, + quotedMessage: quotedMessage, + files: files, + completion: completion + ) + } + + public func sendText( + text: String, + meta: [String: JSONCodable]? = nil, + shouldStore: Bool = true, + usePost: Bool = false, + ttl: Int? = nil, + quotedMessage: MessageImpl? = nil, + files: [InputFile]?, + completion: ((Swift.Result) -> Void)? = nil + ) { + sendText( + text: text, + shouldStore: shouldStore, + usePost: usePost, + ttl: ttl, + mentionedUsers: nil, + referencedChannels: nil, + textLinks: nil, + quotedMessage: quotedMessage, + files: files, + completion: completion + ) + } + + public func invite(user: UserImpl, completion: ((Swift.Result) -> Void)? = nil) { + target.invite( + user: user, + completion: completion + ) + } + + public func inviteMultiple(users: [UserImpl], completion: ((Swift.Result<[MembershipImpl], Error>) -> Void)? = nil) { + target.inviteMultiple( + users: users, + completion: completion + ) + } + + public func getMembers( + limit: Int? = nil, + page: PubNubHashedPage? = nil, + filter: String? = nil, + sort: [PubNub.MembershipSortField] = [], + completion: ((Swift.Result<(memberships: [MembershipImpl], page: PubNubHashedPage?), Error>) -> Void)? = nil + ) { + target.getMembers( + limit: limit, + page: page, + filter: filter, + sort: sort, + completion: completion + ) + } + + public func connect(callback: @escaping (MessageImpl) -> Void) -> AutoCloseable { + target.connect( + callback: callback + ) + } + + public func join( + custom: [String: JSONCodableScalar]? = nil, + callback: ((MessageImpl) -> Void)? = nil, + completion: ((Swift.Result<(membership: MembershipImpl, disconnect: AutoCloseable?), Error>) -> Void)? = nil + ) { + target.join( + custom: custom, + callback: callback, + completion: completion + ) + } + + public func leave(completion: ((Swift.Result) -> Void)? = nil) { + target.leave( + completion: completion + ) + } + + public func getPinnedMessage(completion: ((Swift.Result) -> Void)? = nil) { + target.getPinnedMessage( + completion: completion + ) + } + + public func getMessage( + timetoken: Timetoken, + completion: ((Swift.Result) -> Void)? = nil + ) { + target.getMessage( + timetoken: timetoken, + completion: completion + ) + } + + public func registerForPush(completion: ((Swift.Result) -> Void)? = nil) { + target.registerForPush( + completion: completion + ) + } + + public func unregisterFromPush(completion: ((Swift.Result) -> Void)? = nil) { + target.unregisterFromPush( + completion: completion + ) + } + + public func pinMessage(message: MessageImpl, completion: ((Swift.Result) -> Void)? = nil) { + target.pinMessage(message: message) { + switch $0 { + case let .success(channel): + completion?(.success(ThreadChannelImpl(channel: channel.channel))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func unpinMessage(completion: ((Swift.Result) -> Void)? = nil) { + target.unpinMessage { + switch $0 { + case let .success(channel): + completion?(.success(ThreadChannelImpl(channel: channel.channel))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func streamUpdates(callback: @escaping (ChannelImpl?) -> Void) -> AutoCloseable { + target.streamUpdates( + callback: callback + ) + } + + public func streamReadReceipts(callback: @escaping (([Timetoken: [String]]) -> Void)) -> AutoCloseable { + target.streamReadReceipts( + callback: callback + ) + } + + public func getFiles( + limit: Int = 100, + next: String? = nil, + completion: ((Swift.Result<(files: [GetFileItem], page: PubNubHashedPage?), Error>) -> Void)? = nil + ) { + target.getFiles( + limit: limit, + next: next, + completion: completion + ) + } + + public func deleteFile(id: String, name: String, completion: ((Swift.Result) -> Void)? = nil) { + target.deleteFile( + id: id, + name: name, + completion: completion + ) + } + + public func streamPresence(callback: @escaping (Set) -> Void) -> AutoCloseable { + target.streamPresence( + callback: callback + ) + } + + public func getUserSuggestions( + text: String, + limit: Int = 10, + completion: ((Swift.Result<[MembershipImpl], Error>) -> Void)? = nil + ) { + target.getUserSuggestions( + text: text, + limit: limit, + completion: completion + ) + } + + public func getMessageReportsHistory( + startTimetoken: Timetoken? = nil, + endTimetoken: Timetoken? = nil, + count: Int = 25, + completion: ((Swift.Result<(events: [EventWrapper], isMore: Bool), Error>) -> Void)? + ) { + target.getMessageReportsHistory( + startTimetoken: startTimetoken, + endTimetoken: endTimetoken, + count: count, + completion: completion + ) + } + + public func streamMessageReports(callback: @escaping (any Event) -> Void) -> AutoCloseable { + target.streamMessageReports( + callback: callback + ) + } + + // swiftlint:disable:next file_length +} diff --git a/Sources/Entities/ThreadMessageImpl.swift b/Sources/Entities/ThreadMessageImpl.swift new file mode 100644 index 0000000..c73bb86 --- /dev/null +++ b/Sources/Entities/ThreadMessageImpl.swift @@ -0,0 +1,230 @@ +// +// ThreadMessageImpl.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +public final class ThreadMessageImpl { + let target: BaseMessage + + public convenience init( + chat: ChatImpl, + parentChannelId: String, + timetoken: Timetoken, + content: EventContent.TextMessageContent, + channelId: String, + userId: String, + actions: [String: [String: [Action]]]? = nil, + meta: [String: JSONCodable]? = nil, + mentionedUsers: MessageMentionedUsers? = nil, + referencedChannels: MessageReferencedChannels? = nil, + quotedMessage: QuotedMessage? = nil + ) { + let underlyingThreadMessage = PubNubChat.ThreadMessageImpl( + chat: chat.chat, + parentChannelId: parentChannelId, + timetoken: Int64(timetoken), + content: content.transform(), + channelId: channelId, + userId: userId, + actions: actions?.transform(), + meta: meta?.compactMapValues { $0.rawValue }, + mentionedUsers: mentionedUsers?.transform(), + referencedChannels: referencedChannels?.transform(), + quotedMessage: quotedMessage?.transform() + ) + self.init( + message: underlyingThreadMessage + ) + } + + convenience init?(message: PubNubChat.ThreadMessage?) { + if let message { + self.init(message: message) + } else { + return nil + } + } + + init(message: PubNubChat.ThreadMessage) { + target = BaseMessage(message: message) + } +} + +extension ThreadMessageImpl { + func asMessage() -> MessageImpl { + MessageImpl(message: target.message) + } +} + +extension ThreadMessageImpl: ThreadMessage { + public var chat: ChatImpl { target.chat } + public var timetoken: Timetoken { target.timetoken } + public var content: EventContent.TextMessageContent { target.content } + public var channelId: String { target.channelId } + public var userId: String { target.userId } + public var actions: [String: [String: [Action]]]? { target.actions } + public var meta: [String: JSONCodable]? { target.meta } + public var mentionedUsers: MessageMentionedUsers? { target.mentionedUsers } + public var referencedChannels: MessageReferencedChannels? { target.referencedChannels } + public var quotedMessage: QuotedMessage? { target.quotedMessage } + public var text: String { target.text } + public var deleted: Bool { target.deleted } + public var hasThread: Bool { target.hasThread } + public var type: String { target.type } + public var files: [File] { target.files } + public var reactions: [String: [Action]] { target.reactions } + public var textLinks: [TextLink]? { target.textLinks } + public var parentChannelId: String { target.message.parentChannelId } + + public static func streamUpdatesOn( + messages: [ThreadMessageImpl], + callback: @escaping (([ThreadMessageImpl]) -> Void) + ) -> AutoCloseable { + AutoCloseableImpl( + PubNubChat.ThreadMessageCompanion.shared.streamUpdatesOn(messages: messages.map(\.target.message)) { + if let messages = $0 as? [PubNubChat.ThreadMessage] { + callback(messages.map { + ThreadMessageImpl(message: $0) + }) + } + } + ) + } + + public func pinToParentChannel(completion: ((Swift.Result) -> Void)?) { + target.message.pinToParentChannel().async( + caller: self + ) { (result: FutureResult) in + switch result.result { + case let .success(channel): + completion?(.success(ChannelImpl(channel: channel))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func unpinFromParentChannel(completion: ((Swift.Result) -> Void)?) { + target.message.unpinFromParentChannel().async( + caller: self + ) { (result: FutureResult) in + switch result.result { + case let .success(channel): + completion?(.success(ChannelImpl(channel: channel))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func hasUserReaction(reaction: String) -> Bool { + target.hasUserReaction( + reaction: reaction + ) + } + + public func editText( + newText: String, + completion: ((Swift.Result) -> Void)? = nil + ) { + target.editText( + newText: newText, + completion: completion + ) + } + + public func delete( + soft: Bool = false, + preserveFiles: Bool = false, + completion: ((Swift.Result) -> Void)? = nil + ) { + target.delete( + soft: soft, + preserveFiles: preserveFiles, + completion: completion + ) + } + + public func getThread(completion: ((Swift.Result) -> Void)? = nil) { + target.getThread( + completion: completion + ) + } + + public func forward(channelId: String, completion: ((Swift.Result) -> Void)?) { + target.forward( + channelId: channelId, + completion: completion + ) + } + + public func pin(completion: ((Swift.Result) -> Void)? = nil) { + target.pin( + completion: completion + ) + } + + public func report( + reason: String, + completion: ((Swift.Result) -> Void)? = nil + ) { + target.report( + reason: reason, + completion: completion + ) + } + + public func createThread(completion: ((Swift.Result) -> Void)? = nil) { + target.createThread( + completion: completion + ) + } + + public func removeThread(completion: ((Swift.Result) -> Void)? = nil) { + target.removeThread( + completion: completion + ) + } + + public func toggleReaction( + reaction: String, + completion: ((Swift.Result) -> Void)? = nil + ) { + target.toggleReaction(reaction: reaction) { + switch $0 { + case let .success(message): + completion?(.success(ThreadMessageImpl(message: message.message))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func streamUpdates(completion: @escaping ((ThreadMessageImpl) -> Void)) -> AutoCloseable { + target.streamUpdates { [weak self] in + if self != nil { + completion(ThreadMessageImpl(message: $0.message)) + } + } + } + + public func restore(completion: ((Swift.Result) -> Void)? = nil) { + target.restore { + switch $0 { + case let .success(message): + completion?(.success(ThreadMessageImpl(message: message.message))) + case let .failure(error): + completion?(.failure(error)) + } + } + } +} diff --git a/Sources/Entities/User.swift b/Sources/Entities/User.swift new file mode 100644 index 0000000..5c9a581 --- /dev/null +++ b/Sources/Entities/User.swift @@ -0,0 +1,74 @@ +// +// User.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubSDK + +public protocol User { + associatedtype ChatType: Chat + + var chat: ChatType { get } + var id: String { get } + var name: String? { get } + var externalId: String? { get } + var profileUrl: String? { get } + var email: String? { get } + var custom: [String: JSONCodableScalar]? { get } + var status: String? { get } + var type: String? { get } + var updated: String? { get } + var lastActiveTimestamp: TimeInterval? { get } + + static func streamUpdatesOn( + users: [ChatType.ChatUserType], + callback: @escaping (([ChatType.ChatUserType]) -> Void) + ) -> AutoCloseable + + func update( + name: String?, + externalId: String?, + profileUrl: String?, + email: String?, + custom: [String: JSONCodableScalar]?, + status: String?, + type: String?, + completion: ((Swift.Result) -> Void)? + ) + + func delete( + soft: Bool, + completion: ((Swift.Result) -> Void)? + ) + + func wherePresent( + completion: ((Swift.Result<[String], Error>) -> Void)? + ) + + func isPresentOn( + channelId: String, + completion: ((Swift.Result) -> Void)? + ) + + func getMemberships( + limit: Int?, + page: PubNubHashedPage?, + filter: String?, + sort: [PubNub.MembershipSortField], + completion: ((Swift.Result<(memberships: [ChatType.ChatMembershipType], page: PubNubHashedPage?), Error>) -> Void)? + ) + + func streamUpdates( + callback: @escaping ((ChatType.ChatUserType?) -> Void) + ) -> AutoCloseable + + func active( + completion: ((Swift.Result) -> Void)? + ) +} diff --git a/Sources/Entities/UserImpl.swift b/Sources/Entities/UserImpl.swift new file mode 100644 index 0000000..4c7b218 --- /dev/null +++ b/Sources/Entities/UserImpl.swift @@ -0,0 +1,215 @@ +// +// UserImpl.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +public final class UserImpl { + let user: PubNubChat.User + + public convenience init( + chat: ChatImpl, + id: String, + name: String? = nil, + externalId: String? = nil, + profileUrl: String? = nil, + email: String? = nil, + custom: [String: JSONCodableScalar]? = nil, + status: String? = nil, + type: String? = nil, + updated: String? = nil, + lastActiveTimestamp: Timetoken? = nil + ) { + let underlyingUser = PubNubChat.UserImpl( + chat: chat.chat, + id: id, + name: name, + externalId: externalId, + profileUrl: profileUrl, + email: email, + custom: custom?.compactMapValues { $0.rawValue }, + status: status, + type: type, + updated: updated, + lastActiveTimestamp: lastActiveTimestamp?.asKotlinLong() + ) + self.init( + user: underlyingUser + ) + } + + convenience init?(user: PubNubChat.User?) { + if let user { + self.init(user: user) + } else { + return nil + } + } + + init(user: PubNubChat.User) { + self.user = user + } +} + +extension UserImpl: User { + public var chat: ChatImpl { ChatAdapter.map(chat: user.chat).chat } + public var id: String { user.id } + public var name: String? { user.name } + public var externalId: String? { user.externalId } + public var profileUrl: String? { user.profileUrl } + public var email: String? { user.email } + public var custom: [String: JSONCodableScalar]? { user.custom?.mapToScalars() } + public var status: String? { user.status } + public var type: String? { user.type } + public var updated: String? { user.updated } + public var lastActiveTimestamp: TimeInterval? { user.lastActiveTimestamp?.doubleValue } + + public static func streamUpdatesOn(users: [UserImpl], callback: @escaping (([UserImpl]) -> Void)) -> AutoCloseable { + AutoCloseableImpl( + PubNubChat.UserImpl.Companion.shared.streamUpdatesOn(users: users.compactMap { $0.user }) { + if let users = $0 as? [PubNubChat.User] { + callback(users.map { + UserImpl(user: $0) + }) + } + } + ) + } + + public func update( + name: String? = nil, + externalId: String? = nil, + profileUrl: String? = nil, + email: String? = nil, + custom: [String: JSONCodableScalar]? = nil, + status: String? = nil, + type: String? = nil, + completion: ((Swift.Result) -> Void)? = nil + ) { + user.update( + name: name, + externalId: externalId, + profileUrl: profileUrl, + email: email, + custom: custom?.asCustomObject(), + status: status, + type: type + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(user): + completion?(.success(UserImpl(user: user))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func delete( + soft: Bool = false, + completion: ((Swift.Result) -> Void)? = nil + ) { + user.delete( + soft: soft + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(user): + completion?(.success(UserImpl(user: user))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func wherePresent(completion: ((Swift.Result<[String], Error>) -> Void)? = nil) { + user.wherePresent().async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(channels): + completion?(.success(channels)) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func isPresentOn( + channelId: String, + completion: ((Swift.Result) -> Void)? = nil + ) { + user.isPresentOn( + channelId: channelId + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(boolResult): + completion?(.success(boolResult)) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func getMemberships( + limit: Int? = nil, + page: PubNubHashedPage? = nil, + filter: String? = nil, + sort: [PubNub.MembershipSortField] = [], + completion: ((Swift.Result<(memberships: [ChatType.ChatMembershipType], page: PubNubHashedPage?), Error>) -> Void)? = nil + ) { + user.getMemberships( + limit: limit?.asKotlinInt, + page: page?.transform(), + filter: filter, + sort: sort.compactMap { $0.transform } + ).async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(response): + completion?(.success(( + memberships: response.memberships.compactMap { + $0 as? PubNubChat.Membership + }.map { + MembershipImpl(membership: $0) + }, + page: PubNubHashedPageBase( + start: response.next?.pageHash, + end: response.prev?.pageHash, + totalCount: Int(response.total) + ) + ))) + case let .failure(error): + completion?(.failure(error)) + } + } + } + + public func streamUpdates(callback: @escaping ((UserImpl?) -> Void)) -> AutoCloseable { + AutoCloseableImpl( + user.streamUpdates { [weak self] in + if self != nil { + if let user = $0 { + callback(UserImpl(user: user)) + } else { + callback(nil) + } + } + } + ) + } + + public func active(completion: ((Swift.Result) -> Void)? = nil) { + user.active().async(caller: self) { (result: FutureResult) in + switch result.result { + case let .success(boolResult): + completion?(.success(boolResult)) + case let .failure(error): + completion?(.failure(error)) + } + } + } +} diff --git a/Sources/Extensions/Dictionary.swift b/Sources/Extensions/Dictionary.swift new file mode 100644 index 0000000..8f2f295 --- /dev/null +++ b/Sources/Extensions/Dictionary.swift @@ -0,0 +1,111 @@ +// +// Dictionary.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +extension [String: Any] { + func mapToScalars() -> [String: JSONCodableScalar] { + compactMapValues { element -> JSONCodableScalar? in + if let element = element as? Int { + return element + } else if let element = element as? Double { + return element + } else if let element = element as? Bool { + return element + } else { + return element as? JSONCodableScalar + } + } + } +} + +extension [String: [String: [Action]]] { + func transform() -> [String: [String: [PubNubChat.PNFetchMessageItem.Action]]] { + compactMapValues { + $0.compactMapValues { + $0.map { + PubNubChat.PNFetchMessageItem.Action( + uuid: $0.uuid, + actionTimetoken: Int64($0.actionTimetoken) + ) + } + } + } + } +} + +extension [String: JSONCodableScalar] { + func asCustomObject() -> PubNubChat.CustomObject { + PubNubChat.CustomObject(value: self) + } +} + +extension [String: [PubNubChat.PNFetchMessageItem.Action]] { + func transform() -> [String: [Action]] { + compactMapValues { + $0.map { + Action( + uuid: $0.uuid, + actionTimetoken: Timetoken($0.actionTimetoken) + ) + } + } + } +} + +extension [String: [String: [PNFetchMessageItem.Action]]] { + func transform() -> [String: [String: [Action]]] { + compactMapValues { + $0.compactMapValues { + $0.map { + Action( + uuid: $0.uuid, + actionTimetoken: Timetoken($0.actionTimetoken) + ) + } + } + } + } +} + +extension [KotlinInt: PubNubChat.MessageMentionedUser] { + func transform() -> MessageMentionedUsers { + reduce(into: MessageMentionedUsers()) { res, currentItem in + res[currentItem.key.intValue] = MessageMentionedUser( + id: currentItem.value.id, + name: currentItem.value.name + ) + } + } +} + +extension [KotlinInt: PubNubChat.MessageReferencedChannel] { + func transform() -> MessageReferencedChannels { + reduce(into: MessageReferencedChannels()) { res, currentItem in + res[currentItem.key.intValue] = MessageReferencedChannel( + id: currentItem.value.id, + name: currentItem.value.name + ) + } + } +} + +extension [PubNubSwiftChatSDK.ChannelType: Int64] { + func transform() -> [PubNubChat.ChannelType: Any] { + ChatConfigurationKt.RateLimitPerChannel( + direct: KotlinDurationUtils.companion.toMilliseconds(interval: self[.direct] ?? 0), + group: KotlinDurationUtils.companion.toMilliseconds(interval: self[.group] ?? 0), + public: KotlinDurationUtils.companion.toMilliseconds(interval: self[.public] ?? 0), + unknown: KotlinDurationUtils.companion.toMilliseconds(interval: self[.unknown] ?? 0) + ) + } +} diff --git a/Sources/Extensions/Int.swift b/Sources/Extensions/Int.swift new file mode 100644 index 0000000..08c15d8 --- /dev/null +++ b/Sources/Extensions/Int.swift @@ -0,0 +1,18 @@ +// +// Int.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat + +extension Int { + var asKotlinInt: KotlinInt { + KotlinInt(integerLiteral: self) + } +} diff --git a/Sources/Extensions/PubNub.MembershipSortField.swift b/Sources/Extensions/PubNub.MembershipSortField.swift new file mode 100644 index 0000000..3811f4f --- /dev/null +++ b/Sources/Extensions/PubNub.MembershipSortField.swift @@ -0,0 +1,35 @@ +// +// PubNub.MembershipSortField.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +extension PubNub.MembershipSortField { + func transform() -> PNSortKey? { + switch property { + case let .object(objectSortProperty): + switch objectSortProperty { + case .id: + return ascending ? PNSortKeyPNAsc(key: PNMembershipKey.channelId) : PNSortKeyPNDesc(key: .channelId) + case .name: + return ascending ? PNSortKeyPNAsc(key: PNMembershipKey.channelName) : PNSortKeyPNDesc(key: .channelName) + case .updated: + return ascending ? PNSortKeyPNAsc(key: PNMembershipKey.channelUpdated) : PNSortKeyPNDesc(key: .channelUpdated) + case .type, .status: + return nil + } + case .status: + return nil + case .updated: + return ascending ? PNSortKeyPNAsc(key: PNMembershipKey.updated) : PNSortKeyPNDesc(key: .updated) + } + } +} diff --git a/Sources/Extensions/PubNub.ObjectSortField.swift b/Sources/Extensions/PubNub.ObjectSortField.swift new file mode 100644 index 0000000..a540d61 --- /dev/null +++ b/Sources/Extensions/PubNub.ObjectSortField.swift @@ -0,0 +1,30 @@ +// +// PubNub.ObjectSortField.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +extension PubNub.ObjectSortField { + func transform() -> PNSortKey? { + switch property { + case .id: + return ascending ? PNSortKeyPNAsc(key: .id) : PNSortKeyPNDesc(key: .id) + case .name: + return ascending ? PNSortKeyPNAsc(key: .name) : PNSortKeyPNDesc(key: .name) + case .type: + return ascending ? PNSortKeyPNAsc(key: .type) : PNSortKeyPNDesc(key: .type) + case .status: + return ascending ? PNSortKeyPNAsc(key: .status) : PNSortKeyPNDesc(key: .status) + case .updated: + return ascending ? PNSortKeyPNAsc(key: .updated) : PNSortKeyPNDesc(key: .updated) + } + } +} diff --git a/Sources/Extensions/PubNub.PushEnvironment.swift b/Sources/Extensions/PubNub.PushEnvironment.swift new file mode 100644 index 0000000..7513dfa --- /dev/null +++ b/Sources/Extensions/PubNub.PushEnvironment.swift @@ -0,0 +1,24 @@ +// +// PubNub.PushEnvironment.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +extension PubNub.PushEnvironment { + func transform() -> PubNubChat.PNPushEnvironment { + switch self { + case .development: + .development + case .production: + .production + } + } +} diff --git a/Sources/Extensions/PubNub.PushService.swift b/Sources/Extensions/PubNub.PushService.swift new file mode 100644 index 0000000..3f3ab9a --- /dev/null +++ b/Sources/Extensions/PubNub.PushService.swift @@ -0,0 +1,28 @@ +// +// PubNub.PushService.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +extension PubNub.PushService { + func transform() -> PubNubChat.PNPushType { + switch self { + case .gcm: + .gcm + case .fcm: + .fcm + case .apns: + .apns + case .mpns: + .mpns + } + } +} diff --git a/Sources/Extensions/PubNubChat+Transform.swift b/Sources/Extensions/PubNubChat+Transform.swift new file mode 100644 index 0000000..423414c --- /dev/null +++ b/Sources/Extensions/PubNubChat+Transform.swift @@ -0,0 +1,65 @@ +// +// PubNubChat+Transform.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +extension PubNubChat.QuotedMessage { + func transform() -> QuotedMessage { + QuotedMessage( + timetoken: Timetoken(timetoken), + text: text, + userId: userId + ) + } +} + +extension PubNubChat.File { + func transform() -> File { + File( + name: name, + id: id, + url: url, + type: type + ) + } +} + +extension PubNubChat.TextLink { + func transform() -> TextLink { + TextLink( + startIndex: Int(startIndex), + endIndex: Int(endIndex), + link: link + ) + } +} + +extension [PubNubChat.File] { + func transform() -> [File] { + map { $0.transform() } + } +} + +extension [PubNubChat.TextLink] { + func transform() -> [TextLink] { + map { $0.transform() } + } +} + +extension PubNubChat.EventContent.TextMessageContent { + func transform() -> EventContent.TextMessageContent { + EventContent.TextMessageContent( + text: text, + files: files?.transform() + ) + } +} diff --git a/Sources/Extensions/PubNubChat.ChannelType.swift b/Sources/Extensions/PubNubChat.ChannelType.swift new file mode 100644 index 0000000..a8de366 --- /dev/null +++ b/Sources/Extensions/PubNubChat.ChannelType.swift @@ -0,0 +1,28 @@ +// +// PubNubChat.ChannelType.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat + +extension PubNubChat.ChannelType { + func transform() -> ChannelType { + if self == .direct { + .direct + } else if self == .public_ { + .public + } else if self == .group { + .group + } else if self == .unknown { + .unknown + } else { + .unknown + } + } +} diff --git a/Sources/Extensions/PubNubChat.Event.swift b/Sources/Extensions/PubNubChat.Event.swift new file mode 100644 index 0000000..609e6d9 --- /dev/null +++ b/Sources/Extensions/PubNubChat.Event.swift @@ -0,0 +1,71 @@ +// +// PubNubChat.Event.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +extension PubNubChat.EventContent { + func map() -> EventContent { + switch self { + case let content as PubNubChat.EventContent.Typing: + EventContent.Typing( + value: content.value + ) + case let content as PubNubChat.EventContent.Receipt: + EventContent.Receipt( + messageTimetoken: Timetoken(content.messageTimetoken) + ) + case let content as PubNubChat.EventContent.UnknownMessageFormat: + EventContent.UnknownMessageFormat( + element: content.jsonElement.value + ) + case let content as PubNubChat.EventContent.Invite: + EventContent.Invite( + channelType: content.channelType.transform(), + channelId: content.channelId + ) + case let content as PubNubChat.EventContent.Mention: + EventContent.Mention( + messageTimetoken: Timetoken(content.messageTimetoken), + channel: content.channel, + parentChannel: content.parentChannel + ) + case let content as PubNubChat.EventContent.Report: + EventContent.Report( + text: content.text, + reason: content.reason, + reportedMessageTimetoken: content.reportedMessageTimetoken?.uint64Value, + reportedMessageChannelId: content.reportedMessageChannelId, + reportedUserId: content.reportedUserId + ) + case let content as PubNubChat.EventContent.Custom: + EventContent.Custom( + data: content.data, + method: content.method == .signal ? .signal : .publish + ) + case let content as PubNubChat.EventContent.Moderation: + EventContent.Moderation( + channelId: content.channelId, + restriction: content.restriction.transform(), + reason: content.reason + ) + case let content as PubNubChat.EventContent.TextMessageContent: + EventContent.TextMessageContent( + text: content.text, + files: content.files?.map { File(name: $0.name, id: $0.id, url: $0.url, type: $0.type) } + ) + default: + EventContent.UnknownMessageFormat( + element: self + ) + } + } +} diff --git a/Sources/Extensions/PubNubChat.KotlinArray.swift b/Sources/Extensions/PubNubChat.KotlinArray.swift new file mode 100644 index 0000000..094f327 --- /dev/null +++ b/Sources/Extensions/PubNubChat.KotlinArray.swift @@ -0,0 +1,30 @@ +// +// PubNubChat.KotlinArray.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat + +func transformKotlinArray(_ array: KotlinArray, mapper: (T) -> U) -> [U] { + var iterator: KotlinIterator? = array.iterator() + var resultingItems: [U] = [] + + while iterator?.hasNext() ?? false { + guard let item = iterator?.next() as? T else { + continue + } + resultingItems.append(mapper(item)) + + if iterator?.hasNext() ?? false { + iterator = iterator?.next() as? KotlinIterator + } + } + + return resultingItems +} diff --git a/Sources/Extensions/PubNubChat.PNPushEnvironment.swift b/Sources/Extensions/PubNubChat.PNPushEnvironment.swift new file mode 100644 index 0000000..c824a31 --- /dev/null +++ b/Sources/Extensions/PubNubChat.PNPushEnvironment.swift @@ -0,0 +1,26 @@ +// +// PubNubChat.PNPushEnvironment.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +extension PubNubChat.PNPushEnvironment { + func transform() -> PubNub.PushEnvironment { + switch self { + case .development: + .development + case .production: + .production + default: + .development + } + } +} diff --git a/Sources/Extensions/PubNubChat.PNPushType.swift b/Sources/Extensions/PubNubChat.PNPushType.swift new file mode 100644 index 0000000..147b36c --- /dev/null +++ b/Sources/Extensions/PubNubChat.PNPushType.swift @@ -0,0 +1,30 @@ +// +// PubNubChat.PNPushType.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +extension PubNubChat.PNPushType { + func transform() -> PubNub.PushService { + switch self { + case .apns: + .apns + case .mpns: + .mpns + case .gcm: + .fcm + case .apns2: + .apns + default: + .fcm + } + } +} diff --git a/Sources/Extensions/PubNubChat.RestrictionType.swift b/Sources/Extensions/PubNubChat.RestrictionType.swift new file mode 100644 index 0000000..94feb1d --- /dev/null +++ b/Sources/Extensions/PubNubChat.RestrictionType.swift @@ -0,0 +1,24 @@ +// +// PubNubChat.RestrictionType.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat + +extension PubNubChat.RestrictionType { + func transform() -> PubNubSwiftChatSDK.RestrictionType { + if self == .ban { + .ban + } else if self == .mute { + .mute + } else { + .lift + } + } +} diff --git a/Sources/Extensions/PubNubHashedPage.swift b/Sources/Extensions/PubNubHashedPage.swift new file mode 100644 index 0000000..6055614 --- /dev/null +++ b/Sources/Extensions/PubNubHashedPage.swift @@ -0,0 +1,25 @@ +// +// PubNubHashedPage.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +extension PubNubHashedPage { + func transform() -> PNPage? { + if let start { + PNPage.PNNext(pageHash: start) + } else if let end { + PNPage.PNPrev(pageHash: end) + } else { + nil + } + } +} diff --git a/Sources/Extensions/Timetoken.swift b/Sources/Extensions/Timetoken.swift new file mode 100644 index 0000000..a255257 --- /dev/null +++ b/Sources/Extensions/Timetoken.swift @@ -0,0 +1,19 @@ +// +// Timetoken.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +extension Timetoken { + func asKotlinLong() -> KotlinLong { + KotlinLong(value: Int64(self)) + } +} diff --git a/Sources/Miscellaneous/AutoCloseable.swift b/Sources/Miscellaneous/AutoCloseable.swift new file mode 100644 index 0000000..a926367 --- /dev/null +++ b/Sources/Miscellaneous/AutoCloseable.swift @@ -0,0 +1,42 @@ +// +// AutoCloseable.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat + +public protocol AutoCloseable { + func close() +} + +class AutoCloseableImpl { + private let underlying: PubNubChat.KotlinAutoCloseable + + init?(_ underlying: PubNubChat.KotlinAutoCloseable?) { + if let underlying { + self.underlying = underlying + } else { + return nil + } + } + + init(_ underlying: PubNubChat.KotlinAutoCloseable) { + self.underlying = underlying + } + + deinit { + underlying.close() + } +} + +extension AutoCloseableImpl: AutoCloseable { + func close() { + underlying.close() + } +} diff --git a/Sources/Miscellaneous/ChatAdapter.swift b/Sources/Miscellaneous/ChatAdapter.swift new file mode 100644 index 0000000..c322f7a --- /dev/null +++ b/Sources/Miscellaneous/ChatAdapter.swift @@ -0,0 +1,57 @@ +// +// ChatAdapter.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat + +class ChatAdapter { + static var associations: [Association] = [] + + private init() {} + + static func map(chat: PubNubChat.Chat) -> Association { + if let association = associations.first(where: { !$0.isEmpty() && $0.rawChat === chat }) { + association + } else { + preconditionFailure("Cannot find Chat object matching \(chat)") + } + } + + static func associate(chat: ChatImpl, rawChat: PubNubChat.ChatImpl) { + if !associations.contains(where: { !$0.isEmpty() && $0.chat !== chat && $0.rawChat !== rawChat }) { + associations.append(.init(chat: chat, rawChat: rawChat)) + } + } + + static func clean() { + associations.removeAll { + $0.isEmpty() + } + } + + class Association { + // swiftlint:disable:next force_unwrapping + var chat: ChatImpl { _chat! } + // swiftlint:disable:next force_unwrapping + var rawChat: PubNubChat.ChatImpl { _rawChat! } + + private weak var _chat: ChatImpl? + private weak var _rawChat: PubNubChat.ChatImpl? + + init(chat: ChatImpl, rawChat: PubNubChat.ChatImpl) { + _chat = chat + _rawChat = rawChat + } + + func isEmpty() -> Bool { + _chat == nil || _rawChat == nil + } + } +} diff --git a/Sources/Miscellaneous/Constants.swift b/Sources/Miscellaneous/Constants.swift new file mode 100644 index 0000000..8c8de91 --- /dev/null +++ b/Sources/Miscellaneous/Constants.swift @@ -0,0 +1,13 @@ +// +// Constants.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation + +let objectNoLongerExists = "Object no longer exists" diff --git a/Sources/Miscellaneous/FutureResult.swift b/Sources/Miscellaneous/FutureResult.swift new file mode 100644 index 0000000..45b28bb --- /dev/null +++ b/Sources/Miscellaneous/FutureResult.swift @@ -0,0 +1,114 @@ +// +// FutureResult.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat + +class FutureResult { + let caller: C + let result: Swift.Result + + init(result: Swift.Result, caller: C) { + self.result = result + self.caller = caller + } +} + +class WeakFutureResult { + weak var caller: C? + let result: Swift.Result + + init(result: Swift.Result, caller: C?) { + self.result = result + self.caller = caller + } +} + +class BaseFutureConsumer: PubNubChat.Consumer { + func extractValueFromKotlinResult(p: Any?) -> Swift.Result { + if let result = p as? PubNubChat.Result { + if let exception = result.exceptionOrNull() { + return .failure(ChatError(underlying: exception.asError(), message: exception.message ?? "")) + } else if let value = result.getOrNull() as? T { + return .success(value) + } + } + + return .failure(ChatError(message: "Unexpected argument of type \(type(of: T.self))")) + } + + func accept(p: Any?) { + notifyCaller(with: extractValueFromKotlinResult(p: p)) + } + + func notifyCaller(with _: Swift.Result) { + // Provide implementation in each subclasses + } +} + +class StrongFutureConsumer: BaseFutureConsumer { + private var caller: C? + private var callback: ((FutureResult) -> Void)? + + init(caller: C, callback: @escaping (FutureResult) -> Void) { + self.caller = caller + self.callback = callback + } + + override func notifyCaller(with swiftResult: Swift.Result) { + switch swiftResult { + case let .success(value): + // swiftlint:disable:next force_unwrapping + callback?(.init(result: .success(value), caller: caller!)) + case let .failure(failure): + // swiftlint:disable:next force_unwrapping + callback?(.init(result: .failure(failure), caller: caller!)) + } + + caller = nil + callback = nil + } +} + +class WeakFutureConsumer: BaseFutureConsumer { + private weak var caller: C? + private var callback: ((WeakFutureResult) -> Void)? + + init(caller: C, callback: @escaping (WeakFutureResult) -> Void) { + self.caller = caller + self.callback = callback + } + + override func notifyCaller(with swiftResult: Swift.Result) { + if let caller { + switch swiftResult { + case let .success(value): + callback?(.init(result: .success(value), caller: caller)) + case let .failure(failure): + callback?(.init(result: .failure(failure), caller: caller)) + } + } else { + callback?(.init(result: .failure(ChatError(message: "The caller object does not exists")), caller: nil)) + } + + callback = nil + caller = nil + } +} + +extension PNFuture { + func async(caller: C, callback: @escaping ((FutureResult) -> Void)) { + async(callback: StrongFutureConsumer(caller: caller, callback: callback)) + } + + func weakAsync(caller: C, callback: @escaping ((WeakFutureResult) -> Void)) { + async(callback: WeakFutureConsumer(caller: caller, callback: callback)) + } +} diff --git a/Sources/Models/Action.swift b/Sources/Models/Action.swift new file mode 100644 index 0000000..09b238b --- /dev/null +++ b/Sources/Models/Action.swift @@ -0,0 +1,22 @@ +// +// Action.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubSDK + +public struct Action { + public var uuid: String + public var actionTimetoken: Timetoken + + public init(uuid: String, actionTimetoken: Timetoken) { + self.uuid = uuid + self.actionTimetoken = actionTimetoken + } +} diff --git a/Sources/Models/ChannelType.swift b/Sources/Models/ChannelType.swift new file mode 100644 index 0000000..ca26bb6 --- /dev/null +++ b/Sources/Models/ChannelType.swift @@ -0,0 +1,33 @@ +// +// ChannelType.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +public enum ChannelType: String, CaseIterable { + case direct + case group + case `public` + case unknown + + func transform() -> PubNubChat.ChannelType { + switch self { + case .direct: + .direct + case .group: + .group + case .public: + .public_ + case .unknown: + .unknown + } + } +} diff --git a/Sources/Models/ChatError.swift b/Sources/Models/ChatError.swift new file mode 100644 index 0000000..27715e8 --- /dev/null +++ b/Sources/Models/ChatError.swift @@ -0,0 +1,21 @@ +// +// ChatError.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation + +public struct ChatError: Error { + public let underlying: Error? + public let message: String + + init(underlying: Error? = nil, message: String? = nil) { + self.underlying = underlying + self.message = message ?? "" + } +} diff --git a/Sources/Models/CreateDirectConversationResult.swift b/Sources/Models/CreateDirectConversationResult.swift new file mode 100644 index 0000000..ae1aafc --- /dev/null +++ b/Sources/Models/CreateDirectConversationResult.swift @@ -0,0 +1,17 @@ +// +// CreateDirectConversationResult.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation + +public struct CreateDirectConversationResult { + public var channel: C + public var hostMembership: M + public var inviteeMembership: M +} diff --git a/Sources/Models/CreateGroupConversationResult.swift b/Sources/Models/CreateGroupConversationResult.swift new file mode 100644 index 0000000..51ccb7a --- /dev/null +++ b/Sources/Models/CreateGroupConversationResult.swift @@ -0,0 +1,17 @@ +// +// CreateGroupConversationResult.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation + +public struct CreateGroupConversationResult { + public var channel: C + public var hostMembership: M + public var inviteeMemberships: [M] +} diff --git a/Sources/Models/EmitEventMethod.swift b/Sources/Models/EmitEventMethod.swift new file mode 100644 index 0000000..b60bb6f --- /dev/null +++ b/Sources/Models/EmitEventMethod.swift @@ -0,0 +1,16 @@ +// +// EmitEventMethod.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation + +public enum EmitEventMethod { + case signal + case publish +} diff --git a/Sources/Models/Event.swift b/Sources/Models/Event.swift new file mode 100644 index 0000000..20ac4a1 --- /dev/null +++ b/Sources/Models/Event.swift @@ -0,0 +1,35 @@ +// +// Event.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubSDK + +public protocol Event { + associatedtype C: Chat + associatedtype T: EventContent + + var chat: C { get } + var timetoken: Timetoken { get } + var payload: T { get } + var channelId: String { get } + var userId: String { get } +} + +public struct EventWrapper { + public var event: any Event +} + +public struct EventImpl: Event { + public let chat: ChatType + public let timetoken: Timetoken + public let payload: T + public let channelId: String + public let userId: String +} diff --git a/Sources/Models/EventContent.swift b/Sources/Models/EventContent.swift new file mode 100644 index 0000000..7179b5c --- /dev/null +++ b/Sources/Models/EventContent.swift @@ -0,0 +1,291 @@ +// +// EventContent.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +public class EventContent { + // MARK: - Report + + public class Report: EventContent { + public let text: String? + public let reason: String + public let reportedMessageTimetoken: Timetoken? + public let reportedMessageChannelId: String? + public let reportedUserId: String? + + public init( + text: String? = nil, + reason: String, + reportedMessageTimetoken: Timetoken? = nil, + reportedMessageChannelId: String? = nil, + reportedUserId: String? = nil + ) { + self.text = text + self.reason = reason + self.reportedMessageTimetoken = reportedMessageTimetoken + self.reportedMessageChannelId = reportedMessageChannelId + self.reportedUserId = reportedUserId + } + } + + // MARK: - Typing + + public class Typing: EventContent { + public let value: Bool + + public init(value: Bool) { + self.value = value + } + } + + // MARK: - Receipt + + public class Receipt: EventContent { + public let messageTimetoken: Timetoken + + public init(messageTimetoken: Timetoken) { + self.messageTimetoken = messageTimetoken + } + } + + // MARK: - Mention + + public class Mention: EventContent { + public let messageTimetoken: Timetoken + public let channel: String + public let parentChannel: String? + + public init(messageTimetoken: Timetoken, channel: String, parentChannel: String? = nil) { + self.messageTimetoken = messageTimetoken + self.channel = channel + self.parentChannel = parentChannel + } + } + + // MARK: - Invite + + public class Invite: EventContent { + public let channelType: ChannelType + public let channelId: String + + public init(channelType: ChannelType, channelId: String) { + self.channelType = channelType + self.channelId = channelId + } + } + + // MARK: - Custom + + public class Custom: EventContent { + public let data: [String: Any] + public let method: EmitEventMethod + + public init(data: [String: Any], method: EmitEventMethod) { + self.data = data + self.method = method + } + } + + // MARK: - Moderation + + public class Moderation: EventContent { + public let channelId: String + public let restriction: RestrictionType + public let reason: String? + + public init(channelId: String, restriction: RestrictionType, reason: String?) { + self.channelId = channelId + self.restriction = restriction + self.reason = reason + } + } + + // MARK: - TextMessageContent + + public class TextMessageContent: EventContent { + public let text: String + public let files: [File]? + + public init(text: String, files: [File]? = nil) { + self.text = text + self.files = files + } + + func transform() -> PubNubChat.EventContent.TextMessageContent { + PubNubChat.EventContent.TextMessageContent( + text: text, + files: files?.map { + PubNubChat.File( + name: $0.name, + id: $0.id, + url: $0.url, + type: $0.type + ) + } + ) + } + } + + // MARK: - UnknownMessageFormat + + public class UnknownMessageFormat: EventContent { + public let element: Any? + + public init(element: Any? = nil) { + self.element = element + } + } +} + +// MARK: - EventContent Transformations + +extension EventContent { + static func from(rawValue: PubNubChat.EventContent) -> EventContent { + switch rawValue { + case let content as PubNubChat.EventContent.Typing: + EventContent.Typing( + value: content.value + ) + case let content as PubNubChat.EventContent.Report: + EventContent.Report( + text: content.text, + reason: content.reason, + reportedMessageTimetoken: content.reportedMessageTimetoken?.uint64Value, + reportedMessageChannelId: content.reportedMessageChannelId, + reportedUserId: content.reportedUserId + ) + case let conent as PubNubChat.EventContent.Receipt: + EventContent.Receipt( + messageTimetoken: Timetoken(conent.messageTimetoken) + ) + case let content as PubNubChat.EventContent.Mention: + EventContent.Mention( + messageTimetoken: Timetoken(content.messageTimetoken), + channel: content.channel, + parentChannel: content.parentChannel + ) + case let content as PubNubChat.EventContent.Invite: + EventContent.Invite( + channelType: content.channelType.transform(), + channelId: content.channelId + ) + case let content as PubNubChat.EventContent.Custom: + EventContent.Custom( + data: content.data, + method: content.method == .signal ? .signal : .publish + ) + case let content as PubNubChat.EventContent.TextMessageContent: + EventContent.TextMessageContent( + text: content.text, + files: content.files?.compactMap { $0.transform() } + ) + case let content as PubNubChat.EventContent.UnknownMessageFormat: + EventContent.UnknownMessageFormat( + element: content.jsonElement.value + ) + default: + EventContent.UnknownMessageFormat( + element: rawValue + ) + } + } +} + +// MARK: - EventContent Transformations + +extension EventContent { + static func transform(content: EventContent) -> PubNubChat.EventContent { + switch content { + case let content as EventContent.Typing: + PubNubChat.EventContent.Typing(value: content.value) + case let content as EventContent.Report: + PubNubChat.EventContent.Report( + text: content.text, + reason: content.reason, + reportedMessageTimetoken: content.reportedMessageTimetoken?.asKotlinLong(), + reportedMessageChannelId: content.reportedMessageChannelId, + reportedUserId: content.reportedUserId + ) + case let content as EventContent.Receipt: + PubNubChat.EventContent.Receipt( + messageTimetoken: Int64(content.messageTimetoken) + ) + case let content as EventContent.Mention: + PubNubChat.EventContent.Mention( + messageTimetoken: Int64(content.messageTimetoken), + channel: content.channel, + parentChannel: content.parentChannel + ) + case let content as EventContent.Invite: + PubNubChat.EventContent.Invite( + channelType: content.channelType.transform(), + channelId: content.channelId + ) + case let content as EventContent.Custom: + PubNubChat.EventContent.Custom( + data: content.data, + method: content.method == .signal ? .signal : .publish + ) + case let content as EventContent.Moderation: + PubNubChat.EventContent.Moderation( + channelId: content.channelId, + restriction: content.restriction.transform(), + reason: content.reason + ) + case let content as EventContent.TextMessageContent: + PubNubChat.EventContent.TextMessageContent( + text: content.text, + files: content.files?.map { + PubNubChat.File( + name: $0.name, + id: $0.id, + url: $0.url, + type: $0.type + ) + } + ) + case let content as EventContent.UnknownMessageFormat: + PubNubChat.EventContent.UnknownMessageFormat( + jsonElement: JsonElement(value: content.element) + ) + default: + PubNubChat.EventContent.UnknownMessageFormat( + jsonElement: JsonElement(value: nil) + ) + } + } +} + +extension EventContent { + static func classIdentifier(type: EventContent.Type) -> any KotlinKClass { + switch type { + case is Report.Type: + KClassConstants.Companion.shared.report + case is Typing.Type: + KClassConstants.Companion.shared.typing + case is Receipt.Type: + KClassConstants.Companion.shared.receipt + case is Mention.Type: + KClassConstants.Companion.shared.mention + case is Invite.Type: + KClassConstants.Companion.shared.invite + case is Custom.Type: + KClassConstants.Companion.shared.custom + case is Moderation.Type: + KClassConstants.Companion.shared.moderation + case is TextMessageContent.Type: + KClassConstants.Companion.shared.textMessageContent + default: + KClassConstants.Companion.shared.unknownMessageFormat + } + } +} diff --git a/Sources/Models/File.swift b/Sources/Models/File.swift new file mode 100644 index 0000000..85c0cd2 --- /dev/null +++ b/Sources/Models/File.swift @@ -0,0 +1,25 @@ +// +// File.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation + +public struct File { + public var name: String + public var id: String + public var url: String + public var type: String? + + public init(name: String, id: String, url: String, type: String?) { + self.name = name + self.id = id + self.url = url + self.type = type + } +} diff --git a/Sources/Models/GetCurrentUserMentionsResult.swift b/Sources/Models/GetCurrentUserMentionsResult.swift new file mode 100644 index 0000000..33352e3 --- /dev/null +++ b/Sources/Models/GetCurrentUserMentionsResult.swift @@ -0,0 +1,44 @@ +// +// GetCurrentUserMentionsResult.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubSDK + +public struct GetCurrentUserMentionsResult { + public var enhancedMentionsData: [UserMentionDataWrapper] + public var isMore: Bool +} + +public struct UserMentionDataWrapper { + public var userMentionData: any UserMentionData +} + +public protocol UserMentionData { + associatedtype M: Message + + var event: EventContent.Mention { get } + var message: M? { get } + var userId: String { get } +} + +public struct ChannelMentionData: UserMentionData { + public var event: EventContent.Mention + public var message: M? + public var userId: String + public var channelId: String +} + +public struct ThreadMentionData: UserMentionData { + public var event: EventContent.Mention + public var message: M? + public var userId: String + public var parentChannelId: String + public var threadChannelId: String +} diff --git a/Sources/Models/GetFileItem.swift b/Sources/Models/GetFileItem.swift new file mode 100644 index 0000000..f702468 --- /dev/null +++ b/Sources/Models/GetFileItem.swift @@ -0,0 +1,23 @@ +// +// GetFileItem.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation + +public class GetFileItem { + public var name: String + public var id: String + public var url: String + + init(name: String, id: String, url: String) { + self.name = name + self.id = id + self.url = url + } +} diff --git a/Sources/Models/GetUnreadMessagesCount.swift b/Sources/Models/GetUnreadMessagesCount.swift new file mode 100644 index 0000000..189598d --- /dev/null +++ b/Sources/Models/GetUnreadMessagesCount.swift @@ -0,0 +1,17 @@ +// +// GetUnreadMessagesCount.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation + +public struct GetUnreadMessagesCount { + public var channel: C + public var membership: M + public var count: UInt64 +} diff --git a/Sources/Models/InputFile.swift b/Sources/Models/InputFile.swift new file mode 100644 index 0000000..e5e4ecb --- /dev/null +++ b/Sources/Models/InputFile.swift @@ -0,0 +1,52 @@ +// +// InputFile.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +public struct InputFile { + public var name: String + public var type: String + public var source: PubNub.FileUploadContent + + public init(name: String, type: String, source: PubNub.FileUploadContent) { + self.name = name + self.type = type + self.source = source + } + + func transform() -> PubNubChat.InputFile? { + switch source { + case let .file(url): + PubNubChat.InputFile( + name: name, + type: type, + source: PubNubChat.FileUploadContent(url: url) + ) + case let .data(data, contentType): + PubNubChat.InputFile( + name: name, + type: type, + source: PubNubChat.DataUploadContent(data: data, contentType: contentType) + ) + case let .stream(stream, contentType, contentLength): + PubNubChat.InputFile( + name: name, + type: type, + source: PubNubChat.StreamUploadContent( + stream: stream, + contentType: contentType, + contentLength: Int32(contentLength) + ) + ) + } + } +} diff --git a/Sources/Models/MessageActionType.swift b/Sources/Models/MessageActionType.swift new file mode 100644 index 0000000..14574ca --- /dev/null +++ b/Sources/Models/MessageActionType.swift @@ -0,0 +1,17 @@ +// +// MessageActionType.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation + +public enum MessageActionType: String { + case reactions + case deleted + case edited +} diff --git a/Sources/Models/MessageMentionedUser.swift b/Sources/Models/MessageMentionedUser.swift new file mode 100644 index 0000000..e1b9249 --- /dev/null +++ b/Sources/Models/MessageMentionedUser.swift @@ -0,0 +1,35 @@ +// +// MessageMentionedUser.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat + +public typealias MessageMentionedUsers = [Int: MessageMentionedUser] + +public struct MessageMentionedUser { + public var id: String + public var name: String + + public init(id: String, name: String) { + self.id = id + self.name = name + } +} + +extension MessageMentionedUsers { + func transform() -> [KotlinInt: PubNubChat.MessageMentionedUser] { + reduce(into: [KotlinInt: PubNubChat.MessageMentionedUser]()) { res, item in + res[item.key.asKotlinInt] = PubNubChat.MessageMentionedUser( + id: item.value.id, + name: item.value.name + ) + } + } +} diff --git a/Sources/Models/MessageReferencedChannel.swift b/Sources/Models/MessageReferencedChannel.swift new file mode 100644 index 0000000..9895df8 --- /dev/null +++ b/Sources/Models/MessageReferencedChannel.swift @@ -0,0 +1,35 @@ +// +// MessageReferencedChannel.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat + +public typealias MessageReferencedChannels = [Int: MessageReferencedChannel] + +public struct MessageReferencedChannel { + public var id: String + public var name: String + + public init(id: String, name: String) { + self.id = id + self.name = name + } +} + +extension MessageReferencedChannels { + func transform() -> [KotlinInt: PubNubChat.MessageReferencedChannel] { + reduce(into: [KotlinInt: PubNubChat.MessageReferencedChannel]()) { res, item in + res[item.key.asKotlinInt] = PubNubChat.MessageReferencedChannel( + id: item.value.id, + name: item.value.name + ) + } + } +} diff --git a/Sources/Models/QuotedMessage.swift b/Sources/Models/QuotedMessage.swift new file mode 100644 index 0000000..f8924e5 --- /dev/null +++ b/Sources/Models/QuotedMessage.swift @@ -0,0 +1,33 @@ +// +// QuotedMessage.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat +import PubNubSDK + +public struct QuotedMessage { + public var timetoken: Timetoken + public var text: String + public var userId: String + + public init(timetoken: Timetoken, text: String, userId: String) { + self.timetoken = timetoken + self.text = text + self.userId = userId + } + + func transform() -> PubNubChat.QuotedMessage { + PubNubChat.QuotedMessage( + timetoken: Int64(timetoken), + text: text, + userId: userId + ) + } +} diff --git a/Sources/Models/Restriction.swift b/Sources/Models/Restriction.swift new file mode 100644 index 0000000..ac4c10e --- /dev/null +++ b/Sources/Models/Restriction.swift @@ -0,0 +1,45 @@ +// +// Restriction.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat + +public struct Restriction { + public var userId: String + public var channelId: String + public var ban: Bool + public var mute: Bool + public var reason: String? + + public init(userId: String, channelId: String, ban: Bool, mute: Bool, reason: String? = nil) { + self.userId = userId + self.channelId = channelId + self.ban = ban + self.mute = mute + self.reason = reason + } +} + +public enum RestrictionType { + case ban + case mute + case lift + + func transform() -> PubNubChat.RestrictionType { + switch self { + case .ban: + .ban + case .mute: + .mute + case .lift: + .lift + } + } +} diff --git a/Sources/Models/TextLink.swift b/Sources/Models/TextLink.swift new file mode 100644 index 0000000..50ba2a8 --- /dev/null +++ b/Sources/Models/TextLink.swift @@ -0,0 +1,32 @@ +// +// TextLink.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubChat + +public struct TextLink { + public var startIndex: Int + public var endIndex: Int + public var link: String + + public init(startIndex: Int, endIndex: Int, link: String) { + self.startIndex = startIndex + self.endIndex = endIndex + self.link = link + } + + func transform() -> PubNubChat.TextLink { + PubNubChat.TextLink( + startIndex: Int32(startIndex), + endIndex: Int32(endIndex), + link: link + ) + } +} diff --git a/Sources/PubNubSwiftChatSDK_Info.plist b/Sources/PubNubSwiftChatSDK_Info.plist new file mode 100644 index 0000000..649c82f --- /dev/null +++ b/Sources/PubNubSwiftChatSDK_Info.plist @@ -0,0 +1,14 @@ + + + + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleName + $(PRODUCT_NAME) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/Tests/ChannelIntegrationTests.swift b/Tests/ChannelIntegrationTests.swift new file mode 100644 index 0000000..99cae3b --- /dev/null +++ b/Tests/ChannelIntegrationTests.swift @@ -0,0 +1,885 @@ +// +// ChannelTests.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import XCTest +import PubNubSDK + +@testable import PubNubSwiftChatSDK + +class ChannelIntegrationTests: PubNubSwiftChatSDKIntegrationTests { + var channel: ChannelImpl! + + override func customSetUpWitError() throws { + channel = try awaitResultValue { + chat.createChannel( + id: randomString(), + completion: $0 + ) + } + } + + override func customTearDownWithError() throws { + try awaitResult { + channel.delete( + completion: $0 + ) + } + channel = nil + } + + func testChannel_Update() throws { + let newCustom: [String: JSONCodableScalar] = [ + "a": 123, + "b": "xyz" + ] + let updatedChannel = try awaitResultValue { + channel.update( + name: "NewName", + custom: newCustom, + status: "NewStatus", + type: .public, + completion: $0 + ) + } + + XCTAssertEqual(updatedChannel.id, channel.id) + XCTAssertEqual(updatedChannel.name, "NewName") + XCTAssertEqual(updatedChannel.custom?.mapValues { $0.scalarValue }, newCustom.mapValues { $0.scalarValue }) + XCTAssertEqual(updatedChannel.status, "NewStatus") + XCTAssertEqual(updatedChannel.type, .public) + } + + func testChannel_Delete() throws { + try awaitResult { + channel.delete( + soft: false, + completion: $0 + ) + } + XCTAssertNil( + try awaitResultValue { + chat.getChannel( + channelId: channel.id, + completion: $0 + ) + } + ) + } + + func testChannel_SoftDelete() throws { + try awaitResult { + channel.delete( + soft: true, + completion: $0 + ) + } + let retrievedChannel = try awaitResultValue { + chat.getChannel( + channelId: channel.id, + completion: $0 + ) + } + + XCTAssertNotNil(retrievedChannel) + XCTAssertEqual(retrievedChannel?.id, channel.id) + } + + func testChannel_Forward() throws { + let anotherChannel = try XCTUnwrap( + try awaitResultValue { + chat.createChannel( + id: randomString(), + completion: $0 + ) + } + ) + let tt = try awaitResultValue { + anotherChannel.sendText( + text: "Some text to send", + completion: $0 + ) + } + let message = try XCTUnwrap( + try awaitResultValue { + anotherChannel.getMessage( + timetoken: tt, + completion: $0 + ) + } + ) + try awaitResultValue { + channel.forward( + message: message, + completion: $0 + ) + } + + let retrievedMssgsFromForwardedChannel = try awaitResultValue { + channel.getHistory( + completion: $0 + ) + } + + XCTAssertEqual(retrievedMssgsFromForwardedChannel.messages.count, 1) + XCTAssertEqual(retrievedMssgsFromForwardedChannel.messages.first?.channelId, channel.id) + + addTeardownBlock { [unowned self] in + try awaitResult { chat.deleteChannel( + id: anotherChannel.id, + completion: $0 + )} + } + } + + func testChannel_StartTyping() throws { + let expectation = expectation(description: "GetTyping") + expectation.assertForOverFulfill = true + expectation.expectedFulfillmentCount = 1 + + let closeable = channel.getTyping { [unowned self] in + XCTAssertEqual($0.count, 1) + XCTAssertEqual(chat.currentUser.id, $0.first) + expectation.fulfill() + } + try awaitResultValue(delay: 1) { + channel.startTyping( + completion: $0 + ) + } + + wait( + for: [expectation], + timeout: 3 + ) + addTeardownBlock { + closeable.close() + } + } + + func testChannel_StopTyping() throws { + XCTAssertNotNil( + try awaitResultValue { + channel.stopTyping(completion: $0) + } + ) + } + + func testChannel_WhoIsPresent() throws { + let joinValue = try awaitResultValue { + channel.join( + completion: $0 + ) + } + + let whoIsPresentValue = try awaitResultValue(delay: 4) { + channel.whoIsPresent( + completion: $0 + ) + } + + XCTAssertEqual(whoIsPresentValue.count, 1) + XCTAssertEqual(whoIsPresentValue.first, chat.currentUser.id) + + addTeardownBlock { + joinValue.disconnect?.close() + } + } + + func testChannel_IsPresent() throws { + let joinValue = try awaitResultValue { + channel.join( + completion: $0 + ) + } + + XCTAssertTrue(try awaitResultValue(delay: 3) { + channel.isPresent( + userId: chat.currentUser.id, + completion: $0 + ) + }) + + addTeardownBlock { + joinValue.disconnect?.close() + } + } + + func testChannel_GetHistory() throws { + for counter in 1...3 { + try awaitResultValue { + channel.sendText( + text: "Text \(counter)", + completion: $0 + ) + } + } + let history = try awaitResultValue(delay: 2) { + channel.getHistory( + completion: $0 + ) + } + + XCTAssertEqual(history.messages.count, 3) + XCTAssertEqual(history.messages[0].text, "Text 1") + XCTAssertEqual(history.messages[1].text, "Text 2") + XCTAssertEqual(history.messages[2].text, "Text 3") + } + + func testChannel_SendText() throws { + var mentionedUsers = MessageMentionedUsers() + var referencedChannels = MessageReferencedChannels() + + mentionedUsers[0] = MessageMentionedUser(id: randomString(), name: "User 1") + referencedChannels[0] = MessageReferencedChannel(id: randomString(), name: "Channel 1") + + let tt = try awaitResultValue { + channel.sendText( + text: "Please welcome @User who joined the @Chann channel!", + meta: ["a": 123, "b": "someString"], + shouldStore: true, + mentionedUsers: mentionedUsers, + referencedChannels: referencedChannels, + completion: $0 + ) + } + + let retrievedMessage = try awaitResultValue { + channel.getMessage( + timetoken: tt, + completion: $0 + ) + } + + XCTAssertEqual(retrievedMessage?.text, "Please welcome @User who joined the @Chann channel!") + XCTAssertEqual(retrievedMessage?.meta?["a"]?.codableValue.rawValue as? Int, 123) + XCTAssertEqual(retrievedMessage?.meta?["b"]?.codableValue.rawValue as? String, "someString") + } + + func testChannel_Invite() throws { + try awaitResultValue { + channel.invite( + user: chat.currentUser, + completion: $0 + ) + } + let member = try XCTUnwrap( + try awaitResultValue { + channel.getMembers( + completion: $0 + ) + }.memberships.first + ) + + XCTAssertEqual(member.user.id, chat.currentUser.id) + } + + func testChannel_InviteMultiple() throws { + let someUser = UserImpl( + chat: chat, + id: randomString() + ) + + try awaitResultValue { + chat.createUser( + user: someUser, + completion: $0 + ) + } + + let resultValue = try awaitResultValue { + channel.inviteMultiple( + users: [chat.currentUser, someUser], + completion: $0 + ) + } + + let firstMatch = try XCTUnwrap( + resultValue.first { + $0.user.id == chat.currentUser.id && $0.channel.id == channel.id + } + ) + let secondMatch = try XCTUnwrap( + resultValue.first { + $0.user.id == someUser.id && $0.channel.id == channel.id + } + ) + + XCTAssertEqual(resultValue.count, 2) + XCTAssertNotNil(firstMatch) + XCTAssertNotNil(secondMatch) + + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteUser(id: someUser.id, completion: $0) + } + } + } + + func testChannel_GetMembers() throws { + let someUser = UserImpl( + chat: chat, + id: randomString() + ) + + try awaitResultValue { + chat.createUser( + user: someUser, + completion: $0 + ) + } + + try awaitResultValue { + channel.inviteMultiple( + users: [chat.currentUser, someUser], + completion: $0 + ) + } + + let memberships = try XCTUnwrap( + try awaitResultValue { + channel.getMembers( + completion: $0 + ) + }.memberships + ) + + let firstMatch = try XCTUnwrap( + memberships.first { + $0.user.id == chat.currentUser.id && $0.channel.id == channel.id + } + ) + let secondMatch = try XCTUnwrap( + memberships.first { + $0.user.id == someUser.id && $0.channel.id == channel.id + } + ) + + XCTAssertEqual(memberships.count, 2) + XCTAssertNotNil(firstMatch) + XCTAssertNotNil(secondMatch) + + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteUser(id: someUser.id, completion: $0) + } + } + } + + func testChannel_Connect() throws { + let expectation = XCTestExpectation(description: "Connect") + expectation.assertForOverFulfill = true + expectation.expectedFulfillmentCount = 1 + + let closeable = channel.connect( + callback: { [unowned self] in + XCTAssertEqual($0.text, "This is a text") + XCTAssertEqual($0.channelId, channel.id) + expectation.fulfill() + } + ) + + try awaitResultValue(delay: 3) { + channel.sendText( + text: "This is a text", + completion: $0 + ) + } + + wait( + for: [expectation], + timeout: 6 + ) + addTeardownBlock { + closeable.close() + } + } + + func testChannel_Join() throws { + let expectation = XCTestExpectation(description: "Connect") + expectation.assertForOverFulfill = true + expectation.expectedFulfillmentCount = 1 + + let joinValue = try awaitResultValue { [unowned self] in + channel.join( + callback: { + XCTAssertEqual($0.text, "This is a text") + XCTAssertEqual($0.channelId, self.channel.id) + expectation.fulfill() + }, + completion: $0 + ) + } + + XCTAssertEqual(joinValue.membership.channel.id, channel.id) + XCTAssertEqual(joinValue.membership.user.id, chat.currentUser.id) + + try awaitResultValue(delay: 3) { + channel.sendText( + text: "This is a text", + completion: $0 + ) + } + + wait( + for: [expectation], + timeout: 7 + ) + addTeardownBlock { + joinValue.disconnect?.close() + } + } + + func testChannel_Leave() throws { + let joinValue = try awaitResultValue { + channel.join( + completion: $0 + ) + } + try awaitResultValue(delay: 3) { + channel.leave( + completion: $0 + ) + } + XCTAssertTrue( + try awaitResultValue(delay: 3) { + channel.getMembers( + completion: $0 + ) + }.memberships.isEmpty + ) + + addTeardownBlock { + joinValue.disconnect?.close() + } + } + + func testChannel_PinMessageGetPinnedMessage() throws { + let tt = try awaitResultValue { + channel.sendText( + text: "Pinned message", + completion: $0 + ) + } + let message = try XCTUnwrap( + try awaitResultValue { + channel.getMessage( + timetoken: tt, + completion: $0 + ) + } + ) + let updatedChannel = try awaitResultValue { + channel.pinMessage( + message: message, + completion: $0 + ) + } + let getPinnedMessage = try XCTUnwrap( + try awaitResultValue { + updatedChannel.getPinnedMessage( + completion: $0 + ) + } + ) + + XCTAssertEqual( + getPinnedMessage.channelId, + channel.id + ) + } + + func testChannel_GetMessage() throws { + let tt = try awaitResultValue { + channel.sendText( + text: "Message text", + completion: $0 + ) + } + let message = try XCTUnwrap( + try awaitResultValue(delay: 2) { + channel.getMessage( + timetoken: tt, + completion: $0 + ) + } + ) + + XCTAssertEqual(message.channelId, channel.id) + XCTAssertEqual(message.userId, chat.currentUser.id) + } + + func testChannel_RegisterUnregisterFromPush() throws { + let pushNotificationsConfig = PushNotificationsConfig( + sendPushes: false, + deviceToken: "4d3f92b6d7a9348e5f2b8c6d1e4f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f", + deviceGateway: .fcm, + apnsEnvironment: .development + ) + let anotherChat = ChatImpl( + chatConfiguration: ChatConfiguration(pushNotificationsConfig: pushNotificationsConfig), + pubNubConfiguration: chat.pubNub.configuration + ) + let anotherChannel = try awaitResultValue { + anotherChat.createChannel( + id: randomString(), + completion: $0 + ) + } + try awaitResultValue { + anotherChannel.registerForPush( + completion: $0 + ) + } + try awaitResultValue { + anotherChannel.unregisterFromPush( + completion: $0 + ) + } + + addTeardownBlock { [unowned self] in + try awaitResult { + anotherChat.deleteChannel( + id: anotherChannel.id, + completion: $0 + ) + } + } + } + + func testChannel_StreamUpdates() throws { + let expectation = expectation(description: "StreamUpdates") + expectation.assertForOverFulfill = true + expectation.expectedFulfillmentCount = 1 + + let closeable = channel.streamUpdates { + XCTAssertEqual($0?.name, "NewName") + XCTAssertEqual($0?.status, "NewStatus") + expectation.fulfill() + } + + try awaitResultValue(delay: 3) { + channel.update( + name: "NewName", + status: "NewStatus", + completion: $0 + ) + } + + wait( + for: [expectation], + timeout: 6 + ) + addTeardownBlock { + closeable.close() + } + } + + func testChannel_StreamReadReceipts() throws { + let expectation = expectation(description: "StreamReadReceipts") + expectation.assertForOverFulfill = true + expectation.expectedFulfillmentCount = 1 + + let anotherUser = try awaitResultValue { + chat.createUser( + user: UserImpl(chat: chat, id: randomString()), + completion: $0 + ) + } + let membership = try awaitResultValue(delay: 3) { + channel.invite( + user: chat.currentUser, + completion: $0 + ) + } + let anotherMembership = try awaitResultValue { + channel.invite( + user: anotherUser, + completion: $0 + ) + } + + let timetoken = try XCTUnwrap(membership.lastReadMessageTimetoken) + let secondTimetoken = try XCTUnwrap(anotherMembership.lastReadMessageTimetoken) + let currentUserId = chat.currentUser.id + let anotherUserId = anotherUser.id + + let closeable = channel.streamReadReceipts { + XCTAssertEqual($0[timetoken]?.count, 1) + XCTAssertEqual($0[timetoken]?.first, currentUserId) + XCTAssertEqual($0[secondTimetoken]?.count, 1) + XCTAssertEqual($0[secondTimetoken]?.first, anotherUserId) + expectation.fulfill() + } + + wait( + for: [expectation], + timeout: 6 + ) + addTeardownBlock { [unowned self] in + closeable.close() + try awaitResult { + chat.deleteUser( + id: anotherUserId, + completion: $0 + ) + } + } + } + + func testChannel_GetFiles() throws { + let fileUrlSession = URLSession( + configuration: URLSessionConfiguration.default, + delegate: FileSessionManager(), + delegateQueue: .main + ) + let newPubNub = PubNub( + configuration: chat.pubNub.configuration, + fileSession: fileUrlSession + ) + let newChat = ChatImpl( + pubNub: newPubNub, + configuration: chat.config + ) + let newChannel = try awaitResultValue { + newChat.createChannel( + id: randomString(), + completion: $0 + ) + } + let inputFile = InputFile( + name: "TxtFile", + type: "text/plain", + source: .data(try XCTUnwrap("Lorem ipsum".data(using: .utf8)), contentType: "text/plain") + ) + + try awaitResultValue(timeout: 10) { + newChannel.sendText( + text: "Text", + files: [inputFile], + completion: $0 + ) + } + + let file = try XCTUnwrap( + try awaitResultValue { + newChannel.getFiles(completion: $0) + }.files.first + ) + + XCTAssertEqual( + file.name, + "TxtFile" + ) + + addTeardownBlock { [unowned self] in + try awaitResultValue { + newPubNub.remove( + fileId: file.id, + filename: file.name, + channel: newChannel.id, + completion: $0 + ) + } + try awaitResult { + newChat.deleteChannel( + id: newChannel.id, + completion: $0 + ) + } + } + } + + func testChannel_DeleteFile() throws { + let fileUrlSession = URLSession( + configuration: URLSessionConfiguration.default, + delegate: FileSessionManager(), + delegateQueue: .main + ) + let newPubNub = PubNub( + configuration: chat.pubNub.configuration, + fileSession: fileUrlSession + ) + let newChat = ChatImpl( + pubNub: newPubNub, + configuration: chat.config + ) + let newChannel = try awaitResultValue { + newChat.createChannel( + id: randomString(), + completion: $0 + ) + } + let inputFile = InputFile( + name: "TxtFile", + type: "text/plain", + source: .data(try XCTUnwrap("Lorem ipsum".data(using: .utf8)), contentType: "text/plain") + ) + + try awaitResultValue(timeout: 30) { + newChannel.sendText( + text: "Text", + files: [inputFile], + completion: $0 + ) + } + + let file = try XCTUnwrap( + try awaitResultValue { + newChannel.getFiles( + limit: 10, + completion: $0 + ) + }.files.first + ) + + try awaitResultValue { + channel.deleteFile( + id: file.id, + name: file.name, + completion: $0 + ) + } + + XCTAssertTrue( + try awaitResultValue { + channel.getFiles( + limit: 10, + completion: $0 + ) + }.files.isEmpty + ) + + addTeardownBlock { [unowned self] in + try awaitResult { + newChat.deleteChannel( + id: newChannel.id, + completion: $0 + ) + } + } + } + + func testChannel_StreamPresence() throws { + let expectation = expectation(description: "StreamPresence") + expectation.assertForOverFulfill = true + expectation.expectedFulfillmentCount = 1 + + let connectCloseable = channel.connect(callback: { + debugPrint("Did receive message: \($0)") + }) + + let presenceCloseable = channel.streamPresence { [unowned self] in + if !$0.isEmpty { + XCTAssertEqual($0.count, 1) + XCTAssertEqual($0.first, chat.currentUser.id) + expectation.fulfill() + } + } + + wait( + for: [expectation], + timeout: 5 + ) + addTeardownBlock { + presenceCloseable.close() + connectCloseable.close() + } + } + + func testChannel_GetUserSuggestions() throws { + let usersToCreate = [ + UserImpl(chat: chat, id: randomString(), name: "user_\(randomString())"), + UserImpl(chat: chat, id: randomString(), name: "user_\(randomString())"), + UserImpl(chat: chat, id: randomString(), name: "user_\(randomString())") + ] + + for user in usersToCreate { + try awaitResultValue { + chat.createUser( + user: user, + completion: $0 + ) + } + } + + try awaitResultValue { + channel.inviteMultiple( + users: usersToCreate, + completion: $0 + ) + } + + let users = try awaitResultValue { + channel.getUserSuggestions( + text: "txt@user_", + completion: $0 + ) + } + + XCTAssertEqual( + users.compactMap { $0.user.name }.sorted(by: <), + usersToCreate.compactMap { $0.name }.sorted(by: <) + ) + + addTeardownBlock { [unowned self] in + for user in usersToCreate { + try awaitResult { + chat.deleteUser( + id: user.id, + completion: $0 + ) + } + } + } + } + + func testChannel_StreamMessageReports() throws { + let expectation = expectation(description: "StreamMessageReports") + expectation.assertForOverFulfill = true + expectation.expectedFulfillmentCount = 1 + + let tt = try awaitResultValue { + channel.sendText( + text: "Some text", + completion: $0 + ) + } + let message = try awaitResultValue { + channel.getMessage( + timetoken: tt, + completion: $0 + ) + } + + let closeable = channel.streamMessageReports { [unowned self] report in + XCTAssertEqual(report.payload.reason, "reportReason") + XCTAssertEqual(report.payload.text, "Some text") + XCTAssertEqual(report.payload.reportedUserId, chat.currentUser.id) + expectation.fulfill() + } + + try awaitResultValue(delay: 3) { + message?.report( + reason: "reportReason", + completion: $0 + ) + } + + wait( + for: [expectation], + timeout: 18 + ) + addTeardownBlock { [unowned self] in + closeable.close() + try awaitResult { message?.delete(completion: $0) } + } + } +} diff --git a/Tests/ChatIntegrationTests.swift b/Tests/ChatIntegrationTests.swift new file mode 100644 index 0000000..0d876b9 --- /dev/null +++ b/Tests/ChatIntegrationTests.swift @@ -0,0 +1,1167 @@ +// +// ChatTests.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubSDK +import XCTest + +@testable import PubNubSwiftChatSDK + +class ChatIntegrationTests: PubNubSwiftChatSDKIntegrationTests { + func testChat_CreateUser() throws { + let user = try awaitResultValue { + chat.createUser( + id: randomString(), + name: "randomUser", + status: "active", + type: "type", + completion: $0 + ) + } + + XCTAssertEqual(user.name, "randomUser") + XCTAssertEqual(user.status, "active") + XCTAssertEqual(user.type, "type") + + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteUser( + id: user.id, + completion: $0 + ) + } + } + } + + func testChat_GetUser() throws { + let user = try awaitResultValue { + chat.getUser( + userId: chat.currentUser.id, + completion: $0 + ) + } + + XCTAssertEqual(user?.id, chat.currentUser.id) + XCTAssertEqual(user?.name, chat.currentUser.name) + } + + func testChat_GetUsers() throws { + let user = try awaitResultValue { + chat.createUser( + id: randomString(), + completion: $0 + ) + } + let users = try awaitResultValue { + chat.getUsers( + limit: 10, + completion: $0 + ) + } + + XCTAssertFalse( + users.users.isEmpty + ) + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteUser( + id: user.id, + completion: $0 + ) + } + } + } + + func testChat_UpdateUser() throws { + let newCustom: [String: JSONCodableScalar] = [ + "someValue": 17253575019298112, + "someStr": "str" + ] + let updatedUser = try awaitResultValue { + chat.currentUser.update( + name: "NewName", + externalId: "NewExternalId", + profileUrl: "https://picsum.photos/200/400", + email: "some.user@pubnub.com", + custom: newCustom, + status: "offline", + type: "regular", + completion: $0 + ) + } + + XCTAssertEqual(updatedUser.id, chat.currentUser.id) + XCTAssertEqual(updatedUser.name, "NewName") + XCTAssertEqual(updatedUser.externalId, "NewExternalId") + XCTAssertEqual(updatedUser.profileUrl, "https://picsum.photos/200/400") + XCTAssertEqual(updatedUser.email, "some.user@pubnub.com") + XCTAssertEqual(updatedUser.custom?.mapValues { $0.scalarValue }, newCustom.mapValues { $0.scalarValue }) + XCTAssertEqual(updatedUser.status, "offline") + XCTAssertEqual(updatedUser.type, "regular") + } + + func testChat_Delete() throws { + let user = try awaitResultValue { + chat.createUser( + id: randomString(), + completion: $0 + ) + } + try awaitResultValue { + chat.deleteUser( + id: user.id, + completion: $0 + ) + } + let retrievedUser = try awaitResultValue { + chat.getUser( + userId: user.id, + completion: $0 + ) + } + + XCTAssertNil( + retrievedUser + ) + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteUser( + id: user.id, + completion: $0 + ) + } + } + } + + func testChat_WherePresent() throws { + let channelId = randomString() + let channel = try awaitResultValue { chat.createChannel(id: channelId, name: channelId, completion: $0) } + let closeable = channel.connect(callback: { _ in }) + + XCTAssertEqual( + try awaitResultValue(delay: 5) { + chat.wherePresent( + userId: chat.currentUser.id, + completion: $0 + ) + }, [channelId] + ) + + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteChannel( + id: channel.id, + completion: $0 + ) + } + closeable.close() + } + } + + func testChat_IsPresent() throws { + let channelId = randomString() + let channel = try awaitResultValue { chat.createChannel(id: channelId, name: channelId, completion: $0) } + let closeable = channel.connect(callback: { _ in }) + + let joinValue = try awaitResultValue { + channel.join( + completion: $0 + ) + } + + XCTAssertTrue(try awaitResultValue(delay: 3) { + chat.isPresent( + userId: chat.currentUser.id, + channelId: channelId, + completion: $0 + ) + }) + + addTeardownBlock { [unowned self] in + joinValue.disconnect?.close() + closeable.close() + + try awaitResult { + chat.deleteChannel( + id: channelId, + completion: $0 + ) + } + } + } + + func testChat_CreateChannel() throws { + let customField: [String: JSONCodableScalar] = [ + "someValue": 17253575019298112, + "someStr": "str" + ] + let channel = try awaitResultValue { + chat.createChannel( + id: randomString(), + name: "ChannelName", + description: "ChannelDescription", + custom: customField, + type: .unknown, + status: "status", + completion: $0 + ) + } + + XCTAssertEqual(channel.name, "ChannelName") + XCTAssertEqual(channel.description, "ChannelDescription") + XCTAssertEqual(channel.type, .unknown) + XCTAssertEqual(channel.status, "status") + XCTAssertEqual(channel.custom?.mapValues { $0.scalarValue }, customField.mapValues { $0.scalarValue }) + + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteChannel( + id: channel.id, + completion: $0 + ) + } + } + } + + func testChat_GetChannel() throws { + let channel = try awaitResultValue { + chat.createChannel( + id: randomString(), + name: "ChannelName", + completion: $0 + ) + } + let retrievedChannel = try awaitResultValue { + chat.getChannel( + channelId: channel.id, + completion: $0 + ) + } + + XCTAssertEqual(retrievedChannel?.id, channel.id) + } + + func testChat_GetChannels() throws { + let channel = try awaitResultValue { + chat.createChannel( + id: randomString(), + completion: $0 + ) + } + let retrievedChannels = try awaitResultValue { + chat.getChannels( + limit: 10, + completion: $0 + ) + } + + XCTAssertFalse( + retrievedChannels.channels.isEmpty + ) + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteChannel( + id: channel.id, + completion: $0 + ) + } + } + } + + func testChat_UpdateChannel() throws { + let channel = try awaitResultValue { + chat.createChannel( + id: randomString(), + completion: $0 + ) + } + let customField: [String: JSONCodableScalar] = [ + "someValue": 17253575019298112, + "someStr": "str" + ] + let updatedChannel = try awaitResultValue { + chat.updateChannel( + id: channel.id, + name: "NewName", + custom: customField, + description: "NewDescription", + status: "status", + type: .unknown, + completion: $0 + ) + } + + XCTAssertEqual(updatedChannel.id, channel.id) + XCTAssertEqual(updatedChannel.name, "NewName") + XCTAssertEqual(updatedChannel.custom?.mapValues { $0.scalarValue }, customField.mapValues { $0.scalarValue }) + XCTAssertEqual(updatedChannel.description, "NewDescription") + XCTAssertEqual(updatedChannel.status, "status") + XCTAssertEqual(updatedChannel.type, .unknown) + + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteChannel( + id: channel.id, + completion: $0 + ) + } + } + } + + func testChat_DeleteChannel() throws { + let channel = try awaitResultValue { + chat.createChannel( + id: randomString(), + completion: $0 + ) + } + try awaitResultValue { + chat.deleteChannel( + id: channel.id, + completion: $0 + ) + } + let retrievedChannel = try awaitResultValue { + chat.getChannel( + channelId: channel.id, + completion: $0 + ) + } + + XCTAssertNil( + retrievedChannel + ) + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteChannel( + id: channel.id, + completion: $0 + ) + } + } + } + + func testChat_Forward() throws { + let firstChannel = try XCTUnwrap( + try awaitResultValue { + chat.createChannel( + id: randomString(), + completion: $0 + ) + } + ) + let secondChannel = try XCTUnwrap( + try awaitResultValue { + chat.createChannel( + id: randomString(), + completion: $0 + ) + } + ) + let tt = try awaitResultValue { + firstChannel.sendText( + text: "TxtMessage", + completion: $0 + ) + } + let message = try XCTUnwrap( + try awaitResultValue { + firstChannel.getMessage( + timetoken: tt, + completion: $0 + ) + } + ) + let forwardResValue = try awaitResultValue { + chat.forwardMessage( + message: message, + channelId: secondChannel.id, + completion: $0 + ) + } + + XCTAssertNotNil( + try awaitResultValue { + secondChannel.getMessage( + timetoken: forwardResValue, + completion: $0 + ) + } + ) + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteChannel( + id: firstChannel.id, + completion: $0 + ) + } + try awaitResult { + chat.deleteChannel( + id: secondChannel.id, + completion: $0 + ) + } + } + } + + func testChat_WhoIsPresent() throws { + let channel = try awaitResultValue { + chat.createChannel( + id: randomString(), + name: "ChannelName", + completion: $0 + ) + } + let joinValue = try awaitResultValue { + channel.join( + completion: $0 + ) + } + let whoIsPresentValue = try awaitResultValue(delay: 4) { + chat.whoIsPresent( + channelId: channel.id, + completion: $0 + ) + } + + XCTAssertEqual(whoIsPresentValue.count, 1) + XCTAssertEqual(whoIsPresentValue.first, chat.currentUser.id) + + addTeardownBlock { [unowned self] in + joinValue.disconnect?.close() + try awaitResult { + chat.deleteChannel( + id: channel.id, + completion: $0 + ) + } + } + } + + func testChat_EmitEvent() throws { + let expectation = expectation(description: "Emit Event") + expectation.assertForOverFulfill = true + expectation.expectedFulfillmentCount = 1 + + let channel = try awaitResultValue { + chat.createChannel( + id: randomString(), + name: "ChannelName", + completion: $0 + ) + } + + let closeable = channel.getTyping { [unowned self] in + XCTAssertEqual($0, [chat.currentUser.id]) + expectation.fulfill() + } + + try awaitResultValue(delay: 3) { + chat.emitEvent( + channelId: channel.id, + payload: EventContent.Typing(value: true), + completion: $0 + ) + } + wait( + for: [expectation], + timeout: 6 + ) + addTeardownBlock { [unowned self] in + closeable.close() + try awaitResult { + chat.deleteChannel( + id: channel.id, + completion: $0 + ) + } + } + } + + func testChat_CreatePublicConversation() throws { + let customField: [String: JSONCodableScalar] = [ + "someValue": 17253575019298112, + "someStr": "str" + ] + let channel = try awaitResultValue { + chat.createPublicConversation( + channelName: "ChannelName", + channelDescription: "ChannelDescription", + channelCustom: customField, + channelStatus: "status", + completion: $0 + ) + } + + XCTAssertEqual(channel.name, "ChannelName") + XCTAssertEqual(channel.description, "ChannelDescription") + XCTAssertEqual(channel.custom?.mapValues { $0.scalarValue }, customField.mapValues { $0.scalarValue }) + XCTAssertEqual(channel.status, "status") + XCTAssertEqual(channel.type, .public) + + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteChannel( + id: channel.id, + completion: $0 + ) + } + } + } + + func testChat_CreateDirectConversation() throws { + let customField: [String: JSONCodableScalar] = [ + "someValue": 17253575019298112, + "someStr": "str" + ] + let membershipCustom: [String: JSONCodableScalar] = [ + "val1": 123, + "val2": "lorem ipsum" + ] + + let anotherUser = try awaitResultValue { + chat.createUser( + id: randomString(), + name: "AnotherUser", + completion: $0 + ) + } + + let resultValue = try awaitResultValue { + chat.createDirectConversation( + invitedUser: anotherUser, + channelName: "ChannelName", + channelDescription: "ChannelDescription", + channelCustom: customField, + channelStatus: "status", + membershipCustom: membershipCustom, + completion: $0 + ) + } + + XCTAssertEqual(resultValue.channel.name, "ChannelName") + XCTAssertEqual(resultValue.channel.description, "ChannelDescription") + XCTAssertEqual(resultValue.channel.custom?.mapValues { $0.scalarValue }, customField.mapValues { $0.scalarValue }) + XCTAssertEqual(resultValue.channel.status, "status") + XCTAssertEqual(resultValue.channel.type, .direct) + + let inviteeMembership = try XCTUnwrap(resultValue.inviteeMembership) + let hostMembership = try XCTUnwrap(resultValue.hostMembership) + + XCTAssertEqual(inviteeMembership.user.id, anotherUser.id) + XCTAssertEqual(hostMembership.user.id, chat.currentUser.id) + XCTAssertEqual(hostMembership.custom?.mapValues { $0.scalarValue }, membershipCustom.mapValues { $0.scalarValue }) + + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteChannel( + id: resultValue.channel.id, + completion: $0 + ) + } + try awaitResult { + chat.deleteUser( + id: anotherUser.id, + completion: $0 + ) + } + } + } + + func testChat_GroupConversation() throws { + let anotherUser = try awaitResultValue { + chat.createUser( + id: randomString(), + name: "AnotherUser", + completion: $0 + ) + } + + let customField: [String: JSONCodableScalar] = [ + "someValue": 17253575019298112, + "someStr": "str" + ] + let membershipCustom: [String: JSONCodableScalar] = [ + "val1": 123, + "val2": "lorem ipsum" + ] + + let resultValue = try awaitResultValue { + chat.createGroupConversation( + invitedUsers: [anotherUser], + channelName: "ChannelName", + channelDescription: "ChannelDescription", + channelCustom: customField, + channelStatus: "status", + membershipCustom: membershipCustom, + completion: $0 + ) + } + + XCTAssertEqual(resultValue.channel.name, "ChannelName") + XCTAssertEqual(resultValue.channel.description, "ChannelDescription") + XCTAssertEqual(resultValue.channel.custom?.mapValues { $0.scalarValue }, customField.mapValues { $0.scalarValue }) + XCTAssertEqual(resultValue.channel.status, "status") + XCTAssertEqual(resultValue.channel.type, .group) + + let inviteeMembership = try XCTUnwrap(resultValue.inviteeMemberships.first) + let hostMembership = try XCTUnwrap(resultValue.hostMembership) + + XCTAssertEqual(inviteeMembership.user.id, anotherUser.id) + XCTAssertEqual(hostMembership.user.id, chat.currentUser.id) + XCTAssertEqual(hostMembership.custom?.mapValues { $0.scalarValue }, membershipCustom.mapValues { $0.scalarValue }) + + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteChannel( + id: resultValue.channel.id, + completion: $0 + ) + } + try awaitResult { + chat.deleteUser( + id: anotherUser.id, + completion: $0 + ) + } + } + } + + func testChat_ListenForEvents() throws { + let expectation = expectation(description: "Listen For Events") + expectation.assertForOverFulfill = true + expectation.expectedFulfillmentCount = 1 + + let channel = try awaitResultValue { + chat.createChannel( + id: randomString(), + completion: $0 + ) + } + let closeable = chat.listenForEvents( + type: EventContent.Typing.self, + channelId: channel.id + ) { [unowned self] in + XCTAssertTrue($0.event.payload.value) + XCTAssertEqual($0.event.channelId, channel.id) + XCTAssertEqual($0.event.userId, chat.currentUser.id) + expectation.fulfill() + } + + try awaitResult(delay: 3) { + channel.startTyping( + completion: $0 + ) + } + + wait( + for: [expectation], + timeout: 6 + ) + addTeardownBlock { [unowned self] in + closeable.close() + try awaitResult { + chat.deleteChannel( + id: channel.id, + completion: $0 + ) + } + } + } + + func testChat_RegisterUnregisterPushChannels() throws { + let pushNotificationsConfig = PushNotificationsConfig( + sendPushes: false, + deviceToken: "4d3f92b6d7a9348e5f2b8c6d1e4f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f", + deviceGateway: .fcm, + apnsEnvironment: .development + ) + let anotherChat = ChatImpl( + pubNub: chat.pubNub, + configuration: ChatConfiguration(pushNotificationsConfig: pushNotificationsConfig) + ) + let anotherChannel = try awaitResultValue { + anotherChat.createChannel( + id: randomString(), + completion: $0 + ) + } + try awaitResultValue { + anotherChat.registerPushChannels( + channels: [anotherChannel.id], + completion: $0 + ) + } + try awaitResultValue { + anotherChat.unregisterPushChannels( + channels: [anotherChannel.id], + completion: $0 + ) + } + + addTeardownBlock { [unowned self] in + try awaitResult { + anotherChat.deleteChannel( + id: anotherChannel.id, + completion: $0 + ) + } + } + } + + func testChat_UnregisterAllPushChannels() throws { + let pushNotificationsConfig = PushNotificationsConfig( + sendPushes: false, + deviceToken: "4d3f92b6d7a9348e5f2b8c6d1e4f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f", + deviceGateway: .fcm, + apnsEnvironment: .development + ) + let anotherChat = ChatImpl( + pubNub: chat.pubNub, + configuration: ChatConfiguration(pushNotificationsConfig: pushNotificationsConfig) + ) + + let anotherChannel = try awaitResultValue { + anotherChat.createChannel( + id: randomString(), + completion: $0 + ) + } + try awaitResultValue { + anotherChat.registerPushChannels( + channels: [anotherChannel.id], + completion: $0 + ) + } + try awaitResultValue { + anotherChat.unregisterAllPushChannels( + completion: $0 + ) + } + let pushChannels = try awaitResultValue { + anotherChat.getPushChannels( + completion: $0 + ) + } + + XCTAssertTrue( + pushChannels.isEmpty + ) + addTeardownBlock { [unowned self] in + try awaitResult { + anotherChat.deleteChannel( + id: anotherChannel.id, + completion: $0 + ) + } + } + } + + func testChat_GetThread() throws { + let channel = try XCTUnwrap( + try awaitResultValue { + chat.createChannel( + id: randomString(), + completion: $0 + ) + } + ) + let testMessage = try XCTUnwrap( + try awaitResultValue { + channel.getMessage( + timetoken: try awaitResultValue { + channel.sendText( + text: "text", + shouldStore: true, + completion: $0 + ) + }, + completion: $0 + ) + } + ) + let threadChannel = try awaitResultValue { + testMessage.createThread( + completion: $0 + ) + } + + try awaitResultValue { + threadChannel.sendText( + text: "Text", + completion: $0 + ) + } + + let retrievedThreadChannel = try awaitResultValue { + chat.getThreadChannel( + message: testMessage, + completion: $0 + ) + } + + XCTAssertEqual(retrievedThreadChannel.id, threadChannel.id) + XCTAssertEqual(retrievedThreadChannel.parentChannelId, channel.id) + + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteChannel(id: threadChannel.id, completion: $0) + } + try awaitResult { + chat.deleteChannel(id: channel.id, completion: $0) + } + } + } + + func testChat_GetUnreadMessagesCount() throws { + let channel = try awaitResultValue { + chat.createChannel( + id: randomString(), + completion: $0 + ) + } + + try awaitResultValue { + channel.invite( + user: chat.currentUser, + completion: $0 + ) + } + + for _ in (1...3) { + try awaitResultValue { + channel.sendText( + text: "Some new text", + completion: $0 + ) + } + } + + let getUnreadMessagesCount = try XCTUnwrap( + try awaitResultValue { + chat.getUnreadMessagesCount( + completion: $0 + ) + }.first + ) + + XCTAssertEqual(getUnreadMessagesCount.count, 3) + XCTAssertEqual(getUnreadMessagesCount.channel.id, channel.id) + XCTAssertEqual(getUnreadMessagesCount.membership.user.id, chat.currentUser.id) + + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteChannel( + id: channel.id, + completion: $0 + ) + } + } + } + + func testChat_MarkAllMessagesAsRead() throws { + let channel = try awaitResultValue { + chat.createChannel( + id: randomString(), + completion: $0 + ) + } + + try awaitResultValue { + channel.invite( + user: chat.currentUser, + completion: $0 + ) + } + + for _ in (1...3) { + try awaitResultValue { + channel.sendText( + text: "Some new text", + completion: $0 + ) + } + } + + try awaitResultValue(delay: 2) { + chat.markAllMessagesAsRead( + completion: $0 + ) + } + + let getUnreadMessagesCount = try awaitResultValue(delay: 2) { + chat.getUnreadMessagesCount( + completion: $0 + ) + } + + XCTAssertTrue( + getUnreadMessagesCount.isEmpty + ) + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteChannel( + id: channel.id, + completion: $0 + ) + } + } + } + + func testChat_GetChannelSuggestions() throws { + let channelName = "channel_\(randomString())" + let channelId = channelName + + let channel = try awaitResultValue { + chat.createChannel( + id: channelId, + name: channelName, + completion: $0 + ) + } + + let channelSuggestion = try XCTUnwrap( + try awaitResultValue { + chat.getChannelSuggestions( + text: "aaa#channel_", + completion: $0 + ) + }.first + ) + + XCTAssertEqual(channelSuggestion.id, channel.id) + XCTAssertEqual(channelSuggestion.name, channel.name) + + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteChannel( + id: channel.id, + completion: $0 + ) + } + } + } + + func testChat_GetUserSuggestions() throws { + let username = "some_user_\(randomString())" + let channelId = randomString() + + let channel = try awaitResultValue { + chat.createChannel( + id: channelId, + completion: $0 + ) + } + + let user = try awaitResultValue { + chat.createUser( + id: username, + name: username, + completion: $0 + ) + } + try awaitResultValue { + channel.invite( + user: user, + completion: $0 + ) + } + + let userSuggestion = try XCTUnwrap( + try awaitResultValue { + chat.getUserSuggestions( + text: "aaa@some_user_", + completion: $0 + ) + }.first + ) + + XCTAssertEqual(userSuggestion.id, user.id) + XCTAssertEqual(userSuggestion.name, user.name) + + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteUser( + id: user.id, + completion: $0 + ) + } + try awaitResult { + chat.deleteChannel( + id: channel.id, + completion: $0 + ) + } + } + } + + func testChat_GetEventsHistory() throws { + let channel = try awaitResultValue { + chat.createChannel( + id: randomString(), + name: "Channel name", + completion: $0 + ) + } + + try awaitResultValue { + channel.invite( + user: chat.currentUser, + completion: $0 + ) + } + + let mentionedUser = MessageMentionedUser( + id: chat.currentUser.id, + name: chat.currentUser.name ?? "" + ) + + let tt = try awaitResultValue { + channel.sendText( + text: "Some text @\(chat.currentUser.name ?? "")", + mentionedUsers: MessageMentionedUsers(uniqueKeysWithValues: zip([0], [mentionedUser])), + completion: $0 + ) + } + + let history = try awaitResultValue { + chat.getEventsHistory( + channelId: chat.currentUser.id, + completion: $0 + ) + } + + let mentionEvent = try XCTUnwrap(history.events.compactMap { $0.event.payload as? EventContent.Mention }.first) + let inviteEvent = try XCTUnwrap(history.events.compactMap { $0.event.payload as? EventContent.Invite }.first) + + XCTAssertEqual(mentionEvent.channel, channel.id) + XCTAssertEqual(mentionEvent.messageTimetoken, tt) + XCTAssertEqual(inviteEvent.channelId, channel.id) + XCTAssertEqual(inviteEvent.channelType, .unknown) + + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteChannel( + id: channel.id, + completion: $0 + ) + } + } + } + + func testChat_GetCurrentUserMentions() throws { + let channel = try awaitResultValue { + chat.createChannel( + id: randomString(), + name: "Channel name", + completion: $0 + ) + } + try awaitResultValue { + channel.invite( + user: chat.currentUser, + completion: $0 + ) + } + let mentionedUser = MessageMentionedUser( + id: chat.currentUser.id, + name: chat.currentUser.name ?? "" + ) + + let textToSend = "Some text @\(chat.currentUser.name ?? "")" + let mentionedUsers = MessageMentionedUsers(uniqueKeysWithValues: zip([0], [mentionedUser])) + + let timetoken = try awaitResultValue { + channel.sendText( + text: textToSend, + mentionedUsers: mentionedUsers, + completion: $0 + ) + } + + let userMentionData = try XCTUnwrap( + try awaitResultValue { + chat.getCurrentUserMentions( + completion: $0 + ) + }.mentions.first + ) + + XCTAssertEqual(userMentionData.userMentionData.userId, chat.currentUser.id) + XCTAssertEqual(userMentionData.userMentionData.message?.text, textToSend) + XCTAssertEqual(userMentionData.userMentionData.message?.timetoken, timetoken) + XCTAssertEqual(userMentionData.userMentionData.event.channel, channel.id) + + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteChannel( + id: channel.id, + completion: $0 + ) + } + } + } + + func testChat_CustomPayload() throws { + let getMessagePublishBody: GetMessagePublishBody? = { txtContent, _, _ in + return ["payload": txtContent.text] + } + let getMessageResponseBody: GetMessageResponseBody? = { value, _, _ in + if let decodedValue = try? value.decode([String: String].self) { + return EventContent.TextMessageContent(text: decodedValue["payload"] ?? "") + } else { + return nil + } + } + + let customPayloads = CustomPayloads( + getMessagePublishBody: getMessagePublishBody, + getMessageResponseBody: getMessageResponseBody + ) + let anotherChat = ChatImpl( + pubNub: chat.pubNub, + configuration: ChatConfiguration(customPayloads: customPayloads) + ) + let channel = try awaitResultValue { + anotherChat.createChannel( + id: randomString(), + completion: $0 + ) + } + + try awaitResultValue { + channel.sendText( + text: "Some text", + completion: $0 + ) + } + + let message = try XCTUnwrap( + try awaitResultValue { + channel.getHistory( + count: 1, + completion: $0 + ) + }.messages.first + ) + + XCTAssertEqual(message.text, "Some text") + XCTAssertTrue(message.files.isEmpty) + + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteChannel( + id: channel.id, + completion: $0 + ) + } + } + } +} diff --git a/Tests/MembershipIntegrationTests.swift b/Tests/MembershipIntegrationTests.swift new file mode 100644 index 0000000..bf50911 --- /dev/null +++ b/Tests/MembershipIntegrationTests.swift @@ -0,0 +1,162 @@ +// +// MembershipTests.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import XCTest +import PubNubSwiftChatSDK +import PubNubSDK + +final class MembershipTests: PubNubSwiftChatSDKIntegrationTests { + var channel: ChannelImpl! + var membership: MembershipImpl! + + override func customSetUpWitError() throws { + channel = try awaitResultValue { + chat.createChannel( + id: randomString(), + completion: $0 + ) + } + membership = try awaitResultValue { + channel.invite( + user: chat.currentUser, + completion: $0 + ) + } + } + + override func customTearDownWithError() throws { + try awaitResult { + chat.deleteChannel( + id: channel.id, + completion: $0 + ) + } + channel = nil + membership = nil + } + + func testMembership_SetLastReadMessage() throws { + let message = MessageImpl( + chat: chat, + timetoken: Timetoken(Int(Date().timeIntervalSince1970 * 10000000)), + content: .init(text: "Lorem ipsum"), + channelId: channel.id, + userId: chat.currentUser.id + ) + let value = try awaitResultValue { + membership.setLastReadMessage( + message: message, + completion: $0 + ) + } + XCTAssertEqual( + value.lastReadMessageTimetoken, + message.timetoken + ) + } + + func testMembership_Update() throws { + let newCustom: [String: JSONCodableScalar] = [ + "a": 1, + "b": "Lorem ipsum", + "c": 3.557 + ] + let newValue = try awaitResultValue { + membership.update( + custom: newCustom, + completion: $0 + ) + } + XCTAssertEqual( + newCustom.mapValues { $0.scalarValue }, + newValue.custom?.mapValues { $0.scalarValue } + ) + } + + func testMembership_SetLastReadMessageTimetoken() throws { + let timetoken = Timetoken(Int(Date().timeIntervalSince1970 * 10000000)) + let value = try awaitResultValue { membership.setLastReadMessageTimetoken(timetoken, completion: $0) } + + XCTAssertEqual( + value.lastReadMessageTimetoken, + timetoken + ) + } + + func testMembership_GetUnreadMessagesCount() throws { + for _ in (1...3) { + try awaitResultValue { + channel.sendText( + text: "Some new text", + completion: $0 + ) + } + } + XCTAssertEqual( + try awaitResultValue(delay: 2) { membership.getUnreadMessagesCount(completion: $0) }, 3 + ) + } + + func testMembership_StreamUpdates() throws { + let expectation = expectation(description: "MembershipStreamUpdates") + expectation.assertForOverFulfill = true + expectation.expectedFulfillmentCount = 1 + + let closeable = membership.streamUpdates { [unowned self] membership in + XCTAssertEqual(membership?.channel.id, self.membership.channel.id) + XCTAssertEqual(membership?.user.id, self.membership.user.id) + expectation.fulfill() + } + + try awaitResultValue(delay: 3) { + membership.update( + custom: ["a": 1, "b": "Text"], + completion: $0 + ) + } + + wait( + for: [expectation], + timeout: 6 + ) + addTeardownBlock { + closeable.close() + } + } + + func testMembership_GlobalStreamUpdates() throws { + let expectation = expectation(description: "MembershipStreamUpdates") + expectation.assertForOverFulfill = true + expectation.expectedFulfillmentCount = 1 + + let closeable = MembershipImpl.streamUpdatesOn(memberships: [membership]) { [unowned self] in + let receivedMembership = $0[0] + XCTAssertEqual(receivedMembership.channel.id, membership.channel.id) + XCTAssertEqual(receivedMembership.user.id, membership.user.id) + expectation.fulfill() + } + + try awaitResultValue(delay: 3) { + membership.update( + custom: ["a": 1, "b": "Text"], + completion: $0 + ) + } + + wait( + for: [expectation], + timeout: 6 + ) + addTeardownBlock { + closeable.close() + } + } +} diff --git a/Tests/MessageIntegrationTests.swift b/Tests/MessageIntegrationTests.swift new file mode 100644 index 0000000..115dce8 --- /dev/null +++ b/Tests/MessageIntegrationTests.swift @@ -0,0 +1,435 @@ +// +// MessageTests.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import XCTest +import PubNubSwiftChatSDK +import PubNubSDK + +final class MessageIntegrationTests: PubNubSwiftChatSDKIntegrationTests { + var channel: ChannelImpl! + var testMessage: MessageImpl! + + override func customSetUpWitError() throws { + channel = try XCTUnwrap( + try awaitResultValue { + chat.createChannel( + id: randomString(), + completion: $0 + ) + } + ) + testMessage = try XCTUnwrap( + try awaitResultValue { + channel.getMessage( + timetoken: try awaitResultValue { + channel?.sendText( + text: "text", + shouldStore: true, + completion: $0 + ) + }, + completion: $0 + ) + } + ) + } + + override func customTearDownWithError() throws { + try awaitResult { testMessage.delete(completion: $0) } + try awaitResult { chat.deleteChannel(id: channel.id, completion: $0) } + + testMessage = nil + channel = nil + } + + func testMessage_HasUserReactions() throws { + XCTAssertFalse(testMessage.hasUserReaction(reaction: "someReaction")) + } + + func testMessage_EditText() throws { + let currentMessageText = testMessage.text + let newText = "NewTextValue" + + XCTAssertNotEqual( + currentMessageText, + newText + ) + + let editedMessage = try awaitResultValue { + testMessage.editText( + newText: newText, + completion: $0 + ) + } + + XCTAssertEqual( + editedMessage.text, + newText + ) + } + + func testMessage_Delete() throws { + XCTAssertNil(try awaitResultValue { + testMessage.delete( + soft: false, + preserveFiles: false, + completion: $0 + ) + }) + } + + func testMessage_SoftDelete() throws { + let value = try awaitResultValue { + testMessage.delete( + soft: true, + preserveFiles: false, + completion: $0 + ) + } + + let actions = try XCTUnwrap(value?.actions) + let deletedActions = try XCTUnwrap(actions[chat.deleteMessageActionName]) + + XCTAssertNotNil(value) + XCTAssertEqual(deletedActions.count, 1) + XCTAssertFalse(deletedActions.isEmpty) + } + + func testMessage_GetThread() throws { + let threadChannel = try awaitResultValue { + testMessage.createThread( + completion: $0 + ) + } + + try awaitResultValue { + threadChannel.sendText( + text: "Text text text", + completion: $0 + ) + } + + let message = try awaitResultValue { + channel.getMessage( + timetoken: testMessage.timetoken, + completion: $0 + ) + } + + XCTAssertNotNil(try awaitResultValue { + message?.getThread(completion: $0) + }) + + addTeardownBlock { [unowned self] in + try awaitResult { + testMessage.removeThread( + completion: $0 + ) + } + } + } + + func testMessage_Forward() throws { + let anotherChannel = try XCTUnwrap( + try awaitResultValue { + chat.createChannel( + id: randomString(), + completion: $0 + ) + } + ) + + let forwardValue = try awaitResultValue { + testMessage.forward( + channelId: anotherChannel.id, + completion: $0 + ) + } + let message = try awaitResultValue { + anotherChannel.getMessage( + timetoken: forwardValue, + completion: $0 + ) + } + + XCTAssertNotNil(message) + XCTAssertEqual(message?.text, testMessage.text) + XCTAssertEqual(message?.userId, testMessage.userId) + + addTeardownBlock { [unowned self] in + try awaitResultValue { + chat.deleteChannel( + id: anotherChannel.id, + completion: $0 + ) + } + } + } + + func testMessage_Pin() throws { + let resultingChannel = try awaitResultValue { + testMessage.pin( + completion: $0 + ) + } + let message = try awaitResultValue(delay: 2) { + resultingChannel.getPinnedMessage( + completion: $0 + ) + } + + XCTAssertNotNil(message) + XCTAssertEqual(resultingChannel.id, testMessage.channelId) + } + + func testMessage_Report() throws { + XCTAssertNotNil(try awaitResultValue { + testMessage.report( + reason: "ReportReason", + completion: $0 + ) + }) + } + + func testMessage_CreateThread() throws { + let threadChannel = try awaitResultValue { + testMessage.createThread(completion: $0) + } + XCTAssertEqual( + threadChannel.parentMessage.timetoken, + testMessage.timetoken + ) + XCTAssertEqual( + threadChannel.parentChannelId, + testMessage.channelId + ) + + try awaitResultValue(delay: 1) { + threadChannel.sendText( + text: "Text text text", + meta: nil, + shouldStore: true, + usePost: false, + ttl: nil, + mentionedUsers: nil, + referencedChannels: nil, + textLinks: nil, + quotedMessage: nil, + files: nil, + completion: $0 + ) + } + + let retrievedMessage = try XCTUnwrap( + try awaitResultValue(delay: 2) { + channel.getMessage( + timetoken: testMessage.timetoken, + completion: $0 + ) + } + ) + + XCTAssertTrue( + retrievedMessage.hasThread + ) + + addTeardownBlock { [unowned self] in + try awaitResult { + testMessage.removeThread( + completion: $0 + ) + } + try awaitResult { + retrievedMessage.delete( + completion: $0 + ) + } + } + } + + func testMessage_RemoveThread() throws { + let threadChannel = try awaitResultValue { + testMessage.createThread( + completion: $0 + ) + } + try awaitResultValue(delay: 1) { + threadChannel.sendText( + text: "Text text text", + completion: $0 + ) + } + let message = try XCTUnwrap( + try awaitResultValue(delay: 1) { + channel.getMessage( + timetoken: testMessage.timetoken, + completion: $0 + ) + } + ) + try awaitResultValue(delay: 1) { + message.removeThread(completion: $0) + } + + let retrievedMessage = try XCTUnwrap( + try awaitResultValue(delay: 1) { + channel.getMessage( + timetoken: testMessage.timetoken, + completion: $0 + ) + } + ) + + XCTAssertFalse( + retrievedMessage.hasThread + ) + + addTeardownBlock { [unowned self] in + try awaitResult { + testMessage.removeThread( + completion: $0 + ) + } + try awaitResult { + retrievedMessage.delete( + completion: $0 + ) + } + } + } + + func testMessage_ToggleReaction() throws { + let result = try awaitResultValue { + testMessage.toggleReaction( + reaction: ":+1", + completion: $0 + ) + } + + let reaction = try XCTUnwrap(result.reactions[":+1"]?.first) + let userId = reaction.uuid + + XCTAssertEqual(userId, chat.currentUser.id) + } + + func testMessage_StreamUpdates() throws { + let expectation = expectation(description: "StreamUpdates") + expectation.assertForOverFulfill = true + expectation.expectedFulfillmentCount = 1 + + let timetoken = try awaitResultValue { + channel?.sendText( + text: "Some text \(randomString())", + completion: $0 + ) + } + let message = try XCTUnwrap( + try awaitResultValue { + channel.getMessage( + timetoken: timetoken, + completion: $0 + ) + } + ) + + let closeable = message.streamUpdates { + XCTAssertTrue($0.hasUserReaction(reaction: "myReaction")) + XCTAssertEqual($0.channelId, message.channelId) + XCTAssertEqual($0.userId, message.userId) + expectation.fulfill() + } + + try awaitResultValue(delay: 3) { + message.toggleReaction( + reaction: "myReaction", + completion: $0 + ) + } + + wait( + for: [expectation], + timeout: 6 + ) + addTeardownBlock { [unowned self] in + closeable.close() + try awaitResult { message.delete(completion: $0) } + } + } + + func testMessage_GlobalStreamUpdates() throws { + let expectation = expectation(description: "StreamUpdates") + expectation.assertForOverFulfill = true + expectation.expectedFulfillmentCount = 1 + + let timetoken = try awaitResultValue { + channel?.sendText( + text: "Some text \(randomString())", + completion: $0 + ) + } + let message = try XCTUnwrap( + try awaitResultValue { + channel.getMessage( + timetoken: timetoken, + completion: $0 + ) + } + ) + + let closeable = MessageImpl.streamUpdatesOn(messages: [message]) { + let receivedMessage = $0[0] + XCTAssertTrue(receivedMessage.hasUserReaction(reaction: "myReaction")) + XCTAssertEqual(receivedMessage.channelId, message.channelId) + XCTAssertEqual(receivedMessage.userId, message.userId) + expectation.fulfill() + } + + try awaitResultValue(delay: 3) { + message.toggleReaction( + reaction: "myReaction", + completion: $0 + ) + } + + wait( + for: [expectation], + timeout: 6 + ) + addTeardownBlock { [unowned self] in + closeable.close() + try awaitResult { message.delete(completion: $0) } + } + } + + func testMessage_Restore() throws { + let message = try awaitResultValue { + testMessage.delete( + soft: true, + completion: $0 + ) + } + + let actions = try XCTUnwrap(message?.actions) + let deletedActions = try XCTUnwrap(actions[chat.deleteMessageActionName]) + + XCTAssertNotNil(message) + XCTAssertEqual(deletedActions.count, 1) + XCTAssertFalse(deletedActions.isEmpty) + + let restoredMessage = try awaitResultValue { + message?.restore( + completion: $0 + ) + } + + XCTAssertTrue(restoredMessage.actions?.isEmpty ?? false) + } +} diff --git a/Tests/PubNubSwiftChatSDKIntegrationTests.swift b/Tests/PubNubSwiftChatSDKIntegrationTests.swift new file mode 100644 index 0000000..9303c8f --- /dev/null +++ b/Tests/PubNubSwiftChatSDKIntegrationTests.swift @@ -0,0 +1,181 @@ +// +// SwiftChatSDKIntegrationTests.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import XCTest +import PubNubChat +import PubNubSwiftChatSDK +import PubNubSDK + +// MARK: - SwiftChatSDKIntegrationTests + +class PubNubSwiftChatSDKIntegrationTests: XCTestCase { + var chat: PubNubSwiftChatSDK.ChatImpl! + + private lazy var configuration: [String: String] = { + readPropertyList() + }() + + override func setUpWithError() throws { + try super.setUpWithError() + + let pubNubConfiguration = PubNubConfiguration( + publishKey: configuration["publishKey"]!, + subscribeKey: configuration["subscribeKey"]!, + userId: randomString() + ) + chat = ChatImpl( + chatConfiguration: ChatConfiguration(storeUserActivityTimestamps: true), + pubNubConfiguration: pubNubConfiguration + ) + + try awaitResultValue { chat.initialize(completion: $0) } + try customSetUpWitError() + } + + override func tearDownWithError() throws { + try customTearDownWithError() + try awaitResultValue { chat.deleteUser(id: chat.currentUser.id, completion: $0) } + + chat = nil + + try super.tearDownWithError() + } +} + +extension PubNubSwiftChatSDKIntegrationTests { + func customSetUpWitError() throws {} + func customTearDownWithError() throws {} +} + +// MARK: - Helpers + +extension PubNubSwiftChatSDKIntegrationTests { + private func readPropertyList() -> [String: String] { + let resourceName = "PubNubSwiftChatSDKTests" + let resourceExtension = "plist" + + guard let infoPlistPath = Bundle( + for: PubNubSwiftChatSDKIntegrationTests.self + ).url( + forResource: resourceName, + withExtension: resourceExtension + ) else { + fatalError("Cannot read \(resourceName).\(resourceExtension) file") + } + + guard let infoPlistData = try? Data(contentsOf: infoPlistPath) else { + fatalError("Cannot read content of \(resourceName).\(resourceExtension) file") + } + + guard let dictionary = try? PropertyListSerialization.propertyList( + from: infoPlistData, + options: [], + format: nil + ) as? [String: String] else { + fatalError("Cannot serialize \(resourceName).\(resourceExtension) into Dictionary") + } + + return dictionary + } +} + +extension PubNubSwiftChatSDKIntegrationTests { + func randomString(length: Int = 6) -> String { + // Define the characters set (alphanumeric) + let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + // Ensure length is within the desired range + let length = max(1, min(length, 6)) + // Generate the random string + return String((0..( + delay: TimeInterval = 0, + timeout: TimeInterval = 5, + operation: (@escaping (Swift.Result) -> Void) throws -> Void + ) throws -> T { + try awaitResult( + delay: delay, + timeout: timeout, + operation: operation + ).get() + } + + // Synchronously waits for an asynchronous operation that returns a `Result` + // and retrieves the `Error` value + @discardableResult + func awaitResultError( + delay: TimeInterval = 0, + timeout: TimeInterval = 5, + operation: (@escaping (Swift.Result) -> Void) throws -> Void + ) throws -> E { + switch try awaitResult(delay: delay, timeout: timeout, operation: operation) { + case .success: + fatalError("Unexpected condition") + case .failure(let error): + return error + } + } + + // Synchronously waits for an asynchronous operation that returns a Result, + // and then returns it to the caller + @discardableResult + func awaitResult( + delay: TimeInterval = 0, + timeout: TimeInterval = 5, + operation: (@escaping (Swift.Result) -> Void) throws -> Void + ) throws -> Swift.Result { + + // Waits for the specified number of seconds if the call needs to be delayed + if delay > 0 { + wait(delay) + } + + // Create an XCTestExpectation to pause the test execution + // until the asynchronous operation completes + let expectation = expectation(description: "Waiting for an async operation") + expectation.assertForOverFulfill = true + expectation.expectedFulfillmentCount = 1 + + // A variable to store the result of the asynchronous operation + var result: Swift.Result! + + // Execute the asynchronous operation and capture the result + try operation { res in + result = res + expectation.fulfill() + } + + // Wait for the expectation to be fulfilled or until the timeout occurs + wait(for: [expectation], timeout: timeout + 1) + + // If the result is a success, return the value + return result + } +} + +extension PubNubSwiftChatSDKIntegrationTests { + private func wait(_ duration: TimeInterval) { + // Define the expectation to fulfill + let expectation = expectation(description: "Waiting for \(duration) seconds") + // Dispatch a delay on a background queue + DispatchQueue.main.asyncAfter(deadline: .now() + duration) { + expectation.fulfill() + } + // Wait for the expectation to be fulfilled or timeout + wait(for: [expectation], timeout: duration + 1) + } +} diff --git a/Tests/PubNubSwiftChatSDKTests.plist b/Tests/PubNubSwiftChatSDKTests.plist new file mode 100644 index 0000000..39dbb33 --- /dev/null +++ b/Tests/PubNubSwiftChatSDKTests.plist @@ -0,0 +1,12 @@ + + + + + publishKey + + subscribeKey + + userId + + + diff --git a/Tests/PubNubSwiftChatSDKTests.xctestplan b/Tests/PubNubSwiftChatSDKTests.xctestplan new file mode 100644 index 0000000..7025d27 --- /dev/null +++ b/Tests/PubNubSwiftChatSDKTests.xctestplan @@ -0,0 +1,24 @@ +{ + "configurations" : [ + { + "id" : "B2FFD29C-23BF-4550-A839-AF5A3B18B379", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + "testTimeoutsEnabled" : true + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:PubNubSwiftChatSDK.xcodeproj", + "identifier" : "3DB73A102C4FE13C007FE249", + "name" : "PubNubSwiftChatSDKTests" + } + } + ], + "version" : 1 +} diff --git a/Tests/ThreadChannelIntegrationTests.swift b/Tests/ThreadChannelIntegrationTests.swift new file mode 100644 index 0000000..948a85f --- /dev/null +++ b/Tests/ThreadChannelIntegrationTests.swift @@ -0,0 +1,87 @@ +// +// ThreadChannelTests.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import PubNubSwiftChatSDK +import PubNubSDK +import XCTest + +class ThreadChannelIntegrationTests: PubNubSwiftChatSDKIntegrationTests { + var parentChannel: ChannelImpl! + var threadChannel: ThreadChannelImpl! + + override func customSetUpWitError() throws { + parentChannel = try XCTUnwrap( + try awaitResultValue { + chat.createChannel( + id: randomString(), + completion: $0 + ) + } + ) + let testMessage = try XCTUnwrap( + try awaitResultValue { + parentChannel.getMessage( + timetoken: try awaitResultValue { + parentChannel?.sendText( + text: "Message", + completion: $0 + ) + }, + completion: $0 + ) + } + ) + threadChannel = try XCTUnwrap( + try awaitResultValue { + testMessage.createThread( + completion: $0 + ) + } + ) + + try awaitResultValue { + threadChannel.sendText( + text: "Reply in a thread", + completion: $0 + ) + } + } + + override func customTearDownWithError() throws { + try awaitResult { parentChannel.delete(completion: $0) } + try awaitResult { threadChannel.delete(completion: $0) } + } + + func testThreadChannel_PinMessageToParentChannel() throws { + let message = try XCTUnwrap( + try awaitResultValue { + threadChannel.getHistory( + completion: $0 + ) + }.messages.first + ) + + let updatedChannel = try awaitResultValue { + threadChannel.pinMessageToParentChannel( + message: message, + completion: $0 + ) + } + + XCTAssertNotNil( + try awaitResultValue { + updatedChannel.getPinnedMessage( + completion: $0 + ) + } + ) + } +} diff --git a/Tests/ThreadMessageIntegrationTests.swift b/Tests/ThreadMessageIntegrationTests.swift new file mode 100644 index 0000000..3c8ef82 --- /dev/null +++ b/Tests/ThreadMessageIntegrationTests.swift @@ -0,0 +1,339 @@ +// +// ThreadMessageTests.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import XCTest +import PubNubSwiftChatSDK +import PubNubSDK + +class ThreadMessageIntegrationTests: PubNubSwiftChatSDKIntegrationTests { + var channel: ChannelImpl! + var threadChannel: ThreadChannelImpl! + var threadMessage: ThreadMessageImpl! + var threadMessageTimetoken: Timetoken! + + override func customSetUpWitError() throws { + channel = try XCTUnwrap( + try awaitResultValue { + chat.createChannel( + id: randomString(), + completion: $0 + ) + } + ) + let testMessage = try XCTUnwrap( + try awaitResultValue { + channel.getMessage( + timetoken: try awaitResultValue { + channel?.sendText( + text: "text", + completion: $0 + ) + }, + completion: $0 + ) + } + ) + threadChannel = try XCTUnwrap( + try awaitResultValue { + testMessage.createThread( + completion: $0 + ) + } + ) + + threadMessageTimetoken = try awaitResultValue { + threadChannel.sendText( + text: "Some text", + completion: $0 + ) + } + + threadMessage = try XCTUnwrap( + try awaitResultValue(delay: 2) { + threadChannel.getHistory(completion: $0) + }.messages.first + ) + } + + override func customTearDownWithError() throws { + try awaitResult { threadMessage.delete(completion: $0) } + try awaitResult { threadChannel.delete(completion: $0) } + try awaitResult { chat.deleteChannel(id: channel.id, completion: $0) } + + channel = nil + threadChannel = nil + threadMessage = nil + threadMessageTimetoken = nil + } + + func testThreadMessage_HasUserReactions() throws { + XCTAssertFalse(threadMessage.hasUserReaction(reaction: "someReaction")) + } + + func testThreadMessage_EditText() throws { + let currentMessageText = threadMessage.text + let newText = "NewTextValue" + + XCTAssertNotEqual( + currentMessageText, + newText + ) + + let editedMessage = try awaitResultValue { + threadMessage.editText( + newText: newText, + completion: $0 + ) + } + + XCTAssertEqual( + editedMessage.text, + newText + ) + } + + func testThreadMessage_Delete() throws { + XCTAssertNil(try awaitResultValue { + threadMessage.delete( + soft: false, + preserveFiles: false, + completion: $0 + ) + }) + } + + func testThreadMessage_SoftDelete() throws { + let value = try awaitResultValue { + threadMessage.delete( + soft: true, + preserveFiles: false, + completion: $0 + ) + } + + let actions = try XCTUnwrap(value?.actions) + let deletedActions = try XCTUnwrap(actions[chat.deleteMessageActionName]) + + XCTAssertNotNil(value) + XCTAssertFalse(deletedActions.isEmpty) + } + + func testThreadMessage_GetThread() throws { + let error = try awaitResultError { + threadMessage.getThread( + completion: $0 + ) + } + + XCTAssertEqual((error as? ChatError)?.message, "This message is not a thread.") + } + + func testThreadMessage_Forward() throws { + let anotherChannel = try XCTUnwrap( + try awaitResultValue { + chat.createChannel( + id: randomString(), + completion: $0 + ) + } + ) + + let forwardValue = try awaitResultValue { + threadMessage.forward( + channelId: anotherChannel.id, + completion: $0 + ) + } + let message = try awaitResultValue { + anotherChannel.getMessage( + timetoken: forwardValue, + completion: $0 + ) + } + + XCTAssertNotNil(message) + XCTAssertEqual(message?.text, threadMessage.text) + XCTAssertEqual(message?.userId, threadMessage.userId) + + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteChannel( + id: anotherChannel.id, + completion: $0 + ) + } + } + } + + func testThreadMessage_Pin() throws { + let resultingChannel = try awaitResultValue { + threadMessage.pin( + completion: $0 + ) + } + let message = try awaitResultValue(delay: 2) { + resultingChannel.getPinnedMessage( + completion: $0 + ) + } + + XCTAssertNotNil(message) + XCTAssertEqual(resultingChannel.id, threadMessage.channelId) + } + + func testThreadMessage_Report() throws { + XCTAssertNotNil(try awaitResultValue { + threadMessage.report(reason: "ReportReason", completion: $0) + }) + } + + func testThreadMessage_CreateThread() throws { + let error = try awaitResultError { + threadMessage.createThread( + completion: $0 + ) + } + + XCTAssertEqual((error as? ChatError)?.message, "Only one level of thread nesting is allowed.") + } + + func testThreadMessage_RemoveThread() throws { + let error = try awaitResultError { + threadMessage.removeThread( + completion: $0 + ) + } + + XCTAssertEqual((error as? ChatError)?.message, "There is no thread to be deleted.") + } + + func testThreadMessage_ToggleReaction() throws { + let result = try awaitResultValue { + threadMessage.toggleReaction( + reaction: ":+1", + completion: $0 + ) + } + + let reaction = try XCTUnwrap(result.reactions[":+1"]?.first) + let userId = reaction.uuid + + XCTAssertEqual( + userId, + chat.currentUser.id + ) + } + + func testThreadMessage_StreamUpdates() throws { + let expectation = expectation(description: "StreamUpdates") + expectation.assertForOverFulfill = true + expectation.expectedFulfillmentCount = 1 + + let message = try XCTUnwrap( + try awaitResultValue { + threadChannel.getHistory(completion: $0) + }.messages.first + ) + + let closeable = message.streamUpdates { + XCTAssertTrue($0.hasUserReaction(reaction: "myReaction")) + XCTAssertEqual($0.channelId, message.channelId) + XCTAssertEqual($0.userId, message.userId) + expectation.fulfill() + } + + try awaitResultValue(delay: 3) { + message.toggleReaction( + reaction: "myReaction", + completion: $0 + ) + } + + wait( + for: [expectation], + timeout: 6 + ) + addTeardownBlock { [unowned self] in + closeable.close() + try awaitResult { message.delete(completion: $0) } + } + } + + func testThreadMessage_GlobalStreamUpdates() throws { + let expectation = expectation(description: "StreamUpdates") + expectation.assertForOverFulfill = true + expectation.expectedFulfillmentCount = 1 + + let message = try XCTUnwrap( + try awaitResultValue { + threadChannel.getHistory(completion: $0) + }.messages.first + ) + + let closeable = ThreadMessageImpl.streamUpdatesOn(messages: [message]) { + let receivedMessage = $0[0] + XCTAssertTrue(receivedMessage.hasUserReaction(reaction: "myReaction")) + XCTAssertEqual(receivedMessage.channelId, message.channelId) + XCTAssertEqual(receivedMessage.userId, message.userId) + expectation.fulfill() + } + + try awaitResultValue(delay: 3) { + message.toggleReaction( + reaction: "myReaction", + completion: $0 + ) + } + + wait( + for: [expectation], + timeout: 6 + ) + addTeardownBlock { [unowned self] in + closeable.close() + try awaitResult { message.delete(completion: $0) } + } + } + + func testThreadMessage_PinMessageToParentChannel() throws { + let resultingChannel = try awaitResultValue { + threadMessage.pinToParentChannel( + completion: $0 + ) + } + let pinnedMessage = try awaitResultValue { + resultingChannel.getPinnedMessage( + completion: $0 + ) + } + + XCTAssertNotNil(pinnedMessage) + } + + func testThreadMessage_UnpinMessageFromParentChannel() throws { + try awaitResultValue { + threadMessage.pinToParentChannel( + completion: $0 + ) + } + let updatedChannel = try awaitResultValue { + threadMessage.unpinFromParentChannel( + completion: $0 + ) + } + let pinnedMessage = try awaitResultValue { + updatedChannel.getPinnedMessage( + completion: $0 + ) + } + XCTAssertNil(pinnedMessage) + } +} diff --git a/Tests/UserIntegrationTests.swift b/Tests/UserIntegrationTests.swift new file mode 100644 index 0000000..3af4a1c --- /dev/null +++ b/Tests/UserIntegrationTests.swift @@ -0,0 +1,331 @@ +// +// UserTests.swift +// +// Copyright (c) PubNub Inc. +// All rights reserved. +// +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. +// + +import Foundation +import XCTest +import PubNubSwiftChatSDK +import PubNubSDK + +final class UserIntegrationTests: PubNubSwiftChatSDKIntegrationTests { + func testableUser() -> PubNubSwiftChatSDK.UserImpl { + UserImpl( + chat: chat, + id: randomString(), + name: "Marian Salazar", + externalId: "5445888223", + profileUrl: "https://picsum.photos/100/200", + email: "marian.salazar@pubnub.com", + custom: ["age": 36, "city": "London"], + status: "active", + type: "admin" + ) + } + + func testUser_CreateUser() throws { + let user = testableUser() + let createdUser = try awaitResultValue { chat.createUser(user: user, completion: $0) } + + XCTAssertEqual(createdUser.id, user.id) + XCTAssertEqual(createdUser.name, user.name) + XCTAssertEqual(createdUser.externalId, user.externalId) + XCTAssertEqual(createdUser.profileUrl, user.profileUrl) + XCTAssertEqual(createdUser.email, user.email) + XCTAssertEqual(createdUser.custom?.mapValues { $0.scalarValue }, user.custom?.mapValues { $0.scalarValue }) + XCTAssertEqual(createdUser.status, user.status) + XCTAssertEqual(createdUser.type, user.type) + + addTeardownBlock { [unowned self] in + try awaitResult { chat.deleteUser( + id: user.id, + completion: $0 + )} + } + } + + func testUser_UpdateUser() throws { + let newCustom: [String: JSONCodableScalar] = [ + "age": 21, + "city": "Birmingham" + ] + let updatedUser = try awaitResultValue { + chat.currentUser.update( + name: "Markus Koller", + externalId: "11111111", + profileUrl: "https://picsum.photos/200/300", + email: "markus.koller@pubnub.com", + custom: newCustom, + status: "inactive", + type: "regular", + completion: $0 + ) + } + + XCTAssertEqual(updatedUser.name, "Markus Koller") + XCTAssertEqual(updatedUser.externalId, "11111111") + XCTAssertEqual(updatedUser.profileUrl, "https://picsum.photos/200/300") + XCTAssertEqual(updatedUser.email, "markus.koller@pubnub.com") + XCTAssertEqual(updatedUser.custom?.compactMapValues { $0.scalarValue }, newCustom.mapValues { $0.scalarValue }) + XCTAssertEqual(updatedUser.status, "inactive") + XCTAssertEqual(updatedUser.type, "regular") + } + + func testUser_UpdateNotExistingUser() throws { + let someUser = testableUser() + let error = try awaitResultError { + someUser.update( + name: "NewName", + externalId: "NewExternalId", + completion: $0 + ) + } + + XCTAssertEqual((error as? ChatError)?.message, "User does not exist") + } + + func testUser_IsPresentOn() throws { + let channelId = randomString() + let createdChannel = try awaitResultValue { chat.createChannel(id: channelId, name: channelId, completion: $0) } + let closeable = createdChannel.connect(callback: { _ in }) + + XCTAssertTrue(try awaitResultValue(delay: 4) { + chat.currentUser.isPresentOn( + channelId: createdChannel.id, + completion: $0 + ) + }) + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteChannel( + id: createdChannel.id, + completion: $0 + ) + } + closeable.close() + } + } + + func testUser_DeleteUser() throws { + let createdUser = try awaitResultValue { + chat.createUser( + user: testableUser(), + completion: $0 + ) + } + let deletedUser = try awaitResultValue { + createdUser.delete( + soft: false, + completion: $0 + ) + } + XCTAssertEqual( + createdUser.id, + deletedUser.id + ) + } + + func testUser_DeleteNotExistingUser() throws { + let someUser = testableUser() + let error = try awaitResultError { someUser.delete(soft: false, completion: $0) } + + XCTAssertEqual((error as? ChatError)?.message, "User does not exist") + } + + func testUser_WherePresent() throws { + let channelId = randomString() + let channel = try awaitResultValue { chat.createChannel(id: channelId, name: channelId, completion: $0) } + let closeable = channel.connect(callback: { _ in }) + + XCTAssertEqual( + try awaitResultValue(delay: 5) { + chat.currentUser.wherePresent(completion: $0) + }, [channelId] + ) + + addTeardownBlock { [unowned self] in + try awaitResult { + chat.deleteChannel( + id: channel.id, + completion: $0 + ) + } + closeable.close() + } + } + + func testUser_IsActive() throws { + let channelId = randomString() + let channel = try awaitResultValue { chat.createChannel(id: channelId, name: channelId, completion: $0) } + let closeable = channel.connect(callback: { _ in }) + + XCTAssertTrue( + try awaitResultValue(delay: 4) { + chat.currentUser.active( + completion: $0 + ) + } + ) + + addTeardownBlock { [unowned self] in + try awaitResultValue { + chat.deleteChannel( + id: channel.id, + completion: $0 + ) + } + closeable.close() + } + } + + func testUser_GetMemberships() throws { + let channel = try awaitResultValue { + chat.createChannel( + id: randomString(), + completion: $0 + ) + } + try awaitResultValue { + channel.invite( + user: chat.currentUser, + completion: $0 + ) + } + let resultValue = try awaitResultValue { + chat.currentUser.getMemberships( + limit: nil, + page: nil, + filter: nil, + sort: [], + completion: $0 + ) + } + + XCTAssertEqual((try XCTUnwrap(resultValue.memberships.first)).user.id, chat.currentUser.id) + XCTAssertEqual((try XCTUnwrap(resultValue.memberships.first)).channel.id, channel.id) + + addTeardownBlock { [unowned self] in + try awaitResultValue { + chat.deleteChannel( + id: channel.id, + completion: $0 + ) + } + } + } + + func testUser_StreamUpdates() throws { + let expectation = expectation(description: "StreamUpdates") + expectation.assertForOverFulfill = true + expectation.expectedFulfillmentCount = 1 + + let createdUser = try awaitResultValue { + chat.createUser( + user: testableUser(), + completion: $0 + ) + } + let closeable = createdUser.streamUpdates { user in + XCTAssertEqual(user?.name, "NewName") + XCTAssertEqual(user?.externalId, "NewExternalId") + XCTAssertEqual(user?.profileUrl, "NewProfileUrl") + XCTAssertEqual(user?.email, "NewEmail") + XCTAssertEqual(user?.custom?.mapValues { $0.scalarValue }, ["city": "Manchester"].mapValues { $0.scalarValue }) + XCTAssertEqual(user?.status, "NewStatus") + XCTAssertEqual(user?.type, "NewType") + expectation.fulfill() + } + + try awaitResultValue(delay: 3) { + createdUser.update( + name: "NewName", + externalId: "NewExternalId", + profileUrl: "NewProfileUrl", + email: "NewEmail", + custom: ["city": "Manchester"], + status: "NewStatus", + type: "NewType", + completion: $0 + ) + } + + wait( + for: [expectation], + timeout: 6 + ) + addTeardownBlock { [unowned self] in + // First, close AutoCloseable to prevent receiving stream updates while performing + // cleanup at the end of the test + closeable.close() + + try awaitResult { + chat.deleteUser( + id: createdUser.id, + completion: $0 + ) + } + } + } + + func testUser_GlobalStreamUpdates() throws { + let expectation = expectation(description: "StreamUpdates") + expectation.assertForOverFulfill = true + expectation.expectedFulfillmentCount = 2 + + let firstUser = try awaitResultValue { chat.createUser(user: testableUser(), completion: $0) } + let secondUser = try awaitResultValue { chat.createUser(user: testableUser(), completion: $0) } + + let closeable = UserImpl.streamUpdatesOn(users: [firstUser, secondUser]) { users in + if users.first?.id ?? "" == firstUser.id { + expectation.fulfill() + } else if users.first?.id ?? "" == secondUser.id { + expectation.fulfill() + } else { + XCTFail("Unexpected condition") + } + } + + try [firstUser, secondUser].forEach { user in + try awaitResultValue(delay: 3) { + user.update( + name: randomString(), + externalId: nil, + profileUrl: nil, + email: nil, + custom: nil, + status: nil, + type: nil, + completion: $0 + ) + } + } + + wait( + for: [expectation], + timeout: 6 + ) + addTeardownBlock { [unowned self] in + // First, close AutoCloseable to prevent receiving stream updates while performing + // cleanup at the end of the test + closeable.close() + + try awaitResult { + chat.deleteUser( + id: firstUser.id, + completion: $0 + ) + } + try awaitResult { + chat.deleteUser( + id: secondUser.id, + completion: $0 + ) + } + } + } +}