├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cartfile ├── Cartfile.resolved ├── Configs ├── Faye.plist ├── FayeTests.plist ├── GetStream.plist └── GetStreamTests.plist ├── Faye.podspec ├── Faye ├── Channel.swift ├── ChannelName.swift ├── Client.swift ├── Message.swift └── RepeatingTimer.swift ├── Gemfile ├── GetStream.podspec ├── GetStream.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── Faye.xcscheme │ └── GetStream-iOS.xcscheme ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── Core │ ├── Activity │ │ ├── Activity.swift │ │ ├── ActivityEndpoint.swift │ │ ├── ActivityProtocol.swift │ │ ├── Client+Activity.swift │ │ ├── EnrichedActivity.swift │ │ └── OriginalRepresentable.swift │ ├── Client │ │ ├── BaseURL.swift │ │ ├── Client+Setup.swift │ │ ├── Client.swift │ │ ├── ClientError.swift │ │ ├── ClientLogger.swift │ │ ├── Enrichable.swift │ │ ├── MissingReference.swift │ │ ├── Pagination.swift │ │ ├── Response.swift │ │ ├── Result+ParseResponse.swift │ │ ├── StreamTargetType.swift │ │ └── Token.swift │ ├── Collection │ │ ├── Client+Collection.swift │ │ ├── CollectionEndpoint.swift │ │ ├── CollectionObject.swift │ │ └── CollectionObjectProtocol.swift │ ├── Extensions │ │ ├── Bundle+Extensions.swift │ │ ├── Codable+Extensions.swift │ │ ├── Dictionary+Extensions.swift │ │ └── Result+Extensions.swift │ ├── Feed │ │ ├── Aggregated Feed │ │ │ ├── AggregatedFeed.swift │ │ │ ├── Group.swift │ │ │ └── Result+ParseGroup.swift │ │ ├── Feed.swift │ │ ├── FeedEndpoint.swift │ │ ├── FeedId.swift │ │ ├── FlatFeed.swift │ │ ├── Following │ │ │ ├── Feed+Following.swift │ │ │ └── Follower.swift │ │ ├── Notification Feed │ │ │ ├── NotificationFeed.swift │ │ │ └── NotificationGroup.swift │ │ └── Result+ParseFeed.swift │ ├── Files and Images │ │ ├── Client+Files.swift │ │ ├── File.swift │ │ ├── FilesEndpoint.swift │ │ ├── ImageProcess.swift │ │ ├── Result+ParseUpload.swift │ │ └── Swime+Extensions.swift │ ├── GetStream.swift │ ├── Moya │ │ ├── AuthorizationMoyaPlugin.swift │ │ └── MoyaError+ClientError.swift │ ├── Open Graph │ │ ├── Client+OpenGraph.swift │ │ └── OpenGraphEndpoint.swift │ ├── Reactions │ │ ├── Client+Reaction.swift │ │ ├── Reaction.swift │ │ ├── ReactionEndpoint.swift │ │ ├── ReactionExtraDataProtocol.swift │ │ ├── ReactionKind.swift │ │ ├── ReactionProtocol.swift │ │ ├── Reactionable.swift │ │ ├── Reactions.swift │ │ └── Result+ParseReaction.swift │ └── User │ │ ├── Client+User.swift │ │ ├── User.swift │ │ ├── UserEndpoint.swift │ │ └── UserProtocol.swift ├── Faye │ ├── Client+Faye.swift │ ├── Feed+Faye.swift │ └── SubscriptionResponse.swift └── Token │ └── Token+Generator.swift ├── Tests ├── Core │ ├── AggregatedFeedTests.swift │ ├── ClientParsingTests.swift │ ├── ClientTests.swift │ ├── CollectionTests.swift │ ├── ExtensionsTests.swift │ ├── FeedTests.swift │ ├── FilesTests.swift │ ├── NotificationFeedTests.swift │ ├── OGTests.swift │ ├── ReactionTests.swift │ └── UserTests.swift ├── Extensions │ ├── String+Extensions.swift │ └── TestCase.swift ├── Faye │ └── FayeTests.swift └── Token │ └── TokenTests.swift ├── docs ├── Classes.html ├── Classes │ ├── AggregatedFeed.html │ ├── Client.html │ ├── Client │ │ ├── Config.html │ │ └── RateLimit.html │ ├── ClientLogger.html │ ├── CollectionObject.html │ ├── CollectionObject │ │ ├── CollectionObjectCodingKeys.html │ │ └── DataCodingKeys.html │ ├── EnrichedActivity.html │ ├── Feed.html │ ├── FlatFeed.html │ ├── Group.html │ ├── NotificationFeed.html │ ├── NotificationGroup.html │ ├── Reaction.html │ ├── SubscribedChannel.html │ ├── User.html │ └── User │ │ ├── DataCodingKeys.html │ │ └── UserCodingKeys.html ├── Enums.html ├── Enums │ ├── ClientError.html │ ├── ClientError │ │ └── Info.html │ ├── FeedMarkOption.html │ ├── Pagination.html │ ├── ReactionExtraData.html │ ├── ReactionsError.html │ ├── StringOrInt.html │ └── SubscriptionError.html ├── Extensions.html ├── Extensions │ ├── Array.html │ ├── Bundle.html │ ├── Bundle │ │ └── StreamKey.html │ ├── Client.html │ ├── Data.html │ ├── Date.html │ ├── DateFormatter.html │ ├── DateFormatter │ │ └── Stream.html │ ├── Dictionary.html │ ├── Image.html │ ├── JSONDecoder.html │ ├── JSONEncoder.html │ ├── MoyaError.html │ ├── ReactionKind.html │ ├── Result.html │ ├── String.html │ ├── Swime.html │ ├── Task.html │ └── Token.html ├── Protocols.html ├── Protocols │ ├── ActivityProtocol.html │ ├── CollectionObjectProtocol.html │ ├── Enrichable.html │ ├── ISO8601DateFormatter.html │ ├── Missable.html │ ├── OriginalRepresentable.html │ ├── ReactionProtocol.html │ ├── Reactionable.html │ └── UserProtocol.html ├── Structs.html ├── Structs │ ├── BaseURL.html │ ├── BaseURL │ │ ├── Location.html │ │ └── Service.html │ ├── Comment.html │ ├── EmptyReactionExtraData.html │ ├── EnrichingActivityError.html │ ├── FeedId.html │ ├── FeedReactionsOptions.html │ ├── File.html │ ├── Follower.html │ ├── ImageProcess.html │ ├── ImageProcess │ │ ├── CropMode.html │ │ └── ResizeStrategy.html │ ├── MissingReference.html │ ├── OGAudioResponse.html │ ├── OGImageResponse.html │ ├── OGResponse.html │ ├── OGVideoResponse.html │ ├── Reaction.html │ ├── Reactions.html │ ├── Response.html │ └── SubscriptionResponse.html ├── Typealiases.html ├── _config.yml ├── badge.svg ├── css │ ├── highlight.css │ └── jazzy.css ├── docsets │ ├── GetStream.docset │ │ └── Contents │ │ │ ├── Info.plist │ │ │ └── Resources │ │ │ ├── Documents │ │ │ ├── Classes.html │ │ │ ├── Classes │ │ │ │ ├── AggregatedFeed.html │ │ │ │ ├── Client.html │ │ │ │ ├── Client │ │ │ │ │ ├── Config.html │ │ │ │ │ └── RateLimit.html │ │ │ │ ├── ClientLogger.html │ │ │ │ ├── CollectionObject.html │ │ │ │ ├── CollectionObject │ │ │ │ │ ├── CollectionObjectCodingKeys.html │ │ │ │ │ └── DataCodingKeys.html │ │ │ │ ├── EnrichedActivity.html │ │ │ │ ├── Feed.html │ │ │ │ ├── FlatFeed.html │ │ │ │ ├── Group.html │ │ │ │ ├── NotificationFeed.html │ │ │ │ ├── NotificationGroup.html │ │ │ │ ├── Reaction.html │ │ │ │ ├── SubscribedChannel.html │ │ │ │ ├── User.html │ │ │ │ └── User │ │ │ │ │ ├── DataCodingKeys.html │ │ │ │ │ └── UserCodingKeys.html │ │ │ ├── Enums.html │ │ │ ├── Enums │ │ │ │ ├── ClientError.html │ │ │ │ ├── ClientError │ │ │ │ │ └── Info.html │ │ │ │ ├── FeedMarkOption.html │ │ │ │ ├── Pagination.html │ │ │ │ ├── ReactionExtraData.html │ │ │ │ ├── ReactionsError.html │ │ │ │ ├── StringOrInt.html │ │ │ │ └── SubscriptionError.html │ │ │ ├── Extensions.html │ │ │ ├── Extensions │ │ │ │ ├── Array.html │ │ │ │ ├── Bundle.html │ │ │ │ ├── Bundle │ │ │ │ │ └── StreamKey.html │ │ │ │ ├── Client.html │ │ │ │ ├── Data.html │ │ │ │ ├── Date.html │ │ │ │ ├── DateFormatter.html │ │ │ │ ├── DateFormatter │ │ │ │ │ └── Stream.html │ │ │ │ ├── Dictionary.html │ │ │ │ ├── Image.html │ │ │ │ ├── JSONDecoder.html │ │ │ │ ├── JSONEncoder.html │ │ │ │ ├── MoyaError.html │ │ │ │ ├── ReactionKind.html │ │ │ │ ├── Result.html │ │ │ │ ├── String.html │ │ │ │ ├── Swime.html │ │ │ │ ├── Task.html │ │ │ │ └── Token.html │ │ │ ├── Protocols.html │ │ │ ├── Protocols │ │ │ │ ├── ActivityProtocol.html │ │ │ │ ├── CollectionObjectProtocol.html │ │ │ │ ├── Enrichable.html │ │ │ │ ├── ISO8601DateFormatter.html │ │ │ │ ├── Missable.html │ │ │ │ ├── OriginalRepresentable.html │ │ │ │ ├── ReactionProtocol.html │ │ │ │ ├── Reactionable.html │ │ │ │ └── UserProtocol.html │ │ │ ├── Structs.html │ │ │ ├── Structs │ │ │ │ ├── BaseURL.html │ │ │ │ ├── BaseURL │ │ │ │ │ ├── Location.html │ │ │ │ │ └── Service.html │ │ │ │ ├── Comment.html │ │ │ │ ├── EmptyReactionExtraData.html │ │ │ │ ├── EnrichingActivityError.html │ │ │ │ ├── FeedId.html │ │ │ │ ├── FeedReactionsOptions.html │ │ │ │ ├── File.html │ │ │ │ ├── Follower.html │ │ │ │ ├── ImageProcess.html │ │ │ │ ├── ImageProcess │ │ │ │ │ ├── CropMode.html │ │ │ │ │ └── ResizeStrategy.html │ │ │ │ ├── MissingReference.html │ │ │ │ ├── OGAudioResponse.html │ │ │ │ ├── OGImageResponse.html │ │ │ │ ├── OGResponse.html │ │ │ │ ├── OGVideoResponse.html │ │ │ │ ├── Reaction.html │ │ │ │ ├── Reactions.html │ │ │ │ ├── Response.html │ │ │ │ └── SubscriptionResponse.html │ │ │ ├── Typealiases.html │ │ │ ├── _config.yml │ │ │ ├── badge.svg │ │ │ ├── css │ │ │ │ ├── highlight.css │ │ │ │ └── jazzy.css │ │ │ ├── img │ │ │ │ ├── carat.png │ │ │ │ ├── dash.png │ │ │ │ ├── gh.png │ │ │ │ └── spinner.gif │ │ │ ├── index.html │ │ │ ├── js │ │ │ │ ├── jazzy.js │ │ │ │ ├── jazzy.search.js │ │ │ │ ├── jquery.min.js │ │ │ │ ├── lunr.min.js │ │ │ │ └── typeahead.jquery.js │ │ │ ├── search.json │ │ │ └── undocumented.json │ │ │ └── docSet.dsidx │ └── GetStream.tgz ├── img │ ├── carat.png │ ├── dash.png │ ├── gh.png │ └── spinner.gif ├── index.html ├── js │ ├── jazzy.js │ ├── jazzy.search.js │ ├── jquery.min.js │ ├── lunr.min.js │ └── typeahead.jquery.js ├── search.json └── undocumented.json └── fastlane ├── Fastfile └── README.md /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: buh 7 | 8 | --- 9 | 10 | ## What did you do? 11 | 12 | 13 | ## What did you expect to happen? 14 | 15 | 16 | ## What happened instead? 17 | 18 | 19 | ## GetStream Environment 20 | **GetStream version:** 21 | **Xcode version:** 22 | **Swift version:** 23 | **Platform(s) running GetStream:** 24 | **macOS version running Xcode:** 25 | 26 | ## Additional context 27 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Submit a pull request 2 | 3 | ## CLA 4 | 5 | - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required). 6 | - [ ] The code changes follow best practices 7 | - [ ] Code changes are tested (add some information if not applicable) 8 | 9 | ## Description of the pull request -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | release: 11 | types: 12 | - created 13 | 14 | jobs: 15 | build-and-test: 16 | runs-on: macos-latest 17 | steps: 18 | - uses: actions/checkout@v1 19 | - name: Cache dependencies 20 | uses: actions/cache@v1.1.0 21 | id: carthage-cache 22 | with: 23 | path: Carthage 24 | key: ${{ runner.os }}-carthage-stable-${{ hashFiles('**/Cartfile.resolved') }} 25 | restore-keys: | 26 | ${{ runner.os }}-carthage-stable- 27 | - name: Install Carthage dependencies 28 | if: steps.carthage-cache.outputs.cache-hit != 'true' 29 | run: echo 'BUILD_LIBRARY_FOR_DISTRIBUTION=YES'>/tmp/config.xcconfig; XCODE_XCCONFIG_FILE=/tmp/config.xcconfig carthage update --platform iOS --new-resolver --no-use-binaries --cache-builds; rm /tmp/config.xcconfig 30 | - name: Clean and build the GetStream scheme 31 | run: xcodebuild clean test -project GetStream.xcodeproj -scheme GetStream-iOS -destination "platform=iOS Simulator,name=iPhone 11 Pro" 32 | - name: Post Codecov report 33 | run: bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_TOKEN }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | # 6 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 7 | 8 | ## Build generated 9 | build/ 10 | DerivedData/ 11 | 12 | ## Various settings 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata/ 22 | 23 | ## Other 24 | *.moved-aside 25 | *.xccheckout 26 | *.xcscmblueprint 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | *.dSYM.zip 32 | *.dSYM 33 | 34 | ## Playgrounds 35 | timeline.xctimeline 36 | playground.xcworkspace 37 | 38 | # Swift Package Manager 39 | # 40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 41 | # Packages/ 42 | # Package.pins 43 | .build/ 44 | 45 | # CocoaPods 46 | # 47 | # We recommend against adding the Pods directory to your .gitignore. However 48 | # you should judge for yourself, the pros and cons are mentioned at: 49 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 50 | Pods/ 51 | 52 | # Carthage 53 | # 54 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 55 | Carthage/Checkouts 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots 68 | fastlane/test_output 69 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Stream Feeds Swift SDK 2 | 3 | We're glad you want to contribute to the Stream team 🎉 4 | 5 | --- 6 | 7 | _So you..._ 8 | 9 | ### Got stuck on something 💭 10 | 11 | Please check [stackoverflow](https://stackoverflow.com/questions/tagged/getstream-io) and ask your questions there. 12 | If your question is not generic, you can send us a [support request](https://getstream.io/support). 13 | 14 | ### Found a bug 🐞 15 | 16 | Please create a github issue with as much info as possible (follow the Issue Template closely). 17 | 18 | ### Have a feature request 📈 19 | 20 | Please create a github issue with as much info as possible. 21 | 22 | ### Fixed a bug 🩹 23 | 24 | Please open a PR with as much info as possible: clear description of the problem and the solution. 25 | Include the relevant github issue number if applicable. 26 | 27 | Make sure Changelog is updated correspondingly (we'll probably change wording but it'll help us immensely) 28 | 29 | Before submitting, please make sure you're finished with the PR (and all tests pass) and do not make changes until it's reviewed. 30 | 31 | ### Implemented or changed a feature 🌈 32 | 33 | Guidelines on "Fixed a bug" part is applicable. 34 | 35 | ## Our Release flow 🚀 36 | 37 | We make sure to follow all QA test procedure for minor and major releases. 38 | 39 | We accumulate changes and release them in batches, unless high priority. 40 | We make sure to put staged changes (on master but not released in a version) as "Upcoming" in [CHANGELOG](https://github.com/GetStream/stream-swift/blob/master/CHANGELOG.md). 41 | 42 | If possible, we deprecate stuff before removing them directly. Deprecated stuff will be removed after a minor release, and will include a migration/upgrade guide. 43 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "Moya/Moya.git" ~> 14.0 2 | github "daltoniam/Starscream" ~> 4.0 3 | github "sendyhalim/Swime" ~> 3.0 4 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "Alamofire/Alamofire" "4.9.1" 2 | github "GetStream/Moya" "d9cc074358ce9fee16b04ee8994fc30e623ea841" 3 | github "antitypical/Result" "4.1.0" 4 | github "daltoniam/Starscream" "3.1.1" 5 | github "sendyhalim/Swime" "3.0.6" 6 | -------------------------------------------------------------------------------- /Configs/Faye.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2018 Stream.io Inc. All rights reserved. 23 | 24 | 25 | -------------------------------------------------------------------------------- /Configs/FayeTests.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Configs/GetStream.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2018 Stream.io Inc. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Configs/GetStreamTests.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Faye.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "Faye" 3 | s.version = "2.2.5" 4 | s.summary = "Faye Swift Client for GetStream" 5 | s.homepage = "https://github.com/GetStream/stream-swift" 6 | s.license = { :type => "BSD-3", :file => "LICENSE" } 7 | s.author = { "GetStream" => "support@getstream.io" } 8 | s.swift_version = "5.0" 9 | s.ios.deployment_target = "9.0" 10 | s.osx.deployment_target = "10.10" 11 | s.watchos.deployment_target = "3.0" 12 | s.tvos.deployment_target = "9.0" 13 | s.source = { :git => "https://github.com/GetStream/stream-swift.git", :tag => s.version.to_s } 14 | s.source_files = "Faye/*" 15 | s.framework = "Foundation" 16 | s.dependency "Starscream", "~> 4.0" 17 | end 18 | -------------------------------------------------------------------------------- /Faye/Channel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Channel.swift 3 | // Faye 4 | // 5 | // Created by Alexey Bukhtin on 26/11/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public typealias ChannelSubscription = (_ data: Data) -> Void 12 | 13 | public final class Channel: Equatable { 14 | 15 | private weak var client: Client? 16 | let subscription: ChannelSubscription 17 | public let name: ChannelName 18 | public var ext: [String: String]? 19 | 20 | public init(_ name: ChannelName, client: Client, subscription: @escaping ChannelSubscription) { 21 | self.name = "/".appending(name.slashTrimmed()) 22 | self.client = client 23 | self.subscription = subscription 24 | } 25 | 26 | public func unsubscribe() { 27 | client?.remove(channel: self) 28 | } 29 | 30 | public static func == (lhs: Channel, rhs: Channel) -> Bool { 31 | return lhs.name == rhs.name 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Faye/ChannelName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChannelName.swift 3 | // Faye 4 | // 5 | // Created by Alexey Bukhtin on 28/11/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public typealias ChannelName = String 12 | 13 | extension ChannelName { 14 | 15 | /// Check if the channel name is a wildcard. 16 | private var isWildcard: Bool { 17 | guard let last = last else { 18 | return false 19 | } 20 | 21 | return last == "*" 22 | } 23 | 24 | /// Check if the channel name is a wildcard for multiple segments. 25 | private var isMultipleSegmentsWildcard: Bool { 26 | guard isWildcard else { 27 | return false 28 | } 29 | 30 | return self[utf8.index(before: endIndex)] == "*" 31 | } 32 | 33 | /// Extract a base name for the wildcard without "`*`". 34 | private var wildcardBase: ChannelName? { 35 | guard let index = firstIndex(where: { $0 == "*" }) else { 36 | return nil 37 | } 38 | 39 | return ChannelName(self[.. true` 46 | /// - `foo/bar + /foo -> false` 47 | /// - `foo/* + /foo -> true` 48 | /// - `foo/* + /foo/bar -> true` 49 | /// - `foo/* + /foo/bar/baz -> false` 50 | /// - `foo/** + /foo/bar/baz -> true` 51 | /// 52 | /// - Parameters: 53 | /// - channelName: an another channel name 54 | func match(with channelName: ChannelName) -> Bool { 55 | if self == channelName { 56 | return true 57 | } 58 | 59 | if channelName.isWildcard || channelName.count == 0 { 60 | return false 61 | } 62 | 63 | guard isWildcard else { 64 | return self == channelName 65 | } 66 | 67 | guard let wildcardBase = self.wildcardBase, channelName.prefix(wildcardBase.count) == wildcardBase else { 68 | return false 69 | } 70 | 71 | let index = channelName.index(channelName.startIndex, offsetBy: wildcardBase.count) 72 | let segment = String(channelName[index...]).slashTrimmed() 73 | let hasMultipleSegments = segment.contains("/") 74 | 75 | return !hasMultipleSegments || isMultipleSegmentsWildcard 76 | } 77 | 78 | /// Create a channel name with the current wildcard and a given segment. 79 | /// Return self if the current channel name is not a wildcard. 80 | /// 81 | /// - Examples: 82 | /// - (segment: `bar`) `foo/* --> foo/bar` 83 | /// - (segment: `bar/baz`) `foo/** --> foo/bar/baz` 84 | func wildcard(with segment: ChannelName?) throws -> ChannelName { 85 | guard isWildcard else { 86 | return self 87 | } 88 | 89 | let wildcardBase = self.wildcardBase ?? self 90 | 91 | guard let segment = segment else { 92 | throw Error.wildcardWithoutSegment(self) 93 | } 94 | 95 | let hasMultipleSegments = segment.contains("/") 96 | 97 | guard isMultipleSegmentsWildcard || !hasMultipleSegments else { 98 | throw Error.wildcardIsNotMatchedWithSegment(segment) 99 | } 100 | 101 | return wildcardBase.appending("/").appending(segment.slashTrimmed()) 102 | } 103 | } 104 | 105 | // MARK: - Error 106 | 107 | extension ChannelName { 108 | public enum Error: Swift.Error { 109 | case wildcardWithoutSegment(_ wildcard: ChannelName) 110 | case wildcardIsNotMatchedWithSegment(_ segment: ChannelName) 111 | } 112 | } 113 | 114 | // MARK: - Helper 115 | 116 | extension String { 117 | /// Remove "`/`" char from the string. 118 | func slashTrimmed() -> String { 119 | return trimmingCharacters(in: CharacterSet(charactersIn: "/")) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Faye/Message.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Message.swift 3 | // Faye 4 | // 5 | // Created by Alexey Bukhtin on 28/11/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Message: Codable { 12 | 13 | var clientId: String? 14 | var connectionType: String? 15 | var version: String? 16 | var minimumVersion: String? 17 | var supportedConnectionTypes: [String]? 18 | var advice: Advice? 19 | var successful: Bool? 20 | public let channel: ChannelName 21 | public var id: String? 22 | public var subscription: String? 23 | public var ext: [String: String]? 24 | public var error: String? 25 | 26 | init(_ bayeuxChannel: BayeuxChannel, _ channel: Channel?, clientId: String?) { 27 | id = Message.Counter.value 28 | self.clientId = clientId 29 | self.channel = bayeuxChannel.rawValue 30 | 31 | switch bayeuxChannel { 32 | case .handshake: 33 | version = "1.0" 34 | minimumVersion = "1.0" 35 | supportedConnectionTypes = ["websocket"] 36 | case .connect: 37 | connectionType = "websocket" 38 | case .subscribe, .unsubscribe: 39 | subscription = channel?.name 40 | ext = channel?.ext 41 | } 42 | } 43 | } 44 | 45 | // MARK: Counter 46 | 47 | extension Message { 48 | struct Counter { 49 | private static var id = 0 50 | 51 | static var value: String { 52 | id += 1 53 | return String(id, radix: 16) 54 | } 55 | } 56 | } 57 | 58 | // MARK: Advice 59 | 60 | struct Advice: Codable { 61 | let reconnect: Reconnection 62 | let interval: Int 63 | let timeout: Int 64 | } 65 | 66 | extension Advice { 67 | enum Reconnection: String, Codable { 68 | case none 69 | case handshake 70 | case retry 71 | } 72 | } 73 | 74 | // MARK: Bayeux Channels 75 | 76 | enum BayeuxChannel: ChannelName { 77 | case handshake = "/meta/handshake" 78 | case connect = "/meta/connect" 79 | case subscribe = "/meta/subscribe" 80 | case unsubscribe = "/meta/unsubscribe" 81 | } 82 | -------------------------------------------------------------------------------- /Faye/RepeatingTimer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepeatingTimer.swift 3 | // Alamofire 4 | // 5 | // Created by Alexey Bukhtin on 22/02/2019. 6 | // 7 | 8 | import Foundation 9 | 10 | public final class RepeatingTimer { 11 | private enum State { 12 | case suspended 13 | case resumed 14 | } 15 | 16 | public typealias EventHandler = () -> Void 17 | 18 | let timeInterval: DispatchTimeInterval 19 | private let queue: DispatchQueue? 20 | public var eventHandler: EventHandler? 21 | private var state: State = .suspended 22 | 23 | private lazy var timer: DispatchSourceTimer = { 24 | let timer = DispatchSource.makeTimerSource(queue: queue) 25 | timer.schedule(deadline: .now() + timeInterval, repeating: timeInterval) 26 | timer.setEventHandler { [weak self] in self?.eventHandler?() } 27 | return timer 28 | }() 29 | 30 | public init(timeInterval: DispatchTimeInterval, queue: DispatchQueue? = nil, eventHandler: EventHandler? = nil) { 31 | self.timeInterval = timeInterval 32 | self.queue = queue 33 | self.eventHandler = eventHandler 34 | } 35 | 36 | deinit { 37 | timer.setEventHandler {} 38 | timer.cancel() 39 | /// If the timer is suspended, calling cancel without resuming 40 | /// triggers a crash. This is documented here https://forums.developer.apple.com/thread/15902 41 | resume() 42 | eventHandler = nil 43 | } 44 | 45 | public func resume() { 46 | if state == .resumed { 47 | return 48 | } 49 | 50 | state = .resumed 51 | timer.resume() 52 | } 53 | 54 | public func suspend() { 55 | if state == .suspended { 56 | return 57 | } 58 | 59 | state = .suspended 60 | timer.suspend() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | -------------------------------------------------------------------------------- /GetStream.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "GetStream" 3 | s.version = "2.2.5" 4 | s.summary = "Swift Client - Build Activity Feeds & Streams with GetStream.io https://getstream.io" 5 | s.homepage = "https://github.com/GetStream/stream-swift" 6 | s.license = { :type => "BSD-3", :file => "LICENSE" } 7 | s.author = { "GetStream" => "support@getstream.io" } 8 | s.social_media_url = "https://getstream.io" 9 | s.swift_version = "5.0" 10 | s.platform = :ios, "11.0" 11 | s.source = { :git => "https://github.com/GetStream/stream-swift.git", :tag => s.version.to_s } 12 | s.default_subspecs = "Core", "Faye" 13 | 14 | s.subspec "Core" do |ss| 15 | ss.source_files = "Sources/Core/**/*" 16 | ss.framework = "Foundation" 17 | ss.dependency "Moya", "~> 14.0" 18 | ss.dependency "Swime", "~> 3.0" 19 | end 20 | 21 | s.subspec "Faye" do |ss| 22 | ss.source_files = "Sources/Faye/*" 23 | ss.dependency "GetStream/Core" 24 | ss.dependency "Faye" 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /GetStream.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /GetStream.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /GetStream.xcodeproj/xcshareddata/xcschemes/GetStream-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 46 | 47 | 51 | 52 | 53 | 54 | 60 | 61 | 62 | 63 | 65 | 71 | 72 | 73 | 74 | 75 | 85 | 86 | 92 | 93 | 94 | 95 | 101 | 102 | 108 | 109 | 110 | 111 | 113 | 114 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2018 Stream.io Inc, and individual contributors. 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted 6 | provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this list of 9 | conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of 12 | conditions and the following disclaimer in the documentation and/or other materials 13 | provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its contributors may 16 | be used to endorse or promote products derived from this software without specific prior 17 | written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 20 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 21 | AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 22 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 26 | OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Alamofire", 6 | "repositoryURL": "https://github.com/Alamofire/Alamofire.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "11928850d7273a8cd41bb766f2fc93b4d724b79b", 10 | "version": "5.0.4" 11 | } 12 | }, 13 | { 14 | "package": "Moya", 15 | "repositoryURL": "https://github.com/Moya/Moya.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "b3e5a233e0d85fd4d69f561c80988590859c7dee", 19 | "version": "14.0.0" 20 | } 21 | }, 22 | { 23 | "package": "Nimble", 24 | "repositoryURL": "https://github.com/Quick/Nimble", 25 | "state": { 26 | "branch": null, 27 | "revision": "e9d769113660769a4d9dd3afb855562c0b7ae7b0", 28 | "version": "7.3.4" 29 | } 30 | }, 31 | { 32 | "package": "Quick", 33 | "repositoryURL": "https://github.com/Quick/Quick", 34 | "state": { 35 | "branch": null, 36 | "revision": "f2b5a06440ea87eba1a167cab37bf6496646c52e", 37 | "version": "1.3.4" 38 | } 39 | }, 40 | { 41 | "package": "ReactiveSwift", 42 | "repositoryURL": "https://github.com/Moya/ReactiveSwift.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "f195d82bb30e412e70446e2b4a77e1b514099e88", 46 | "version": "6.1.0" 47 | } 48 | }, 49 | { 50 | "package": "RxSwift", 51 | "repositoryURL": "https://github.com/ReactiveX/RxSwift.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "c1bd31b397d87a54467af4161dde9d6b27720c19", 55 | "version": "5.1.0" 56 | } 57 | }, 58 | { 59 | "package": "Starscream", 60 | "repositoryURL": "https://github.com/daltoniam/Starscream.git", 61 | "state": { 62 | "branch": null, 63 | "revision": "df8d82047f6654d8e4b655d1b1525c64e1059d21", 64 | "version": "4.0.4" 65 | } 66 | }, 67 | { 68 | "package": "Swime", 69 | "repositoryURL": "https://github.com/sendyhalim/Swime", 70 | "state": { 71 | "branch": null, 72 | "revision": "918dc9f85dbcfd231e35be2e51452b0ac72665c7", 73 | "version": "3.0.6" 74 | } 75 | } 76 | ] 77 | }, 78 | "version": 1 79 | } 80 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "GetStream", 8 | platforms: [ 9 | .iOS(.v11) 10 | ], 11 | products: [ 12 | .library(name: "GetStream", targets: ["GetStream"]), 13 | .library(name: "Faye", targets: ["Faye"]), 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/Moya/Moya.git", .upToNextMajor(from: "14.0.0")), 17 | .package(url: "https://github.com/daltoniam/Starscream.git", .upToNextMajor(from: "4.0.0")), 18 | .package(url: "https://github.com/sendyhalim/Swime", .upToNextMajor(from: "3.0.0")), 19 | ], 20 | targets: [ 21 | .target(name: "GetStream", dependencies: ["Moya", "Faye", "Swime"], path: "Sources", exclude: ["Token"]), 22 | .target(name: "Faye", dependencies: ["Starscream"], path: "Faye"), 23 | .testTarget(name: "GetStreamTests", dependencies: ["GetStream"], path: "Tests", exclude: ["Token"]), 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stream Swift Client 2 | 3 | [![Build Status](https://github.com/GetStream/stream-swift/workflows/CI/badge.svg)](https://github.com/GetStream/stream-swift/actions) 4 | [![Code Coverage](https://codecov.io/gh/GetStream/stream-swift/branch/master/graph/badge.svg)](https://codecov.io/gh/GetStream/stream-swift) 5 | [![Language: Swift 4.2](https://img.shields.io/badge/Swift-4.2-orange.svg)](https://swift.org) 6 | [![Documentation](https://github.com/GetStream/stream-swift/blob/master/docs/badge.svg)](https://getstream.github.io/stream-swift/) 7 | [![CocoaPods compatible](https://img.shields.io/cocoapods/v/GetStream.svg)](https://cocoapods.org/pods/GetStream) 8 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 9 | [![Swift Package Manager compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager) 10 | 11 | [stream-swift](https://github.com/GetStream/stream-swift) is a Swift client for [Stream](https://getstream.io/). 12 | 13 | You can sign up for a Stream account at https://getstream.io/get_started. 14 | 15 | [API Docs](https://getstream.github.io/stream-swift/) 16 | 17 | [API Examples](https://github.com/GetStream/stream-swift/wiki) 18 | 19 | ## :warning: Client-side SDK no longer actively maintained by Stream 20 | 21 | A Feeds integration includes a combination of server-side and client-side code and the interface can vary widely which is why we are no longer focussing on supporting this SDK. If you are starting from scratch we recommend you only use the server-side SDKs. 22 | 23 | This is by no means a reflection of our commitment to maintaining and improving the Feeds API which will always be a product that we support. 24 | 25 | We continue to welcome pull requests from community members in case you want to improve this SDK. 26 | 27 | ## Installation 28 | 29 | ### CocoaPods 30 | 31 | For Stream, use the following entry in your `Podfile`: 32 | 33 | for Swift 5: 34 | ``` 35 | pod 'GetStream', '~> 2.0' 36 | ``` 37 | for Swift 4.2: 38 | ``` 39 | pod 'GetStream', '~> 1.0' 40 | ``` 41 | Then run `pod install`. 42 | 43 | In any file you'd like to use Stream in, don't forget to import the framework with `import GetStream`. 44 | 45 | ### Swift Package Manager 46 | 47 | To integrate using Apple's Swift package manager, add the following as a dependency to your `Package.swift`: 48 | ``` 49 | .package(url: "https://github.com/GetStream/stream-swift.git", .upToNextMajor(from: "1.0.0")) 50 | ``` 51 | 52 | ### Carthage 53 | 54 | Make the following entry in your Cartfile: 55 | ``` 56 | github "GetStream/stream-swift" 57 | ``` 58 | Then run `carthage update`. 59 | 60 | ## Quick start 61 | 62 | ```swift 63 | // Setup a shared Stream client. 64 | Client.config = .init(apiKey: "<#ApiKey#>", appId: "<#AppId#>", token: "<#Token#>") 65 | 66 | // Setup a Stream current user with the userId from the Token. 67 | Client.shared.createCurrentUser() { _ in 68 | // Do all your requests from here. Reload feeds and etc. 69 | } 70 | 71 | // Create a user feed. 72 | let userFeed = Client.shared.flatFeed(feedSlug: "user") 73 | 74 | // Create an Activity. You can make own Activity class or struct with custom properties. 75 | let activity = Activity(actor: User.current!, verb: "add", object: "picture:10", foreignId: "picture:10") 76 | 77 | userFeed?.add(activity) { result in 78 | // A result of the adding of the activity. 79 | print(result) 80 | } 81 | 82 | // Create a following relationship between "timeline" feed and "user" feed: 83 | let timelineFeed = Client.shared.flatFeed(feedSlug: "timeline") 84 | 85 | timelineFeed?.follow(toTarget: userFeed!.feedId, activityCopyLimit: 1) { result in 86 | print(result) 87 | } 88 | 89 | // Read timeline and user's post appears in the feed: 90 | timelineFeed?.get(pagination: .limit(10)) { result in 91 | let response = try! result.get() 92 | print(response.results) 93 | } 94 | 95 | // Remove an activity by referencing it's foreignId 96 | userFeed?.remove(foreignId: "picture:10") { result in 97 | print(result) 98 | } 99 | ``` 100 | 101 | More API examples [here](https://github.com/GetStream/stream-swift/wiki) 102 | 103 | ## Copyright and License Information 104 | 105 | Copyright (c) 2016-2018 Stream.io Inc, and individual contributors. All rights reserved. 106 | 107 | See the file "[LICENSE](https://github.com/GetStream/stream-swift/blob/master/LICENSE)" for information on the history of this software, terms & conditions for usage, and a DISCLAIMER OF ALL WARRANTIES. 108 | -------------------------------------------------------------------------------- /Sources/Core/Activity/Activity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Activity.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 11/03/2019. 6 | // Copyright © 2019 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | /// A default activity type. 10 | public typealias Activity = EnrichedActivity 11 | -------------------------------------------------------------------------------- /Sources/Core/Activity/ActivityProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityProtocol.swift 3 | // GetStream 4 | // 5 | // Created by Alexey Bukhtin on 13/11/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A verb type of the Activity. 12 | /// - Note: Verb type is useful for the making of static verb strings in the extension of the Verb type. 13 | public typealias Verb = String 14 | 15 | /// A protocol for the Activity type. 16 | public protocol ActivityProtocol: Enrichable, Reactionable, OriginalRepresentable { 17 | associatedtype ActorType = Enrichable 18 | associatedtype ObjectType = Enrichable 19 | 20 | /// The Stream id of the activity. 21 | var id: String { get set } 22 | /// The actor performing the activity. 23 | var actor: ActorType { get } 24 | /// The verb of the activity. 25 | var verb: Verb { get } 26 | /// The object of the activity. 27 | /// - Note: It shouldn't be empty. 28 | var object: ObjectType { get } 29 | /// A unique ID from your application for this activity. IE: pin:1 or like:300. 30 | var foreignId: String? { get set } 31 | /// The optional time of the activity, isoformat. Default is the current time. 32 | var time: Date? { get set } 33 | /// An array allows you to specify a list of feeds to which the activity should be copied. 34 | /// One way to think about it is as the CC functionality of email. 35 | var feedIds: FeedIds? { get set } 36 | } 37 | 38 | // MARK: - Enrichable 39 | 40 | extension ActivityProtocol { 41 | /// The activity is enrichable and here is a referenceId by default. 42 | /// For example, in case when you need to make a repost activity where `object` would be the original activity. 43 | public var referenceId: String { 44 | return "SA:\(id)" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Core/Activity/OriginalRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OriginalRepresentable.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 12/03/2019. 6 | // Copyright © 2019 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A protocol with a reference to the original object of itself. 12 | public protocol OriginalRepresentable {} 13 | 14 | extension OriginalRepresentable { 15 | /// The original object. 16 | /// 17 | /// In case if the reactionable object has a referance to the original reactionable object, then it should be redefined here. 18 | /// 19 | /// For example: Reposted activity should have a reference to the original activity 20 | /// and all reactions of a reposted activity should referenced to the original activity reactions. 21 | /// Usually the original activity should be stored in the activity object property as an enum 22 | /// and in this case the origin property could be redefined in this way: 23 | /// ``` 24 | /// public var original: Activity { 25 | /// if case .repost(let original) = object { 26 | /// return original 27 | /// } 28 | /// 29 | /// return self 30 | /// } 31 | /// ``` 32 | public var original: Self { 33 | return self 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Core/Client/BaseURL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseURL.swift 3 | // GetStream 4 | // 5 | // Created by Alexey Bukhtin on 12/11/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A base URL for the `Client`. 12 | public struct BaseURL { 13 | static let placeholderURL = URL(string: "https://getstream.io")! 14 | 15 | let url: URL 16 | 17 | /// Create a base URL. 18 | /// 19 | /// - Parameters: 20 | /// - location: a location of the server for the `Client`. 21 | /// - service: a service type. 22 | /// - version: a version of API. 23 | public init(location: Location = .default, service: Service = .api, version: String = "1.0") { 24 | url = URL(string: "https://\(location.rawValue)\(service.rawValue).stream-io-api.com/\(service.rawValue)/v\(version)/")! 25 | } 26 | 27 | /// Create a base URL with a custom URL. 28 | public init(customURL: URL) { 29 | url = customURL 30 | } 31 | 32 | func endpointURLString(targetPath: String) -> String { 33 | return (targetPath.isEmpty ? url : url.appendingPathComponent(targetPath)).absoluteString 34 | } 35 | } 36 | 37 | extension BaseURL { 38 | /// A service type. 39 | public enum Service: String { 40 | case api 41 | case personalization 42 | case analytics 43 | } 44 | 45 | /// A location type. 46 | public enum Location: String { 47 | case `default` = "" 48 | case usEast = "us-east-" 49 | case europeWest = "eu-west-" 50 | case singapore = "singapore-" 51 | } 52 | } 53 | 54 | extension BaseURL: CustomStringConvertible { 55 | public var description: String { 56 | return url.absoluteString 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/Core/Client/Client+Setup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Client+Setup.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 09/12/2019. 6 | // Copyright © 2019 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Moya 11 | 12 | #if canImport(Faye) 13 | import Faye 14 | #else 15 | extension Client { 16 | struct FayeClient { func disconnect() {} } 17 | static let fayeClient = FayeClient() 18 | } 19 | #endif 20 | 21 | // MARK: - Client User Setup 22 | 23 | extension Client { 24 | 25 | /// Setup the current user with a default `User` type. 26 | /// 27 | /// - Parameters: 28 | /// - completion: a completion block with an `User` object in the `Result`. 29 | /// - token: the user Client token. 30 | /// - Returns: an object to cancel the request. 31 | @discardableResult 32 | public func setupUser(token: Token, completion: @escaping UserCompletion) -> Cancellable { 33 | parseToken(token) 34 | return setupUser(User(id: currentUserId ?? ""), token: token, completion: completion) 35 | } 36 | 37 | /// Setup the current user with a custom `User` type. 38 | /// 39 | /// - Parameters: 40 | /// - user: a custom user object. 41 | /// - token: the user Client token. 42 | /// - completion: a completion block with a custom `User` object in the `Result`. 43 | /// - Returns: an object to cancel the request. 44 | @discardableResult 45 | public func setupUser(_ user: T, token: Token, completion: @escaping UserCompletion) -> Cancellable { 46 | parseToken(token) 47 | 48 | guard let currentUserId = currentUserId else { 49 | let error = ClientError.clientSetup("The current user id is empty") 50 | logger?.log(error.description) 51 | completion(.failure(error)) 52 | return SimpleCancellable() 53 | } 54 | 55 | guard user.id == currentUserId else { 56 | let error = ClientError.clientSetup("The current user id is not the same as in Token") 57 | logger?.log(error.description) 58 | completion(.failure(error)) 59 | return SimpleCancellable() 60 | } 61 | 62 | ClientLogger.logger("👤", "", "User id: \(currentUserId)") 63 | ClientLogger.logger("🀄️", "", "Token: \(token)") 64 | 65 | return create(user: user) { [weak self] result in 66 | if let user = try? result.get() { 67 | Client.shared.currentUser = user 68 | self?.logger?.log("👤 The current user was setupped with id: \(user.id)") 69 | } else if let error = result.error { 70 | self?.logger?.log(error.localizedDescription) 71 | } 72 | 73 | completion(result) 74 | } 75 | } 76 | 77 | private func parseToken(_ token: Token) { 78 | disconnect() 79 | 80 | if token.isEmpty { 81 | return 82 | } 83 | 84 | if let userId = token.userId { 85 | self.token = token 86 | currentUserId = userId 87 | } 88 | } 89 | 90 | /// Disconnect from Stream and reset the current user. 91 | /// 92 | /// Resets and removes the user/token pair as well as relevant network connections. 93 | /// - Note: To restore the connection, use `Client.setupUser` to set a valid user/token pair. 94 | public func disconnect() { 95 | logger?.log("🧹 Disconnect. Reset Client User, Token, URLSession and WebSocket. Old user id: \(currentUserId ?? "").") 96 | self.token = "" 97 | currentUserId = nil 98 | currentUser = nil 99 | 100 | if NSClassFromString("Faye.Client") != nil { 101 | Client.fayeClient.disconnect() 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/Core/Client/ClientError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClientError.swift 3 | // GetStream 4 | // 5 | // Created by Alexey Bukhtin on 09/11/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum ClientError: LocalizedError, CustomStringConvertible { 12 | case unexpectedError(Error?) 13 | case unexpectedResponse(String) 14 | case unknownError(_ localizedDescription: String, _ error: Error?) 15 | case clientSetup(String) 16 | case parameterInvalid(String) 17 | case jsonInvalid(String?) 18 | case jsonDecode(_ localizedDescription: String, _ error: Error?, _ data: Data) 19 | case jsonEncode(_ localizedDescription: String, _ error: Error?) 20 | case network(_ description: String, _ error: Error?) 21 | case server(Info) 22 | 23 | public var description: String { 24 | switch self { 25 | case .unexpectedError(let error): 26 | return "Unexpected behaviour: \(error?.localizedDescription ?? "")" 27 | case .unexpectedResponse: 28 | return "Unexpected response" 29 | case .unknownError(let localizedDescription, _): 30 | return "Unexpected behaviour with error: \(localizedDescription)" 31 | case .clientSetup(let description): 32 | return "Client setup: \(description)" 33 | case .parameterInvalid(let name): 34 | return "Parameter is not valid: \(name)" 35 | case .jsonInvalid(let description): 36 | return "A server response is not a JSON: \(description ?? "")" 37 | case let .jsonDecode(localizedDescription, _, data): 38 | return "JSON decoding error: \(localizedDescription). Data: \(data.count) bytes" 39 | case .jsonEncode(let localizedDescription, _): 40 | return "JSON encoding error: \(localizedDescription)" 41 | case .network(let description, _): 42 | return "Moya error: \(description)" 43 | case .server(let info): 44 | return info.description 45 | } 46 | } 47 | 48 | public var localizedDescription: String { 49 | return description 50 | } 51 | 52 | public var errorDescription: String? { 53 | return description 54 | } 55 | 56 | static func warning(for json: Any, missedParameter parameter: String, from: String = #function) { 57 | print("⚠️", from, "JSON does not have a parameter \"\(parameter)\" in:", json) 58 | } 59 | } 60 | 61 | // MARK: - Client Error Info 62 | 63 | extension ClientError { 64 | /// A client error details. 65 | public struct Info: CustomStringConvertible { 66 | public let info: String 67 | public let code: Int 68 | public let statusCode: Int 69 | public let exception: String 70 | public let json: JSON 71 | 72 | init(json: JSON) { 73 | guard let detail = json["detail"] as? String, 74 | let code = json["code"] as? Int, 75 | let statusCode = json["status_code"] as? Int, 76 | let exception = json["exception"] as? String else { 77 | info = "" 78 | self.code = 0 79 | self.statusCode = 0 80 | self.exception = "" 81 | self.json = json 82 | return 83 | } 84 | 85 | info = detail 86 | self.code = code 87 | self.statusCode = statusCode 88 | self.exception = exception 89 | self.json = [:] 90 | } 91 | 92 | public var description: String { 93 | return exception.isEmpty ? "JSON response \(json)" : "\(exception)[\(code)] Status Code: \(statusCode), \(info)" 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/Core/Client/Enrichable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Enrichable.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 20/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A protocol for enrichable objects. 12 | public protocol Enrichable: Missable { 13 | /// A referenceId for an enrichable object. 14 | var referenceId: String { get } 15 | } 16 | 17 | // MARK: - String Enrichable 18 | 19 | extension String: Enrichable { 20 | 21 | /// A reference id, e.g. for User: "SU:42" 22 | public var referenceId: String { 23 | return self 24 | } 25 | 26 | public static func missed() -> String { 27 | return "!missed_reference" 28 | } 29 | 30 | public var isMissedReference: Bool { 31 | return self == String.missed() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Core/Client/MissingReference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MissingReference.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 06/11/2019. 6 | // Copyright © 2019 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A wrapper for missing reference object. 12 | public struct MissingReference: Codable { 13 | /// A decoded or missed value. 14 | public let value: T 15 | /// True if the value was missed. 16 | public let isMissed: Bool 17 | /// A decoding error instead of missing reference case. 18 | public let decodingError: Error? 19 | /// An enrichind activity error instead of object value. 20 | public let enrichingActivityError: EnrichingActivityError? 21 | 22 | init(_ value: T) { 23 | self.value = value 24 | isMissed = false 25 | enrichingActivityError = nil 26 | decodingError = nil 27 | } 28 | 29 | public init(from decoder: Decoder) throws { 30 | let container = try decoder.singleValueContainer() 31 | 32 | do { 33 | value = try container.decode(T.self) 34 | isMissed = false 35 | enrichingActivityError = nil 36 | decodingError = nil 37 | } catch { 38 | value = T.missed() 39 | 40 | if let enrichingActivityError = try? container.decode(EnrichingActivityError.self) { 41 | self.enrichingActivityError = enrichingActivityError 42 | 43 | // The reference is missing. 44 | if enrichingActivityError.isReferenceNotFound { 45 | isMissed = true 46 | decodingError = nil 47 | return 48 | } 49 | } else { 50 | enrichingActivityError = nil 51 | } 52 | 53 | isMissed = false 54 | decodingError = error 55 | 56 | // Show the decoding error that wasn't related to the missing reference. 57 | if Client.keepBadDecodedObjectsAsMissed { 58 | print("⚠️ Decoding was failed for type: \(T.self)", error) 59 | 60 | if let enrichingActivityError = self.enrichingActivityError { 61 | print("⚠️", enrichingActivityError) 62 | } 63 | } else { 64 | throw error 65 | } 66 | } 67 | } 68 | 69 | public func encode(to encoder: Encoder) throws { 70 | var container = encoder.singleValueContainer() 71 | try container.encode(value) 72 | } 73 | 74 | /// The default missed reference value. 75 | public static func missed() -> MissingReference { 76 | return MissingReference(T.missed()) 77 | } 78 | } 79 | 80 | /// Missable is using to wrap objects with enrichment, where they was deleted and dependencies lost the link. 81 | public protocol Missable: Codable { 82 | 83 | /// Check if the object is a missing reference. 84 | var isMissedReference: Bool { get } 85 | 86 | /// A placeholder for a missed object will be use in 2 cases: 87 | /// 1. A decoding error because a missing reference. 88 | /// 2. Any decoding error if `Client.keepBadDecodedObjectsAsMissed` is enabled. 89 | static func missed() -> Self 90 | } 91 | -------------------------------------------------------------------------------- /Sources/Core/Client/Pagination.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pagination.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 11/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Pagination options. 12 | public enum Pagination: Decodable { 13 | /// The default value for the pagination limit is 25. 14 | public static let defaultLimit = 25 15 | 16 | /// Default limit is 25. (Defined in `FeedPagination.defaultLimit`) 17 | case none 18 | 19 | /// The amount of activities requested from the APIs. 20 | case limit(_ limit: Int) 21 | 22 | /// The offset of requesting activities. 23 | /// - Note: Using `lessThan` or `lessThanOrEqual` for pagination is preferable to using `offset`. 24 | case offset(_ offset: Int) 25 | 26 | /// Filter the feed on ids greater than the given value. 27 | case greaterThan(_ id: String) 28 | 29 | /// Filter the feed on ids greater than or equal to the given value. 30 | case greaterThanOrEqual(_ id: String) 31 | 32 | /// Filter the feed on ids smaller than the given value. 33 | case lessThan(_ id: String) 34 | 35 | /// Filter the feed on ids smaller than or equal to the given value. 36 | case lessThanOrEqual(_ id: String) 37 | 38 | /// Combine `Pagination`'s with each other. 39 | /// 40 | /// It's easy to use with the `+` operator. Examples: 41 | /// ``` 42 | /// var pagination = .limit(10) + .greaterThan("news123") 43 | /// pagination += .lessThan("news987") 44 | /// print(pagination) 45 | /// // It will print: 46 | /// // and(pagination: .and(pagination: .limit(10), another: .greaterThan("news123")), 47 | /// // another: .lessThan("news987")) 48 | /// ``` 49 | indirect case and(pagination: Pagination, another: Pagination) 50 | 51 | public init(from decoder: Decoder) throws { 52 | let container = try decoder.singleValueContainer() 53 | let urlString = try container.decode(String.self) 54 | var pagination: Pagination = .none 55 | 56 | if let urlComponents = URLComponents(string: urlString), let queryItems = urlComponents.queryItems { 57 | queryItems.forEach { queryItem in 58 | if let value = queryItem.value, !value.isEmpty { 59 | switch queryItem.name { 60 | case "limit": 61 | if let intValue = Int(value) { 62 | pagination += .limit(intValue) 63 | } 64 | case "offset": 65 | if let intValue = Int(value) { 66 | pagination += .offset(intValue) 67 | } 68 | case "id_gt": 69 | pagination += .greaterThan(value) 70 | case "id_gte": 71 | pagination += .greaterThanOrEqual(value) 72 | case "id_lt": 73 | pagination += .lessThan(value) 74 | case "id_lte": 75 | pagination += .lessThanOrEqual(value) 76 | default: 77 | break 78 | } 79 | } 80 | } 81 | } 82 | 83 | self = pagination 84 | } 85 | 86 | /// Parameters for a request. 87 | var parameters: [String: Any] { 88 | var params: [String: Any] = [:] 89 | 90 | switch self { 91 | case .none: 92 | return [:] 93 | case .limit(let limit): 94 | params["limit"] = limit 95 | case let .offset(offset): 96 | params["offset"] = offset 97 | case let .greaterThan(id): 98 | params["id_gt"] = id 99 | case let .greaterThanOrEqual(id): 100 | params["id_gte"] = id 101 | case let .lessThan(id): 102 | params["id_lt"] = id 103 | case let .lessThanOrEqual(id): 104 | params["id_lte"] = id 105 | case let .and(pagination1, pagination2): 106 | params = pagination1.parameters.merged(with: pagination2.parameters) 107 | } 108 | 109 | return params 110 | } 111 | } 112 | 113 | // MARK: - Helper Operator 114 | 115 | extension Pagination { 116 | /// An operator for combining Pagination's. 117 | public static func +(lhs: Pagination, rhs: Pagination) -> Pagination { 118 | if case .none = lhs { 119 | return rhs 120 | } 121 | 122 | if case .none = rhs { 123 | return lhs 124 | } 125 | 126 | return .and(pagination: lhs, another: rhs) 127 | } 128 | 129 | /// An operator for combining Pagination's. 130 | public static func +=(lhs: inout Pagination, rhs: Pagination) { 131 | lhs = lhs + rhs 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Sources/Core/Client/Response.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Response.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 11/01/2019. 6 | // Copyright © 2019 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A general response object. 12 | public struct Response: Decodable { 13 | enum CodingKeys: String, CodingKey { 14 | case results 15 | case next 16 | case duration 17 | case unseenCount = "unseen" 18 | case unreadCount = "unread" 19 | } 20 | 21 | /// Response results of generic objects. 22 | public let results: [T] 23 | /// A pagination option for the next page of objects. 24 | public internal(set) var next: Pagination? 25 | /// A duration of the response. 26 | public internal(set) var duration: String? 27 | /// A number of unseen notifications. 28 | public internal(set) var unseenCount: Int? 29 | /// A number of unread notifications. 30 | public internal(set) var unreadCount: Int? 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Core/Client/Result+ParseResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result+ParseResponse.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 13/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Moya 11 | 12 | typealias CompletionObject = (_ result: Result) -> Void 13 | typealias CompletionObjects = (_ result: Result, ClientError>) -> Void 14 | 15 | // MARK: - Result Parsing 16 | 17 | extension Result where Success == Moya.Response, Failure == ClientError { 18 | 19 | /// Parse a response and return the status code. 20 | func parseStatusCode(_ callbackQueue: DispatchQueue, _ completion: @escaping StatusCodeCompletion) { 21 | do { 22 | let response = try get() 23 | callbackQueue.async { completion(.success(response.statusCode)) } 24 | } catch { 25 | if let clientError = error as? ClientError { 26 | callbackQueue.async { completion(.failure(clientError)) } 27 | } 28 | } 29 | } 30 | 31 | /// Parse a `Decodable` object. 32 | func parse(_ callbackQueue: DispatchQueue, _ completion: @escaping CompletionObject) { 33 | parse(block: { 34 | let response = try get() 35 | let object = try JSONDecoder.default.decode(T.self, from: response.data) 36 | callbackQueue.async { completion(.success(object)) } 37 | }, catch: { error in 38 | callbackQueue.async { completion(.failure(error)) } 39 | }) 40 | } 41 | 42 | /// Parse `Decodable` objects with `ResultsContainer`. 43 | func parse(_ callbackQueue: DispatchQueue, _ completion: @escaping CompletionObjects) { 44 | parse(block: { 45 | let moyaResponse = try get() 46 | var response = try JSONDecoder.default.decode(Response.self, from: moyaResponse.data) 47 | 48 | if let next = response.next, case .none = next { 49 | response.next = nil 50 | } 51 | 52 | callbackQueue.async { completion(.success(response)) } 53 | }, catch: { error in 54 | callbackQueue.async { completion(.failure(error)) } 55 | }) 56 | } 57 | 58 | /// Try to parse a block or catch and return an error. 59 | func parse(block: () throws -> Void, catch errorBlock: @escaping (_ error: ClientError) -> Void) { 60 | do { 61 | try block() 62 | } catch let error as ClientError { 63 | errorBlock(error) 64 | } catch let error as DecodingError { 65 | if error.errorDescription == "Expected to decode Dictionary but found a string/data instead." { 66 | print("⚠️ Probably you are trying to get activities with enrichment. Usually, the actor field has wrong value.\n" 67 | + "☝️ The enrichments is enabled by default in requests. Try to switch it off with the parameter: `enrich: false`." 68 | + "ℹ️ Learn more about it here: https://getstream.io/docs/#enrichment_introduction") 69 | } 70 | 71 | if case .success(let response) = self { 72 | errorBlock(ClientError.jsonDecode(error.localizedDescription, error, response.data)) 73 | } else { 74 | errorBlock(ClientError.jsonDecode(error.localizedDescription, error, Data())) 75 | } 76 | } catch { 77 | errorBlock(ClientError.unknownError(error.localizedDescription, error)) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/Core/Client/StreamTargetType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StreamTargetType.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 17/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Moya 11 | 12 | protocol StreamTargetType: TargetType { 13 | var sampleJSON: String { get } 14 | } 15 | 16 | extension StreamTargetType { 17 | var baseURL: URL { 18 | return BaseURL.placeholderURL 19 | } 20 | 21 | var headers: [String : String]? { 22 | return Client.headers 23 | } 24 | 25 | var sampleJSON: String { 26 | return "" 27 | } 28 | 29 | var sampleData: Data { 30 | return sampleJSON.data(using: .utf8)! 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Core/Client/Token.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Token.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 03/01/2019. 6 | // Copyright © 2019 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A JWT token including a signature generated with the HS256 algorithm. 12 | /// You can find more information on JWT at https://jwt.io/introduction 13 | public typealias Token = String 14 | 15 | extension Token { 16 | 17 | var isValid: Bool { 18 | return payload != nil 19 | } 20 | 21 | var payload: JSON? { 22 | let parts = split(separator: ".") 23 | 24 | if parts.count == 3, 25 | let payloadData = jwtDecodeBase64(String(parts[1])), 26 | let json = (try? JSONSerialization.jsonObject(with: payloadData)) as? JSON { 27 | return json 28 | } 29 | 30 | return nil 31 | } 32 | 33 | /// A user id from the Token. 34 | public var userId: String? { 35 | return payload?["user_id"] as? String 36 | } 37 | 38 | private func jwtDecodeBase64(_ input: String) -> Data? { 39 | let removeEndingCount = input.count % 4 40 | let ending = removeEndingCount > 0 ? String(repeating: "=", count: 4 - removeEndingCount) : "" 41 | let base64 = input.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") + ending 42 | 43 | return Data(base64Encoded: base64) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/Core/Collection/Client+Collection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Client+Collection.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 18/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A collection object completion block. 12 | public typealias CollectionObjectCompletion = (_ result: Result) -> Void 13 | 14 | // MARK: - Client Collections 15 | 16 | extension Client { 17 | 18 | /// Add a collection object to the collection with a given name. 19 | /// 20 | /// - Parameters: 21 | /// - collectionObject: a collection object. 22 | /// - completion: a completion block with a collection object that was added. 23 | /// - Returns: an object to cancel the request. 24 | @discardableResult 25 | public func add(collectionObject: T, 26 | completion: @escaping CollectionObjectCompletion) -> Cancellable { 27 | return request(endpoint: CollectionEndpoint.add(collectionObject)) { [weak self] result in 28 | if let self = self { 29 | result.parse(self.callbackQueue, completion) 30 | } 31 | } 32 | } 33 | 34 | /// Retreive a collection object from the collection by the collection object id. 35 | /// 36 | /// - Parameters: 37 | /// - typeOf: a type of a custom collection object type that conformed to `CollectionObjectProtocol`. 38 | /// - collectionName: a collection name. 39 | /// - collectionObjectId: a collection object id. 40 | /// - completion: a completion block with a requested collection object. 41 | /// - Returns: an object to cancel the request. 42 | @discardableResult 43 | public func get(typeOf: T.Type, 44 | collectionName: String, 45 | collectionObjectId: String, 46 | completion: @escaping CollectionObjectCompletion) -> Cancellable { 47 | return request(endpoint: CollectionEndpoint.get(collectionName, collectionObjectId)) { [weak self] result in 48 | if let self = self { 49 | result.parse(self.callbackQueue, completion) 50 | } 51 | } 52 | } 53 | 54 | /// Update a collection object. 55 | /// 56 | /// - Parameters: 57 | /// - collectionObject: a collection object. 58 | /// - completion: a completion block with an updated collection object. 59 | /// - Returns: an object to cancel the request. 60 | @discardableResult 61 | public func update(collectionObject: T, 62 | completion: @escaping CollectionObjectCompletion) -> Cancellable { 63 | return request(endpoint: CollectionEndpoint.update(collectionObject)) { [weak self] result in 64 | if let self = self { 65 | result.parse(self.callbackQueue, completion) 66 | } 67 | } 68 | } 69 | 70 | /// Delete a collection object. 71 | /// 72 | /// - Parameters: 73 | /// - collectionObject: a collection object. 74 | /// - completion: a completion block with a response status code. 75 | /// - Returns: an object to cancel the request. 76 | @discardableResult 77 | public func delete(collectionObject: T, 78 | completion: @escaping StatusCodeCompletion) -> Cancellable { 79 | guard let objectId = collectionObject.id else { 80 | callbackQueue.async { completion(.failure(.jsonInvalid("Collection Object id is empty"))) } 81 | return SimpleCancellable() 82 | } 83 | 84 | return delete(collectionName: collectionObject.collectionName, collectionObjectId: objectId, completion: completion) 85 | } 86 | 87 | /// Delete a collection object with a given collection name and object id. 88 | /// 89 | /// - Parameters: 90 | /// - collectionName: a collection name. 91 | /// - collectionObjectId: a collection object id. 92 | /// - completion: a completion block with a response status code. 93 | /// - Returns: an object to cancel the request. 94 | @discardableResult 95 | public func delete(collectionName: String, 96 | collectionObjectId: String, 97 | completion: @escaping StatusCodeCompletion) -> Cancellable { 98 | return request(endpoint: CollectionEndpoint.delete(collectionName, collectionObjectId)) { [weak self] result in 99 | if let self = self { 100 | result.parseStatusCode(self.callbackQueue, completion) 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/Core/Collection/CollectionEndpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionEndpoint.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 18/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Moya 11 | 12 | enum CollectionEndpoint { 13 | case add(_ collectionObject: CollectionObjectProtocol) 14 | case get(_ collectionName: String, _ collectionObjectId: String) 15 | case update(_ collectionObject: CollectionObjectProtocol) 16 | case delete(_ collectionName: String, _ collectionObjectId: String) 17 | } 18 | 19 | extension CollectionEndpoint: StreamTargetType { 20 | 21 | var path: String { 22 | switch self { 23 | case .add(let collectionObject): 24 | return "collections/\(collectionObject.collectionName)/" 25 | case .get(let collectionName, let objectId), .delete(let collectionName, let objectId): 26 | return "collections/\(collectionName)/\(objectId)/" 27 | case .update(let collectionObject): 28 | return "collections/\(collectionObject.collectionName)/\(collectionObject.id ?? "")/" 29 | } 30 | } 31 | 32 | var method: Moya.Method { 33 | switch self { 34 | case .add: 35 | return .post 36 | case .get: 37 | return .get 38 | case .update: 39 | return .put 40 | case .delete: 41 | return .delete 42 | } 43 | } 44 | 45 | var task: Task { 46 | switch self { 47 | case .add(let collectionObject): 48 | return .requestJSONEncodable(collectionObject) 49 | case .get, .delete: 50 | return .requestPlain 51 | case .update(let collectionObject): 52 | return .requestJSONEncodable(collectionObject) 53 | } 54 | } 55 | 56 | var sampleJSON: String { 57 | switch self { 58 | case .add, .get: 59 | return """ 60 | {"duration":"4.15ms","id":"123","collection":"food","foreign_id":"food:123","data":{ "name": "Burger" },"created_at":"2018-12-24T13:35:02.290307Z","updated_at":"2018-12-24T13:35:02.290307Z"} 61 | """ 62 | case .update: 63 | return """ 64 | {"duration":"4.15ms","id":"123","collection":"food","foreign_id":"food:123","data":{ "name": "Burger2" },"created_at":"2018-12-24T13:35:02.290307Z","updated_at":"2018-12-24T13:35:02.290307Z"} 65 | """ 66 | case .delete: 67 | return "{}" 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/Core/Collection/CollectionObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionObject.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 18/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A collection object with basic properties of `CollectionObjectProtocol`. 12 | /// You can inherit this class with extra properties on your own `CollectionObject` type. 13 | /// - Note: Please, check the `CollectionObjectProtocol` documentation to implement your User subclass properly. 14 | open class CollectionObject: CollectionObjectProtocol { 15 | public enum CollectionObjectCodingKeys: String, CodingKey { 16 | case collectionName = "collection" 17 | case id 18 | case created = "created_at" 19 | case updated = "updated_at" 20 | case foreignId = "foreign_id" 21 | } 22 | 23 | public enum DataCodingKeys: String, CodingKey { 24 | case data 25 | } 26 | 27 | public let collectionName: String 28 | public var id: String? 29 | public var foreignId: String? 30 | public var created: Date = Date() 31 | public var updated: Date = Date() 32 | 33 | required public init(collectionName: String, id: String? = nil) { 34 | self.collectionName = collectionName 35 | self.id = id 36 | } 37 | 38 | required public init(from decoder: Decoder) throws { 39 | let container = try decoder.container(keyedBy: CollectionObjectCodingKeys.self) 40 | collectionName = try container.decode(String.self, forKey: .collectionName) 41 | id = try container.decode(String.self, forKey: .id) 42 | foreignId = try container.decode(String.self, forKey: .foreignId) 43 | created = try container.decode(Date.self, forKey: .created) 44 | updated = try container.decode(Date.self, forKey: .updated) 45 | } 46 | 47 | open func encode(to encoder: Encoder) throws { 48 | var container = encoder.container(keyedBy: CollectionObjectCodingKeys.self) 49 | try container.encodeIfPresent(id, forKey: .id) 50 | } 51 | 52 | public static func missed() -> Self { 53 | return .init(collectionName: "!missed_reference", id: "!missed_reference") 54 | } 55 | 56 | public var isMissedReference: Bool { 57 | return collectionName == "!missed_reference" && id == "!missed_reference" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/Core/Collection/CollectionObjectProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionObjectProtocol.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 18/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A collection object protocol. 12 | /// 13 | /// This protocol describe basic properties. You can extend them with own type, 14 | /// but you have to implement `Encodable` and `Decodable` protocols in a specific way: 15 | /// - the protocol properties present on the root level of the user structure, 16 | /// - additinal properties should be encoded/decoded in the nested `data` container. 17 | /// 18 | /// Here is an example of a JSON responce: 19 | /// ``` 20 | /// { 21 | /// "id": "burger", 22 | /// "collection": "food", 23 | /// "foreign_id":"food:burger" 24 | /// "data": { "name": "Burger" }, 25 | /// "created_at": "2018-12-17T15:23:26.591179Z", 26 | /// "updated_at": "2018-12-17T15:23:26.591179Z", 27 | /// "duration":"0.45ms" 28 | /// } 29 | /// ``` 30 | /// 31 | /// You can extend our opened `CollectionObject` class for the default protocol properties. 32 | /// 33 | /// Example with custom properties: 34 | /// ``` 35 | /// final class Food: CollectionObject { 36 | /// private enum CodingKeys: String, CodingKey { 37 | /// case name 38 | /// } 39 | /// 40 | /// var name: String 41 | /// 42 | /// init(name: String, id: String? = nil) { 43 | /// self.name = name 44 | /// super.init(collectionName: "food", id: id) 45 | /// } 46 | /// 47 | /// required init(from decoder: Decoder) throws { 48 | /// let dataContainer = try decoder.container(keyedBy: DataCodingKeys.self) 49 | /// let container = try dataContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: .data) 50 | /// name = try container.decode(String.self, forKey: .name) 51 | /// try super.init(from: decoder) 52 | /// } 53 | /// 54 | /// override func encode(to encoder: Encoder) throws { 55 | /// var dataContainer = encoder.container(keyedBy: DataCodingKeys.self) 56 | /// var container = dataContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: .data) 57 | /// try container.encode(name, forKey: .name) 58 | /// try super.encode(to: encoder) 59 | /// } 60 | /// } 61 | /// ``` 62 | public protocol CollectionObjectProtocol: Enrichable { 63 | /// A collection name. 64 | var collectionName: String { get } 65 | /// A collection object id. 66 | var id: String? { get } 67 | /// An foreign id of the collection object. The format is `:`. 68 | var foreignId: String? { get } 69 | /// When the collection object was created. 70 | var created: Date { get } 71 | /// When the collection object was last updated. 72 | var updated: Date { get } 73 | } 74 | 75 | // MARK: - Enrichable 76 | 77 | extension CollectionObjectProtocol { 78 | public var referenceId: String { 79 | guard let id = id, !id.isEmpty else { 80 | return "SO:\(collectionName)" 81 | } 82 | 83 | return "SO:\(collectionName):\(id)" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/Core/Extensions/Bundle+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle+Extensions.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 28/02/2019. 6 | // Copyright © 2019 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Bundle { 12 | enum StreamKey: String { 13 | case streamAPIKey = "Stream API Key" 14 | case streamAppId = "Stream App Id" 15 | case streamToken = "Stream Token" 16 | } 17 | 18 | /// API key from the bundle. 19 | public var streamAPIKey: String? { 20 | return streamValue(for: .streamAPIKey) 21 | } 22 | 23 | /// App id from the bundle. 24 | public var streamAppId: String? { 25 | return streamValue(for: .streamAppId) 26 | } 27 | 28 | /// Token from the bundle. 29 | public var streamToken: String? { 30 | return streamValue(for: .streamToken) 31 | } 32 | 33 | private func streamValue(for key: StreamKey) -> String? { 34 | if let value = infoDictionary?[key.rawValue] as? String, !value.isEmpty { 35 | return value 36 | } 37 | 38 | return nil 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Core/Extensions/Codable+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Codable+Extensions.swift 3 | // GetStream 4 | // 5 | // Created by Alexey Bukhtin on 12/11/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - JSONDecoder Stream 12 | 13 | extension JSONDecoder { 14 | /// A default decoder. 15 | public static var `default`: JSONDecoder = stream 16 | 17 | /// A Stream decoder. 18 | public static let stream: JSONDecoder = { 19 | let decoder = JSONDecoder() 20 | 21 | /// A custom decoding for a date. 22 | decoder.dateDecodingStrategy = .custom { decoder throws -> Date in 23 | let container = try decoder.singleValueContainer() 24 | let string: String = try container.decode(String.self) 25 | 26 | if string.hasSuffix("Z") { 27 | if let date = DateFormatter.Stream.iso8601DateFormatter.date(from: string) { 28 | return date 29 | } 30 | } else if let date = string.streamDate { 31 | return date 32 | } 33 | 34 | throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)") 35 | } 36 | 37 | return decoder 38 | }() 39 | } 40 | 41 | // MARK: - JSONEncoder Stream 42 | 43 | extension JSONEncoder { 44 | /// A default encoder. 45 | public static var `default`: JSONEncoder = stream 46 | 47 | /// A Stream encoder. 48 | public static let stream: JSONEncoder = { 49 | let encoder = JSONEncoder() 50 | 51 | /// A custom encoding for the custom ISO8601 date. 52 | encoder.dateEncodingStrategy = .custom { date, encoder throws in 53 | var container = encoder.singleValueContainer() 54 | try container.encode(DateFormatter.Stream.default.string(from: date)) 55 | } 56 | 57 | return encoder 58 | }() 59 | } 60 | 61 | // MARK: - JSON Encoder Helper 62 | 63 | struct AnyEncodable: Encodable { 64 | let encodable: Encodable 65 | 66 | public init(_ encodable: Encodable) { 67 | self.encodable = encodable 68 | } 69 | 70 | func encode(to encoder: Encoder) throws { 71 | try encodable.encode(to: encoder) 72 | } 73 | } 74 | 75 | // MARK: - Date Formatter Helper 76 | 77 | extension DateFormatter { 78 | /// A Stream Client date formatter. 79 | public struct Stream { 80 | public static let `default`: DateFormatter = { 81 | let formatter = DateFormatter() 82 | formatter.calendar = Calendar(identifier: .iso8601) 83 | formatter.locale = Locale(identifier: "en_US_POSIX") 84 | formatter.timeZone = TimeZone(secondsFromGMT: 0) 85 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" 86 | 87 | return formatter 88 | }() 89 | 90 | public static let iso8601DateFormatter: ISO8601DateFormatter = { 91 | if #available(iOS 11.2, macOS 10.13, *) { 92 | let formatter = Foundation.ISO8601DateFormatter() 93 | formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] 94 | return formatter 95 | } 96 | 97 | let formatter = DateFormatter() 98 | formatter.calendar = Calendar(identifier: .iso8601) 99 | formatter.locale = Locale(identifier: "en_US_POSIX") 100 | formatter.timeZone = TimeZone(secondsFromGMT: 0) 101 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX" 102 | 103 | return formatter 104 | }() 105 | } 106 | } 107 | 108 | // MARK: - Date Stream 109 | 110 | extension Date { 111 | /// Convert a date to the Stream Client date string. 112 | public var stream: String { 113 | return DateFormatter.Stream.default.string(from: self) 114 | } 115 | } 116 | 117 | // MARK: - String Date Stream 118 | 119 | extension String { 120 | /// Convert a string to the Stream Client date. 121 | public var streamDate: Date? { 122 | return DateFormatter.Stream.default.date(from: self) 123 | } 124 | } 125 | 126 | // MARK: - ISO8601 Date Formatter 127 | 128 | public protocol ISO8601DateFormatter { 129 | func date(from: String) -> Date? 130 | } 131 | 132 | @available(iOS 10.0, macOS 10.13, *) 133 | extension Foundation.ISO8601DateFormatter: ISO8601DateFormatter {} 134 | extension DateFormatter: ISO8601DateFormatter {} 135 | -------------------------------------------------------------------------------- /Sources/Core/Extensions/Dictionary+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dictionary+Extensions.swift 3 | // GetStream 4 | // 5 | // Created by Alexey Bukhtin on 12/11/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | /// Creates a dictionary by merging the given dictionary into this 13 | /// dictionary, replacing values with values of the other dictionary. 14 | extension Dictionary { 15 | func merged(with other: Dictionary) -> Dictionary { 16 | return merging(other) { _, new in new } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Core/Extensions/Result+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result+Extensions.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 10/12/2019. 6 | // Copyright © 2019 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | extension Result { 10 | /// Get the error from the result if it failed. 11 | public var error: Error? { 12 | if case .failure(let error) = self { 13 | return error 14 | } 15 | 16 | return nil 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Core/Feed/Aggregated Feed/AggregatedFeed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AggregatedFeed.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 20/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// `AggregatedFeed` are good for consuming activities in an "aggregated"-like manner. You cannot follow an aggregated feed, 12 | /// but you may on occasion want to add activities to one. 13 | public final class AggregatedFeed: Feed { 14 | 15 | /// Receive an aggregated feed activities type of `Activity`. 16 | /// 17 | /// - Parameters: 18 | /// - enrich: when using collections, you can request to enrich activities to include them. 19 | /// - pagination: a pagination options. 20 | /// - reactionsOptions: options to include reactions to activities. Check optionsin docs for `FeedReactionsOptions` 21 | /// - completion: a completion handler with a group of the `Activity` type. 22 | /// - Returns: an object to cancel the request. 23 | @discardableResult 24 | public func get(enrich: Bool = true, 25 | pagination: Pagination = .none, 26 | includeReactions reactionsOptions: FeedReactionsOptions = [], 27 | completion: @escaping GroupCompletion>) -> Cancellable { 28 | return get(typeOf: Activity.self, 29 | enrich: enrich, 30 | pagination: pagination, 31 | includeReactions: reactionsOptions, 32 | completion: completion) 33 | } 34 | 35 | /// Receive an aggregated feed activities with a custom activity type. 36 | /// 37 | /// - Parameters: 38 | /// - typeOf: a type of custom activities that conformed to `ActivityProtocol`. 39 | /// - enrich: when using collections, you can request to enrich activities to include them. 40 | /// - pagination: a pagination options. 41 | /// - reactionsOptions: options to include reactions to activities. Check optionsin docs for `FeedReactionsOptions` 42 | /// - completion: a completion handler with a group with a custom activity type. 43 | /// - Returns: an object to cancel the request. 44 | @discardableResult 45 | public func get(typeOf: T.Type, 46 | enrich: Bool = true, 47 | pagination: Pagination = .none, 48 | includeReactions reactionsOptions: FeedReactionsOptions = [], 49 | completion: @escaping GroupCompletion>) -> Cancellable { 50 | let endpoint = FeedEndpoint.get(feedId, enrich, pagination, "", .none, reactionsOptions) 51 | return Client.shared.request(endpoint: endpoint) { [weak self] result in 52 | if let self = self { 53 | result.parseGroup(self.callbackQueue, completion) 54 | } 55 | } 56 | } 57 | } 58 | 59 | // MARK: - Client Aggregated Feed 60 | 61 | extension Client { 62 | /// Get an aggregated feed with a given feed group `feedSlug` and `userId`. 63 | public func aggregatedFeed(feedSlug: String, userId: String) -> AggregatedFeed { 64 | return aggregatedFeed(FeedId(feedSlug: feedSlug, userId: userId)) 65 | } 66 | 67 | /// Get an aggregated feed with a given feed group `feedSlug` for the current user if it specified in the Token. 68 | /// 69 | /// - Note: If the current user is nil in the Token, then the returned feed would be nil. 70 | /// 71 | /// - Parameters: 72 | /// - feedSlug: a feed group name. 73 | public func aggregatedFeed(feedSlug: String) -> AggregatedFeed? { 74 | guard let userId = currentUserId else { 75 | return nil 76 | } 77 | 78 | return aggregatedFeed(FeedId(feedSlug: feedSlug, userId: userId)) 79 | } 80 | 81 | /// Get an aggregated feed with a given `feedId`. 82 | public func aggregatedFeed(_ feedId: FeedId) -> AggregatedFeed { 83 | return AggregatedFeed(feedId) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/Core/Feed/Aggregated Feed/Group.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Group.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 20/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An aggregated group type. 12 | public class Group: Decodable { 13 | private enum CodingKeys: String, CodingKey { 14 | case id 15 | case group 16 | case verb 17 | case activitiesCount = "activity_count" 18 | case actorsCount = "actor_count" 19 | case created = "created_at" 20 | case updated = "updated_at" 21 | case activities 22 | } 23 | 24 | /// A group id. 25 | public let id: String 26 | /// A group name. 27 | public let group: String 28 | /// A verb. 29 | public let verb: Verb 30 | /// A number of activities in the group. 31 | public let activitiesCount: Int 32 | /// A number of actors in the group. 33 | public let actorsCount: Int 34 | /// A created date. 35 | public let created: Date 36 | /// An updated date. 37 | public let updated: Date 38 | /// A list of activities. 39 | public let activities: [T] 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Core/Feed/Aggregated Feed/Result+ParseGroup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result+ParseGroup.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 20/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Moya 11 | 12 | /// An aggregated group completion block. 13 | public typealias GroupCompletion> = (_ result: Result, ClientError>) -> Void 14 | 15 | // MARK: - Result Group Parsing 16 | 17 | extension Result where Success == Moya.Response, Failure == ClientError { 18 | func parseGroup>(_ callbackQueue: DispatchQueue, 19 | _ completion: @escaping GroupCompletion) { 20 | parse(block: { 21 | let moyaResponse = try get() 22 | let response = try JSONDecoder.default.decode(Response.self, from: moyaResponse.data) 23 | callbackQueue.async { completion(.success(response)) } 24 | }, catch: { error in 25 | callbackQueue.async { completion(.failure(error)) } 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Core/Feed/Feed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Feed.swift 3 | // GetStream 4 | // 5 | // Created by Alexey Bukhtin on 09/11/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Moya 11 | 12 | typealias ActivityResponse = EnrichedActivity 13 | 14 | /// A followers completion block. 15 | public typealias FollowersCompletion = (_ result: Result, ClientError>) -> Void 16 | 17 | /// A superclass for feeds: `FlatFeed`, `AggregatedFeed` and `NotificationFeed`. 18 | public class Feed: CustomStringConvertible { 19 | /// A feed id. 20 | public let feedId: FeedId 21 | /// A separated callback queue from `client.callbackQueue` for completion requests. 22 | public var callbackQueue: DispatchQueue 23 | 24 | /// Returns a feedId description of the feed. 25 | public var description: String { 26 | return feedId.description 27 | } 28 | 29 | /// Create a general feed. 30 | /// 31 | /// - Parameters: 32 | /// - feedId: a `FeedId` 33 | /// - client: a Stream client. 34 | /// - callbackQueue: a callback queue for completion requests. If nil, then `client.callbackQueue` would be used. 35 | public init(_ feedId: FeedId, callbackQueue: DispatchQueue? = nil) { 36 | self.feedId = feedId 37 | self.callbackQueue = callbackQueue ?? Client.shared.callbackQueue 38 | } 39 | } 40 | 41 | // MARK: - Feed Activity 42 | 43 | extension Feed { 44 | /// Add a new activity. 45 | /// 46 | /// - Parameters: 47 | /// - activity: an activity to add. 48 | /// - completion: a completion block with the activity that was added. 49 | /// - Returns: an object to cancel the request. 50 | @discardableResult 51 | public func add(_ activity: T, completion: @escaping ActivityCompletion) -> Cancellable { 52 | return Client.shared.request(endpoint: FeedActivityEndpoint.add(activity, feedId: feedId)) { [weak self] result in 53 | guard let self = self else { 54 | return 55 | } 56 | 57 | if case .failure(let clientError) = result { 58 | self.callbackQueue.async { completion(.failure(clientError)) } 59 | return 60 | } 61 | 62 | /// The response is always for a not enriched activity. 63 | /// Check if the given activity is not enriched. 64 | if T.ActorType.self == String.self, T.ObjectType.self == String.self { 65 | result.parse(self.callbackQueue, completion) 66 | return 67 | } 68 | 69 | /// Parse the response with the default `Activity` and populate the given activity with `id` and `time` properties. 70 | let activityCompletion: ActivityCompletion = { (result: Result) in 71 | do { 72 | let addedActivity = try result.get() 73 | var activity = activity 74 | activity.id = addedActivity.id 75 | 76 | if activity.time == nil { 77 | activity.time = addedActivity.time 78 | } 79 | 80 | self.callbackQueue.async { completion(.success(activity)) } 81 | } catch { 82 | self.callbackQueue.async { completion(.failure(.unexpectedError(error))) } 83 | } 84 | } 85 | 86 | result.parse(self.callbackQueue, activityCompletion) 87 | } 88 | } 89 | 90 | /// Remove an activity by the activityId. 91 | /// 92 | /// - Parameters: 93 | /// - activityId: an activityId to remove. 94 | /// - completion: a completion block with removed activityId. 95 | /// - Returns: an object to cancel the request. 96 | @discardableResult 97 | public func remove(activityId: String, completion: @escaping RemovedCompletion) -> Cancellable { 98 | return Client.shared.request(endpoint: FeedEndpoint.deleteById(activityId, feedId: feedId)) { [weak self] result in 99 | if let self = self { 100 | result.parseRemoved(self.callbackQueue, completion) 101 | } 102 | } 103 | } 104 | 105 | /// Remove an activity by the foreignId. 106 | /// 107 | /// - Parameters: 108 | /// - foreignId: an foreignId to remove. 109 | /// - completion: a completion block with removed activityId. 110 | /// - Returns: an object to cancel the request. 111 | @discardableResult 112 | public func remove(foreignId: String, completion: @escaping RemovedCompletion) -> Cancellable { 113 | return Client.shared.request(endpoint: FeedEndpoint.deleteByForeignId(foreignId, feedId: feedId)) { [weak self] result in 114 | if let self = self { 115 | result.parseRemoved(self.callbackQueue, completion) 116 | } 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Sources/Core/Feed/FeedId.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedId.swift 3 | // GetStream 4 | // 5 | // Created by Alexey Bukhtin on 12/11/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A feed identifier based on `feedSlug` and `userId`. 12 | public struct FeedId: CustomStringConvertible, Codable { 13 | 14 | /// The name of the feed group, for instance user, trending, flat, timeline etc. For example: flat, timeline. 15 | public let feedSlug: String 16 | /// The owner of the given feed. 17 | public let userId: String 18 | 19 | /// The feed group id, e.g. `timeline123` 20 | public var together: String { 21 | return feedSlug.appending(userId) 22 | } 23 | 24 | /// The feed group id with the colon separator, e.g. `timeline:123` 25 | public var togetherWithColon: String { 26 | if userId.isEmpty { 27 | return feedSlug 28 | } 29 | 30 | return feedSlug.appending(":").appending(userId) 31 | } 32 | 33 | /// The feed group id with the slash separator, e.g. `timeline/123` 34 | public var togetherWithSlash: String { 35 | if userId.isEmpty { 36 | return feedSlug 37 | } 38 | 39 | return feedSlug.appending("/").appending(userId) 40 | } 41 | 42 | public var description: String { 43 | return togetherWithColon 44 | } 45 | 46 | public init(feedSlug: String, userId: String) { 47 | self.feedSlug = feedSlug 48 | self.userId = userId 49 | } 50 | 51 | public init?(feedSlug: String) { 52 | if let userId = Client.shared.currentUserId { 53 | self.feedSlug = feedSlug 54 | self.userId = userId 55 | } else { 56 | return nil 57 | } 58 | } 59 | 60 | public init(from decoder: Decoder) throws { 61 | let container = try decoder.singleValueContainer() 62 | let id = try container.decode(String.self) 63 | 64 | if id.isEmpty { 65 | throw DecodingError.dataCorruptedError(in: container, 66 | debugDescription: "Cannot initialize FeedId from an empty string") 67 | } 68 | 69 | let pair = id.split(separator: ":").map { String($0) } 70 | 71 | if pair.count != 2 { 72 | throw DecodingError.dataCorruptedError(in: container, 73 | debugDescription: "Cannot initialize FeedId from a currupted string: \(id)") 74 | } 75 | 76 | self.init(feedSlug: pair[0], userId: pair[1]) 77 | } 78 | 79 | public func encode(to encoder: Encoder) throws { 80 | var container = encoder.singleValueContainer() 81 | try container.encode(description) 82 | } 83 | } 84 | 85 | extension FeedId: Equatable { 86 | public static func == (lhs: FeedId, rhs: FeedId) -> Bool { 87 | return lhs.feedSlug == rhs.feedSlug && lhs.userId == rhs.userId 88 | } 89 | } 90 | 91 | extension FeedId { 92 | public static let any = FeedId(feedSlug: "*", userId: "") 93 | public static let user = FeedId(feedSlug: "user") 94 | public static let timeline = FeedId(feedSlug: "timeline") 95 | public static let notification = FeedId(feedSlug: "notification") 96 | 97 | /// A user feed id with the given userId. 98 | public static func user(with userId: String) -> FeedId { 99 | return FeedId(feedSlug: "user", userId: userId) 100 | } 101 | } 102 | 103 | // MARK: - FeedIds 104 | 105 | public typealias FeedIds = [FeedId] 106 | 107 | extension Array where Element == FeedId { 108 | var value: String { 109 | return map { $0.description }.joined(separator: ",") 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Sources/Core/Feed/FlatFeed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlatFeed.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 20/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// `FlatFeed` are the only feeds that can be followed, and therefore are a good type to setup for adding activities. 12 | /// `FlatFeed` can also be used to consume activities from other feeds - in a "timeline"-like manner. 13 | public final class FlatFeed: Feed { 14 | 15 | /// Receive a feed activities type of `Activity`. 16 | /// 17 | /// - Parameters: 18 | /// - enrich: when using collections, you can request to enrich activities to include them. 19 | /// - pagination: a pagination options. 20 | /// - ranking: the custom ranking formula used to sort the feed, must be defined in the dashboard. 21 | /// - reactionsOptions: options to include reactions to activities. Check options in docs for `FeedReactionsOptions` 22 | /// - completion: a completion handler with an array of the `Activity` type. 23 | /// - Returns: 24 | /// - a cancellable object to cancel the request. 25 | @discardableResult 26 | public func get(enrich: Bool = true, 27 | pagination: Pagination = .none, 28 | ranking: String? = nil, 29 | includeReactions reactionsOptions: FeedReactionsOptions = [], 30 | completion: @escaping ActivitiesCompletion) -> Cancellable { 31 | return get(typeOf: Activity.self, 32 | enrich: enrich, 33 | pagination: pagination, 34 | ranking: ranking, 35 | includeReactions: reactionsOptions, 36 | completion: completion) 37 | } 38 | 39 | /// Receive a feed activities with a custom activity type. 40 | /// 41 | /// - Parameters: 42 | /// - typeOf: a type of custom activities that conformed to `ActivityProtocol`. 43 | /// - enrich: when using collections, you can request to enrich activities to include them. 44 | /// - pagination: a pagination options. 45 | /// - ranking: the custom ranking formula used to sort the feed, must be defined in the dashboard. 46 | /// - reactionsOptions: options to include reactions to activities. Check options in docs for `FeedReactionsOptions` 47 | /// - completion: a completion handler with an array of a custom activity type. 48 | /// - Returns: 49 | /// - a cancellable object to cancel the request. 50 | @discardableResult 51 | public func get(typeOf: T.Type, 52 | enrich: Bool = true, 53 | pagination: Pagination = .none, 54 | ranking: String? = nil, 55 | includeReactions reactionsOptions: FeedReactionsOptions = [], 56 | completion: @escaping ActivitiesCompletion) -> Cancellable { 57 | let endpoint = FeedEndpoint.get(feedId, enrich, pagination, ranking ?? "", .none, reactionsOptions) 58 | return Client.shared.request(endpoint: endpoint) { [weak self] result in 59 | if let self = self { 60 | result.parse(self.callbackQueue, completion) 61 | } 62 | } 63 | } 64 | } 65 | 66 | // MARK: - Client Flat Feed 67 | 68 | extension Client { 69 | /// Get a flat feed with a given feed group `feedSlug` and `userId`. 70 | public func flatFeed(feedSlug: String, userId: String) -> FlatFeed { 71 | return flatFeed(FeedId(feedSlug: feedSlug, userId: userId)) 72 | } 73 | 74 | /// Get a flat feed with a given feed group `feedSlug` for the current user if it specified in the Token. 75 | /// 76 | /// - Note: If the current user is nil in the Token, then the returned feed would be nil. 77 | /// 78 | /// - Parameters: 79 | /// - feedSlug: a feed group name. 80 | public func flatFeed(feedSlug: String) -> FlatFeed? { 81 | guard let userId = currentUserId else { 82 | return nil 83 | } 84 | 85 | return flatFeed(FeedId(feedSlug: feedSlug, userId: userId)) 86 | } 87 | 88 | /// Get a flat feed with a given `feedId`. 89 | public func flatFeed(_ feedId: FeedId) -> FlatFeed { 90 | return FlatFeed(feedId) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/Core/Feed/Following/Feed+Following.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Feed+Following.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 02/01/2019. 6 | // Copyright © 2019 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - Feed Following 12 | 13 | extension Feed { 14 | 15 | /// Follow a target feed. 16 | /// 17 | /// - Parameters: 18 | /// - target: the target feed this feed should follow, e.g. user:44. 19 | /// - activityCopyLimit: how many activities should be copied from the target feed, max 1000, default 100. 20 | /// - Returns: an object to cancel the request. 21 | @discardableResult 22 | public func follow(toTarget target: FeedId, activityCopyLimit: Int = 100, completion: @escaping StatusCodeCompletion) -> Cancellable { 23 | let activityCopyLimit = max(0, min(1000, activityCopyLimit)) 24 | let endpoint = FeedEndpoint.follow(feedId, target: target, activityCopyLimit: activityCopyLimit) 25 | 26 | return Client.shared.request(endpoint: endpoint) { [weak self] result in 27 | if let self = self { 28 | result.parseStatusCode(self.callbackQueue, completion) 29 | } 30 | } 31 | } 32 | 33 | /// Unfollow a target feed. 34 | /// 35 | /// - Parameters: 36 | /// - target: the target feed, e.g. user:44. 37 | /// - keepHistory: when provided the activities from target feed will not be kept in the feed. 38 | /// - Note: Unfollow target's activities are purged from the feed unless the `keepHistory` parameter is provided. 39 | /// - Returns: an object to cancel the request. 40 | @discardableResult 41 | public func unfollow(fromTarget target: FeedId, keepHistory: Bool = false, completion: @escaping StatusCodeCompletion) -> Cancellable { 42 | return Client.shared.request(endpoint: FeedEndpoint.unfollow(feedId, 43 | target: target, 44 | keepHistory: keepHistory)) { [weak self] result in 45 | if let self = self { 46 | result.parseStatusCode(self.callbackQueue, completion) 47 | } 48 | } 49 | } 50 | 51 | /// Returns a paginated list of followers. 52 | /// 53 | /// - Parameters: 54 | /// - offset: number of followers to skip before returning results, max 400. 55 | /// - limit: amount of results per request, max 500, default 25. 56 | /// - completion: a result with `Follower`'s or an error. 57 | /// - Note: the number of followers that can be retrieved is limited to 1000. 58 | /// - Returns: an object to cancel the request. 59 | @discardableResult 60 | public func followers(offset: Int = 0, limit: Int = 25, completion: @escaping FollowersCompletion) -> Cancellable { 61 | let limit = max(0, min(500, limit)) 62 | let offset = max(0, min(400, offset)) 63 | 64 | return Client.shared.request(endpoint: FeedEndpoint.followers(feedId, offset: offset, limit: limit)) { [weak self] result in 65 | if let self = self { 66 | result.parse(self.callbackQueue, completion) 67 | } 68 | } 69 | } 70 | 71 | /// Returns a paginated list of the feeds which are followed by the feed. 72 | /// 73 | /// - Parameters: 74 | /// - filter: list of feeds to filter results on. 75 | /// - offset: number of followers to skip before returning results, max 400. 76 | /// - limit: amount of results per request, max 500, default 25. 77 | /// - completion: a result with `Follower`'s or an error. 78 | /// - Note: the number of followers that can be retrieved is limited to 1000. 79 | /// - Returns: an object to cancel the request. 80 | @discardableResult 81 | public func following(filter: FeedIds = [], 82 | offset: Int = 0, 83 | limit: Int = 25, 84 | completion: @escaping FollowersCompletion) -> Cancellable { 85 | let limit = max(0, min(500, limit)) 86 | let offset = max(0, min(400, offset)) 87 | 88 | return Client.shared.request(endpoint: FeedEndpoint.following(feedId, 89 | filter: filter, 90 | offset: offset, 91 | limit: limit)) { [weak self] result in 92 | if let self = self { 93 | result.parse(self.callbackQueue, completion) 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Sources/Core/Feed/Following/Follower.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Follower.swift 3 | // GetStream 4 | // 5 | // Created by Alexey Bukhtin on 19/11/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A feed Follower. 12 | public struct Follower: Decodable { 13 | private enum CodingKeys: String, CodingKey { 14 | case feedId = "feed_id" 15 | case targetFeedId = "target_id" 16 | case created = "created_at" 17 | case updated = "updated_at" 18 | } 19 | 20 | /// A feed id. 21 | public let feedId: FeedId 22 | /// A target feed id. 23 | public let targetFeedId: FeedId 24 | /// A created date. 25 | public let created: Date 26 | /// An updated date. 27 | public let updated: Date? 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Core/Feed/Notification Feed/NotificationFeed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationFeed.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 20/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// The `NotificationFeed` type makes it easy to add notifications to your app. Notifications cannot be followed by other feeds, 12 | /// but you can write directly to a Notification feed. 13 | public final class NotificationFeed: Feed { 14 | 15 | /// Receive a notification feed activities type of `Activity`. 16 | /// 17 | /// - Parameters: 18 | /// - enrich: when using collections, you can request to enrich activities to include them. 19 | /// - pagination: a pagination options. 20 | /// - markOption: mark options to update feed notifications as read/seen. 21 | /// - reactionsOptions: options to include reactions to activities. Check optionsin docs for `FeedReactionsOptions` 22 | /// - completion: a completion handler with a notification group with the `Activity` type. 23 | /// - Returns: 24 | /// - a cancellable object to cancel the request. 25 | @discardableResult 26 | public func get(enrich: Bool = true, 27 | pagination: Pagination = .none, 28 | markOption: FeedMarkOption = .none, 29 | includeReactions reactionsOptions: FeedReactionsOptions = [], 30 | completion: @escaping GroupCompletion>) -> Cancellable { 31 | return get(typeOf: Activity.self, 32 | enrich: enrich, 33 | pagination: pagination, 34 | markOption: markOption, 35 | includeReactions: reactionsOptions, 36 | completion: completion) 37 | } 38 | 39 | /// Receive a notification feed activities with a custom activity type. 40 | /// 41 | /// - Parameters: 42 | /// - typeOf: a type of custom activities that conformed to `ActivityProtocol`. 43 | /// - enrich: when using collections, you can request to enrich activities to include them. 44 | /// - pagination: a pagination options. 45 | /// - markOption: mark options to update feed notifications as read/seen. 46 | /// - reactionsOptions: options to include reactions to activities. Check optionsin docs for `FeedReactionsOptions` 47 | /// - completion: a completion handler with a notification group with a custom activity type. 48 | /// - Returns: 49 | /// - a cancellable object to cancel the request. 50 | @discardableResult 51 | public func get(typeOf: T.Type, 52 | enrich: Bool = true, 53 | pagination: Pagination = .none, 54 | markOption: FeedMarkOption = .none, 55 | includeReactions reactionsOptions: FeedReactionsOptions = [], 56 | completion: @escaping GroupCompletion>) -> Cancellable { 57 | let endpoint = FeedEndpoint.get(feedId, enrich, pagination, "", markOption, reactionsOptions) 58 | return Client.shared.request(endpoint: endpoint) { [weak self] result in 59 | if let self = self { 60 | result.parseGroup(self.callbackQueue, completion) 61 | } 62 | } 63 | } 64 | } 65 | 66 | // MARK: - Client Notification Feed 67 | 68 | extension Client { 69 | /// Get a notification feed with a given feed group `feedSlug` and `userId`. 70 | public func notificationFeed(feedSlug: String, userId: String) -> NotificationFeed { 71 | return notificationFeed(FeedId(feedSlug: feedSlug, userId: userId)) 72 | } 73 | 74 | /// Get a notification feed with a given feed group `feedSlug` for the current user if it specified in the Token. 75 | /// 76 | /// - Note: If the current user is nil in the Token, then the returned feed would be nil. 77 | /// 78 | /// - Parameters: 79 | /// - feedSlug: a feed group name. 80 | public func notificationFeed(feedSlug: String) -> NotificationFeed? { 81 | guard let userId = currentUserId else { 82 | return nil 83 | } 84 | 85 | return notificationFeed(FeedId(feedSlug: feedSlug, userId: userId)) 86 | } 87 | 88 | /// Get a notification feed with a given `feedId`. 89 | public func notificationFeed(_ feedId: FeedId) -> NotificationFeed { 90 | return NotificationFeed(feedId) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/Core/Feed/Notification Feed/NotificationGroup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationGroup.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 20/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A notification group. 12 | public final class NotificationGroup: Group { 13 | private enum CodingKeys: String, CodingKey { 14 | case isSeen = "is_seen" 15 | case isRead = "is_read" 16 | } 17 | 18 | /// True if the notification group is seen. 19 | public let isSeen: Bool 20 | /// True if the notification group is read. 21 | public let isRead: Bool 22 | 23 | public required init(from decoder: Decoder) throws { 24 | let container = try decoder.container(keyedBy: CodingKeys.self) 25 | isSeen = try container.decode(Bool.self, forKey: .isSeen) 26 | isRead = try container.decode(Bool.self, forKey: .isRead) 27 | try super.init(from: decoder) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Core/Feed/Result+ParseFeed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result+ParseFeed.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 13/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Moya 11 | 12 | /// An activity removed completion block. 13 | public typealias RemovedCompletion = (_ result: Result) -> Void 14 | 15 | // MARK: - Result Removed Parsing 16 | 17 | extension Result where Success == Moya.Response, Failure == ClientError { 18 | func parseRemoved(_ callbackQueue: DispatchQueue, _ completion: @escaping RemovedCompletion) { 19 | if case .success(let response) = self { 20 | do { 21 | let json = try response.mapJSON() 22 | 23 | if let json = json as? [String: Any], let removedId = json["removed"] as? String { 24 | callbackQueue.async { completion(.success(removedId)) } 25 | } else { 26 | ClientError.warning(for: json, missedParameter: "removed") 27 | callbackQueue.async { completion(.failure(.unexpectedResponse("`removed` parameter not found"))) } 28 | } 29 | } catch { 30 | callbackQueue.async { completion(.failure(ClientError.jsonDecode(error.localizedDescription, error, response.data))) } 31 | } 32 | } else if case .failure(let error) = self { 33 | callbackQueue.async { completion(.failure(error)) } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Core/Files and Images/File.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 11/01/2019. 6 | // Copyright © 2019 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Swime 11 | 12 | #if os(iOS) || os(watchOS) || os(tvOS) 13 | import UIKit.UIImage 14 | public typealias Image = UIImage 15 | #elseif os(macOS) 16 | import AppKit.NSImage 17 | public typealias Image = NSImage 18 | #endif 19 | 20 | /// A file type. 21 | public struct File { 22 | let name: String 23 | let data: Data 24 | var mimeType: MimeType? 25 | 26 | /// Create a File. 27 | /// 28 | /// - Parameters: 29 | /// - name: the name of the file. 30 | /// - data: the data of the file. 31 | public init(name: String, data: Data) { 32 | self.name = name.trimmingCharacters(in: CharacterSet(charactersIn: ".")) 33 | self.data = data 34 | } 35 | } 36 | 37 | public extension File { 38 | 39 | /// Create a File from a given image. 40 | /// 41 | /// - Parameters: 42 | /// - name: the name of the image. 43 | /// - jpegImage: the image, that would be converted to a JPEG data. 44 | /// - compressionQuality: The quality of the resulting JPEG image, expressed as a value from 0.0 to 1.0. 45 | /// The value 0.0 represents the maximum compression (or lowest quality) 46 | /// while the value 1.0 represents the least compression (or best quality). Default: 0.9. 47 | init?(name: String, jpegImage: Image, compressionQuality: CGFloat = 0.9) { 48 | guard let data = jpegImage.jpegData(compressionQuality: compressionQuality) else { 49 | return nil 50 | } 51 | 52 | self.init(name: name, data: data) 53 | mimeType = Swime.mimeType(byFileExtension: "jpg") 54 | } 55 | 56 | /// Create a File from a given image. 57 | /// 58 | /// - Parameters: 59 | /// - name: the name of the image. 60 | /// - pngImage: the image, that would be converted to a PNG data. 61 | init?(name: String, pngImage: Image) { 62 | guard let data = pngImage.pngData() else { 63 | return nil 64 | } 65 | 66 | self.init(name: name, data: data) 67 | mimeType = Swime.mimeType(byFileExtension: "png") 68 | } 69 | 70 | /// A helper function to create `File`'s from images in working thread. 71 | /// 72 | /// - Parameters: 73 | /// - images: a list of `Image`'s. 74 | /// - process: a process block to create a `File` from a given `Image`. The block can return nil an image needs to skip. 75 | /// - completion: a completion block with a list of `File`'s (could be empty, if images didn't converted to `File`'s. ). 76 | static func files(from images: [Image], 77 | process: @escaping (_ index: Int, _ image: Image) -> File?, 78 | completion: @escaping (_ files: [File]) -> Void) { 79 | guard images.count > 0 else { 80 | completion([]) 81 | return 82 | } 83 | 84 | DispatchQueue(label: "io.getstream.File").async { 85 | var files: [File] = [] 86 | 87 | images.enumerated().forEach { index, image in 88 | if let file = process(index, image) { 89 | files.append(file) 90 | } 91 | } 92 | 93 | DispatchQueue.main.async { completion(files) } 94 | } 95 | } 96 | } 97 | 98 | // MARK: - macOS Image API compatibility. 99 | 100 | #if os(macOS) 101 | extension Image { 102 | func pngData() -> Data? { 103 | return representation(using: .png) 104 | } 105 | 106 | func jpegData(compressionQuality: CGFloat) -> Data? { 107 | return representation(using: .jpeg, properties: [.compressionFactor: compressionQuality]) 108 | } 109 | 110 | private func representation(using fileType: NSBitmapImageRep.FileType, 111 | properties: [NSBitmapImageRep.PropertyKey: Any] = [:]) -> Data? { 112 | var imageRect = CGRect(x: 0, y: 0, width: size.width, height: size.height) 113 | 114 | if let cgImage = cgImage(forProposedRect: &imageRect, context: nil, hints: nil) { 115 | let imageRep = NSBitmapImageRep(cgImage: cgImage) 116 | return imageRep.representation(using: fileType, properties: properties) 117 | } 118 | 119 | return nil 120 | } 121 | } 122 | #endif 123 | -------------------------------------------------------------------------------- /Sources/Core/Files and Images/FilesEndpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilesEndpoint.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 07/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Moya 11 | import Swime 12 | 13 | enum FilesEndpoint { 14 | case uploadFile(_ file: File) 15 | case deleteFile(_ fileURL: URL) 16 | case uploadImage(_ file: File) 17 | case deleteImage(_ imageURL: URL) 18 | case resizeImage(_ imageProcess: ImageProcess) 19 | } 20 | 21 | extension FilesEndpoint: StreamTargetType { 22 | 23 | var path: String { 24 | switch self { 25 | case .uploadFile, .deleteFile: 26 | return "files/" 27 | case .uploadImage, .deleteImage, .resizeImage: 28 | return "images/" 29 | } 30 | } 31 | 32 | var method: Moya.Method { 33 | switch self { 34 | case .uploadFile, .uploadImage: 35 | return .post 36 | case .deleteFile, .deleteImage: 37 | return .delete 38 | case .resizeImage: 39 | return .get 40 | } 41 | } 42 | 43 | var task: Task { 44 | switch self { 45 | case let .uploadFile(file): 46 | return .uploadMultipart([MultipartFormData(provider: .data(file.data), 47 | name: "file", 48 | fileName: file.name, 49 | mimeType: mimeType)]) 50 | case let .deleteFile(fileURL): 51 | return .requestParameters(parameters: ["url": fileURL], encoding: URLEncoding.default) 52 | 53 | case let .uploadImage(file): 54 | return .uploadMultipart([MultipartFormData(provider: .data(file.data), 55 | name: "file", 56 | fileName: file.name, 57 | mimeType: mimeType)]) 58 | 59 | case let .deleteImage(imageURL): 60 | return .requestParameters(parameters: ["url": imageURL], encoding: URLEncoding.default) 61 | 62 | case let .resizeImage(imageProcess): 63 | return .requestJSONEncodable(imageProcess) 64 | } 65 | } 66 | 67 | var sampleJSON: String { 68 | switch self { 69 | case .uploadFile(let file): 70 | if file.data.count > 0 { 71 | return """ 72 | {"file":"http://uploaded.getstream.io/\(file.name)"} 73 | """ 74 | } 75 | case .uploadImage(let file): 76 | if file.data.count > 0 { 77 | return """ 78 | {"file":"http://images.getstream.io/\(file.name)"} 79 | """ 80 | } 81 | case .resizeImage(let imageProcess): 82 | return """ 83 | {"file":"http://images.getstream.io/jpg?crop=\(imageProcess.crop)&h=\(Int(imageProcess.height))&w=\(Int(imageProcess.width))&resize=\(imageProcess.resize.rawValue)&url=\(imageProcess.url)"} 84 | """ 85 | case .deleteFile, .deleteImage: 86 | return "{}" 87 | } 88 | 89 | return "" 90 | } 91 | } 92 | 93 | extension FilesEndpoint { 94 | var mimeType: String { 95 | var mimeType: MimeType? 96 | 97 | switch self { 98 | case .uploadFile(let file), .uploadImage(let file): 99 | mimeType = file.mimeType ?? Swime.mimeType(data: file.data) ?? Swime.mimeType(byFileName: file.name) 100 | default: 101 | break 102 | } 103 | 104 | return mimeType?.mime ?? "application/octet-stream" 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/Core/Files and Images/ImageProcess.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageProcess.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 10/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An image process option type. 12 | public struct ImageProcess: Codable { 13 | private enum CodingKeys: String, CodingKey { 14 | case url 15 | case resize = "resize" 16 | case crop = "crop" 17 | case width = "w" 18 | case height = "h" 19 | } 20 | 21 | /// URL of the image to process. This is the URL returned by the `UploadResult` request. 22 | let url: URL 23 | /// Strategy used to adapt the image the new dimensions. Allowed values are: `clip`, `crop`, `scale`, `fill`. 24 | let resize: ResizeStrategy 25 | /// Cropping modes as a comma separated list. Allowed values are top, bottom, left, right, center. 26 | let crop: String 27 | /// Width of the processed image. 28 | let width: Int 29 | /// Height of the processed image. 30 | let height: Int 31 | 32 | public init(url: URL, resize: ResizeStrategy = .clip, crop: CropMode = .center, width: Int, height: Int) { 33 | self.url = url 34 | self.resize = resize 35 | self.crop = crop.description 36 | self.width = width 37 | self.height = height 38 | } 39 | } 40 | 41 | extension ImageProcess { 42 | public enum ResizeStrategy: String, Codable { 43 | case clip 44 | case crop 45 | case scale 46 | case fill 47 | } 48 | } 49 | 50 | extension ImageProcess { 51 | public struct CropMode: OptionSet, Codable { 52 | public let rawValue: Int 53 | 54 | public init(rawValue: Int) { 55 | self.rawValue = rawValue 56 | } 57 | 58 | public static let top = CropMode(rawValue: 1 << 0) 59 | public static let bottom = CropMode(rawValue: 1 << 1) 60 | public static let left = CropMode(rawValue: 1 << 2) 61 | public static let right = CropMode(rawValue: 1 << 3) 62 | public static let center = CropMode(rawValue: 1 << 4) 63 | 64 | var description: String { 65 | var crops = [String]() 66 | 67 | if self.contains(.top) { 68 | crops.append("top") 69 | } 70 | 71 | if self.contains(.bottom) { 72 | crops.append("bottom") 73 | } 74 | 75 | if self.contains(.left) { 76 | crops.append("left") 77 | } 78 | 79 | if self.contains(.right) { 80 | crops.append("right") 81 | } 82 | 83 | if self.contains(.center) { 84 | crops.append("center") 85 | } 86 | 87 | return crops.joined(separator: ",") 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/Core/Files and Images/Result+ParseUpload.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result+ParseUpload.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 13/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Moya 11 | 12 | /// An upload file completion block. 13 | public typealias UploadCompletion = (_ result: Result) -> Void 14 | /// An upload multiple files completion block. 15 | public typealias MultipleUploadCompletion = (_ result: Result<[URL], ClientError>) -> Void 16 | 17 | // MARK: - Result Upload Parsing 18 | 19 | extension Result where Success == Moya.Response, Failure == ClientError { 20 | func parseUpload(_ callbackQueue: DispatchQueue, _ completion: @escaping UploadCompletion) { 21 | if case .success(let response) = self { 22 | do { 23 | let json = try response.mapJSON() 24 | 25 | if let json = json as? JSON, let urlString = json["file"] as? String, let url = URL(string: urlString) { 26 | callbackQueue.async { completion(.success(url)) } 27 | } else { 28 | ClientError.warning(for: json, missedParameter: "file") 29 | callbackQueue.async { completion(.failure(.unexpectedResponse("`file` parameter not found"))) } 30 | } 31 | 32 | } catch { 33 | if let clientError = error as? ClientError { 34 | callbackQueue.async { completion(.failure(clientError)) } 35 | } 36 | } 37 | } else if case .failure(let error) = self { 38 | callbackQueue.async { completion(.failure(error)) } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Core/Files and Images/Swime+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Swime+Extensions.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 13/03/2019. 6 | // Copyright © 2019 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Swime 11 | 12 | extension Swime { 13 | 14 | /// A simplified way to find a mime type by the given file name. 15 | public static func mimeType(byFileName fileName: String) -> MimeType? { 16 | if fileName.isEmpty { 17 | return nil 18 | } 19 | 20 | return mimeType(byFileExtension: (fileName as NSString).pathExtension) 21 | } 22 | 23 | /// A simplified way to find a mime type by the given file extension. 24 | public static func mimeType(byFileExtension fileExtension: String) -> MimeType? { 25 | if fileExtension.isEmpty { 26 | return nil 27 | } 28 | 29 | for mime in MimeType.all where mime.ext == fileExtension { 30 | return mime 31 | } 32 | 33 | return nil 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Core/GetStream.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetStream.swift 3 | // Stream.io Inc 4 | // 5 | // Created by Alexey Bukhtin on 06/11/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Moya 11 | 12 | public typealias JSON = [String: Any] 13 | public typealias StatusCodeCompletion = (_ result: Result) -> Void 14 | public typealias Cancellable = Moya.Cancellable 15 | 16 | final class SimpleCancellable: Cancellable { 17 | var isCancelled: Bool 18 | 19 | init(isCancelled: Bool = false) { 20 | self.isCancelled = isCancelled 21 | } 22 | 23 | func cancel() { 24 | isCancelled = true 25 | } 26 | } 27 | 28 | final class ProxyCancellable: Cancellable { 29 | var cancellable: Cancellable? 30 | 31 | var isCancelled: Bool { 32 | return cancellable?.isCancelled ?? false 33 | } 34 | 35 | func cancel() { 36 | cancellable?.cancel() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Core/Moya/AuthorizationMoyaPlugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthorizationMoyaPlugin.swift 3 | // GetStream 4 | // 5 | // Created by Alexey Bukhtin on 08/11/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Moya 11 | 12 | final class AuthorizationMoyaPlugin: PluginType { 13 | private let serialQueue = DispatchQueue(label: "com.getstream.io.AuthorizationMoyaPlugin") 14 | private var token: Token 15 | 16 | init(_ token: Token = "") { 17 | self.token = token 18 | } 19 | 20 | func prepare(_ request: URLRequest, target: TargetType) -> URLRequest { 21 | let token = serialQueue.sync { self.token } 22 | 23 | if token.isEmpty { 24 | return request 25 | } 26 | 27 | var request = request 28 | request.addValue("jwt", forHTTPHeaderField: "Stream-Auth-Type") 29 | request.addValue(token, forHTTPHeaderField: "Authorization") 30 | 31 | return request 32 | } 33 | 34 | func updateToken(_ token: Token) { 35 | serialQueue.async { 36 | self.token = token 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Core/Moya/MoyaError+ClientError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoyaError+ClientError.swift 3 | // GetStream 4 | // 5 | // Created by Alexey Bukhtin on 09/11/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Moya 11 | 12 | // MARK: - Moya Client Error 13 | 14 | extension MoyaError { 15 | var clientError: ClientError { 16 | return .network(errorDescription ?? "Unknown", self) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Core/Open Graph/OpenGraphEndpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenGraphEndpoint.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 11/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Moya 11 | 12 | enum OpenGraphEndpoint { 13 | case og(_ ulr: URL) 14 | } 15 | 16 | extension OpenGraphEndpoint: StreamTargetType { 17 | 18 | var path: String { 19 | return "og/" 20 | } 21 | 22 | var method: Moya.Method { 23 | return .get 24 | } 25 | 26 | var task: Task { 27 | switch self { 28 | case .og(let url): 29 | return .requestParameters(parameters: ["url": url], encoding: URLEncoding.default) 30 | } 31 | } 32 | 33 | var sampleJSON: String { 34 | return """ 35 | {"duration":"455.24ms","title":"The Imitation Game (2014)","type":"video.movie","url":"http://www.imdb.com/title/tt2084970/","site_name":"IMDb","description":"Directed by Morten Tyldum. With Benedict Cumberbatch, Keira Knightley, Matthew Goode, Allen Leech. During World War II, the English mathematical genius Alan Turing tries to crack the German Enigma code with help from fellow mathematicians.","images":[{"image":"https://m.media-amazon.com/images/M/MV5BOTgwMzFiMWYtZDhlNS00ODNkLWJiODAtZDVhNzgyNzJhYjQ4L2ltYWdlXkEyXkFqcGdeQXVyNzEzOTYxNTQ@._V1_UY1200_CR87,0,630,1200_AL_.jpg"}]} 36 | """ 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Core/Reactions/Reaction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Reaction.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 12/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A default reaction type with `EmptyReactionExtraData` and `User` types. 12 | public typealias DefaultReaction = Reaction 13 | 14 | /// A reaction type. 15 | public final class Reaction: ReactionProtocol { 16 | private enum CodingKeys: String, CodingKey { 17 | case id 18 | case activityId = "activity_id" 19 | case safeUser = "user" 20 | case kind 21 | case created = "created_at" 22 | case updated = "updated_at" 23 | case data 24 | case parentId = "parent" 25 | case userOwnChildren = "own_children" 26 | case latestChildren = "latest_children" 27 | case childrenCounts = "children_counts" 28 | } 29 | 30 | /// Reaction id. 31 | public let id: String 32 | /// Activity id for the reaction. 33 | public let activityId: String 34 | /// A wrapper for the user of the reaction. 35 | public let safeUser: MissingReference 36 | /// User of the reaction. 37 | public var user: U { 38 | return safeUser.value 39 | } 40 | /// Type of reaction. 41 | public let kind: ReactionKind 42 | /// When the reaction was created. 43 | public let created: Date 44 | /// When the reaction was last updated. 45 | public let updated: Date? 46 | /// An extra data for the reaction. 47 | public let data: T 48 | /// Id of the parent reaction. Empty unless the reaction is a child reaction. 49 | public let parentId: String? 50 | /// User own children reactions, grouped by reaction type. 51 | public var userOwnChildren: [ReactionKind: [Reaction]]? 52 | /// Children reactions, grouped by reaction type. 53 | public var latestChildren: [ReactionKind: [Reaction]] 54 | /// Child reaction count, grouped by reaction kind 55 | public var childrenCounts: [ReactionKind: Int] 56 | 57 | public init(from decoder: Decoder) throws { 58 | let container = try decoder.container(keyedBy: CodingKeys.self) 59 | id = try container.decode(String.self, forKey: .id) 60 | activityId = try container.decode(String.self, forKey: .activityId) 61 | safeUser = try container.decodeIfPresent(MissingReference.self, forKey: .safeUser) ?? MissingReference.missed() 62 | kind = try container.decode(String.self, forKey: .kind) 63 | created = try container.decode(Date.self, forKey: .created) 64 | updated = try container.decode(Date.self, forKey: .updated) 65 | data = try container.decode(T.self, forKey: .data) 66 | parentId = try container.decodeIfPresent(String.self, forKey: .parentId) 67 | userOwnChildren = try container.decodeIfPresent([ReactionKind: [Reaction]].self, forKey: .userOwnChildren) 68 | latestChildren = try container.decode([ReactionKind: [Reaction]].self, forKey: .latestChildren) 69 | childrenCounts = try container.decode([ReactionKind: Int].self, forKey: .childrenCounts) 70 | } 71 | 72 | /// Skip encoding. 73 | public func encode(to encoder: Encoder) throws {} 74 | 75 | /// Equatable. 76 | public static func == (lhs: Reaction, rhs: Reaction) -> Bool { 77 | return lhs === rhs || (!lhs.id.isEmpty && lhs.id == rhs.id) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/Core/Reactions/ReactionExtraDataProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReactionExtraDataProtocol.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 12/02/2019. 6 | // Copyright © 2019 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A reaction extra data protocol. 12 | public typealias ReactionExtraDataProtocol = Codable 13 | 14 | // MARK: - Empty reaction extra data 15 | 16 | /// A default empty type of `ReactionExtraDataProtocol`. 17 | public struct EmptyReactionExtraData: ReactionExtraDataProtocol, Equatable { 18 | /// Shared empty reaction extra data. 19 | public static let shared = EmptyReactionExtraData() 20 | } 21 | 22 | /// MARK: - Comment 23 | 24 | /// Comment reaction extra data. 25 | public struct Comment: ReactionExtraDataProtocol { 26 | public let text: String 27 | } 28 | 29 | // MARK: - Like/Repost/Comment 30 | 31 | /// Combine Likes/Reposts and Comment reaction extra data. 32 | public enum ReactionExtraData: ReactionExtraDataProtocol { 33 | case empty 34 | case comment(_ text: String) 35 | 36 | public func encode(to encoder: Encoder) throws { 37 | switch self { 38 | case .empty: 39 | try EmptyReactionExtraData.shared.encode(to: encoder) 40 | case .comment(let comment): 41 | try Comment(text: comment).encode(to: encoder) 42 | } 43 | } 44 | 45 | public init(from decoder: Decoder) throws { 46 | if let comment = try? Comment(from: decoder) { 47 | self = .comment(comment.text) 48 | } else { 49 | self = .empty 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Core/Reactions/ReactionKind.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReactionKind.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 12/02/2019. 6 | // Copyright © 2019 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A reaction kind type. 12 | public typealias ReactionKind = String 13 | 14 | // MARK: - Common Reaction Kinds 15 | 16 | extension ReactionKind { 17 | /// A shared Like Reaction type. 18 | public static let like: ReactionKind = "like" 19 | /// A shared Comment Reaction type. 20 | public static let comment: ReactionKind = "comment" 21 | /// A shared Repost Reaction type. 22 | public static let repost: ReactionKind = "repost" 23 | } 24 | 25 | // MARK: - Helper for Common Reaction Kinds 26 | 27 | extension ActivityProtocol where ReactionType: ReactionProtocol { 28 | 29 | // MARK: - Likes 30 | 31 | /// True if the current user like the activity. See `ReactionKind.like`. 32 | public var isUserLiked: Bool { 33 | return hasUserOwnReaction(.like) 34 | } 35 | 36 | /// A number of likes. See `ReactionKind.like`. 37 | public var likesCount: Int { 38 | return reactionCounts?[.like] ?? 0 39 | } 40 | 41 | /// A like reaction of the current user. See `ReactionKind.like`. 42 | public var userLikedReaction: ReactionType? { 43 | return userOwnReaction(.like) 44 | } 45 | 46 | // MARK: - Reposts 47 | 48 | /// True if the current user repost the activity. See `ReactionKind.repost`. 49 | public var isUserReposted: Bool { 50 | return hasUserOwnReaction(.repost) 51 | } 52 | 53 | /// A number of reposts. See `ReactionKind.repost`. 54 | public var repostsCount: Int { 55 | return reactionCounts?[.repost] ?? 0 56 | } 57 | 58 | /// A repost reaction of the current user. See `ReactionKind.repost`. 59 | public var userRepostReaction: ReactionType? { 60 | return userOwnReaction(.repost) 61 | } 62 | 63 | // MARK: - Comments 64 | 65 | /// A number of comments. See `ReactionKind.comment`. 66 | public var commentsCount: Int { 67 | return reactionCounts?[.comment] ?? 0 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/Core/Reactions/ReactionProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReactionProtocol.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 28/02/2019. 6 | // Copyright © 2019 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A reaction protocol. 12 | public protocol ReactionProtocol: Codable, Equatable { 13 | associatedtype ExtraDataType = ReactionExtraDataProtocol 14 | associatedtype UserType = UserProtocol 15 | 16 | /// Reaction id. 17 | var id: String { get } 18 | /// A User of the reaction. 19 | var user: UserType { get } 20 | /// Type of reaction. 21 | var kind: ReactionKind { get } 22 | /// An extra data for the reaction. 23 | var data: ExtraDataType { get } 24 | /// User own children reactions, grouped by reaction type. 25 | var userOwnChildren: [ReactionKind: [Self]]? { get set } 26 | /// Children reactions, grouped by reaction type. 27 | var latestChildren: [ReactionKind: [Self]] { get set } 28 | /// Child reaction count, grouped by reaction kind 29 | var childrenCounts: [ReactionKind: Int] { get set } 30 | } 31 | 32 | // MARK: - User own child reactions 33 | 34 | extension ReactionProtocol { 35 | 36 | /// Check if the user has own child reactions for the reaction with a given reaction kind. 37 | /// 38 | /// - Parameter reactionKind: a kind of the child reaction. 39 | /// - Returns: true if exists the child reaction of the user. 40 | public func hasUserOwnChildReaction(_ reactionKind: ReactionKind) -> Bool { 41 | return userOwnChildReactionsCount(reactionKind) > 0 42 | } 43 | 44 | /// The number of user own child reactions with a given reaction kind. 45 | /// 46 | /// - Parameter reactionKind: a kind of the child reaction. 47 | /// - Returns: the number of user own child reactions. 48 | public func userOwnChildReactionsCount(_ reactionKind: ReactionKind) -> Int { 49 | return userOwnChildren?[reactionKind]?.count ?? 0 50 | } 51 | 52 | /// Try to get the first user own child reaction. 53 | /// 54 | /// - Parameter reactionKind: a kind of the child reaction. 55 | /// - Returns: the user child reaction. 56 | public func userOwnChildReaction(_ reactionKind: ReactionKind) -> Self? { 57 | return userOwnChildren?[reactionKind]?.first 58 | } 59 | } 60 | 61 | // MARK: - Managing reactions 62 | 63 | extension ReactionProtocol { 64 | 65 | /// Update the reaction with a new user own child reaction. 66 | /// 67 | /// - Parameter reaction: a new user own reaction. 68 | public mutating func addUserOwnChild(_ reaction: Self) { 69 | var userOwnChildren = self.userOwnChildren ?? [:] 70 | var latestChildren = self.latestChildren 71 | var childrenCounts = self.childrenCounts 72 | userOwnChildren[reaction.kind, default: []].insert(reaction, at: 0) 73 | latestChildren[reaction.kind, default: []].insert(reaction, at: 0) 74 | childrenCounts[reaction.kind, default: 0] += 1 75 | self.userOwnChildren = userOwnChildren 76 | self.latestChildren = latestChildren 77 | self.childrenCounts = childrenCounts 78 | } 79 | 80 | /// Delete an existing user own child reaction for the reaction. 81 | /// 82 | /// - Parameter reaction: an existing user own reaction. 83 | public mutating func removeUserOwnChild(_ reaction: Self) { 84 | var userOwnChildren = self.userOwnChildren ?? [:] 85 | var latestChildren = self.latestChildren 86 | var childrenCounts = self.childrenCounts 87 | 88 | if let firstIndex = userOwnChildren[reaction.kind]?.firstIndex(of: reaction) { 89 | userOwnChildren[reaction.kind, default: []].remove(at: firstIndex) 90 | self.userOwnChildren = userOwnChildren 91 | 92 | if let firstIndex = latestChildren[reaction.kind]?.firstIndex(of: reaction) { 93 | latestChildren[reaction.kind, default: []].remove(at: firstIndex) 94 | self.latestChildren = latestChildren 95 | } 96 | 97 | if let count = childrenCounts[reaction.kind], count > 0 { 98 | childrenCounts[reaction.kind, default: 0] = count - 1 99 | self.childrenCounts = childrenCounts 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/Core/Reactions/Reactionable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Reactionable.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 12/02/2019. 6 | // Copyright © 2019 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A reactionable protocol. 12 | public protocol Reactionable { 13 | associatedtype ReactionType = ReactionProtocol 14 | 15 | /// Include reactions added by current user to all activities. 16 | var userOwnReactions: [ReactionKind: [ReactionType]]? { get set } 17 | /// Include recent reactions to activities. 18 | var latestReactions: [ReactionKind: [ReactionType]]? { get set } 19 | /// Include reaction counts to activities. 20 | var reactionCounts: [ReactionKind: Int]? { get set } 21 | } 22 | 23 | // MARK: - Access 24 | 25 | extension Reactionable where ReactionType: ReactionProtocol { 26 | 27 | /// Check user reactions with a given reaction kind. 28 | /// 29 | /// - Parameter reactionKind: a kind of the reaction. 30 | /// - Returns: true if exists the reaction of the user. 31 | public func hasUserOwnReaction(_ reactionKind: ReactionKind) -> Bool { 32 | return userOwnReactionsCount(reactionKind) > 0 33 | } 34 | 35 | /// The number of user reactions with a given reaction kind. 36 | /// 37 | /// - Parameter reactionKind: a kind of the reaction. 38 | /// - Returns: the number of user reactions. 39 | public func userOwnReactionsCount(_ reactionKind: ReactionKind) -> Int { 40 | return userOwnReactions?[reactionKind]?.count ?? 0 41 | } 42 | 43 | /// Try to get the first user reaction with a given reaction kind. 44 | /// 45 | /// - Parameter reactionKind: a kind of the reaction. 46 | /// - Returns: the user reaction. 47 | public func userOwnReaction(_ reactionKind: ReactionKind) -> ReactionType? { 48 | return userOwnReactions?[reactionKind]?.first 49 | } 50 | } 51 | 52 | // MARK: - Managing 53 | 54 | extension Reactionable where ReactionType: ReactionProtocol { 55 | 56 | /// Update the activity with a new user own reaction. 57 | /// 58 | /// - Parameter reaction: a new user own reaction. 59 | public mutating func addUserOwnReaction(_ reaction: ReactionType) { 60 | var userOwnReactions = self.userOwnReactions ?? [:] 61 | var latestReactions = self.latestReactions ?? [:] 62 | var reactionCounts = self.reactionCounts ?? [:] 63 | userOwnReactions[reaction.kind, default: []].insert(reaction, at: 0) 64 | latestReactions[reaction.kind, default: []].insert(reaction, at: 0) 65 | reactionCounts[reaction.kind, default: 0] += 1 66 | self.userOwnReactions = userOwnReactions 67 | self.latestReactions = latestReactions 68 | self.reactionCounts = reactionCounts 69 | } 70 | 71 | /// Remove an existing own reaction for the activity. 72 | /// 73 | /// - Parameter reaction: an existing user own reaction. 74 | public mutating func removeUserOwnReaction(_ reaction: ReactionType) { 75 | var userOwnReactions = self.userOwnReactions ?? [:] 76 | var latestReactions = self.latestReactions ?? [:] 77 | var reactionCounts = self.reactionCounts ?? [:] 78 | 79 | if let firstIndex = userOwnReactions[reaction.kind]?.firstIndex(where: { $0.id == reaction.id }) { 80 | userOwnReactions[reaction.kind, default: []].remove(at: firstIndex) 81 | self.userOwnReactions = userOwnReactions 82 | 83 | if let firstIndex = latestReactions[reaction.kind]?.firstIndex(where: { $0.id == reaction.id }) { 84 | latestReactions[reaction.kind, default: []].remove(at: firstIndex) 85 | self.latestReactions = latestReactions 86 | } 87 | 88 | if let count = reactionCounts[reaction.kind], count > 0 { 89 | reactionCounts[reaction.kind, default: 0] = count - 1 90 | self.reactionCounts = reactionCounts 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Sources/Core/Reactions/Reactions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Reactions.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 14/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A reactions error. 12 | public enum ReactionsError: LocalizedError, CustomStringConvertible { 13 | case reactionsHaveNoActivity 14 | case enrichingActivityError(_ error: EnrichingActivityError) 15 | 16 | public var description: String { 17 | switch self { 18 | case .reactionsHaveNoActivity: 19 | return "Reactions have not an activity" 20 | case .enrichingActivityError(let error): 21 | return "Enriching activity error: \(error.localizedDescription)" 22 | } 23 | } 24 | 25 | public var localizedDescription: String { 26 | return description 27 | } 28 | 29 | public var errorDescription: String? { 30 | return description 31 | } 32 | } 33 | 34 | /// A reactions type. 35 | public struct Reactions: Decodable { 36 | private enum CodingKeys: String, CodingKey { 37 | case reactions = "results" 38 | case next 39 | } 40 | 41 | private enum ActivityCodingKeys: String, CodingKey { 42 | case activity 43 | } 44 | 45 | /// A list of reactions. 46 | public let reactions: [Reaction] 47 | /// A pagination option for the next page. 48 | public private(set) var next: Pagination? 49 | private var activityContainer: KeyedDecodingContainer.ActivityCodingKeys>? 50 | 51 | public init(from decoder: Decoder) throws { 52 | let container = try decoder.container(keyedBy: CodingKeys.self) 53 | reactions = try container.decode([Reaction].self, forKey: .reactions) 54 | next = try container.decodeIfPresent(Pagination.self, forKey: .next) 55 | 56 | if let next = next, case .none = next { 57 | self.next = nil 58 | } 59 | 60 | activityContainer = try decoder.container(keyedBy: ActivityCodingKeys.self) 61 | } 62 | 63 | /// Get an activity for reactions that was requested by `activityId` and the `withActivityData` property. 64 | /// 65 | /// - Parameter type: the type of `ActivityProtocol` of reactions. 66 | /// - Returns: the activity of reactions. 67 | public func activity(typeOf type: A.Type) throws -> A { 68 | guard let activityContainer = activityContainer else { 69 | throw ReactionsError.reactionsHaveNoActivity 70 | } 71 | 72 | return try activityContainer.decode(type, forKey: .activity) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/Core/Reactions/Result+ParseReaction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result+ParseReaction.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 13/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Moya 11 | 12 | /// A reaction completion block. 13 | public typealias ReactionCompletion = (_ result: Result, ClientError>) -> Void 15 | 16 | /// A reactions completion block. 17 | public typealias ReactionsCompletion = (_ result: Result, ClientError>) -> Void 19 | 20 | /// A default reaction completion block. 21 | public typealias DefaultReactionCompletion = ReactionCompletion 22 | /// A default reactions completion block. 23 | public typealias DefaultReactionsCompletion = ReactionsCompletion 24 | 25 | // MARK: - Result Reactions Parsing 26 | 27 | extension Result where Success == Moya.Response, Failure == ClientError { 28 | 29 | /// Parse the result with a given reaction completion block. 30 | func parseReaction(_ callbackQueue: DispatchQueue, 31 | _ completion: @escaping ReactionCompletion) { 32 | parse(block: { 33 | let response = try get() 34 | let reaction = try JSONDecoder.default.decode(Reaction.self, from: response.data) 35 | callbackQueue.async { completion(.success(reaction)) } 36 | }, catch: { error in 37 | callbackQueue.async { completion(.failure(error)) } 38 | }) 39 | } 40 | 41 | /// Parse the result with a given reaction completion block. 42 | func parseReactions(_ callbackQueue: DispatchQueue, 43 | _ completion: @escaping ReactionsCompletion) { 44 | parse(block: { 45 | let response = try get() 46 | let reactions = try JSONDecoder.default.decode(Reactions.self, from: response.data) 47 | callbackQueue.async { completion(.success(reactions)) } 48 | }, catch: { error in 49 | callbackQueue.async { completion(.failure(error)) } 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Core/User/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 18/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An User class with basic properties of `UserProtocol`. 12 | /// You can inherit this class with extra properties on your own User type. 13 | /// - Note: Please, check the `UserProtocol` documentation to implement your User subclass properly. 14 | open class User: UserProtocol { 15 | private enum UserCodingKeys: String, CodingKey { 16 | case id 17 | case created = "created_at" 18 | case updated = "updated_at" 19 | case followersCount = "followers_count" 20 | case followingCount = "following_count" 21 | } 22 | 23 | /// Coding keys for extra user properties. 24 | public enum DataCodingKeys: String, CodingKey { 25 | case data 26 | } 27 | 28 | /// A user id. 29 | public let id: String 30 | /// An user created date. 31 | public var created: Date = Date() 32 | /// An user updated date. 33 | public var updated: Date = Date() 34 | /// A number of followers. 35 | public var followersCount: Int? 36 | /// A number of followings. 37 | public var followingCount: Int? 38 | 39 | /// Create a user with a given id. 40 | /// 41 | /// - Parameter id: a user id. 42 | required public init(id: String) { 43 | self.id = id 44 | } 45 | 46 | required public init(from decoder: Decoder) throws { 47 | let container = try decoder.container(keyedBy: UserCodingKeys.self) 48 | id = try container.decode(String.self, forKey: .id) 49 | created = try container.decode(Date.self, forKey: .created) 50 | updated = try container.decode(Date.self, forKey: .updated) 51 | followersCount = try container.decodeIfPresent(Int.self, forKey: .followersCount) 52 | followingCount = try container.decodeIfPresent(Int.self, forKey: .followingCount) 53 | } 54 | 55 | open func encode(to encoder: Encoder) throws { 56 | var container = encoder.container(keyedBy: UserCodingKeys.self) 57 | try container.encode(id, forKey: .id) 58 | } 59 | 60 | public static func missed() -> Self { 61 | return .init(id: "!missed_reference") 62 | } 63 | 64 | public var isMissedReference: Bool { 65 | return id == "!missed_reference" 66 | } 67 | } 68 | 69 | extension User: Equatable { 70 | public static func ==(lhs: User, rhs: User) -> Bool { 71 | return lhs === rhs || (!lhs.id.isEmpty && lhs.id == rhs.id) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/Core/User/UserEndpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserEndpoint.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 14/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Moya 11 | 12 | enum UserEndpoint { 13 | case create(_ user: UserProtocol, _ getOrCreate: Bool) 14 | case get(_ userId: String, _ withFollowCounts: Bool) 15 | case update(_ user: UserProtocol) 16 | case delete(_ userId: String) 17 | } 18 | 19 | extension UserEndpoint: StreamTargetType { 20 | 21 | var path: String { 22 | switch self { 23 | case .create: 24 | return "user/" 25 | case .get(let id, _), .delete(let id): 26 | return "user/\(id)/" 27 | case .update(let user): 28 | return "user/\(user.id)/" 29 | } 30 | } 31 | 32 | var method: Moya.Method { 33 | switch self { 34 | case .create: 35 | return .post 36 | case .get: 37 | return .get 38 | case .update: 39 | return .put 40 | case .delete: 41 | return .delete 42 | } 43 | } 44 | 45 | var task: Task { 46 | switch self { 47 | case let .create(user, getOrCreate): 48 | return .requestJSONEncodable(user, urlParameters: ["get_or_create": getOrCreate]) 49 | 50 | case .get(_, let withFollowCounts): 51 | return .requestParameters(parameters: ["with_follow_counts": withFollowCounts], encoding: URLEncoding.default) 52 | 53 | case .update(let user): 54 | return .requestJSONEncodable(user) 55 | 56 | case .delete: 57 | return .requestPlain 58 | } 59 | } 60 | 61 | var sampleJSON: String { 62 | switch self { 63 | case .create, .get: 64 | return """ 65 | {"created_at":"2018-12-20T15:41:25.181144Z","updated_at":"2018-12-20T15:41:25.181144Z","id":"eric","data":{"name":"Eric"},"duration":"2.10ms"} 66 | """ 67 | 68 | case .update: 69 | return """ 70 | {"created_at":"2018-12-20T15:41:25.181144Z","updated_at":"2018-12-20T15:41:25.181144Z","id":"eric","data":{"name":"Eric Updated"},"duration":"2.10ms"} 71 | """ 72 | 73 | case .delete: 74 | return "{}" 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/Core/User/UserProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserProtocol.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 14/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A user protocol. 12 | /// 13 | /// This protocol describe basic properties. You can extend them with own type, 14 | /// but you have to implement `Encodable` and `Decodable` protocols in a specific way: 15 | /// - the protocol properties present on the root level of the user structure, 16 | /// - additinal properties should be encoded/decoded in the nested `data` container. 17 | /// 18 | /// Here is an example of a JSON responce: 19 | /// ``` 20 | /// { 21 | /// "id": "alice123", 22 | /// "data": { "name": "Alice" }, 23 | /// "created_at": "2018-12-17T15:23:26.591179Z", 24 | /// "updated_at": "2018-12-17T15:23:26.591179Z", 25 | /// "duration":"0.45ms" 26 | /// } 27 | /// ``` 28 | /// 29 | /// You can extend our opened `User` class for the default protocol properties. 30 | /// 31 | /// Example with custom properties: 32 | /// ``` 33 | /// final class User: GetStream.User { 34 | /// private enum CodingKeys: String, CodingKey { 35 | /// case name 36 | /// } 37 | /// 38 | /// var name: String 39 | /// 40 | /// init(id: String, name: String) { 41 | /// self.name = name 42 | /// super.init(id: id) 43 | /// } 44 | /// 45 | /// required init(from decoder: Decoder) throws { 46 | /// let dataContainer = try decoder.container(keyedBy: DataCodingKeys.self) 47 | /// let container = try dataContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: .data) 48 | /// name = try container.decode(String.self, forKey: .name) 49 | /// try super.init(from: decoder) 50 | /// } 51 | /// 52 | /// override func encode(to encoder: Encoder) throws { 53 | /// var dataContainer = encoder.container(keyedBy: DataCodingKeys.self) 54 | /// var container = dataContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: .data) 55 | /// try container.encode(name, forKey: .name) 56 | /// try super.encode(to: encoder) 57 | /// } 58 | /// } 59 | /// ``` 60 | /// 61 | /// Here is an example how to use a custom User type: 62 | /// ``` 63 | /// let user = User(id: "alice123", name: "Alice") 64 | /// client.create(user: user) { 65 | /// // Let's try retrieve details of the created user and use custom properties. 66 | /// client.get(typeOf: User.self, userId: "alice123") { 67 | /// let user = try? $0.get() // here the user is a custom User type. 68 | /// print(user?.name) // it will print "Alice". 69 | /// } 70 | /// } 71 | /// ``` 72 | public protocol UserProtocol: Enrichable { 73 | /// A user Id. Must not be empty or longer than 255 characters. 74 | var id: String { get } 75 | /// When the user was created. 76 | var created: Date { get } 77 | /// When the user was last updated. 78 | var updated: Date { get } 79 | /// Number of users that follow this user. 80 | var followersCount: Int? { get } 81 | /// Number of users this user is following. 82 | var followingCount: Int? { get } 83 | } 84 | 85 | // MARK: - Enrichable 86 | 87 | extension UserProtocol { 88 | /// A referenceId for the enrichability. 89 | public var referenceId: String { 90 | return "SU:\(id)" 91 | } 92 | } 93 | 94 | // MARK: - Shared User 95 | 96 | extension UserProtocol { 97 | public static var current: Self? { 98 | return Client.shared.currentUser as? Self 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/Faye/Client+Faye.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Client+Faye.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 30/11/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Faye 11 | 12 | extension Client { 13 | /// Setup a Faye client. 14 | static var fayeClient: Faye.Client = { 15 | Faye.Client.config = .init(url: URL(string: "wss://faye.getstream.io/faye")!) 16 | return .shared 17 | }() 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Faye/Feed+Faye.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Feed+Faye.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 18/02/2019. 6 | // Copyright © 2019 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Faye 11 | 12 | public typealias Subscription = (_ result: Result, SubscriptionError>) -> Void 13 | 14 | public enum SubscriptionError: Error { 15 | case fayeClient(_ error: Faye.Client.Error) 16 | case decoding(_ error: DecodingError) 17 | case unexpected(_ error: Error) 18 | } 19 | 20 | extension Feed { 21 | 22 | /// Subscribe for the updates of the given activity type of `ActivityProtocol`. 23 | /// 24 | /// - Parameters: 25 | /// - type: an `ActivityProtocol` of activities. 26 | /// - decoder: a custom decoder for the given activity type. 27 | /// - subscription: a subscription block with changes. 28 | /// It will retrun a `Result` with `SubscriptionResponse` or `DecodingError`. 29 | /// 30 | /// - Returns: a `SubscribedChannel` keep the subscription util it will be deinit. 31 | /// Store the object in a variable for the getting updates and then set it to nil to unsubscribe. 32 | public func subscribe(typeOf type: T.Type, 33 | decoder: JSONDecoder = .default, 34 | subscription: @escaping Subscription) -> SubscribedChannel { 35 | let channel = Channel(notificationChannelName, client: Client.fayeClient) { [weak self] data in 36 | guard let self = self else { 37 | return 38 | } 39 | 40 | do { 41 | var response = try decoder.decode(SubscriptionResponse.self, from: data) 42 | response.feed = self 43 | self.callbackQueue.async { subscription(.success(response)) } 44 | 45 | } catch let error as DecodingError { 46 | print("❌", #function, error) 47 | self.callbackQueue.async { subscription(.failure(.decoding(error))) } 48 | 49 | } catch { 50 | print("❌", #function, error) 51 | self.callbackQueue.async { subscription(.failure(.unexpected(error))) } 52 | } 53 | } 54 | 55 | channel.ext = ["api_key": Client.shared.apiKey, 56 | "signature": Client.shared.token, 57 | "user_id": notificationChannelName] 58 | 59 | do { 60 | try Client.fayeClient.subscribe(to: channel) 61 | 62 | } catch let error as Faye.Client.Error { 63 | if case .notConnected = error { 64 | Client.fayeClient.connect() 65 | } else { 66 | print("❌", #function, error) 67 | callbackQueue.async { subscription(.failure(.fayeClient(error))) } 68 | } 69 | 70 | } catch { 71 | print("❌", #function, error) 72 | callbackQueue.async { subscription(.failure(.unexpected(error))) } 73 | } 74 | 75 | return SubscribedChannel(channel) 76 | } 77 | 78 | /// A notification channel name. 79 | var notificationChannelName: ChannelName { 80 | return "site-\(Client.shared.appId)-feed-\(feedId.together)" 81 | } 82 | } 83 | 84 | // MARK: - Subscribed Channel 85 | 86 | /// A subscribed channel holder. 87 | public final class SubscribedChannel { 88 | private let channel: Channel 89 | 90 | public init(_ channel: Channel) { 91 | self.channel = channel 92 | } 93 | 94 | deinit { 95 | channel.unsubscribe() 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Sources/Faye/SubscriptionResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SubscriptionResponse.swift 3 | // GetStream-iOS 4 | // 5 | // Created by Alexey Bukhtin on 18/02/2019. 6 | // Copyright © 2019 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A responce object of changes from a subscription. 12 | public struct SubscriptionResponse: Decodable { 13 | 14 | private enum CodingKeys: String, CodingKey { 15 | case feedId = "feed" 16 | case deletedActivitiesIds = "deleted" 17 | case newActivities = "new" 18 | } 19 | 20 | /// A feed of the subscription. 21 | public var feed: Feed? 22 | 23 | /// A `FeedId` of changes. 24 | public let feedId: FeedId 25 | 26 | /// A list of deleted activities ids. 27 | public let deletedActivitiesIds: [String] 28 | 29 | /// A list of new activities. 30 | public let newActivities: [T] 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Token/Token+Generator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Token+Generator.swift 3 | // GetStream 4 | // 5 | // Created by Alexey Bukhtin on 12/11/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import JWT 11 | 12 | fileprivate typealias Claims = [String: String] 13 | 14 | extension Token { 15 | /// Generate the Stream token with a given secret and claims. 16 | /// 17 | /// - Parameters: 18 | /// - secretData: a secret data. 19 | /// - resource: a resource string, e.g. feed 20 | /// - permission: a permissionm e.g. read or write 21 | /// - feedId: a `FeedId` or any as by default. 22 | /// - userId: a `userId`. 23 | public init(secretData: Data, 24 | resource: Resource? = nil, 25 | permission: Permission? = nil, 26 | feedId: FeedId? = nil, 27 | userId: String? = nil) { 28 | let claims: Claims 29 | 30 | if resource == nil, permission == nil, feedId == nil, userId == nil { 31 | claims = Token.claims(resource: .all, 32 | permission: .all, 33 | feedId: .any, 34 | userId: nil) 35 | } else { 36 | claims = Token.claims(resource: resource, 37 | permission: permission, 38 | feedId: feedId, 39 | userId: userId) 40 | } 41 | 42 | self = Token.jwt(secretData: secretData, claims: claims) 43 | } 44 | 45 | private static func jwt(secretData: Data, claims: Claims) -> Token { 46 | return JWT.encode(claims: claims, algorithm: .hs256(secretData)) 47 | } 48 | 49 | private static func claims(resource: Resource?, permission: Permission?, feedId: FeedId?, userId: String?) -> Claims { 50 | var claims: Claims = [:] 51 | 52 | if let resource = resource { 53 | claims["resource"] = resource.rawValue 54 | } 55 | 56 | if let permission = permission { 57 | claims["action"] = permission.rawValue 58 | } 59 | 60 | if let feedId = feedId { 61 | claims["feed_id"] = feedId.togetherWithColon 62 | } 63 | 64 | if let userId = userId { 65 | claims["user_id"] = userId 66 | } 67 | 68 | return claims 69 | } 70 | } 71 | 72 | extension Token { 73 | public enum Resource: String { 74 | /// Allow access to any resource. 75 | case all = "*" 76 | /// Activities Endpoint. 77 | case activities 78 | /// Feed Endpoint. 79 | case feed 80 | /// Following + Followers Endpoint. 81 | case follower 82 | /// Users Endpoint. 83 | case users 84 | } 85 | } 86 | 87 | extension Token { 88 | public enum Permission: String { 89 | case all = "*" 90 | case read 91 | case write 92 | case delete 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Tests/Core/AggregatedFeedTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AggregatedFeedTests.swift 3 | // GetStream-iOS Tests 4 | // 5 | // Created by Alexey Bukhtin on 24/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GetStream 11 | 12 | class AggregatedFeedTests: TestCase { 13 | lazy var aggregated = Client.shared.aggregatedFeed(feedSlug: "aggregated") 14 | 15 | func testAggregated() { 16 | XCTAssertNotNil(aggregated) 17 | XCTAssertEqual(aggregated!.feedId, Client.shared.aggregatedFeed(feedSlug: "aggregated", userId: "eric").feedId) 18 | 19 | expect("get aggregated") { test in 20 | aggregated!.get(typeOf: SimpleActivity.self) { result in 21 | if let groups = try? result.get() { 22 | XCTAssertEqual(groups.results.count, 2) 23 | XCTAssertEqual(groups.results.first!.verb, "verb") 24 | XCTAssertEqual(groups.results.first!.activitiesCount, 2) 25 | XCTAssertEqual(groups.results.first!.activities.count, 2) 26 | XCTAssertEqual(groups.results.first!.actorsCount, 1) 27 | XCTAssertTrue(groups.results.first!.group.hasPrefix("verb_")) 28 | XCTAssertEqual(groups.results.first!.activities.first!.actor, "Me") 29 | XCTAssertEqual(groups.results.first!.activities.first!.verb, "verb") 30 | XCTAssertEqual(groups.results.first!.activities.first!.object, "Message") 31 | } else { 32 | XCTFail("Bad aggregated feed result: \(result)") 33 | } 34 | 35 | test.fulfill() 36 | } 37 | } 38 | } 39 | 40 | func testBadJSON() { 41 | let aggregated = Client.shared.aggregatedFeed(feedSlug: "bad") 42 | 43 | expect("get bad aggregated") { test in 44 | aggregated!.get { result in 45 | if case .failure(let clientError) = result, case .network = clientError { 46 | test.fulfill() 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/Core/ClientParsingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClientParsingTests.swift 3 | // GetStream-iOS Tests 4 | // 5 | // Created by Alexey Bukhtin on 21/11/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Moya 11 | @testable import GetStream 12 | 13 | class ClientParsingTests: TestCase { 14 | let queue = DispatchQueue.main 15 | 16 | func testResultsErrors() { 17 | let response = Response(statusCode: 200, data: Data()) 18 | let responseResult: ClientCompletionResult = .success(response) 19 | 20 | expect("error json decode") { test in 21 | let completion: ActivityCompletion = { result in 22 | if case .failure(let clientError) = result, case .jsonDecode = clientError { 23 | test.fulfill() 24 | } 25 | } 26 | 27 | responseResult.parse(queue, completion) 28 | } 29 | 30 | expect("error result") { test in 31 | let completion: ActivitiesCompletion = { result in 32 | if case .failure(let clientError) = result, case .unknownError = clientError { 33 | test.fulfill() 34 | } 35 | } 36 | 37 | ClientCompletionResult.failure(ClientError.unknownError("", nil)).parse(queue, completion) 38 | } 39 | } 40 | 41 | func testRemovedErrors() { 42 | let response = Response(statusCode: 200, data: Data()) 43 | let responseResult: ClientCompletionResult = .success(response) 44 | 45 | expect("error json decode") { test in 46 | responseResult.parseRemoved(queue) { result in 47 | if case .failure(let clientError) = result, case .jsonDecode = clientError { 48 | test.fulfill() 49 | } 50 | } 51 | } 52 | 53 | expect("error result") { test in 54 | ClientCompletionResult.failure(ClientError.unknownError("", nil)).parseRemoved(queue) { result in 55 | if case .failure(let clientError) = result, case .unknownError = clientError { 56 | test.fulfill() 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/Core/CollectionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionTests.swift 3 | // GetStream-iOS Tests 4 | // 5 | // Created by Alexey Bukhtin on 24/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GetStream 11 | 12 | final class Food: CollectionObject { 13 | private enum CodingKeys: String, CodingKey { 14 | case name 15 | } 16 | 17 | var name: String 18 | 19 | init(name: String, id: String? = nil) { 20 | self.name = name 21 | super.init(collectionName: "food", id: id) 22 | } 23 | 24 | required init(from decoder: Decoder) throws { 25 | let dataContainer = try decoder.container(keyedBy: DataCodingKeys.self) 26 | let container = try dataContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: .data) 27 | name = try container.decode(String.self, forKey: .name) 28 | try super.init(from: decoder) 29 | } 30 | 31 | required init(collectionName: String, id: String? = nil) { 32 | name = "" 33 | super.init(collectionName: collectionName, id: id) 34 | } 35 | 36 | override func encode(to encoder: Encoder) throws { 37 | var dataContainer = encoder.container(keyedBy: DataCodingKeys.self) 38 | var container = dataContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: .data) 39 | try container.encode(name, forKey: .name) 40 | try super.encode(to: encoder) 41 | } 42 | } 43 | 44 | class CollectionTests: TestCase { 45 | 46 | func testAdd() { 47 | let burger = Food(name: "Burger", id: "123") 48 | XCTAssertEqual(burger.referenceId, "SO:food:123") 49 | 50 | expect("add collection object") { test in 51 | Client.shared.add(collectionObject: burger) { result in 52 | if let addedBurger = try? result.get() { 53 | XCTAssertEqual(addedBurger.collectionName, "food") 54 | XCTAssertEqual(addedBurger.foreignId, "food:123") 55 | XCTAssertEqual(addedBurger.name, "Burger") 56 | } else { 57 | XCTFail("Add collection object: \(result)") 58 | } 59 | 60 | test.fulfill() 61 | } 62 | } 63 | } 64 | 65 | func testGet() { 66 | expect("get collection object") { test in 67 | Client.shared.get(typeOf: Food.self, collectionName: "test", collectionObjectId: "obj") { result in 68 | let burger = try! result.get() 69 | XCTAssertEqual(burger.id, "123") 70 | XCTAssertEqual(burger.collectionName, "food") 71 | XCTAssertEqual(burger.name, "Burger") 72 | test.fulfill() 73 | } 74 | } 75 | } 76 | 77 | func testUpdate() { 78 | let burger = Food(name: "Burger2", id: "123") 79 | 80 | expect("update collection object") { test in 81 | Client.shared.update(collectionObject: burger) { result in 82 | let addedBurger = try! result.get() 83 | XCTAssertEqual(addedBurger.name, "Burger2") 84 | test.fulfill() 85 | } 86 | } 87 | } 88 | 89 | func testDelete() { 90 | expect("bad delete collection object") { test in 91 | let burger = Food(name: "Burger") 92 | XCTAssertEqual(burger.referenceId, "SO:food") 93 | Client.shared.delete(collectionObject: burger) { result in 94 | if case .failure(let error) = result, case .jsonInvalid = error { 95 | test.fulfill() 96 | } 97 | } 98 | } 99 | 100 | expect("delete collection object") { test in 101 | let burger = Food(name: "Burger", id: "123") 102 | 103 | Client.shared.delete(collectionObject: burger) { result in 104 | let status = try! result.get() 105 | XCTAssertEqual(status, 200) 106 | test.fulfill() 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Tests/Core/ExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtensionsTests.swift 3 | // GetStream-iOS Tests 4 | // 5 | // Created by Alexey Bukhtin on 20/11/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Moya 11 | @testable import GetStream 12 | 13 | final class ExtensionsTests: XCTestCase { 14 | let decoder = JSONDecoder.default 15 | let encoder = JSONEncoder.default 16 | 17 | let defaultData = """ 18 | { 19 | "actor":"eric", 20 | "foreign_id":"1E42DEB6-7C2F-4DA9-B6E6-0C6E5CC9815D", 21 | "id":"9b5b3540-e825-11e8-8080-800016ff21e4", 22 | "object":"Hello world 3", 23 | "origin":null, 24 | "target":"", 25 | "time":"2018-11-14T15:54:45.268000", 26 | "to":["timeline:jessica"], 27 | "verb":"tweet" 28 | } 29 | """.data(using: .utf8)! 30 | 31 | let iso8601Data = """ 32 | { 33 | "actor":"eric", 34 | "foreign_id":"1E42DEB6-7C2F-4DA9-B6E6-0C6E5CC9815D", 35 | "id":"9b5b3540-e825-11e8-8080-800016ff21e4", 36 | "object":"Hello world 3", 37 | "origin":null, 38 | "target":"", 39 | "time":"2018-11-14T15:54:45.268000Z", 40 | "to":["timeline:jessica"], 41 | "verb":"tweet" 42 | } 43 | """.data(using: .utf8)! 44 | 45 | let badDefaultData = """ 46 | { 47 | "actor":"eric", 48 | "foreign_id":"1E42DEB6-7C2F-4DA9-B6E6-0C6E5CC9815D", 49 | "id":"9b5b3540-e825-11e8-8080-800016ff21e4", 50 | "object":"Hello world 3", 51 | "origin":null, 52 | "target":"", 53 | "time":"2018-11-14", 54 | "to":["timeline:jessica"], 55 | "verb":"tweet" 56 | } 57 | """.data(using: .utf8)! 58 | 59 | // MARK: - Codable 60 | 61 | func testCodable() throws { 62 | let activity = try decoder.decode(SimpleActivity.self, from: defaultData) 63 | XCTAssertEqual(activity.actor, "eric") 64 | XCTAssertEqual(activity.time!, "2018-11-14T15:54:45.268000".streamDate!) 65 | let encodedData = try encoder.encode(activity) 66 | XCTAssertTrue(String(data: encodedData, encoding: .utf8)!.contains("2018-11-14T15:54:45.268")) 67 | } 68 | 69 | func testCodableInvalidData() { 70 | do { 71 | _ = try decoder.decode(SimpleActivity.self, from: badDefaultData) 72 | XCTFail("❌ Empty json data check") 73 | 74 | } catch let error as DecodingError { 75 | if case .dataCorrupted(let context) = error { 76 | XCTAssertEqual(context.debugDescription, "Invalid date: 2018-11-14") 77 | } else { 78 | XCTFail("❌") 79 | } 80 | } catch { 81 | XCTFail("❌") 82 | } 83 | } 84 | 85 | func testAnyCodable() throws { 86 | struct Test: Encodable { 87 | let value: String 88 | } 89 | 90 | let anyEncodable = AnyEncodable(Test(value: "test")) 91 | let jsonData = try encoder.encode(anyEncodable) 92 | XCTAssertEqual(String(data: jsonData, encoding: .utf8)!, "{\"value\":\"test\"}") 93 | } 94 | 95 | // MARK: - Test Date Formatter 96 | 97 | func testISO8601Codable() throws { 98 | let activity = try decoder.decode(SimpleActivity.self, from: iso8601Data) 99 | XCTAssertEqual(activity.actor, "eric") 100 | XCTAssertEqual(activity.time!, "2018-11-14T15:54:45.268000".streamDate!) 101 | } 102 | 103 | func testDateExtension() { 104 | let date = Date() 105 | let streamString = date.stream 106 | let streamDate = streamString.streamDate! 107 | XCTAssertEqual(streamDate, streamDate.stream.streamDate!) 108 | 109 | let formatter = DateFormatter() 110 | formatter.dateStyle = .full 111 | formatter.timeStyle = .full 112 | XCTAssertEqual(formatter.string(from: date), formatter.string(from: streamDate)) 113 | } 114 | 115 | func testStreamTargetType() { 116 | struct Test: StreamTargetType { 117 | var path: String = "" 118 | var method: Moya.Method = Moya.Method.get 119 | var task: Task = .requestPlain 120 | } 121 | 122 | let test = Test() 123 | 124 | XCTAssertEqual(test.baseURL, URL(string: "https://getstream.io")!) 125 | XCTAssertEqual(test.headers?["X-Stream-Client"]!, "stream-swift-client-\(Client.version)") 126 | XCTAssertEqual(test.sampleData, Data()) 127 | } 128 | 129 | func testSimpleCancellable() { 130 | let cancellable = SimpleCancellable() 131 | XCTAssertFalse(cancellable.isCancelled) 132 | cancellable.cancel() 133 | XCTAssertTrue(cancellable.isCancelled) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Tests/Core/FilesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilesTests.swift 3 | // GetStream-iOS Tests 4 | // 5 | // Created by Alexey Bukhtin on 24/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GetStream 11 | 12 | class FilesTests: TestCase { 13 | 14 | let data = Data(base64Encoded: "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==")! 15 | 16 | lazy var image = Image(data: data)! 17 | 18 | func testUpload() { 19 | expect("upload file") { test in 20 | let file = File(name: "test", data: data) 21 | Client.shared.upload(file: file) { 22 | XCTAssertEqual(try! $0.get(), URL(string: "http://uploaded.getstream.io/test")!) 23 | test.fulfill() 24 | } 25 | } 26 | } 27 | 28 | func testDelete() { 29 | expect("delete file") { test in 30 | Client.shared.delete(fileURL: URL(string: "http://uploaded.getstream.io/test")!) { 31 | XCTAssertEqual(try! $0.get(), 200) 32 | test.fulfill() 33 | } 34 | } 35 | 36 | expect("delete image") { test in 37 | Client.shared.delete(imageURL: URL(string: "http://images.getstream.io/test")!) { 38 | XCTAssertEqual(try! $0.get(), 200) 39 | test.fulfill() 40 | } 41 | } 42 | } 43 | 44 | func testUploadImage() { 45 | expect("upload image") { test in 46 | let file = File(name: "jpg", jpegImage: image)! 47 | Client.shared.upload(image: file) { 48 | XCTAssertEqual(try! $0.get(), URL(string: "http://images.getstream.io/jpg")!) 49 | 50 | let file = File(name: "png", pngImage: self.image)! 51 | Client.shared.upload(image: file) { 52 | XCTAssertEqual(try! $0.get(), URL(string: "http://images.getstream.io/png")!) 53 | test.fulfill() 54 | } 55 | } 56 | } 57 | } 58 | 59 | func testImageProcess() { 60 | expect("process image") { test in 61 | let process = ImageProcess(url: URL(string: "http://images.getstream.io/jpg")!, width: 100, height: 100) 62 | 63 | Client.shared.resizeImage(imageProcess: process, completion: { 64 | XCTAssertEqual(try! $0.get(), URL(string: "http://images.getstream.io/jpg?crop=center&h=100&w=100&resize=clip&url=http://images.getstream.io/jpg")!) 65 | test.fulfill() 66 | }) 67 | } 68 | 69 | expect("bad process image") { test in 70 | let process = ImageProcess(url: URL(string: "http://images.getstream.io/jpg")!, width: 1, height: 0) 71 | 72 | Client.shared.resizeImage(imageProcess: process, completion: { 73 | if case .failure(let clientError) = $0, case .parameterInvalid = clientError { 74 | test.fulfill() 75 | } 76 | }) 77 | } 78 | 79 | expect("bad process image") { test in 80 | let process = ImageProcess(url: URL(string: "http://images.getstream.io/jpg")!, width: 0, height: 1) 81 | 82 | Client.shared.resizeImage(imageProcess: process, completion: { 83 | if case .failure(let clientError) = $0, case .parameterInvalid = clientError { 84 | test.fulfill() 85 | } 86 | }) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Tests/Core/NotificationFeedTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationFeedTests.swift 3 | // GetStream-iOS Tests 4 | // 5 | // Created by Alexey Bukhtin on 24/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GetStream 11 | 12 | class NotificationFeedTests: TestCase { 13 | lazy var notificationsFeed = Client.shared.notificationFeed(feedSlug: "notifications") 14 | 15 | func testNotioficationsFeed() { 16 | expect("get notifications") { test in 17 | notificationsFeed?.get(typeOf: SimpleActivity.self) { result in 18 | let notifications = try! result.get() 19 | 20 | XCTAssertEqual(notifications.results.count, 1) 21 | XCTAssertEqual(notifications.results.first!.isSeen, true) 22 | XCTAssertEqual(notifications.results.first!.isRead, false) 23 | XCTAssertEqual(notifications.results.first!.activitiesCount, 6) 24 | XCTAssertEqual(notifications.results.first!.activities.count, 6) 25 | XCTAssertEqual(notifications.results.first!.verb, "test") 26 | XCTAssertTrue(notifications.results.first!.group.hasPrefix("test_")) 27 | XCTAssertEqual(notifications.results.first!.activities.first!.verb, "test") 28 | 29 | test.fulfill() 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/Core/OGTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OGTests.swift 3 | // GetStream-iOS Tests 4 | // 5 | // Created by Alexey Bukhtin on 24/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GetStream 11 | 12 | class OGTests: TestCase { 13 | 14 | func testOG() { 15 | expect("get OpenGraph data") { test in 16 | let url = URL(string: "http://www.imdb.com/title/tt2084970/")! 17 | Client.shared.og(url: url) { 18 | let data = try! $0.get() 19 | XCTAssertEqual(data.title, "The Imitation Game (2014)") 20 | XCTAssertEqual(data.url, url) 21 | XCTAssertEqual(data.siteName, "IMDb") 22 | XCTAssertEqual(data.images!.count, 1) 23 | XCTAssertEqual(data.images!.first!.image, 24 | "https://m.media-amazon.com/images/M/MV5BOTgwMzFiMWYtZDhlNS00ODNkLWJiODAtZDVhNzgyNzJhYjQ4L2ltYWdlXkEyXkFqcGdeQXVyNzEzOTYxNTQ@._V1_UY1200_CR87,0,630,1200_AL_.jpg") 25 | test.fulfill() 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/Core/ReactionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReactionTests.swift 3 | // GetStream-iOS Tests 4 | // 5 | // Created by Alexey Bukhtin on 27/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GetStream 11 | 12 | class ReactionTests: TestCase { 13 | 14 | func testAdd() { 15 | Client.shared.add(reactionTo: .test1, kindOf: .comment, extraData: Comment(text: "Hello!"), userTypeOf: User.self) { 16 | let commentReaction = try! $0.get() 17 | XCTAssertEqual(commentReaction.kind, .comment) 18 | XCTAssertEqual(commentReaction.data.text, "Hello!") 19 | 20 | Client.shared.add(reactionTo: .test1, parentReactionId: commentReaction.parentId, kindOf: .like) { 21 | let likeReaction = try! $0.get() 22 | XCTAssertEqual(likeReaction.kind, .like) 23 | XCTAssertEqual(likeReaction.parentId, commentReaction.id) 24 | } 25 | 26 | Client.shared.add(reactionToParentReaction: commentReaction, kindOf: .like, userTypeOf: User.self) { 27 | let likeReaction = try! $0.get() 28 | XCTAssertEqual(likeReaction.kind, .like) 29 | XCTAssertEqual(likeReaction.parentId, commentReaction.id) 30 | } 31 | } 32 | } 33 | 34 | func testGet() { 35 | Client.shared.get(reactionId: .test1) { 36 | let reaction = try! $0.get() 37 | XCTAssertEqual(reaction.kind, .like) 38 | XCTAssertEqual(reaction.data, EmptyReactionExtraData.shared) 39 | } 40 | 41 | Client.shared.get(reactionId: .test2, extraDataTypeOf: Comment.self, userTypeOf: User.self) { 42 | let reaction = try! $0.get() 43 | XCTAssertEqual(reaction.kind, .comment) 44 | XCTAssertEqual(reaction.data.text, "Hello!") 45 | } 46 | 47 | Client.shared.get(reactionId: .test2) { 48 | let reaction = try! $0.get() 49 | XCTAssertEqual(reaction.kind, .comment) 50 | XCTAssertEqual(reaction.data, EmptyReactionExtraData.shared) 51 | } 52 | } 53 | 54 | func testUpdate() { 55 | Client.shared.update(reactionId: .test2, extraData: ReactionExtraData.comment("Hi!"), userTypeOf: User.self) { 56 | let reaction = try! $0.get() 57 | XCTAssertEqual(reaction.kind, .comment) 58 | 59 | 60 | if case .comment(let text) = reaction.data { 61 | XCTAssertEqual(text, "Hi!") 62 | } 63 | 64 | if let lastLike = reaction.latestChildren[.like]?.first { 65 | XCTAssertEqual(lastLike.kind, .like) 66 | } 67 | 68 | if let lastComment = reaction.latestChildren[.comment]?.first { 69 | XCTAssertEqual(lastComment.kind, .comment) 70 | 71 | if case .comment(let text) = lastComment.data { 72 | XCTAssertEqual(text, "Hey!") 73 | } 74 | } 75 | } 76 | } 77 | 78 | func testDelete() { 79 | Client.shared.delete(reactionId: .test1) { 80 | XCTAssertEqual(try! $0.get(), 200) 81 | } 82 | } 83 | 84 | func testFetchReactions() { 85 | Client.shared.reactions(forUserId: "1") { 86 | let reactions = try! $0.get() 87 | XCTAssertEqual(reactions.reactions.count, 3) 88 | } 89 | 90 | Client.shared.reactions(forUserId: "1", kindOf: .comment, extraDataTypeOf: ReactionExtraData.self, userTypeOf: User.self) { 91 | let reactions = try! $0.get() 92 | XCTAssertEqual(reactions.reactions.count, 2) 93 | 94 | if case .comment(let text) = reactions.reactions[0].data { 95 | XCTAssertEqual(text, "Hey!") 96 | } 97 | 98 | if case .comment(let text) = reactions.reactions[1].data { 99 | XCTAssertEqual(text, "Hi!") 100 | } 101 | } 102 | 103 | Client.shared.reactions(forReactionId: "50539e71-d6bf-422d-ad21-c8717df0c325") { 104 | let reactions = try! $0.get() 105 | XCTAssertEqual(reactions.reactions.count, 2) 106 | } 107 | 108 | Client.shared.reactions(forActivityId: "ce918867-0520-11e9-a11e-0a286b200b2e", withActivityData: true) { 109 | let reactions = try! $0.get() 110 | XCTAssertEqual(reactions.reactions.count, 3) 111 | XCTAssertNotNil(try? reactions.activity(typeOf: SimpleActivity.self)) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Tests/Core/UserTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserTests.swift 3 | // GetStream-iOS Tests 4 | // 5 | // Created by Alexey Bukhtin on 27/12/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GetStream 11 | 12 | final class CustomUser: User { 13 | private enum CodingKeys: String, CodingKey { 14 | case name 15 | } 16 | 17 | var name: String 18 | 19 | init(id: String, name: String) { 20 | self.name = name 21 | super.init(id: id) 22 | } 23 | 24 | required init(from decoder: Decoder) throws { 25 | let dataContainer = try decoder.container(keyedBy: DataCodingKeys.self) 26 | let container = try dataContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: .data) 27 | name = try container.decode(String.self, forKey: .name) 28 | try super.init(from: decoder) 29 | } 30 | 31 | required init(id: String) { 32 | name = "" 33 | super.init(id: id) 34 | } 35 | 36 | override func encode(to encoder: Encoder) throws { 37 | var dataContainer = encoder.container(keyedBy: DataCodingKeys.self) 38 | var container = dataContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: .data) 39 | try container.encode(name, forKey: .name) 40 | try super.encode(to: encoder) 41 | } 42 | } 43 | 44 | class UserTests: TestCase { 45 | func testCreate() { 46 | let user = CustomUser(id: "eric", name: "Eric") 47 | 48 | expect("create user") { test in 49 | Client.shared.create(user: user) { 50 | print($0) 51 | let created = try! $0.get() 52 | XCTAssertEqual(created.name, user.name) 53 | 54 | Client.shared.get(typeOf: CustomUser.self, userId: user.id) { 55 | let loaded = try! $0.get() 56 | XCTAssertEqual(loaded.name, user.name) 57 | loaded.name = "Eric Updated" 58 | XCTAssertNotEqual(loaded.name, user.name) 59 | 60 | Client.shared.update(user: loaded) { 61 | let updated = try! $0.get() 62 | XCTAssertEqual(updated.name, loaded.name) 63 | 64 | Client.shared.delete(userId: updated.id) { 65 | XCTAssertEqual(try! $0.get(), 200) 66 | test.fulfill() 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Tests/Extensions/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extensions.swift 3 | // GetStream 4 | // 5 | // Created by Alexey Bukhtin on 21/11/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | extension String { 12 | static let test1 = "00000000-0000-0000-0000-000000000001" 13 | static let test2 = "00000000-0000-0000-0000-000000000002" 14 | } 15 | -------------------------------------------------------------------------------- /Tests/Extensions/TestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestCase.swift 3 | // GetStream 4 | // 5 | // Created by Alexey Bukhtin on 21/11/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import GetStream 11 | 12 | class TestCase: XCTestCase { 13 | 14 | override class func setUp() { 15 | let provider = NetworkProvider(stubClosure: NetworkProvider.immediatelyStub) 16 | Client.config = .init(apiKey: "apiKey", appId: "appId", networkProvider: provider) 17 | 18 | if User.current == nil { 19 | setupUser(token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiZXJpYyJ9.20YPOjP1-HtwKH7SH3k5CgLLLrhLCLaKDnb8XuiU7oA") 20 | } 21 | } 22 | 23 | class func setupUser(token: Token, shouldFail: Bool = false) { 24 | Client.shared.setupUser(token: token) { result in 25 | if let user = try? result.get(), Client.shared.currentUserId == user.id { 26 | if shouldFail { 27 | XCTFail("User setup should fail, but got user: \(user)") 28 | } else { 29 | XCTAssertEqual(user.id, Client.shared.currentUserId ?? "unwrapped") 30 | } 31 | } else if let error = result.error { 32 | if !shouldFail { 33 | XCTFail("User setup failed with error: \(error)") 34 | } 35 | } else if !shouldFail { 36 | XCTFail("User setup failed: \(result)") 37 | } 38 | } 39 | } 40 | 41 | func expect(_ description: String, timeout: TimeInterval = TimeInterval(1), callback: (_ test: XCTestExpectation) -> Void) { 42 | let test = expectation(description: "⏳ expecting \(description)") 43 | callback(test) 44 | wait(for: [test], timeout: timeout) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/Faye/FayeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FayeTests.swift 3 | // FayeTests 4 | // 5 | // Created by Alexey Bukhtin on 26/11/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class FayeTests: TestCase { 12 | 13 | func testExample() { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/Token/TokenTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TokenTests.swift 3 | // GetStream-iOS Tests 4 | // 5 | // Created by Alexey Bukhtin on 22/11/2018. 6 | // Copyright © 2018 Stream.io Inc. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import JWT 11 | @testable import GetStream 12 | 13 | class TokenTests: XCTestCase { 14 | 15 | func testGenerator() { 16 | let secretData = "xwnkc2rdvm7bp7gn8ddzc6ngbgvskahf6v3su7qj5gp6utyu8rtek8k2vq2ssaav".data(using: .utf8)! 17 | let token = Token(secretData: secretData) 18 | let jwtClaims: ClaimSet = try! JWT.decode(token, algorithm: .hs256(secretData)) 19 | XCTAssertEqual(jwtClaims["resource"] as! String, Token.Resource.all.rawValue) 20 | XCTAssertEqual(jwtClaims["action"] as! String, Token.Permission.all.rawValue) 21 | XCTAssertEqual(jwtClaims["feed_id"] as! String, FeedId.any.description) 22 | } 23 | 24 | func testUserGenerator() { 25 | let secretData = "xwnkc2rdvm7bp7gn8ddzc6ngbgvskahf6v3su7qj5gp6utyu8rtek8k2vq2ssaav".data(using: .utf8)! 26 | let token = Token(secretData: secretData, userId: "eric") 27 | let jwtClaims: ClaimSet = try! JWT.decode(token, algorithm: .hs256(secretData)) 28 | XCTAssertNil(jwtClaims["resource"]) 29 | XCTAssertNil(jwtClaims["action"]) 30 | XCTAssertNil(jwtClaims["feed_id"]) 31 | XCTAssertEqual(jwtClaims["user_id"] as! String, "eric") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /docs/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | documentation 17 | 18 | 19 | documentation 20 | 21 | 22 | 79% 23 | 24 | 25 | 79% 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/docsets/GetStream.docset/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.jazzy.getstream 7 | CFBundleName 8 | GetStream 9 | DocSetPlatformFamily 10 | getstream 11 | isDashDocset 12 | 13 | dashIndexFilePath 14 | index.html 15 | isJavaScriptEnabled 16 | 17 | DashDocSetFamily 18 | dashtoc 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/docsets/GetStream.docset/Contents/Resources/Documents/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /docs/docsets/GetStream.docset/Contents/Resources/Documents/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | documentation 17 | 18 | 19 | documentation 20 | 21 | 22 | 77% 23 | 24 | 25 | 77% 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/docsets/GetStream.docset/Contents/Resources/Documents/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-swift/0785e68cbb3dda1092f65e2fc53d6eb91ba990a1/docs/docsets/GetStream.docset/Contents/Resources/Documents/img/carat.png -------------------------------------------------------------------------------- /docs/docsets/GetStream.docset/Contents/Resources/Documents/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-swift/0785e68cbb3dda1092f65e2fc53d6eb91ba990a1/docs/docsets/GetStream.docset/Contents/Resources/Documents/img/dash.png -------------------------------------------------------------------------------- /docs/docsets/GetStream.docset/Contents/Resources/Documents/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-swift/0785e68cbb3dda1092f65e2fc53d6eb91ba990a1/docs/docsets/GetStream.docset/Contents/Resources/Documents/img/gh.png -------------------------------------------------------------------------------- /docs/docsets/GetStream.docset/Contents/Resources/Documents/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-swift/0785e68cbb3dda1092f65e2fc53d6eb91ba990a1/docs/docsets/GetStream.docset/Contents/Resources/Documents/img/spinner.gif -------------------------------------------------------------------------------- /docs/docsets/GetStream.docset/Contents/Resources/Documents/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | function toggleItem($link, $content) { 12 | var animationDuration = 300; 13 | $link.toggleClass('token-open'); 14 | $content.slideToggle(animationDuration); 15 | } 16 | 17 | function itemLinkToContent($link) { 18 | return $link.parent().parent().next(); 19 | } 20 | 21 | // On doc load + hash-change, open any targetted item 22 | function openCurrentItemIfClosed() { 23 | if (window.jazzy.docset) { 24 | return; 25 | } 26 | var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token'); 27 | $content = itemLinkToContent($link); 28 | if ($content.is(':hidden')) { 29 | toggleItem($link, $content); 30 | } 31 | } 32 | 33 | $(openCurrentItemIfClosed); 34 | $(window).on('hashchange', openCurrentItemIfClosed); 35 | 36 | // On item link ('token') click, toggle its discussion 37 | $('.token').on('click', function(event) { 38 | if (window.jazzy.docset) { 39 | return; 40 | } 41 | var $link = $(this); 42 | toggleItem($link, itemLinkToContent($link)); 43 | 44 | // Keeps the document from jumping to the hash. 45 | var href = $link.attr('href'); 46 | if (history.pushState) { 47 | history.pushState({}, '', href); 48 | } else { 49 | location.hash = href; 50 | } 51 | event.preventDefault(); 52 | }); 53 | 54 | // Clicks on links to the current, closed, item need to open the item 55 | $("a:not('.token')").on('click', function() { 56 | if (location == this.href) { 57 | openCurrentItemIfClosed(); 58 | } 59 | }); 60 | 61 | // KaTeX rendering 62 | if ("katex" in window) { 63 | $($('.math').each( (_, element) => { 64 | katex.render(element.textContent, element, { 65 | displayMode: $(element).hasClass('m-block'), 66 | throwOnError: false, 67 | trust: true 68 | }); 69 | })) 70 | } 71 | -------------------------------------------------------------------------------- /docs/docsets/GetStream.docset/Contents/Resources/Documents/js/jazzy.search.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | var $typeahead = $('[data-typeahead]'); 3 | var $form = $typeahead.parents('form'); 4 | var searchURL = $form.attr('action'); 5 | 6 | function displayTemplate(result) { 7 | return result.name; 8 | } 9 | 10 | function suggestionTemplate(result) { 11 | var t = '
'; 12 | t += '' + result.name + ''; 13 | if (result.parent_name) { 14 | t += '' + result.parent_name + ''; 15 | } 16 | t += '
'; 17 | return t; 18 | } 19 | 20 | $typeahead.one('focus', function() { 21 | $form.addClass('loading'); 22 | 23 | $.getJSON(searchURL).then(function(searchData) { 24 | const searchIndex = lunr(function() { 25 | this.ref('url'); 26 | this.field('name'); 27 | this.field('abstract'); 28 | for (const [url, doc] of Object.entries(searchData)) { 29 | this.add({url: url, name: doc.name, abstract: doc.abstract}); 30 | } 31 | }); 32 | 33 | $typeahead.typeahead( 34 | { 35 | highlight: true, 36 | minLength: 3, 37 | autoselect: true 38 | }, 39 | { 40 | limit: 10, 41 | display: displayTemplate, 42 | templates: { suggestion: suggestionTemplate }, 43 | source: function(query, sync) { 44 | const lcSearch = query.toLowerCase(); 45 | const results = searchIndex.query(function(q) { 46 | q.term(lcSearch, { boost: 100 }); 47 | q.term(lcSearch, { 48 | boost: 10, 49 | wildcard: lunr.Query.wildcard.TRAILING 50 | }); 51 | }).map(function(result) { 52 | var doc = searchData[result.ref]; 53 | doc.url = result.ref; 54 | return doc; 55 | }); 56 | sync(results); 57 | } 58 | } 59 | ); 60 | $form.removeClass('loading'); 61 | $typeahead.trigger('focus'); 62 | }); 63 | }); 64 | 65 | var baseURL = searchURL.slice(0, -"search.json".length); 66 | 67 | $typeahead.on('typeahead:select', function(e, result) { 68 | window.location = baseURL + result.url; 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /docs/docsets/GetStream.docset/Contents/Resources/docSet.dsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-swift/0785e68cbb3dda1092f65e2fc53d6eb91ba990a1/docs/docsets/GetStream.docset/Contents/Resources/docSet.dsidx -------------------------------------------------------------------------------- /docs/docsets/GetStream.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-swift/0785e68cbb3dda1092f65e2fc53d6eb91ba990a1/docs/docsets/GetStream.tgz -------------------------------------------------------------------------------- /docs/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-swift/0785e68cbb3dda1092f65e2fc53d6eb91ba990a1/docs/img/carat.png -------------------------------------------------------------------------------- /docs/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-swift/0785e68cbb3dda1092f65e2fc53d6eb91ba990a1/docs/img/dash.png -------------------------------------------------------------------------------- /docs/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-swift/0785e68cbb3dda1092f65e2fc53d6eb91ba990a1/docs/img/gh.png -------------------------------------------------------------------------------- /docs/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-swift/0785e68cbb3dda1092f65e2fc53d6eb91ba990a1/docs/img/spinner.gif -------------------------------------------------------------------------------- /docs/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | function toggleItem($link, $content) { 12 | var animationDuration = 300; 13 | $link.toggleClass('token-open'); 14 | $content.slideToggle(animationDuration); 15 | } 16 | 17 | function itemLinkToContent($link) { 18 | return $link.parent().parent().next(); 19 | } 20 | 21 | // On doc load + hash-change, open any targetted item 22 | function openCurrentItemIfClosed() { 23 | if (window.jazzy.docset) { 24 | return; 25 | } 26 | var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token'); 27 | $content = itemLinkToContent($link); 28 | if ($content.is(':hidden')) { 29 | toggleItem($link, $content); 30 | } 31 | } 32 | 33 | $(openCurrentItemIfClosed); 34 | $(window).on('hashchange', openCurrentItemIfClosed); 35 | 36 | // On item link ('token') click, toggle its discussion 37 | $('.token').on('click', function(event) { 38 | if (window.jazzy.docset) { 39 | return; 40 | } 41 | var $link = $(this); 42 | toggleItem($link, itemLinkToContent($link)); 43 | 44 | // Keeps the document from jumping to the hash. 45 | var href = $link.attr('href'); 46 | if (history.pushState) { 47 | history.pushState({}, '', href); 48 | } else { 49 | location.hash = href; 50 | } 51 | event.preventDefault(); 52 | }); 53 | 54 | // Clicks on links to the current, closed, item need to open the item 55 | $("a:not('.token')").on('click', function() { 56 | if (location == this.href) { 57 | openCurrentItemIfClosed(); 58 | } 59 | }); 60 | 61 | // KaTeX rendering 62 | if ("katex" in window) { 63 | $($('.math').each( (_, element) => { 64 | katex.render(element.textContent, element, { 65 | displayMode: $(element).hasClass('m-block'), 66 | throwOnError: false, 67 | trust: true 68 | }); 69 | })) 70 | } 71 | -------------------------------------------------------------------------------- /docs/js/jazzy.search.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | var $typeahead = $('[data-typeahead]'); 3 | var $form = $typeahead.parents('form'); 4 | var searchURL = $form.attr('action'); 5 | 6 | function displayTemplate(result) { 7 | return result.name; 8 | } 9 | 10 | function suggestionTemplate(result) { 11 | var t = '
'; 12 | t += '' + result.name + ''; 13 | if (result.parent_name) { 14 | t += '' + result.parent_name + ''; 15 | } 16 | t += '
'; 17 | return t; 18 | } 19 | 20 | $typeahead.one('focus', function() { 21 | $form.addClass('loading'); 22 | 23 | $.getJSON(searchURL).then(function(searchData) { 24 | const searchIndex = lunr(function() { 25 | this.ref('url'); 26 | this.field('name'); 27 | this.field('abstract'); 28 | for (const [url, doc] of Object.entries(searchData)) { 29 | this.add({url: url, name: doc.name, abstract: doc.abstract}); 30 | } 31 | }); 32 | 33 | $typeahead.typeahead( 34 | { 35 | highlight: true, 36 | minLength: 3, 37 | autoselect: true 38 | }, 39 | { 40 | limit: 10, 41 | display: displayTemplate, 42 | templates: { suggestion: suggestionTemplate }, 43 | source: function(query, sync) { 44 | const lcSearch = query.toLowerCase(); 45 | const results = searchIndex.query(function(q) { 46 | q.term(lcSearch, { boost: 100 }); 47 | q.term(lcSearch, { 48 | boost: 10, 49 | wildcard: lunr.Query.wildcard.TRAILING 50 | }); 51 | }).map(function(result) { 52 | var doc = searchData[result.ref]; 53 | doc.url = result.ref; 54 | return doc; 55 | }); 56 | sync(results); 57 | } 58 | } 59 | ); 60 | $form.removeClass('loading'); 61 | $typeahead.trigger('focus'); 62 | }); 63 | }); 64 | 65 | var baseURL = searchURL.slice(0, -"search.json".length); 66 | 67 | $typeahead.on('typeahead:select', function(e, result) { 68 | window.location = baseURL + result.url; 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | # For a list of all available plugins, check out 9 | # 10 | # https://docs.fastlane.tools/plugins/available-plugins 11 | # 12 | 13 | # Uncomment the line if you want fastlane to automatically update itself 14 | update_fastlane 15 | 16 | carthage(use_binaries: false, platform: "iOS", cache_builds: true, new_resolver: true) 17 | 18 | default_platform(:ios) 19 | 20 | platform :ios do 21 | desc "Run all unit tests for GetStream" 22 | lane :tests do 23 | run_tests(scheme: "GetStream-iOS") 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | 5 | Make sure you have the latest version of the Xcode command line tools installed: 6 | 7 | ``` 8 | xcode-select --install 9 | ``` 10 | 11 | Install _fastlane_ using 12 | ``` 13 | [sudo] gem install fastlane -NV 14 | ``` 15 | or alternatively using `brew cask install fastlane` 16 | 17 | # Available Actions 18 | ## iOS 19 | ### ios tests 20 | ``` 21 | fastlane ios tests 22 | ``` 23 | Run all unit tests for GetStream 24 | 25 | ---- 26 | 27 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. 28 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). 29 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 30 | --------------------------------------------------------------------------------