diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index e4c9ab212..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,68 +0,0 @@ -# .circleci/config.yml - -# Specify the config version - version 2 is latest. -version: 2 - -# Define the jobs for the current project. -jobs: - build-and-test: - - environment: - - DESTINATION: "platform=iOS Simulator,name=iPhone XS" - - # Specify the Xcode version to use. - macos: - xcode: "10.0.0" - - # Define the steps required to build the project. - steps: - - # Get the code from the VCS provider. - - checkout - - run: - name: Update Homebrew - command: brew update - - run: - name: Bootstrap Carthage - command: carthage bootstrap --platform ios - - run: - name: Build framework - command: xcodebuild build -project MessageKit.xcodeproj -scheme MessageKit -destination "$DESTINATION" CODE_SIGNING_REQUIRED=NO | xcpretty -c - - run: - name: Build and run tests - command: xcodebuild test -project MessageKit.xcodeproj -scheme MessageKitTests -destination "$DESTINATION" CODE_SIGNING_REQUIRED=NO | xcpretty -c - - run: - name: Fetch CocoaPods Specs - command: curl -sS https://cocoapods-specs.circleci.com/fetch-cocoapods-repo-from-s3.sh | bash - - run: - name: Update Pods - command: cd Example && pod install - - run: - name: Build and analyze Example - command: xcodebuild build analyze -workspace Example/ChatExample.xcworkspace -scheme ChatExample -destination "$DESTINATION" ONLY_ACTIVE_ARCH=NO CODE_SIGNING_REQUIRED=NO | xcpretty -c - - # Run tests. - #- run: - # name: Run tests - # command: fastlane scan - # environment: - # SCAN_DEVICE: iPhone 6 - # SCAN_SCHEME: WebTests - - # Collect XML test results data to show in the UI, - # and save the same XML files under test-results folder - # in the Artifacts tab. - #- store_test_results: - # path: test_output/report.xml - #- store_artifacts: - # path: /tmp/test-results - # destination: scan-test-results - #- store_artifacts: - # path: ~/Library/Logs/scan - # destination: scan-logs - -workflows: - version: 2 - build-and-deploy: - jobs: - - build-and-test diff --git a/.codecov.yml b/.codecov.yml deleted file mode 100644 index 18f175cb8..000000000 --- a/.codecov.yml +++ /dev/null @@ -1,32 +0,0 @@ -codecov: - branch: develop - -coverage: - precision: 2 - round: nearest - range: "60...100" - ignore: - - Tests/* - -status: - project: - default: - target: auto - threshold: 2.0 - branches: - - master - - develop - - patch: - default: - target: auto - branches: - - master - - develop - -comment: - layout: "header, diff, changes, sunburst, uncovered" - branches: - - master - - develop - behavior: default diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 270910dec..000000000 --- a/.editorconfig +++ /dev/null @@ -1,14 +0,0 @@ -root = true - -[*] -end_of_line = lf -insert_final_newline = true - -# Set default charset -[*.{js,py,swift,m,json}] -charset = utf-8 - -# 4 space indentation -[*.swift] -indent_style = space -indent_size = 4 diff --git a/.github/ISSUE_TEMPLATE/---bug-report.md b/.github/ISSUE_TEMPLATE/---bug-report.md new file mode 100644 index 000000000..83e5ed4ab --- /dev/null +++ b/.github/ISSUE_TEMPLATE/---bug-report.md @@ -0,0 +1,49 @@ +--- +name: "\U0001F41B Bug report" +about: Create a report to help us improve +title: '' +labels: bug? +assignees: '' + +--- + + + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps/code to reproduce the behavior: + + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Environment** +- What version of MessageKit are you using? +- What version of iOS are you running on? +- What version of Swift are you running on? +- What device(s) are you testing on? Are these simulators? +- Is the issue you're experiencing reproducible in the example app? + +**Additional context** +Add any other context about the problem here. + diff --git a/.github/ISSUE_TEMPLATE/--feature-request.md b/.github/ISSUE_TEMPLATE/--feature-request.md new file mode 100644 index 000000000..b64ba3031 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/--feature-request.md @@ -0,0 +1,22 @@ +--- +name: "\U0001F4A1Feature request" +about: Suggest an idea for this project +title: '' +labels: feature request +assignees: '' + +--- + + + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/--question-support.md b/.github/ISSUE_TEMPLATE/--question-support.md new file mode 100644 index 000000000..630b27547 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/--question-support.md @@ -0,0 +1,34 @@ +--- +name: "❓ Question/Support" +about: Ask a question about how to use MessageKit +title: '' +labels: question +assignees: '' + +--- + + diff --git a/.github/workflows/ci_pr_example.yml b/.github/workflows/ci_pr_example.yml new file mode 100644 index 000000000..d98c63fda --- /dev/null +++ b/.github/workflows/ci_pr_example.yml @@ -0,0 +1,20 @@ +name: Build Example app + +on: pull_request + +# This allows a subsequently queued workflow run to interrupt previous runs +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' + cancel-in-progress: true + +jobs: + tests: + name: Build Example app + runs-on: macos-15 + steps: + - name: Checkout the Git repository + uses: actions/checkout@v4 + with: + fetch-depth: 10 + - name: Build and run example project + run: make build_example diff --git a/.github/workflows/ci_pr_framework.yml b/.github/workflows/ci_pr_framework.yml new file mode 100644 index 000000000..08bd9b031 --- /dev/null +++ b/.github/workflows/ci_pr_framework.yml @@ -0,0 +1,18 @@ +name: Build Framework + +on: pull_request + +# This allows a subsequently queued workflow run to interrupt previous runs +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' + cancel-in-progress: true + +jobs: + tests: + name: Build Framework + runs-on: macos-15 + steps: + - name: Checkout the Git repository + uses: actions/checkout@v4 + - name: Build framework + run: make framework diff --git a/.github/workflows/ci_pr_tests.yml b/.github/workflows/ci_pr_tests.yml new file mode 100644 index 000000000..cd53f5472 --- /dev/null +++ b/.github/workflows/ci_pr_tests.yml @@ -0,0 +1,18 @@ +name: Tests + +on: pull_request + +# This allows a subsequently queued workflow run to interrupt previous runs +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' + cancel-in-progress: true + +jobs: + tests: + name: Run Tests + runs-on: macos-15 + steps: + - name: Checkout the Git repository + uses: actions/checkout@v4 + - name: Build and run tests + run: make test diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml new file mode 100644 index 000000000..a05b6143f --- /dev/null +++ b/.github/workflows/deploy_docs.yml @@ -0,0 +1,39 @@ +name: Deploy DocC + +on: + push: + branches: + - "main" + +permissions: + contents: write + +# This allows a subsequently queued workflow run to interrupt previous runs +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' + cancel-in-progress: true + +jobs: + build_docs: + runs-on: macos-15 + steps: + - name: Checkout πŸ›ŽοΈ + uses: actions/checkout@v4 + + - name: Build DocC + run: | + swift package resolve; + xcodebuild docbuild -scheme MessageKit -derivedDataPath /tmp/docbuild -destination 'generic/platform=iOS'; + $(xcrun --find docc) process-archive \ + transform-for-static-hosting /tmp/docbuild/Build/Products/Debug-iphoneos/MessageKit.doccarchive \ + --hosting-base-path MessageKit \ + --output-path docs + $(xcrun --find docc) process-archive \ + transform-for-static-hosting /tmp/docbuild/Build/Products/Debug-iphoneos/InputBarAccessoryView.doccarchive \ + --hosting-base-path MessageKit/InputBarAccessoryView \ + --output-path docs/InputBarAccessoryView + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./docs diff --git a/.gitignore b/.gitignore index 7cc39ae50..65697789f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ ## Build generated build/ DerivedData +.build/ +docs/ +docc/ ## Various settings *.pbxuser @@ -29,7 +32,12 @@ xcuserdata *.ipa # CocoaPods -Pods/ +Example/Pods/* +!Example/Pods/SwiftLint # Carthage Carthage + +# SPM +.swiftpm +Package.resolved diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 000000000..6a7941eb6 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,5 @@ +version: 1 +builder: + configs: + - platform: ios + documentation_targets: [MessageKit] diff --git a/.swift-version b/.swift-version deleted file mode 100644 index bf77d5496..000000000 --- a/.swift-version +++ /dev/null @@ -1 +0,0 @@ -4.2 diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 000000000..e6601d9ba --- /dev/null +++ b/.swiftformat @@ -0,0 +1,82 @@ +# options +--swiftversion 5.6 +--self remove # redundantSelf +--importgrouping testable-bottom # sortedImports +--commas always # trailingCommas +--trimwhitespace always # trailingSpace +--indent 2 #indent +--ifdef no-indent #indent +--indentstrings true #indent +--wraparguments before-first # wrapArguments +--wrapparameters before-first # wrapArguments +--wrapcollections before-first # wrapArguments +--wrapconditions before-first # wrapArguments +--wrapreturntype if-multiline #wrapArguments +--closingparen same-line # wrapArguments +--wraptypealiases before-first # wrapArguments +--funcattributes prev-line # wrapAttributes +--typeattributes prev-line # wrapAttributes +--wrapternary before-operators # wrap +--structthreshold 20 # organizeDeclarations +--enumthreshold 20 # organizeDeclarations +--organizetypes class,struct,enum,extension,actor # organizeDeclarations +--extensionacl on-declarations # extensionAccessControl +--patternlet inline # hoistPatternLet +--redundanttype inferred # redundantType +--emptybraces spaced # emptyBraces +--operatorfunc spaced + +# We recommend a max width of 100 but _strictly enforce_ a max width of 130 +--maxwidth 130 # wrap + +# file options +--exclude Package.swift + +# rules +--rules anyObjectProtocol +--rules blankLinesBetweenScopes +--rules consecutiveSpaces +--rules consecutiveBlankLines +--rules duplicateImports +--rules extensionAccessControl +--rules hoistPatternLet +--rules indent +--rules markTypes +--rules organizeDeclarations +--rules redundantParens +--rules redundantReturn +--rules redundantSelf +--rules redundantType +--rules redundantPattern +--rules redundantGet +--rules redundantFileprivate +--rules redundantRawValues +--rules sortedImports +--rules sortDeclarations +--rules strongifiedSelf +--rules trailingCommas +--rules trailingSpace +--rules typeSugar +--rules wrap +--rules wrapMultilineStatementBraces +--rules wrapArguments +--rules wrapAttributes +--rules braces +--rules redundantClosure +--rules redundantInit +--rules redundantVoidReturnType +--rules unusedArguments +--rules spaceInsideBrackets +--rules spaceInsideBraces +--rules spaceAroundBraces +--rules spaceInsideParens +--rules spaceAroundParens +--rules enumNamespaces +--rules blockComments +--rules spaceAroundComments +--rules spaceInsideComments +--rules spaceAroundOperators +--rules blankLinesAtStartOfScope +--rules blankLinesAtEndOfScope +--rules emptyBraces +--rules andOperator \ No newline at end of file diff --git a/.swiftlint.yml b/.swiftlint.yml index 8bf8a3da0..62d8e9482 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,20 +1,40 @@ +only_rules: + - colon + - fatal_error_message + - implicitly_unwrapped_optional + - legacy_cggeometry_functions + - legacy_constant + - legacy_constructor + - legacy_nsgeometry_functions + - operator_usage_whitespace + - return_arrow_whitespace + - trailing_newline + - unused_optional_binding + - vertical_whitespace + - void_return + - custom_rules + +excluded: + - "**/Package.swift" + - "**/.build" + - "Tests" + - "Example" + +colon: + apply_to_dictionaries: false + +indentation: 2 -disabled_rules: - - identifier_name - - trailing_whitespace - - line_length - - type_body_length - - file_length custom_rules: - override_func: # rule identifier - name: "override in func" # rule name. optional. - regex: "override (open|public|private|internal|fileprivate)" # matching pattern - message: "Use like open override or public override instead" # violation message. optional. - severity: warning # violation severity. optional. -opt_in_rules: - - explicit_acl - - explicit_top_level_acl -explicit_acl: error -explicit_top_level_acl: error -included: - - Sources \ No newline at end of file + no_objcMembers: + name: "@objcMembers" + regex: "@objcMembers" + message: "Explicitly use @objc on each member you want to expose to Objective-C" + severity: error + no_direct_standard_out_logs: + name: "Writing log messages directly to standard out is disallowed" + regex: "(\\bprint|\\bdebugPrint|\\bdump|Swift\\.print|Swift\\.debugPrint|Swift\\.dump)\\s*\\(" + match_kinds: + - identifier + message: "Don't commit `print(…)`, `debugPrint(…)`, or `dump(…)` as they write to standard out in release. Either log to a dedicated logging system or silence this warning in debug-only scenarios explicitly using `// swiftlint:disable:next no_direct_standard_out_logs`" + severity: warning diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8b89d351a..000000000 --- a/.travis.yml +++ /dev/null @@ -1,53 +0,0 @@ -language: objective-c -osx_image: xcode8.1 - -env: - global: - - LANG=en_US.UTF-8 - - - PROJECT="MessageKit.xcodeproj" - - IOS_SCHEME="MessageKit" - - IOS_SDK=iphonesimulator10.1 - - matrix: - - - DESTINATION="OS=9.0,name=iPhone 6 Plus" SDK="$IOS_SDK" SCHEME="$IOS_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="YES" POD_LINT="NO" RUN_UI_TESTS="YES" - - DESTINATION="OS=9.1,name=iPhone 6s" SDK="$IOS_SDK" SCHEME="$IOS_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="NO" POD_LINT="NO" RUN_UI_TESTS="NO" - - DESTINATION="OS=9.2,name=iPhone 6s" SDK="$IOS_SDK" SCHEME="$IOS_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="NO" POD_LINT="NO" RUN_UI_TESTS="NO" - - DESTINATION="OS=9.3,name=iPad Pro" SDK="$IOS_SDK" SCHEME="$IOS_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="NO" POD_LINT="NO" RUN_UI_TESTS="NO" - - - DESTINATION="OS=10.0,name=iPhone 6s" SDK="$IOS_SDK" SCHEME="$IOS_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="YES" POD_LINT="NO" RUN_UI_TESTS="YES" - - DESTINATION="OS=10.1,name=iPhone 7" SDK="$IOS_SDK" SCHEME="$IOS_SCHEME" RUN_TESTS="YES" BUILD_EXAMPLE="NO" POD_LINT="NO" RUN_UI_TESTS="NO" - -script: - -- if [ $POD_LINT == "YES" ]; then - pod lib lint; - fi - - -- if [ $BUILD_EXAMPLE == "YES" ]; then - xcodebuild build analyze -project Example/ChatExample.xcodeproj -scheme ChatExample -sdk "$SDK" -destination "$DESTINATION" ONLY_ACTIVE_ARCH=NO CODE_SIGNING_REQUIRED=NO | xcpretty -c; - fi - - -- if [ $RUN_TESTS == "YES" ]; then - xcodebuild analyze test -project "$PROJECT" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO CODE_SIGNING_REQUIRED=NO | xcpretty -c; - else - xcodebuild build analyze -project "$PROJECT" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO CODE_SIGNING_REQUIRED=NO | xcpretty -c; - fi - - -- if [ $RUN_UI_TESTS == "YES" ]; then - xcodebuild test -project Example/ChatExample.xcodeproj -scheme ChatExampleUITests -sdk "$SDK" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO | xcpretty -c; - fi - - -# Build for reporting test coverage -- if [ $RUN_TESTS == "YES" ]; then - xcodebuild test -project MessageKit.xcodeproj -scheme MessageKit -destination "platform=iOS Simulator,name=iPhone 7" CODE_SIGNING_REQUIRED=NO; - fi - - -after_success: -- bash <(curl -s https://codecov.io/bash) diff --git a/Assets/CellStructure.png b/Assets/CellStructure.png new file mode 100644 index 000000000..90e453a70 Binary files /dev/null and b/Assets/CellStructure.png differ diff --git a/Assets/ExampleA.png b/Assets/ExampleA.png new file mode 100644 index 000000000..88b0a4dc9 Binary files /dev/null and b/Assets/ExampleA.png differ diff --git a/Assets/ExampleB.png b/Assets/ExampleB.png new file mode 100644 index 000000000..5b5ce8c60 Binary files /dev/null and b/Assets/ExampleB.png differ diff --git a/Assets/InputBarAccessoryViewLayout.png b/Assets/InputBarAccessoryViewLayout.png new file mode 100644 index 000000000..c0cb30bbe Binary files /dev/null and b/Assets/InputBarAccessoryViewLayout.png differ diff --git a/Assets/MessageInputBarLayout.png b/Assets/MessageInputBarLayout.png deleted file mode 100644 index 1ef5e1c38..000000000 Binary files a/Assets/MessageInputBarLayout.png and /dev/null differ diff --git a/Assets/TypingIndicator.png b/Assets/TypingIndicator.png new file mode 100644 index 000000000..94c6fd861 Binary files /dev/null and b/Assets/TypingIndicator.png differ diff --git a/CHANGELOG.md b/CHANGELOG.md index 2004f327f..9cdd2b6c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,341 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/MessageKit/MessageKit/releases) on GitHub. --------------------------------------- +## Future release + +### Added + +### Fixed + +### Updated + +- Update InputBarAccessoryView to fix some crashes and issues by [@kaspik](https://github.com/Kaspik) + +### Changed + +### Removed + +## 4.3.0 +- Fix for SwiftUI example IBAV position issues (example app) by @Janneman84 in https://github.com/MessageKit/MessageKit/pull/1807 +- Added Coursicle to the list of apps using MessageKit by @monstermac77 in https://github.com/MessageKit/MessageKit/pull/1809 +- duration NaN issue fix by @kkarakamis in https://github.com/MessageKit/MessageKit/pull/1812 +- Add OutyPlay to list of apps using MessageKit by @fabdurso in https://github.com/MessageKit/MessageKit/pull/1820 +- Fix #816 by @RomanPodymov in https://github.com/MessageKit/MessageKit/pull/1819 +- Update Makefile by @Kaspik in https://github.com/MessageKit/MessageKit/pull/1833 +- Added ability to specify additionalBottomSpace for keyboardManager by @Almaz5200 in https://github.com/MessageKit/MessageKit/pull/1821 +- build(deps): Bump rexml from 3.2.5 to 3.2.8 by @dependabot in https://github.com/MessageKit/MessageKit/pull/1839 +- Added listener for keyboard input mode changes (e.g. emoji keyboard) by @raulolmedocheca in https://github.com/MessageKit/MessageKit/pull/1842 +- build(deps): Bump rexml from 3.2.8 to 3.3.6 by @dependabot in https://github.com/MessageKit/MessageKit/pull/1858 +- Fix for overlapping detected matches by @SkiTles55 in https://github.com/MessageKit/MessageKit/pull/1853 +- Fix timestamp label layout when not in fullscreen by @CocoaBob in https://github.com/MessageKit/MessageKit/pull/1854 +- ♻️ Rename plugins to avoid clashing with SwiftLintPlugins by @technocidal in https://github.com/MessageKit/MessageKit/pull/1862 +- Added custom image masking by @GitNirajHub in https://github.com/MessageKit/MessageKit/pull/1860 +- Update to Swift 5.10 + +## 4.2.0 + +### Added + +### Fixed + +- Fix Advanced example hiding indicator [#1792](https://github.com/MessageKit/MessageKit/pull/1792) by [@kaspik](https://github.com/Kaspik) +- Fix hiding typing indicator crash [#1804](https://github.com/MessageKit/MessageKit/pull/1804) by [@Zandor300](https://github.com/Zandor300) + +### Changed + +- Update Github Actions and bump dependencies by [@kaspik](https://github.com/Kaspik) + +### Removed + +## 4.1.0 + +### Added + +- Swiftformat and Swiftlint SwiftPM plugins used for linting and formatting the codebase [#1729](https://github.com/MessageKit/MessageKit/pull/1729) by [@martinpucik](https://github.com/martinpucik) + +### Fixed + +- Fixed iOS 13 deprecation warnings [#1730](https://github.com/MessageKit/MessageKit/pull/1730) by [@kaspik](https://github.com/Kaspik) +- SwiftPM plugins setup [#1732](https://github.com/MessageKit/MessageKit/pull/1732) by [@martinpucik](https://github.com/martinpucik) + +### Changed + +- Updated InputBarAccessoryView to v6.1.1 by [@kaspik](https://github.com/Kaspik) + +## 4.0.0 + +Version 4.0.0 comes with couple of breaking changes, please refer to [MIGRATION_GUIDE.md](https://github.com/MessageKit/MessageKit/blob/main/Documentation/MIGRATION_GUIDE.md) for easy transition from V3 to V4. + +### Added + +- New method in `MessagesLayoutDelegate` for setting message avatar size [ddfc814](https://github.com/MessageKit/MessageKit/commit/ddfc814d325ee5aa238484c90128d32e5a72a49b) by [@martinpucik](https://github.com/martinpucik) +- `MessageInputBarKind` enum for customizing `messageInputBar` inside `inputContainerView` [#1707](https://github.com/MessageKit/MessageKit/pull/1707) by [@martinpucik](https://github.com/martinpucik) + +### Changed + +- **Breaking change**: Dropped CocoaPods support +- **Breaking change**: Dropped support for iOS 12 [2bd234b](https://github.com/MessageKit/MessageKit/commit/2bd234b1e878f392089f166d6868ce644d6c9e95) by [@martinpucik](https://github.com/martinpucik) +- **Breaking change**: Moved messageInputBar from inputAccessoryView to a subview in MessagesViewController [#1704](https://github.com/MessageKit/MessageKit/pull/1704) by [@martinpucik](https://github.com/martinpucik) +- **Breaking change**: Renamed `func currentSender() -> SenderType` to `var currentSender: SenderType` [#1714](https://github.com/MessageKit/MessageKit/pull/1714) by [@martinpucik](https://github.com/martinpucik) +- **Deprecation**: Deprecated `maintainPositionOnKeyboardFrameChangedMoved` in favor of `maintainPositionOnInputBarHeightChanged` which better describes the intended use of this property [#1704](https://github.com/MessageKit/MessageKit/pull/1705) by [@martinpucik](https://github.com/martinpucik) +- **Breaking change**: Added an argument to `messageContainerMaxWidth` [cd4f75b](https://github.com/MessageKit/MessageKit/commit/cd4f75b561129fc25e6c4576000e5a92ccd81cad) by [@martinpucik](https://github.com/martinpucik) + ```swift + MessageSizeCalculator.messageContainerMaxWidth(for message: MessageType) -> CGFloat + ``` + now has IndexPath argument + ```swift + MessageSizeCalculator.messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat + ``` +- **Breaking change**: Added an argument to `messageContainerSize` [cd4f75b](https://github.com/MessageKit/MessageKit/commit/cd4f75b561129fc25e6c4576000e5a92ccd81cad) by [@martinpucik](https://github.com/martinpucik) + ```swift + MessageSizeCalculator.messageContainerSize(for message: MessageType) -> CGSize + ``` + now has IndexPath argument + ```swift + MessageSizeCalculator.messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize + ``` +- Updated InputBarAccessoryView to v6.1.0 [#1716](https://github.com/MessageKit/MessageKit/pull/1716) by [@martinpucik](https://github.com/martinpucik) +- Observe inputBar frame change to update collectionView bottom inset instead of keyboard show/hide notifications [#1726](https://github.com/MessageKit/MessageKit/pull/1726) by [@martinpucik](https://github.com/martinpucik) + +### Fixed + +- Fixed iOS 13 deprecation warnings [#1715](https://github.com/MessageKit/MessageKit/pull/1715) by [@kaspik](https://github.com/Kaspik) +- Updating bottom chat collectionView inset after InputBar container view frame change [#1725](https://github.com/MessageKit/MessageKit/pull/1725) by [@martinpucik](https://github.com/martinpucik) + +### Removed + +- NSConstraintLayoutSet.swift [#1700](https://github.com/MessageKit/MessageKit/pull/1700) by [@martinpucik](https://github.com/martinpucik) +- Deprecated `Sender` struct. Clients should use `SenderType` protocol [#1713](https://github.com/MessageKit/MessageKit/pull/1713) by [@martinpucik](https://github.com/martinpucik) +- Unavailable `MessageInputBar` and `MessageInputBarDelegate`. Clients should use `InputBarAccessoryView` and `InputBarAccessoryViewDelegate` [#1713](https://github.com/MessageKit/MessageKit/pull/1713) by [@martinpucik](https://github.com/martinpucik) +- `func scrollToBottom(animated:)` on `MessagesCollectionView`. Clients should use `func scrollToLastItem(:)` [#1713](https://github.com/MessageKit/MessageKit/pull/1713) by [@martinpucik](https://github.com/martinpucik) + +## 3.8.0 + +### Added + +- New methods in `MessagesLayoutDelegate` for adjusting alignment of message top and bottom labels [#1671](https://github.com/MessageKit/MessageKit/pull/1671) by [@martinpucik](https://github.com/martinpucik) + +### Removed + +- Not used workspace files in example project [#1671](https://github.com/MessageKit/MessageKit/pull/1671) by [@martinpucik](https://github.com/martinpucik) + +## 3.7.0 + +### Fixed + +- Updated InputBarAccessoryView to 5.4.0 with XCode 13 support +- Fixed Example project loading MessageKit through SPM +- Make sure MessageKit works on XCode 13 correctly + +## 3.6.1 + +### Added + +- Added enough data source and delegate methods to display customized `UICollectionViewCell` for MessageTypes other than `.custom` in [#1577](https://github.com/MessageKit/MessageKit/pull/1577) by [@jvigneshcs](https://github.com/jvigneshcs) + +## 3.6.0 + +### Fixed + +- Fixes an issue with Scroll problem on new messages with keyboard open [#1529](https://github.com/MessageKit/MessageKit/pull/1529) by [@politan8](https://github.com/politan8) + +- Fixes time stamp vertical alignment so labels align with messages when showMessageTimestampOnSwipeLeft is true. + by [@kurtsequoia](https://github.com/MessageKit/MessageKit/pull/1556) + +- **Breaking Change** Changed `MessagesLayoutDelegate`'s method for typing indicator size. Typing indicator's size is now correctly calculated based on delegate method `func typingIndicatorViewSize(for layout: MessagesCollectionViewFlowLayout) -> CGSize` [#1563](https://github.com/MessageKit/MessageKit/pull/1563) by [@kaspik](https://github.com/kaspik) + +### Added + +### Changed + +- Changed `resource_bundle` back to `resources` in MessageKit.podspec [#1565](https://github.com/MessageKit/MessageKit/pull/1565) by [@kaspik](https://github.com/kaspik) + +### Removed + +## 3.5.1 + +### Fixed +- `MessagesViewController` now smoothly scrolls messages off screen. [1531](https://github.com/MessageKit/MessageKit/issues/1531) & [1547](https://github.com/MessageKit/MessageKit/pull/1547) by [@mredig](https://github.com/mredig) + +### Changed +- Bump `InputBarAccessoryView` + +## 3.5.0 + +### Fixed + +- maintainPositionOnKeyboardFrameChanged for small contentSize would scroll content out of bounds [#1506](https://github.com/MessageKit/MessageKit/pull/1506) by [@martinpucik](https://github.com/martinpucik) + +### Added + +### Changed + +- Changed `resources` to `resource_bundle` in MessageKit.podspec [#1460](https://github.com/MessageKit/MessageKit/pull/1460) by [@martinpucik](https://github.com/martinpucik) +- Changed dependency manager for Example project to SPM [#1504](https://github.com/MessageKit/MessageKit/pull/1504) by [@martinpucik](https://github.com/martinpucik) +- Deprecated `messagesCollectionView.scrollToBottom` and `scrollsToBottomOnKeyboardBeginsEditing` in favor of `messagesCollectionView.scrollToLastItem` and `scrollsToLastItemOnKeyboardBeginsEditing`. This will be removed in a future release [#1505](https://github.com/MessageKit/MessageKit/pull/1505) by [@martinpucik](https://github.com/martinpucik) + +### Removed + +## 3.4.2 + +- Updated `InputBarAccessoryView` to 5.2.1 with fixed warning on XCode 12 when used via SPM + +## 3.4.1 + +### Fixed + +- Fixes an issue with casting MessageType to a custom type when using LinkPreview cells [#1469](https://github.com/MessageKit/MessageKit/pull/1469) by [@kinoroy](https://github.com/kinoroy) + +- Fixes an issue where the MessagesViewController keyboard observers were not cleaned up when MessagesViewController was no longer visible on screen [#1476](https://github.com/MessageKit/MessageKit/pull/1476) by [@kinoroy](https://github.com/kinoroy) + +## 3.4.0 + +### Changed + +- **Breaking Change** Dropped support for iOS 11, added support for Swift 5.3 SPM and XCode 12 [#1464](https://github.com/MessageKit/MessageKit/pull/1464) by [@kaspik](https://github.com/kaspik) + +## 3.3.0 + +### Fixed + +- Fixes missing insets for link preview messages [#1447](https://github.com/MessageKit/MessageKit/pull/1447) by [@bguidolim](https://github.com/bguidolim) + +### Added + +- Show message time by swiping left over the chat controller. [#1444](https://github.com/MessageKit/MessageKit/pull/1444) by [@amirpirzad](https://github.com/amirpirzad) + +## 3.2.0 + +### Fixed + +- Fixes an incorrect animation of message cells while dragging to dismiss the keyboard [#1433](https://github.com/MessageKit/MessageKit/pull/1433) by [@lhr000lhrmega](https://github.com/lhr000lhrmega) +- Fixes an issue where the video message playback icon was too dark when in dark mode [#1386](https://github.com/MessageKit/MessageKit/pull/1386) by [@kinoroy](https://github.com/kinoroy) +- Fixes an issue where the video message playback button triangle was not centered within the circle [#1386](https://github.com/MessageKit/MessageKit/pull/1386) by [@kinoroy](https://github.com/kinoroy) + +### Added + +- Added option to use Photo messages with remote image URL in Example project [#1294](https://github.com/MessageKit/MessageKit/pull/1294) by [@martinpucik](https://github.com/martinpucik) +- **Breaking Change** Added new `linkPreview` message type, which display a subclass of `TextMessageCell` with support to present title, teaser and a thumbnail image for a link [#1310](https://github.com/MessageKit/MessageKit/pull/1310) by [@bguidolim](https://github.com/bguidolim) +- Added a SwiftUI view using MessageKit in the Example Project by [#1410](https://github.com/MessageKit/MessageKit/pull/1410) [@kinoroy](https://github.com/kinoroy) + +### Changed + +- **Breaking Change** Dropped support for iOS 9 and iOS 10 [#1261](https://github.com/MessageKit/MessageKit/pull/1261) by [@kaspik](https://github.com/kaspik) +- Converted internal, static colors into color assets to better support dark and high contrast modes [#1386](https://github.com/MessageKit/MessageKit/pull/1386) by [@kinoroy](https://github.com/kinoroy) +- Change the video message playback button to use a UIVisualEffectsView to better match the look and feel of iMessage. [#1386](https://github.com/MessageKit/MessageKit/pull/1386) by [@kinoroy](https://github.com/kinoroy) + +## 3.1.0 + +### Fixed + + - Set the proper notification to invalidate layout. MessageKit now relies on `UIApplication` orientation notification instead of `UIDevice`, which invalidates the layout only when it is needed. [#1126](https://github.com/MessageKit/MessageKit/pull/1126) by [@bguidolim](https://github.com/bguidolim) + + - Fixed `requiredInitialScrollViewBottomInset` when `inputAccessoryView` is `nil` [#1218](https://github.com/MessageKit/MessageKit/pull/1218) by [@aabosh](https://github.com/aabosh) + + - Fixed `MessagesCollectionView.scrollToBottom(animated:)` method to properly handle calls made early in the view lifecycle. [#1110](https://github.com/MessageKit/MessageKit/pull/1110) by [@marcetcheverry](https://github.com/marcetcheverry) + + - Fixed `TypingIndicator` `dotColor` for light mode. [#1266](https://github.com/MessageKit/MessageKit/pull/1266) by [@lewis-smith](https://github.com/lewis-smith) + +### Added + +- Add missing textAlignment and textInsets assignments to layoutCellTopLabel method in MessageContentCell. [#1117](https://github.com/MessageKit/MessageKit/pull/1117) by [@mdescalzo](https://github.com/mdescalzo) + +- Add support for styling NSLinkAttribute with existing urlAttributes in MessageLabel. [#1091](https://github.com/MessageKit/MessageKit/pull/1091) by [@marcetcheverry](https://github.com/marcetcheverry) + +- Add loading indicator to AudioMessageCell. [#1084](https://github.com/MessageKit/MessageKit/pull/1084) by [@marcetcheverry](https://github.com/marcetcheverry) + +- Add support for Dark Mode [#1189](https://github.com/MessageKit/MessageKit/pull/1189) by [@Vlada31R](https://github.com/Vlada31R) + +- Add support for `scrollToLastItem` and `scrollsToLastItemOnKeyboardBeginsEditing` [#1247](https://github.com/MessageKit/MessageKit/pull/1247) by [@hyouuu](https://github.com/hyouuu) + +- Added `MessageCellDelegate.didTapImage(in cell: MessageCollectionViewCell)` [#1166](https://github.com/MessageKit/MessageKit/pull/1166) by [@domeniconicoli](https://github.com/domeniconicoli), [#1278](https://github.com/MessageKit/MessageKit/pull/1278) by [@bguidolim](https://github.com/bguidolim), [1285](https://github.com/MessageKit/MessageKit/pull/1285) by [@austinwright](https://github.com/austinwright) + +- Added missing cellTopLabelAlignment to MessageSizeCalculator. [#1113](https://github.com/MessageKit/MessageKit/pull/1113) by [@marcetcheverry](https://github.com/marcetcheverry) + +### Changed + +- **Breaking Change** Updated to Swift 5.0 [#1039](https://github.com/MessageKit/MessageKit/pull/1039) by [@nathantannar4](https://github.com/nathantannar4) + +- Lazily initialize the MessageInputBar on MessagesViewController. [#1092](https://github.com/MessageKit/MessageKit/pull/1092) by [@marcetcheverry](https://github.com/marcetcheverry) + +### Deprecated + +- Deprecated `SenderType.id` in favor of `SenderType.senderId`. This change was previously meant for 3.0.0. [#1201](https://github.com/MessageKit/MessageKit/pull/1201) by [@kinoroy](https://github.com/kinoroy) + +### Removed + +- **Breaking Change** `MessageInputBar`, and `MessageInputBarDelegate` have been obsoleted. Use `InputBarAccessoryView` and `InputBarAccessoryViewDelegate` respectively. This change was previously meant for 3.0.0. [#1201](https://github.com/MessageKit/MessageKit/pull/1201) by [@kinoroy](https://github.com/kinoroy) + +## 3.0.0 + +### Dependency Changes + +- **Breaking Change** The dependency `MessageInputBar` was replaced with `InputBarAccessoryView`. As `MessageInputBar` was previously a fork this means no functionality has been lost but improvements and bug fixes will be present. `InputBarAccessoryView` has more of a following outside of `MessageKit` making its development faster than `MessageInputBar`. Maintaining two versions only increased the workload. You can find the changelog for `InputBarAccessoryView` [here](https://github.com/nathantannar4/InputBarAccessoryView/blob/master/CHANGELOG.md). + +### Changed + +- **Breaking Change** Deprecated the Sender struct in favor of the `SenderType` protocol. +[#909](https://github.com/MessageKit/MessageKit/pull/909) by [@nathantannar4](https://github.com/nathantannar4) + +- **Breaking Change** Deprecated the Sender struct in favor of the `SenderType` protocol. [#909](https://github.com/MessageKit/MessageKit/pull/909) by [@nathantannar4](https://github.com/nathantannar4) + +- **Breaking Change** Add support for audio messages. Added new protocols `AudioControllerDelegate`, `AudioItem` a new cell `AudioMessageCell` and a new controller `BasicAudioController`. +[#892](https://github.com/MessageKit/MessageKit/pull/892) by [@moldovaniosif](https://github.com/moldovaniosif). + +- **Breaking Change** Moved `handleTapGesture` method to `MessageCollectionViewCell` +[#950](https://github.com/MessageKit/MessageKit/pull/950) by [@nathantannar4](https://github.com/nathantannar4) + +- **Breaking Change** Renamed function `layoutBottomLabel(with:)` to `layoutMessageBottomLabel(with:)` in `MessageContentCell` class. +[#920](https://github.com/MessageKit/MessageKit/pull/920) by [@maxxx777](https://github.com/maxxx777) + +### Added + +- **Breaking Change** Add support for share contact. [#1013](https://github.com/MessageKit/MessageKit/pull/1013) by [@moldovaniosif](https://github.com/moldovaniosif) + +- Added typing indicator support, `func setTypingIndicatorViewHidden(_ isHidden: Bool, animated: Bool, whilePerforming updates: (() -> Void)? = nil, completion: ((Bool) -> Void)? = nil)`. Return a custom typing view by conforming to `MessagesDisplayDelegate` or use the [default appearance](https://github.com/nathantannar4/TypingIndicator). Customize the size with `MessagesLayoutDelegate` . +[#989](https://github.com/MessageKit/MessageKit/pull/911) by [@nathantannar4](https://github.com/nathantannar4) + +- Added `AccessoryPosition` class. +[#989](https://github.com/MessageKit/MessageKit/pull/989) by [@subdiox](https://github.com/subdiox) + +- Added `incomingAccessoryViewPosition` and `outgoingAccessoryViewPosition` variables to `MessageSizeCalculator` class. +[#989](https://github.com/MessageKit/MessageKit/pull/989) by [@subdiox](https://github.com/subdiox) + +- Added `setMessageIncomingAccessoryViewPosition(_:)` and `setMessageOutgoingAccessoryViewPosition(_:)` functions to `MessagesCollectionViewFlowLayout` class. +[#989](https://github.com/MessageKit/MessageKit/pull/989) by [@subdiox](https://github.com/subdiox) + +- **Breaking Change** Added `avatarLeadingTrailingPadding` as a property of `CellSizeCalculator` and `MessagesCollectionViewLayoutAttributes` to inset the `AvatarView` layout +[#944](https://github.com/MessageKit/MessageKit/pull/944) by [@nathantannar4](https://github.com/nathantannar4) + +- **Breaking Change** Added `didTapBackground(in:)` function to `MessageCellDelegate` protocol. +[#922](https://github.com/MessageKit/MessageKit/pull/922) by [@kpennacchia](https://github.com/kpennacchia) + +- **Breaking Change** Added `didTapCellBottomLabel(in:)` function to `MessageCellDelegate` protocol. +[#920](https://github.com/MessageKit/MessageKit/pull/920) by [@maxxx777](https://github.com/maxxx777) + +- **Breaking Change** Added `cellBottomLabelAttributedText(for:, at:)` function to `MessagesDataSource` protocol. +[#920](https://github.com/MessageKit/MessageKit/pull/920) by [@maxxx777](https://github.com/maxxx777) + +- **Breaking Change** Added `cellBottomLabelHeight(for:, at:, in messagesCollectionView:)` function to `MessagesLayoutDelegate` protocol. +[#920](https://github.com/MessageKit/MessageKit/pull/920) by [@maxxx777](https://github.com/maxxx777) + +- Added `cellBottomLabel` to `MessageContentCell`. +[#920](https://github.com/MessageKit/MessageKit/pull/920) by [@maxxx777](https://github.com/maxxx777) + +- Added `layoutCellBottomLabel(with:)` function to `MessageContentCell` class. +[#920](https://github.com/MessageKit/MessageKit/pull/920) by [@maxxx777](https://github.com/maxxx777) + +- Added `setMessageIncomingCellBottomLabelAlignment(_:)` and `setMessageOutgoingCellBottomLabelAlignment(_:)` functions to `MessagesCollectionViewFlowLayout` class. +[#920](https://github.com/MessageKit/MessageKit/pull/920) by [@maxxx777](https://github.com/maxxx777) + +- Added `cellBottomLabelAlignment` and `cellBottomLabelSize` variables to `MessagesCollectionViewLayoutAttributes` class. +[#920](https://github.com/MessageKit/MessageKit/pull/920) by [@maxxx777](https://github.com/maxxx777) + +- Added `incomingCellBottomLabelAlignment` and `outgoingCellBottomLabelAlignment` variables to `MessageSizeCalculator` class. +[#920](https://github.com/MessageKit/MessageKit/pull/920) by [@maxxx777](https://github.com/maxxx777) + +- Added `cellBottomLabelSize(for:, at:)` and `cellBottomLabelAlignment(for:)` functions to `MessageSizeCalculator` class. +[#920](https://github.com/MessageKit/MessageKit/pull/920) by [@maxxx777](https://github.com/maxxx777) ## [2.0.0](https://github.com/MessageKit/MessageKit/releases/tag/2.0.0) @@ -18,6 +352,11 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa ## [2.0.0-beta.1](https://github.com/MessageKit/MessageKit/releases/tag/2.0.0-beta.1) +### Fixed + +- Fixed `boundingRect(with:options:)` miscalculation of `MessageLabel` by using `NSLayoutManager`, like text `αŠΛ˜Μ΄ΝˆΜκˆŠΛ˜Μ΄ΝˆΜ€αŠβ‹†βœ©`、`Tomorrow is the day`. +[#824](https://github.com/MessageKit/MessageKit/pull/824) by [@zhongwuzw](https://github.com/zhongwuzw). + ### Changed - **Breaking Change** Updated codebase to Swift 4.2 [#883](https://github.com/MessageKit/MessageKit/pull/883) by [@nathantannar4](https://github.com/nathantannar4) @@ -27,13 +366,16 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa ### Added +- **Breaking Change** Added `.hashtag`, .`mention` to detect theses pattern inside the `messageLabel`. We also add `.custom(pattern: YOUR_PATTERN)` to `DetectorType` to manage and deal with your own regular expression. +[#913](https://github.com/MessageKit/MessageKit/pull/913) by [@JulienKode](https://github.com/julienkode). + - Added support for detection and handling of `NSLink`s inside of messages. [#815](https://github.com/MessageKit/MessageKit/pull/815) by [@jnic](https://github.com/jnic) - Added customizable `accessoryView`, with a new `MessagesDisplayDelegate` function `configureAccessoryView`, and corresponding size & padding properties in `MessageSizeCalculator`. The `accessoryView` is aligned to the center of the `messageContainerView`. [#710](https://github.com/MessageKit/MessageKit/pull/710) by [@hyouuu](https://github.com/hyouuu) -- Added a tap gesture recognition to the `accessoryView` which calls the `MessageCellDelagate` function `didTapAccessoryView(in:)`. +- Added a tap gesture recognition to the `accessoryView` which calls the `MessageCellDelegate` function `didTapAccessoryView(in:)`. [#834](https://github.com/MessageKit/MessageKit/pull/834) by [@nathantannar4](https://github.com/nathantannar4) - Added `additionalBottomInset` property that allows to adjust the bottom content inset automatically set on the messages collection view by the view controller. @@ -41,7 +383,7 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa ### Fixed -- **Breaking Change** Fixed typo of `scrollsToBottomOnKeybordBeginsEditing` to `scrollsToBottomOnKeyboardBeginsEditing`. +- **Breaking Change** Fixed typo of `scrollsToBottomOnKeyboardBeginsEditing` to `scrollsToBottomOnKeyboardBeginsEditing`. [#856](https://github.com/MessageKit/MessageKit/pull/856) by [@p-petrenko](https://github.com/p-petrenko) - Fixed a bug that prevented `MessageLabel` from laying out properly when contained by superviews using autolayout. @@ -193,7 +535,7 @@ You must now set this font explicitly through the `emojiMessageSizeCalculator` o - **Breaking Change** Removed the `showsDateHeaderAfterTimeInterval` property of `MessagesCollectionView`. [#615](https://github.com/MessageKit/MessageKit/pull/615) by [@SD10](https://github.com/sd10). -- **Breaking Change** Removed the `reuseIdentifer` method from `MessageCollectionViewCell`, `TextMessageCell`, +- **Breaking Change** Removed the `reuseIdentifier` method from `MessageCollectionViewCell`, `TextMessageCell`, `LocationMessageCell`, `MediaMessageCell`, and `MessageContentCell`. [#615](https://github.com/MessageKit/MessageKit/pull/615) by [@SD10](https://github.com/sd10). @@ -399,8 +741,8 @@ typed as `MessageLabel` and are now regular `UILabel`s. ### Fixed -- **Breaking Change** Fixed all instances of misspelled `inital` property. `Avatar.inital` has changed to `Avatar.initial` -and the initializer has changed from `public init(image: UIImage? = nil, initals: String = "?")` to `public init(image: UIImage? = nil, initials: String = "?")`. +- **Breaking Change** Fixed all instances of misspelled `initial` property. `Avatar.initial` has changed to `Avatar.initial` +and the initializer has changed from `public init(image: UIImage? = nil, initials: String = "?")` to `public init(image: UIImage? = nil, initials: String = "?")`. [#298](https://github.com/MessageKit/MessageKit/issues/298) by [@sidmclaughlin](https://github.com/sidmclaughlin). - Fixed `MessageInputBar`'s `translucent` functionality. @@ -433,7 +775,7 @@ moved their methods into the `MessagesLayoutDelegate` protocol. - Fixed `contentInset.top` adjustment of the `MessagesCollectionView` on iOS versions less than 11 where it was found that messages appeared under the navigation var [#334](https://github.com/MessageKit/MessageKit/pull/334) by [@nathantannar4](https://github.com/nathantannar4). -- Fixed `cellbottomLabel` origin X for the `.messageLeading` alignment and +- Fixed `cellBottomLabel` origin X for the `.messageLeading` alignment and origin Y so that the `cellBottomLabel` is always under the `MessageContainerView`. [#326](https://github.com/MessageKit/MessageKit/pull/326) by [@SD10](https://github.com/sd10). @@ -453,7 +795,7 @@ origin Y so that the `cellBottomLabel` is always under the `MessageContainerView - Fixed a bug that caused a race condition to be met when invalidating the `intrinsicContentSize` of the `MessageInputBar` which froze the app during a "Select" or "Select All" long press [#313](https://github.com/MessageKit/MessageKit/pull/313) by [@zhongwuzw](https://github.com/nathantannar4). -- Fixed a bug that the `placeholderLabel` `subview` of `InputTextView` leads to ambiguous content size because of uncorrect `Auto Layout`. +- Fixed a bug that the `placeholderLabel` `subview` of `InputTextView` leads to ambiguous content size because of incorrect `Auto Layout`. [#310](https://github.com/MessageKit/MessageKit/pull/310) by [@zhongwuzw](https://github.com/zhongwuzw). - Fixed a bug that the `leftStackView`、`rightStackView` `subview` of `MessageInputBar` leads to ambiguous `Auto Layout` issue because of typo. @@ -489,7 +831,7 @@ origin Y so that the `cellBottomLabel` is always under the `MessageContainerView ### Fixed -- Fixed a bug that prevented the `textAllignment` property of `InputTextView`'s `placeholderLabel` from having noticable differences when changed to `.center` or `.right`. +- Fixed a bug that prevented the `textAlignment` property of `InputTextView`'s `placeholderLabel` from having noticeable differences when changed to `.center` or `.right`. [#262](https://github.com/MessageKit/MessageKit/pull/262) by [@nathantannar4](https://github.com/nathantannar4). - Initial `contentInset.bottom` reference changed from `messageInputBar` to `inputAccessoryView` to allow custom `inputAccessoryView`'s that don't break the initial layout. @@ -520,7 +862,7 @@ when `extendedLayoutIncludesOpaqueBars` is `true`. - **Breaking Change** `.emoji(String)` case to `MessageData` enum. [#222](https://github.com/MessageKit/MessageKit/pull/222) by [@SirArkimdes](https://github.com/SirArkimedes). -- **Breaking Change** `TextMessageDisplayDelegate` to handle `enabledDetecors(for:at:in)` and moves `textColor(for:at:in)` to this namespace. +- **Breaking Change** `TextMessageDisplayDelegate` to handle `enabledDetectors(for:at:in)` and moves `textColor(for:at:in)` to this namespace. [#230](https://github.com/MessageKit/MessageKit/pull/230) by [@SD10](https://github.com/sd10) - `LocationMessageDisplayDelegate` to customize a location messages appearance and add a `MKAnnotationView` to location message snapshots. @@ -538,7 +880,7 @@ when `extendedLayoutIncludesOpaqueBars` is `true`. - `scrollsToBottomOnKeyboardDidBeginEditing` property to automatically scroll to the bottom of `MessagesCollectionView` when the keyboard begins editing. [#217](https://github.com/MessageKit/MessageKit/pull/217) by [@SD10](https://github.com/SD10). -- `additionalTopContentInset` property to `MessagesColectionViewController` to allow users to account for extra subviews. +- `additionalTopContentInset` property to `MessagesCollectionViewController` to allow users to account for extra subviews. [#218](https://github.com/MessageKit/MessageKit/pull/218) by [@SD10](https://github.com/SD10). - `messagePadding(for:at:in)` method to `MessagesLayoutDelegate` to dynamically set padding around `MessageContainerView`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eb2c5b40a..0f970dd6e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,7 +67,7 @@ Writing clean code and upholding project standards is as important as adding new If any of the sections above are unclear and require further explanation, *do not hesitate to reach out*. MessageKit strives to build an inclusive open source community and to make contributing as easy as possible for members of all experience levels. -**You can get in touch with the MessageKit core team directly by joining our open Slack community channel: [here](https://join.slack.com/t/messagekit/shared_invite/MjI0NDkxNjgwMzA3LTE1MDIzMTU0MjUtMzJhZDZlNTkxMA).** +**You can get in touch with the MessageKit core team directly by joining our open Slack community channel: [here](https://join.slack.com/t/messagekit/shared_invite/MjI4NzIzNzMyMzU0LTE1MDMwODIzMDUtYzllYzIyNTU4MA).** ---- diff --git a/Cartfile b/Cartfile deleted file mode 100644 index 00560f59f..000000000 --- a/Cartfile +++ /dev/null @@ -1 +0,0 @@ -github "MessageKit/MessageInputBar" "0.4.0" diff --git a/Cartfile.private b/Cartfile.private deleted file mode 100644 index 70f82c404..000000000 --- a/Cartfile.private +++ /dev/null @@ -1,2 +0,0 @@ -github "Quick/Quick" ~> 2.0.0 -github "Quick/Nimble" ~> 8.0.1 diff --git a/Cartfile.resolved b/Cartfile.resolved deleted file mode 100644 index 468e49847..000000000 --- a/Cartfile.resolved +++ /dev/null @@ -1,3 +0,0 @@ -github "MessageKit/MessageInputBar" "0.4.0" -github "Quick/Nimble" "v8.0.1" -github "Quick/Quick" "v2.0.0" diff --git a/Dangerfile b/Dangerfile new file mode 100644 index 000000000..339e23910 --- /dev/null +++ b/Dangerfile @@ -0,0 +1,55 @@ +# +# MIT License +# +# Copyright (c) 2017-2022 MessageKit +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# This runs on CI +mergeable_state = github.pr_json["mergeable_state"] + +# Make it more obvious that a PR a draft +if mergeable_state == "draft" + warn("PR is marked as Draft") +end + +# Make it more obvious that a PR is a work in progress and shouldn't be merged yet +if github.pr_title.include? "[WIP]" + warn("PR is marked as Work in Progress") +end + +# Mainly to encourage writing up some reasoning about the PR, rather than just leaving a title +if github.pr_body.length < 5 + fail("Please provide a summary in the Pull Request description") +end + +declared_hashtag = github.pr_title.include?("#trivial") +hasChangelogEntry = git.modified_files.include?("CHANGELOG.md") +if !hasChangelogEntry && !declared_hashtag + fail("Please include a CHANGELOG entry. \nYou can find it at [CHANGELOG.md](https://github.com/MessageKit/MessageKit/blob/master/CHANGELOG.md).") +end + +# Warn when there is a big PR +if git.lines_of_code > 1000 + warn("Big Pull Request - Please consider splitting up your changes into smaller Pull Requests.") +end + +swiftlint.config_file = '.swiftlint.yml' +swiftlint.binary_path = '.build/artifacts/messagekit/SwiftLintBinary.artifactbundle/swiftlint-0.47.1-macos/bin/swiftlint' +swiftlint.lint_files inline_mode:true, fail_on_error:true diff --git a/CHANGELOG_GUIDELINES.md b/Documentation/CHANGELOG_GUIDELINES.md similarity index 92% rename from CHANGELOG_GUIDELINES.md rename to Documentation/CHANGELOG_GUIDELINES.md index d644f1e1d..3470e2e49 100644 --- a/CHANGELOG_GUIDELINES.md +++ b/Documentation/CHANGELOG_GUIDELINES.md @@ -12,7 +12,6 @@ Here you can find the general guidelines for maintaining the Changelog (or addin - Mention whether you follow Semantic Versioning. ... with MessageKit-specific additions: -- Keep an unreleased section at the top. - Add PR number and a GitHub tag at the end of each entry. - Each breaking change entry should have **Breaking Change** label at the beginning of this entry. - **Breaking Change** entries should be placed at the top of the section it's in. @@ -44,11 +43,11 @@ Here you can find the general guidelines for maintaining the Changelog (or addin ### Fixed -- Fixed a bug that prevented the `textAllignment` property of `InputTextView`'s `placeholderLabel` from having noticable differences when changed to `.center` or `.right`. +- Fixed a bug that prevented the `textAlignment` property of `InputTextView`'s `placeholderLabel` from having noticeable differences when changed to `.center` or `.right`. [#262](https://github.com/MessageKit/MessageKit/pull/262) by [@nathantannar4](https://github.com/nathantannar4). ### Removed - **Breaking Change** Removed `additionalTopContentInset` property of `MessagesViewController` because this is no longer necessary when `extendedLayoutIncludesOpaqueBars` is `true`. -[#250](https://github.com/MessageKit/MessageKit/pull/250) by [@SD10](https://github.com/SD10). \ No newline at end of file +[#250](https://github.com/MessageKit/MessageKit/pull/250) by [@SD10](https://github.com/SD10). diff --git a/Documentation/CUSTOM_CELLS.md b/Documentation/CUSTOM_CELLS.md new file mode 100644 index 000000000..db52220f0 --- /dev/null +++ b/Documentation/CUSTOM_CELLS.md @@ -0,0 +1,89 @@ +# MessageKit Custom Cell Guide(s) + +- [How can I add a custom cell?](#how-can-i-add-a-custom-cell) + +## How can I add a custom cell? + +**Note:** If you choose to use the `.custom` kind you are responsible for all of the cell's layout. You can design the cell in code or Interface Builder. Any `UICollectionViewCell` can be returned for custom cells which means any of the styling you provide from the `MessageDisplayDelegate` will not affect your custom cell, even if you subclass your cell from `MessageContentCell`. + +**Creating a custom cell involves four parts:** +1. Build a cell in Interface Builder or code that inherits from `UICollectionViewCell` +2. Set the size of your cell. Subclass `MessageSizeCalculator` if you want your cell to have the default MessageKit layout design. Subclass `CellSizeCalculator` if you want to further customize your own cell design. The implementation of this class will allow your custom cell to automatically size itself within the `messagesCollectionView`. +3. Add your custom cell size to the collection view flow layout. Subclass `MessagesCollectionViewFlowLayout`, and use the custom message size calculator from step 2, above. +4. Register your custom cell and reference your custom collection view flow layout. + + +### Example: + +Let's take a look at what it takes to create a custom cell that displays a red block for your custom message. + +- **1. Custom Cell**: We create a cell that inherits from `UICollectionViewCell` +**MyCustomCell.swift** +```swift +open class MyCustomCell: UICollectionViewCell { + open func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) { + self.contentView.backgroundColor = UIColor.red + } +} +``` + +- **2. MessageSizeCalculator**: We set the size of our cell by creating a class that inherits from `MessageSizeCalculator` +**CustomMessageSizeCalculator.swift** +```swift +open class CustomMessageSizeCalculator: MessageSizeCalculator { + open override func messageContainerSize(for message: MessageType) -> CGSize { + // Customize this function implementation to size your content appropriately. This example simply returns a constant size + // Refer to the default MessageKit cell implementations, and the Example App to see how to size a custom cell dynamically + return CGSize(width: 300, height: 130) + } +} +``` + +- **3. MessageFlowLayout**: We add our custom message size calculator to our collection view layout by creating a class that inherits from `MessagesCollectionViewFlowLayout` +**MyCustomMessagesFlowLayout.swift** +```swift +open class MyCustomMessagesFlowLayout: MessagesCollectionViewFlowLayout { + lazy open var customMessageSizeCalculator = CustomMessageSizeCalculator(layout: self) + + override open func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator { + //before checking the messages check if section is reserved for typing otherwise it will cause IndexOutOfBounds error + if isSectionReservedForTypingIndicator(indexPath.section) { + return typingIndicatorSizeCalculator + } + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + if case .custom = message.kind { + return customMessageSizeCalculator + } + return super.cellSizeCalculatorForItem(at: indexPath); + } +} +``` +- **4. Implementation**: We register our custom cell and reference our newly created `MyCustomMessagesFlowLayout.swift` +**ConversationViewController.swift** +```swift +internal class ConversationViewController: MessagesViewController { + override func viewDidLoad() { + super.viewDidLoad() + messagesCollectionView = MessagesCollectionView(frame: .zero, collectionViewLayout: MyCustomMessagesFlowLayout()) + messagesCollectionView.register(MyCustomCell.self) + //... + } +//... + override open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { + fatalError("Ouch. nil data source for messages") + } + //before checking the messages check if section is reserved for typing otherwise it will cause IndexOutOfBounds error + if isSectionReservedForTypingIndicator(indexPath.section){ + return super.collectionView(collectionView, cellForItemAt: indexPath) + } + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + if case .custom = message.kind { + let cell = messagesCollectionView.dequeueReusableCell(MyCustomCell.self, for: indexPath) + cell.configure(with: message, at: indexPath, and: messagesCollectionView) + return cell + } + return super.collectionView(collectionView, cellForItemAt: indexPath) + } +} +``` diff --git a/Documentation/FAQs.md b/Documentation/FAQs.md index 5ed0760fe..ae6943dde 100644 --- a/Documentation/FAQs.md +++ b/Documentation/FAQs.md @@ -5,7 +5,8 @@ - [How can I move the `AvatarView` to prevent it from overlapping text in the `MessageBottomLabel` or `CellTopLabel`?](#how-can-i-move-the-avatarview-to-prevent-it-from-overlapping-text-in-the-messagebottomlabel-or-celltoplabel) - [How can I dismiss the keyboard?](#how-can-i-dismiss-the-keyboard) - [How can I get a reference to the `MessageType` in the `MessageCellDelegate` methods?](#how-can-i-get-a-reference-to-the-messagetype-in-the-messagecelldelegate-methods) - +- [Animations are laggy/Scrolling is not smooth/General poor performance](#animations-are-laggyscrolling-is-not-smoothgeneral-poor-performance) +- [How can I use MessageKit with SwiftUI](#how-can-i-use-messagekit-with-swiftui) ## Why doesn't the `MessageInputBar` appear in my controller? @@ -105,3 +106,11 @@ func didTapMessage(in cell: MessageCollectionViewCell) { let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) } ``` + +## Animations are laggy/Scrolling is not smooth/General poor performance + +In general, if you're experiencing performance issues, you should look through the implementation of your MessageKit delegate methods like `var currentSender`, etc to find any expensive/blocking method calls or operations. Some delegate methods are called many times per message rendered and thus if their implementations are not efficient then performance can significantly degrade. Avoid doing blocking activities like synchronous database lookups, keychain access, networking, etc. in your MessageKit delegate methods. You should instead cache what you need. + +## How can I use MessageKit with SwiftUI? + +MessageKit support of SwiftUI is experimental at best and there is no active work being done on adding more support diff --git a/Documentation/MANUAL_INSTALLATION.md b/Documentation/MANUAL_INSTALLATION.md new file mode 100644 index 000000000..4c7cdfdcd --- /dev/null +++ b/Documentation/MANUAL_INSTALLATION.md @@ -0,0 +1,22 @@ +## Embedded Framework Installation + +- `cd` to your project directory, initialize git, and Add MessageKit as a git [submodule](https://git-scm.com/docs/git-submodule) by running the following command: + +```bash +$ git submodule add https://github.com/MessageKit/MessageKit.git +``` + +- `cd` to the new `MessageKit` folder and trigger [carthage](https://github.com/Carthage/Carthage) update by the following command: + +```bash +$ carthage update --platform iOS +``` + +- Open `MessageKit` folder, and drag the `MessageKit.xcodeproj` into the Project Navigator of your application's Xcode project. It should appear nested underneath your application's blue project icon. +- Select the `MessageKit.xcodeproj` in the Project Navigator and verify the deployment target matches that of your application target. +- Next, select your application project in the Project Navigator (blue project icon), navigate to the target configuration window and select the application target. +- In the tab bar at the top of that window, open the "General" panel. +- Click on the `+` button under the "Embedded Binaries" section. +- You will see two different `MessageKit.xcodeproj` folders each with two different versions of the `MessageKit.framework` nested inside a `Products` folder. +- Select the top `MessageKit.framework` for iOS and the bottom one for OS X. +- Voila! Now you can `import MessageKit` and build the project. diff --git a/Documentation/MIGRATION_GUIDE.md b/Documentation/MIGRATION_GUIDE.md new file mode 100644 index 000000000..84e4a78a4 --- /dev/null +++ b/Documentation/MIGRATION_GUIDE.md @@ -0,0 +1,26 @@ +## MessageKit 4.0 Migration Guide + +Version 4.0 contains some breaking changes if you want to upgrade from the previous version. In this documentation, we will cover most of the noticeable API changes. + +### Migration + +- BREAKING CHANGE: + ```swift + MessageSizeCalculator.messageContainerMaxWidth(for message: MessageType) -> CGFloat + ``` + now has IndexPath argument + ```swift + MessageSizeCalculator.messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat + ``` +- BREAKING CHANGE: + ```swift + MessageSizeCalculator.messageContainerSize(for message: MessageType) -> CGSize + ``` + now has IndexPath argument + ```swift + MessageSizeCalculator.messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize + ``` +- BREAKING CHANGE: + Renamed `func currentSender() -> SenderType` to `var currentSender: SenderType` +- BREAKING CHANGE: + Deprecated `maintainPositionOnKeyboardFrameChangedMoved` in favor of `maintainPositionOnInputBarHeightChanged`. diff --git a/Documentation/MessageInputBar.md b/Documentation/MessageInputBar.md index ccf8ad633..92accfb3a 100644 --- a/Documentation/MessageInputBar.md +++ b/Documentation/MessageInputBar.md @@ -1,6 +1,6 @@ # MessageInputBar -The `MessageInputBar` is the reactive `InputAccessoryView` of the `MessagesViewController`. It has a fairly basic delegate, `MessageInputBarDelegate`, that can be used to detect when the send button is pressed however its individual `InputBarButtonItem`'s can be individually set up to react to various events. +The `MessageInputBar` is the reactive `InputAccessoryView` of the `MessagesViewController`. It has a fairly basic delegate, `InputBarAccessoryViewDelegate`, that can be used to detect when the send button is pressed however its individual `InputBarButtonItem`'s can be individually set up to react to various events. ## Basic Setup @@ -34,7 +34,7 @@ func messageInputBar(_ inputBar: MessageInputBar, textViewTextDidChangeTo text: The layout of the `MessageInputBar` is made of of 3 `UIStackView`'s and an `InputTextView` (subclass of `UITextView`). The padding of the subviews can be easily adjusted by changing the `padding` and `textViewPadding` properties. The constraints will automatically be updated. - + ```swift H:|-(padding.left)-[UIStackView(leftStackViewWidthContant)]-(textViewPadding.left)-[InputTextView]-(textViewPadding.right)-[UIStackView(rightStackViewWidthContant)]-(padding.right)-| diff --git a/Documentation/QuickStart.md b/Documentation/QuickStart.md index eb33a196b..5cdbf7cc3 100644 --- a/Documentation/QuickStart.md +++ b/Documentation/QuickStart.md @@ -6,7 +6,7 @@ The driving force behind **MessageKit** is the `MessageType` protocol which prov ```Swift public protocol MessageType { - var sender: Sender { get } + var sender: SenderType { get } var messageId: String { get } @@ -15,46 +15,36 @@ public protocol MessageType { var kind: MessageKind { get } } ``` -First, each `MessageType` is required to have a `Sender` which contains two properties, `id` and `displayName`: +First, each `MessageType` is required to have a `SenderType` which contains two properties, `senderId` and `displayName`: ### Sender ```Swift -public struct Sender { +public protocol SenderType { - public let id: String - - public let displayName: String + var senderId: String { get } + + var displayName: String { get } } + ``` -**MessageKit** uses the `Sender` type to determine if a message was sent by the current user or to the current user. +**MessageKit** uses the `SenderType` type to determine if a message was sent by the current user or to the current user. Second, each message must have its own `messageId` which is a unique `String` identifier for the message. Third, each message must have a `sentDate` which represents the `Date` that each message was sent. Fourth, each message must specify what kind of message it is through the `kind: MessageKind` property: -### MessageKind +### [MessageKind](https://github.com/MessageKit/MessageKit#default-cells) -```Swift -public enum MessageKind { - case text(String) - case attributedText(NSAttributedString) - case emoji(String) - case photo(MediaItem) - case video(MediaItem) - case location(LocationItem) - case custom(Any?) -} -``` -`MessageData` has 7 different cases representing the types of messages that **MessageKit** can display. +`MessageKind` has 8 different cases representing the types of messages that **MessageKit** can display. - `text(String)` - Use this case if you just want to display a normal text message without any attributes. -- **NOTE**: You must also specify the `UIFont` you want to use for this text by setting the `messageLabelFont` property of the `textMessageSizeCalculator` in `MessagesCollectionViewFlowLayout`. - -- `emoji(String)` - Use this case to display a message that only contains emoji. -- **NOTE**: You must also specify the `UIFont` you want to use for this text by setting the `messageLabelFont` property of the `emojiMessageSizeCalculator` in `MessagesCollectionViewFlowLayout`. + - **NOTE**: You must also specify the `UIFont` you want to use for this text by setting the `messageLabelFont` property of the `textMessageSizeCalculator` in `MessagesCollectionViewFlowLayout`. - `attributedText(NSAttributedString)` - Use this case if you want to display a text message with attributes. -- **NOTE**: It is recommended that you use `attributedText` for text messages. + - **NOTE**: It is recommended that you use `attributedText` for text messages. + +- `emoji(String)` - Use this case to display a message that only contains emoji. + - **NOTE**: You must also specify the `UIFont` you want to use for this text by setting the `messageLabelFont` property of the `emojiMessageSizeCalculator` in `MessagesCollectionViewFlowLayout`. - `photo(MediaItem)` - Use this case to display a photo message. @@ -62,6 +52,10 @@ public enum MessageKind { - `location(LocationItem)` - Use this case to display a location message. +- `audio(AudioItem)` - Use this case to display an audio message. + +- `contact(ContactItem)` - Use this case to display a contact message. + # MessagesViewController ## Subclassing MessagesViewController @@ -98,14 +92,21 @@ class ChatViewController: MessagesViewController { You must implement the following 3 methods to conform to `MessagesDataSource`: ```Swift + +public struct Sender: SenderType { + public let senderId: String + + public let displayName: String +} + // Some global variables for the sake of the example. Using globals is not recommended! -let sender = Sender(id: "any_unique_id", displayName: "Steven") +let sender = Sender(senderId: "any_unique_id", displayName: "Steven") let messages: [MessageType] = [] extension ChatViewController: MessagesDataSource { - func currentSender() -> Sender { - return Sender(id: "any_unique_id", displayName: "Steven") + var currentSender: SenderType { + return Sender(senderId: "any_unique_id", displayName: "Steven") } func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int { diff --git a/Example/.swiftlint.yml b/Example/.swiftlint.yml deleted file mode 100644 index 821c4a93a..000000000 --- a/Example/.swiftlint.yml +++ /dev/null @@ -1,15 +0,0 @@ - -disabled_rules: - - identifier_name - - trailing_whitespace - - line_length - - type_body_length - - file_length -custom_rules: - override_func: # rule identifier - name: "override in func" # rule name. optional. - regex: "override (open|public|private|internal|fileprivate)" # matching pattern - message: "Use like open override or public override instead" # violation message. optional. - severity: warning # violation severity. optional. -included: - - Sources \ No newline at end of file diff --git a/Example/ChatExample.xcodeproj/project.pbxproj b/Example/ChatExample.xcodeproj/project.pbxproj index 2b7dc8da8..49465a9fd 100644 --- a/Example/ChatExample.xcodeproj/project.pbxproj +++ b/Example/ChatExample.xcodeproj/project.pbxproj @@ -3,10 +3,19 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ + 032A15DD25965D9A00E00FE3 /* AlertService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032A15DC25965D9A00E00FE3 /* AlertService.swift */; }; + 032A15E225965E1100E00FE3 /* CameraInputBarAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032A15E125965E1100E00FE3 /* CameraInputBarAccessoryView.swift */; }; + 138A04D428254AD300E2780F /* CustomInputBarExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138A04D328254AD300E2780F /* CustomInputBarExampleViewController.swift */; }; + 13EFA5D727AC5631003002CC /* MessageKit in Frameworks */ = {isa = PBXBuildFile; productRef = 13EFA5D627AC5631003002CC /* MessageKit */; }; + 13EFA5D927AC5634003002CC /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 13EFA5D827AC5634003002CC /* Kingfisher */; }; + 1C5433DD24C389C300A5383B /* MessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C5433DC24C389C300A5383B /* MessagesView.swift */; }; + 1C5433DF24C38DBF00A5383B /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C5433DE24C38DBF00A5383B /* SwiftUI.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 1C5433E224C3A33600A5383B /* SwiftUIExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C5433E124C3A33600A5383B /* SwiftUIExampleView.swift */; }; + 383B9EB32172A1C4008AB91A /* MockUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383B9EB22172A1C4008AB91A /* MockUser.swift */; }; 385C2922211FF32E0010B4BA /* CustomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C2920211FF32E0010B4BA /* CustomCell.swift */; }; 385C2923211FF32E0010B4BA /* TableViewCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C2921211FF32E0010B4BA /* TableViewCells.swift */; }; 385C2927211FF33B0010B4BA /* MockSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C2925211FF33A0010B4BA /* MockSocket.swift */; }; @@ -20,18 +29,26 @@ 385C2942211FF38F0010B4BA /* AdvancedExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C293B211FF38E0010B4BA /* AdvancedExampleViewController.swift */; }; 385C2943211FF38F0010B4BA /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C293C211FF38E0010B4BA /* SettingsViewController.swift */; }; 385C2944211FF38F0010B4BA /* MessageContainerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C293D211FF38F0010B4BA /* MessageContainerController.swift */; }; - 385C2945211FF38F0010B4BA /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C293E211FF38F0010B4BA /* NavigationController.swift */; }; 385C2946211FF38F0010B4BA /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C293F211FF38F0010B4BA /* LaunchViewController.swift */; }; 385C2947211FF38F0010B4BA /* BasicExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C2940211FF38F0010B4BA /* BasicExampleViewController.swift */; }; 385C2948211FF38F0010B4BA /* ChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C2941211FF38F0010B4BA /* ChatViewController.swift */; }; + 38CCCC592258419300DD5482 /* AutocompleteExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38CCCC582258419300DD5482 /* AutocompleteExampleViewController.swift */; }; + 50739F9621C5090A008CA369 /* BasicAudioController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50739F9521C5090A008CA369 /* BasicAudioController.swift */; }; + 5074EF4E2163555900D82952 /* sound2.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 5074EF4C2163555900D82952 /* sound2.m4a */; }; + 5074EF4F2163555900D82952 /* sound1.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 5074EF4D2163555900D82952 /* sound1.m4a */; }; + 5168957524F7AD560058C643 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5168957124F7AD560058C643 /* Assets.xcassets */; }; + 5168957624F7AD560058C643 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5168957224F7AD560058C643 /* LaunchScreen.storyboard */; }; + 63ECDABE24182A470016C349 /* MessageSubviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63ECDABD24182A470016C349 /* MessageSubviewViewController.swift */; }; + 63ECDAC2241889630016C349 /* MessageSubviewContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63ECDAC1241889630016C349 /* MessageSubviewContainerViewController.swift */; }; 882B5E811CF7D53600B6E160 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882B5E781CF7D53600B6E160 /* AppDelegate.swift */; }; - 882B5E821CF7D53600B6E160 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 882B5E791CF7D53600B6E160 /* Assets.xcassets */; }; - 882B5E831CF7D53600B6E160 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 882B5E7A1CF7D53600B6E160 /* LaunchScreen.storyboard */; }; 882B5E901CF7D56000B6E160 /* ChatExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882B5E8E1CF7D56000B6E160 /* ChatExampleUITests.swift */; }; 882B5E951CF7D56E00B6E160 /* ChatExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882B5E931CF7D56E00B6E160 /* ChatExampleTests.swift */; }; - A444625E4F1C8CB1B33A17F8 /* Pods_ChatExampleUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2AC6E3F5C11E39F57598DBE6 /* Pods_ChatExampleUITests.framework */; }; - C1DF6DF39F66906000EC76CF /* Pods_ChatExampleTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56F0AC85B38034EC92CCBC7D /* Pods_ChatExampleTests.framework */; }; - C7CA53A1B85256A5097E7DC7 /* Pods_ChatExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B316705C4717C3B4C916D62 /* Pods_ChatExample.framework */; }; + 9961327FB4F9B96CFD84A126 /* (null) in Frameworks */ = {isa = PBXBuildFile; }; + 9B49D528263C31FC008804B5 /* CustomLayoutExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B49D527263C31FC008804B5 /* CustomLayoutExampleViewController.swift */; }; + 9B49D52D263C3E1F008804B5 /* CustomTextLayoutSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B49D52C263C3E1F008804B5 /* CustomTextLayoutSizeCalculator.swift */; }; + 9B49D53A263D9606008804B5 /* CustomLayoutSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B49D539263D9606008804B5 /* CustomLayoutSizeCalculator.swift */; }; + 9B49D542263DA6F9008804B5 /* CustomMessageContentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B49D541263DA6F9008804B5 /* CustomMessageContentCell.swift */; }; + 9B49D547263DAA29008804B5 /* CustomTextMessageContentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B49D546263DAA29008804B5 /* CustomTextMessageContentCell.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -51,22 +68,16 @@ }; /* End PBXContainerItemProxy section */ -/* Begin PBXCopyFilesBuildPhase section */ - 882B5EA41CF7D8D100B6E160 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - /* Begin PBXFileReference section */ - 0364943D08CDBE656E6F6DF8 /* Pods-ChatExampleTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatExampleTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ChatExampleTests/Pods-ChatExampleTests.debug.xcconfig"; sourceTree = ""; }; - 2AC6E3F5C11E39F57598DBE6 /* Pods_ChatExampleUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ChatExampleUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 032A15DC25965D9A00E00FE3 /* AlertService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertService.swift; sourceTree = ""; }; + 032A15E125965E1100E00FE3 /* CameraInputBarAccessoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraInputBarAccessoryView.swift; sourceTree = ""; }; + 138A04D328254AD300E2780F /* CustomInputBarExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomInputBarExampleViewController.swift; sourceTree = ""; }; + 13CCA04225793002005C19BB /* MessageKit */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MessageKit; sourceTree = ""; }; + 13EFA5D027AC5368003002CC /* MessageKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MessageKit; path = ..; sourceTree = ""; }; + 1C5433DC24C389C300A5383B /* MessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesView.swift; sourceTree = ""; }; + 1C5433DE24C38DBF00A5383B /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; + 1C5433E124C3A33600A5383B /* SwiftUIExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIExampleView.swift; sourceTree = ""; }; + 383B9EB22172A1C4008AB91A /* MockUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUser.swift; sourceTree = ""; }; 385C2920211FF32E0010B4BA /* CustomCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomCell.swift; sourceTree = ""; }; 385C2921211FF32E0010B4BA /* TableViewCells.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableViewCells.swift; sourceTree = ""; }; 385C2925211FF33A0010B4BA /* MockSocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockSocket.swift; sourceTree = ""; }; @@ -80,28 +91,31 @@ 385C293B211FF38E0010B4BA /* AdvancedExampleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdvancedExampleViewController.swift; sourceTree = ""; }; 385C293C211FF38E0010B4BA /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; 385C293D211FF38F0010B4BA /* MessageContainerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageContainerController.swift; sourceTree = ""; }; - 385C293E211FF38F0010B4BA /* NavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = ""; }; 385C293F211FF38F0010B4BA /* LaunchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = ""; }; 385C2940211FF38F0010B4BA /* BasicExampleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicExampleViewController.swift; sourceTree = ""; }; 385C2941211FF38F0010B4BA /* ChatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatViewController.swift; sourceTree = ""; }; - 3B316705C4717C3B4C916D62 /* Pods_ChatExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ChatExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 56F0AC85B38034EC92CCBC7D /* Pods_ChatExampleTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ChatExampleTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 38CCCC582258419300DD5482 /* AutocompleteExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteExampleViewController.swift; sourceTree = ""; }; + 50739F9521C5090A008CA369 /* BasicAudioController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicAudioController.swift; sourceTree = ""; }; + 5074EF4C2163555900D82952 /* sound2.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = sound2.m4a; sourceTree = ""; }; + 5074EF4D2163555900D82952 /* sound1.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = sound1.m4a; sourceTree = ""; }; + 5168957124F7AD560058C643 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 5168957324F7AD560058C643 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 5168957424F7AD560058C643 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 63ECDABD24182A470016C349 /* MessageSubviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSubviewViewController.swift; sourceTree = ""; }; + 63ECDAC1241889630016C349 /* MessageSubviewContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSubviewContainerViewController.swift; sourceTree = ""; }; 882B5E331CF7D4B900B6E160 /* ChatExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ChatExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 882B5E491CF7D4B900B6E160 /* ChatExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ChatExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 882B5E541CF7D4B900B6E160 /* ChatExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ChatExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 882B5E781CF7D53600B6E160 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 882B5E791CF7D53600B6E160 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 882B5E7B1CF7D53600B6E160 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 882B5E7F1CF7D53600B6E160 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 882B5E8E1CF7D56000B6E160 /* ChatExampleUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatExampleUITests.swift; sourceTree = ""; }; 882B5E8F1CF7D56000B6E160 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 882B5E931CF7D56E00B6E160 /* ChatExampleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatExampleTests.swift; sourceTree = ""; }; 882B5E941CF7D56E00B6E160 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 9E0D67CD75BA7EB323FD391B /* Pods-ChatExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatExample.release.xcconfig"; path = "Pods/Target Support Files/Pods-ChatExample/Pods-ChatExample.release.xcconfig"; sourceTree = ""; }; - A830E27DBE0B66B89C5D2EB8 /* Pods-ChatExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatExample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ChatExample/Pods-ChatExample.debug.xcconfig"; sourceTree = ""; }; - B0DD3C951C9D064B5E6D6644 /* Pods-ChatExampleUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatExampleUITests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ChatExampleUITests/Pods-ChatExampleUITests.debug.xcconfig"; sourceTree = ""; }; - B2F1C412A96DE613A0AC31F8 /* Pods-ChatExampleUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatExampleUITests.release.xcconfig"; path = "Pods/Target Support Files/Pods-ChatExampleUITests/Pods-ChatExampleUITests.release.xcconfig"; sourceTree = ""; }; - BFE5859D088A740A7D43E1B1 /* Pods-ChatExampleTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatExampleTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-ChatExampleTests/Pods-ChatExampleTests.release.xcconfig"; sourceTree = ""; }; + 9B49D527263C31FC008804B5 /* CustomLayoutExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomLayoutExampleViewController.swift; sourceTree = ""; }; + 9B49D52C263C3E1F008804B5 /* CustomTextLayoutSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextLayoutSizeCalculator.swift; sourceTree = ""; }; + 9B49D539263D9606008804B5 /* CustomLayoutSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomLayoutSizeCalculator.swift; sourceTree = ""; }; + 9B49D541263DA6F9008804B5 /* CustomMessageContentCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomMessageContentCell.swift; sourceTree = ""; }; + 9B49D546263DAA29008804B5 /* CustomTextMessageContentCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextMessageContentCell.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -109,7 +123,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - C7CA53A1B85256A5097E7DC7 /* Pods_ChatExample.framework in Frameworks */, + 13EFA5D927AC5634003002CC /* Kingfisher in Frameworks */, + 1C5433DF24C38DBF00A5383B /* SwiftUI.framework in Frameworks */, + 13EFA5D727AC5631003002CC /* MessageKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -117,7 +133,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - C1DF6DF39F66906000EC76CF /* Pods_ChatExampleTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,17 +140,38 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A444625E4F1C8CB1B33A17F8 /* Pods_ChatExampleUITests.framework in Frameworks */, + 9961327FB4F9B96CFD84A126 /* (null) in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 13EFA5CF27AC5368003002CC /* Packages */ = { + isa = PBXGroup; + children = ( + 13EFA5D027AC5368003002CC /* MessageKit */, + ); + name = Packages; + sourceTree = ""; + }; + 1C5433E024C3A32400A5383B /* SwiftUI */ = { + isa = PBXGroup; + children = ( + 1C5433DC24C389C300A5383B /* MessagesView.swift */, + 1C5433E124C3A33600A5383B /* SwiftUIExampleView.swift */, + ); + path = SwiftUI; + sourceTree = ""; + }; 385C2924211FF3310010B4BA /* Views */ = { isa = PBXGroup; children = ( + 1C5433E024C3A32400A5383B /* SwiftUI */, + 032A15E125965E1100E00FE3 /* CameraInputBarAccessoryView.swift */, 385C2920211FF32E0010B4BA /* CustomCell.swift */, + 9B49D541263DA6F9008804B5 /* CustomMessageContentCell.swift */, + 9B49D546263DAA29008804B5 /* CustomTextMessageContentCell.swift */, 385C2921211FF32E0010B4BA /* TableViewCells.swift */, ); path = Views; @@ -144,8 +180,11 @@ 385C2929211FF33D0010B4BA /* Models */ = { isa = PBXGroup; children = ( + 9B49D539263D9606008804B5 /* CustomLayoutSizeCalculator.swift */, + 9B49D52C263C3E1F008804B5 /* CustomTextLayoutSizeCalculator.swift */, 385C2926211FF33B0010B4BA /* MockMessage.swift */, 385C2925211FF33A0010B4BA /* MockSocket.swift */, + 383B9EB22172A1C4008AB91A /* MockUser.swift */, ); path = Models; sourceTree = ""; @@ -161,6 +200,8 @@ 385C2933211FF3670010B4BA /* Extensions */ = { isa = PBXGroup; children = ( + 032A15DC25965D9A00E00FE3 /* AlertService.swift */, + 385C292A211FF3450010B4BA /* Settings+UserDefaults.swift */, 385C292F211FF3630010B4BA /* UIColor+Extensions.swift */, 385C2930211FF3630010B4BA /* UIViewController+Extensions.swift */, ); @@ -180,27 +221,45 @@ isa = PBXGroup; children = ( 385C293B211FF38E0010B4BA /* AdvancedExampleViewController.swift */, + 38CCCC582258419300DD5482 /* AutocompleteExampleViewController.swift */, 385C2940211FF38F0010B4BA /* BasicExampleViewController.swift */, 385C2941211FF38F0010B4BA /* ChatViewController.swift */, + 9B49D527263C31FC008804B5 /* CustomLayoutExampleViewController.swift */, 385C293F211FF38F0010B4BA /* LaunchViewController.swift */, 385C293D211FF38F0010B4BA /* MessageContainerController.swift */, - 385C293E211FF38F0010B4BA /* NavigationController.swift */, + 63ECDAC1241889630016C349 /* MessageSubviewContainerViewController.swift */, + 63ECDABD24182A470016C349 /* MessageSubviewViewController.swift */, 385C293C211FF38E0010B4BA /* SettingsViewController.swift */, + 138A04D328254AD300E2780F /* CustomInputBarExampleViewController.swift */, ); path = "View Controllers"; sourceTree = ""; }; - 7523553CCB4B87460CE4DED6 /* Pods */ = { + 50739F9421C5075D008CA369 /* AudioController */ = { + isa = PBXGroup; + children = ( + 50739F9521C5090A008CA369 /* BasicAudioController.swift */, + ); + path = AudioController; + sourceTree = ""; + }; + 5074EF4B2163554900D82952 /* Sounds */ = { isa = PBXGroup; children = ( - A830E27DBE0B66B89C5D2EB8 /* Pods-ChatExample.debug.xcconfig */, - 9E0D67CD75BA7EB323FD391B /* Pods-ChatExample.release.xcconfig */, - 0364943D08CDBE656E6F6DF8 /* Pods-ChatExampleTests.debug.xcconfig */, - BFE5859D088A740A7D43E1B1 /* Pods-ChatExampleTests.release.xcconfig */, - B0DD3C951C9D064B5E6D6644 /* Pods-ChatExampleUITests.debug.xcconfig */, - B2F1C412A96DE613A0AC31F8 /* Pods-ChatExampleUITests.release.xcconfig */, - ); - name = Pods; + 5074EF4D2163555900D82952 /* sound1.m4a */, + 5074EF4C2163555900D82952 /* sound2.m4a */, + ); + path = Sounds; + sourceTree = ""; + }; + 5168957024F7AD560058C643 /* Resources */ = { + isa = PBXGroup; + children = ( + 5168957124F7AD560058C643 /* Assets.xcassets */, + 5168957224F7AD560058C643 /* LaunchScreen.storyboard */, + 5168957424F7AD560058C643 /* Info.plist */, + ); + path = Resources; sourceTree = ""; }; 882B5E2A1CF7D4B900B6E160 = { @@ -209,9 +268,9 @@ 882B5E771CF7D53600B6E160 /* Sources */, 882B5E921CF7D56D00B6E160 /* Tests */, 882B5E8D1CF7D56000B6E160 /* UITests */, + 13EFA5CF27AC5368003002CC /* Packages */, 882B5E341CF7D4B900B6E160 /* Products */, - 7523553CCB4B87460CE4DED6 /* Pods */, - B0B06214A325DCCE2620950A /* Frameworks */, + 955AE3B8EFF8FDB8F572D3F7 /* Frameworks */, ); sourceTree = ""; }; @@ -229,16 +288,15 @@ isa = PBXGroup; children = ( 882B5E781CF7D53600B6E160 /* AppDelegate.swift */, - 385C2949211FF3930010B4BA /* View Controllers */, + 50739F9421C5075D008CA369 /* AudioController */, 385C293A211FF3800010B4BA /* Data Generation */, + 385C2933211FF3670010B4BA /* Extensions */, 385C292E211FF3540010B4BA /* Layout */, 385C2929211FF33D0010B4BA /* Models */, + 5168957024F7AD560058C643 /* Resources */, + 5074EF4B2163554900D82952 /* Sounds */, + 385C2949211FF3930010B4BA /* View Controllers */, 385C2924211FF3310010B4BA /* Views */, - 882B5E791CF7D53600B6E160 /* Assets.xcassets */, - 882B5E7F1CF7D53600B6E160 /* Info.plist */, - 882B5E7A1CF7D53600B6E160 /* LaunchScreen.storyboard */, - 385C292A211FF3450010B4BA /* Settings+UserDefaults.swift */, - 385C2933211FF3670010B4BA /* Extensions */, ); path = Sources; sourceTree = ""; @@ -261,12 +319,11 @@ path = Tests; sourceTree = ""; }; - B0B06214A325DCCE2620950A /* Frameworks */ = { + 955AE3B8EFF8FDB8F572D3F7 /* Frameworks */ = { isa = PBXGroup; children = ( - 3B316705C4717C3B4C916D62 /* Pods_ChatExample.framework */, - 56F0AC85B38034EC92CCBC7D /* Pods_ChatExampleTests.framework */, - 2AC6E3F5C11E39F57598DBE6 /* Pods_ChatExampleUITests.framework */, + 13CCA04225793002005C19BB /* MessageKit */, + 1C5433DE24C38DBF00A5383B /* SwiftUI.framework */, ); name = Frameworks; sourceTree = ""; @@ -278,19 +335,21 @@ isa = PBXNativeTarget; buildConfigurationList = 882B5E5D1CF7D4B900B6E160 /* Build configuration list for PBXNativeTarget "ChatExample" */; buildPhases = ( - 011D1D45151418DF94DA1AD7 /* [CP] Check Pods Manifest.lock */, - 882B5E2F1CF7D4B900B6E160 /* Sources */, 882B5E301CF7D4B900B6E160 /* Frameworks */, + 882B5E2F1CF7D4B900B6E160 /* Sources */, 882B5E311CF7D4B900B6E160 /* Resources */, - 882B5EA41CF7D8D100B6E160 /* Embed Frameworks */, - 3C8DFCD3063DA0DF099EC7B6 /* [CP] Embed Pods Frameworks */, - 7E0AFF9D207BB460004DFD4C /* ShellScript */, ); buildRules = ( ); dependencies = ( + 13EFA5D527AC557F003002CC /* PBXTargetDependency */, + 13EFA5D327AC557A003002CC /* PBXTargetDependency */, ); name = ChatExample; + packageProductDependencies = ( + 13EFA5D627AC5631003002CC /* MessageKit */, + 13EFA5D827AC5634003002CC /* Kingfisher */, + ); productName = ChatExample; productReference = 882B5E331CF7D4B900B6E160 /* ChatExample.app */; productType = "com.apple.product-type.application"; @@ -299,7 +358,6 @@ isa = PBXNativeTarget; buildConfigurationList = 882B5E601CF7D4B900B6E160 /* Build configuration list for PBXNativeTarget "ChatExampleTests" */; buildPhases = ( - 52AB594E03799821A66AFF18 /* [CP] Check Pods Manifest.lock */, 882B5E451CF7D4B900B6E160 /* Sources */, 882B5E461CF7D4B900B6E160 /* Frameworks */, 882B5E471CF7D4B900B6E160 /* Resources */, @@ -318,7 +376,6 @@ isa = PBXNativeTarget; buildConfigurationList = 882B5E631CF7D4B900B6E160 /* Build configuration list for PBXNativeTarget "ChatExampleUITests" */; buildPhases = ( - 1CC06D6D4B06C7DEBF56FCA1 /* [CP] Check Pods Manifest.lock */, 882B5E501CF7D4B900B6E160 /* Sources */, 882B5E511CF7D4B900B6E160 /* Frameworks */, 882B5E521CF7D4B900B6E160 /* Resources */, @@ -340,7 +397,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 1200; ORGANIZATIONNAME = MessageKit; TargetAttributes = { 882B5E321CF7D4B900B6E160 = { @@ -360,13 +417,16 @@ }; buildConfigurationList = 882B5E2E1CF7D4B900B6E160 /* Build configuration list for PBXProject "ChatExample" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 882B5E2A1CF7D4B900B6E160; + packageReferences = ( + 13CCA06125793E24005C19BB /* XCRemoteSwiftPackageReference "Kingfisher" */, + ); productRefGroup = 882B5E341CF7D4B900B6E160 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -383,8 +443,10 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 882B5E821CF7D53600B6E160 /* Assets.xcassets in Resources */, - 882B5E831CF7D53600B6E160 /* LaunchScreen.storyboard in Resources */, + 5074EF4F2163555900D82952 /* sound1.m4a in Resources */, + 5168957624F7AD560058C643 /* LaunchScreen.storyboard in Resources */, + 5168957524F7AD560058C643 /* Assets.xcassets in Resources */, + 5074EF4E2163555900D82952 /* sound2.m4a in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -404,119 +466,43 @@ }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - 011D1D45151418DF94DA1AD7 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-ChatExample-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 1CC06D6D4B06C7DEBF56FCA1 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-ChatExampleUITests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 3C8DFCD3063DA0DF099EC7B6 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-ChatExample/Pods-ChatExample-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/MessageInputBar/MessageInputBar.framework", - "${BUILT_PRODUCTS_DIR}/MessageKit/MessageKit.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MessageInputBar.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MessageKit.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ChatExample/Pods-ChatExample-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 52AB594E03799821A66AFF18 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-ChatExampleTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 7E0AFF9D207BB460004DFD4C /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; - }; -/* End PBXShellScriptBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ 882B5E2F1CF7D4B900B6E160 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9B49D53A263D9606008804B5 /* CustomLayoutSizeCalculator.swift in Sources */, 385C2922211FF32E0010B4BA /* CustomCell.swift in Sources */, + 9B49D542263DA6F9008804B5 /* CustomMessageContentCell.swift in Sources */, 385C2946211FF38F0010B4BA /* LaunchViewController.swift in Sources */, + 9B49D52D263C3E1F008804B5 /* CustomTextLayoutSizeCalculator.swift in Sources */, + 1C5433E224C3A33600A5383B /* SwiftUIExampleView.swift in Sources */, 385C2939211FF37B0010B4BA /* Lorem.swift in Sources */, 385C2928211FF33B0010B4BA /* MockMessage.swift in Sources */, + 63ECDABE24182A470016C349 /* MessageSubviewViewController.swift in Sources */, 385C2923211FF32E0010B4BA /* TableViewCells.swift in Sources */, 385C2942211FF38F0010B4BA /* AdvancedExampleViewController.swift in Sources */, + 50739F9621C5090A008CA369 /* BasicAudioController.swift in Sources */, 385C2937211FF37B0010B4BA /* SampleData.swift in Sources */, 385C2931211FF3630010B4BA /* UIColor+Extensions.swift in Sources */, 385C2932211FF3630010B4BA /* UIViewController+Extensions.swift in Sources */, + 138A04D428254AD300E2780F /* CustomInputBarExampleViewController.swift in Sources */, 385C292B211FF3450010B4BA /* Settings+UserDefaults.swift in Sources */, - 385C2945211FF38F0010B4BA /* NavigationController.swift in Sources */, + 9B49D547263DAA29008804B5 /* CustomTextMessageContentCell.swift in Sources */, + 9B49D528263C31FC008804B5 /* CustomLayoutExampleViewController.swift in Sources */, + 032A15DD25965D9A00E00FE3 /* AlertService.swift in Sources */, 385C2948211FF38F0010B4BA /* ChatViewController.swift in Sources */, 385C2944211FF38F0010B4BA /* MessageContainerController.swift in Sources */, + 383B9EB32172A1C4008AB91A /* MockUser.swift in Sources */, 385C2943211FF38F0010B4BA /* SettingsViewController.swift in Sources */, 385C2927211FF33B0010B4BA /* MockSocket.swift in Sources */, 385C2947211FF38F0010B4BA /* BasicExampleViewController.swift in Sources */, + 1C5433DD24C389C300A5383B /* MessagesView.swift in Sources */, 882B5E811CF7D53600B6E160 /* AppDelegate.swift in Sources */, 385C292D211FF3520010B4BA /* CustomMessageFlowLayout.swift in Sources */, + 032A15E225965E1100E00FE3 /* CameraInputBarAccessoryView.swift in Sources */, + 38CCCC592258419300DD5482 /* AutocompleteExampleViewController.swift in Sources */, + 63ECDAC2241889630016C349 /* MessageSubviewContainerViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -539,6 +525,14 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 13EFA5D327AC557A003002CC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = 13EFA5D227AC557A003002CC /* Kingfisher */; + }; + 13EFA5D527AC557F003002CC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = 13EFA5D427AC557F003002CC /* MessageKit */; + }; 882B5E4B1CF7D4B900B6E160 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 882B5E321CF7D4B900B6E160 /* ChatExample */; @@ -552,10 +546,10 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - 882B5E7A1CF7D53600B6E160 /* LaunchScreen.storyboard */ = { + 5168957224F7AD560058C643 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( - 882B5E7B1CF7D53600B6E160 /* Base */, + 5168957324F7AD560058C643 /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; @@ -567,6 +561,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -586,12 +581,13 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_IDENTITY = "Apple Development"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -610,12 +606,12 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -624,6 +620,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -643,12 +640,13 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_IDENTITY = "Apple Development"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; @@ -661,10 +659,10 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -672,87 +670,102 @@ }; 882B5E5E1CF7D4B900B6E160 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = A830E27DBE0B66B89C5D2EB8 /* Pods-ChatExample.debug.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + INFOPLIST_FILE = "$(SRCROOT)/Sources/Resources/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.messagekit.ChatExample; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; }; name = Debug; }; 882B5E5F1CF7D4B900B6E160 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9E0D67CD75BA7EB323FD391B /* Pods-ChatExample.release.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + INFOPLIST_FILE = "$(SRCROOT)/Sources/Resources/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.messagekit.ChatExample; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 4.2; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; 882B5E611CF7D4B900B6E160 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 0364943D08CDBE656E6F6DF8 /* Pods-ChatExampleTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; INFOPLIST_FILE = Tests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.ChatExampleTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ChatExample.app/ChatExample"; }; name = Debug; }; 882B5E621CF7D4B900B6E160 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = BFE5859D088A740A7D43E1B1 /* Pods-ChatExampleTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; INFOPLIST_FILE = Tests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.ChatExampleTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 4.2; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ChatExample.app/ChatExample"; }; name = Release; }; 882B5E641CF7D4B900B6E160 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B0DD3C951C9D064B5E6D6644 /* Pods-ChatExampleUITests.debug.xcconfig */; buildSettings = { INFOPLIST_FILE = UITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.ChatExampleUITests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; TEST_TARGET_NAME = ChatExample; }; name = Debug; }; 882B5E651CF7D4B900B6E160 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B2F1C412A96DE613A0AC31F8 /* Pods-ChatExampleUITests.release.xcconfig */; buildSettings = { INFOPLIST_FILE = UITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.ChatExampleUITests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 4.2; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; TEST_TARGET_NAME = ChatExample; }; name = Release; @@ -797,6 +810,38 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 13CCA06125793E24005C19BB /* XCRemoteSwiftPackageReference "Kingfisher" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/onevcat/Kingfisher"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 8.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 13EFA5D227AC557A003002CC /* Kingfisher */ = { + isa = XCSwiftPackageProductDependency; + package = 13CCA06125793E24005C19BB /* XCRemoteSwiftPackageReference "Kingfisher" */; + productName = Kingfisher; + }; + 13EFA5D427AC557F003002CC /* MessageKit */ = { + isa = XCSwiftPackageProductDependency; + productName = MessageKit; + }; + 13EFA5D627AC5631003002CC /* MessageKit */ = { + isa = XCSwiftPackageProductDependency; + productName = MessageKit; + }; + 13EFA5D827AC5634003002CC /* Kingfisher */ = { + isa = XCSwiftPackageProductDependency; + package = 13CCA06125793E24005C19BB /* XCRemoteSwiftPackageReference "Kingfisher" */; + productName = Kingfisher; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 882B5E2B1CF7D4B900B6E160 /* Project object */; } diff --git a/Example/ChatExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Example/ChatExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 831fce3b2..919434a62 100644 --- a/Example/ChatExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/Example/ChatExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExample.xcscheme b/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExample.xcscheme index 0fa1ed4fa..729b56c93 100644 --- a/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExample.xcscheme +++ b/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExample.xcscheme @@ -1,6 +1,6 @@ + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES"> @@ -54,17 +54,6 @@ - - - - - - - - - - - - - - diff --git a/Example/Podfile b/Example/Podfile deleted file mode 100644 index bf44d23b1..000000000 --- a/Example/Podfile +++ /dev/null @@ -1,16 +0,0 @@ -platform :ios, '9.0' - -target 'ChatExample' do - use_frameworks! - pod 'MessageKit', :path => '../' - pod 'MessageInputBar', :git => 'https://github.com/MessageKit/MessageInputBar.git', :branch => 'master' - -target 'ChatExampleTests' do - inherit! :search_paths -end - -target 'ChatExampleUITests' do - inherit! :search_paths -end - -end diff --git a/Example/Podfile.lock b/Example/Podfile.lock deleted file mode 100644 index 6d8fef549..000000000 --- a/Example/Podfile.lock +++ /dev/null @@ -1,30 +0,0 @@ -PODS: - - MessageInputBar (0.4.1): - - MessageInputBar/Core (= 0.4.1) - - MessageInputBar/Core (0.4.1) - - MessageKit (1.0.0): - - MessageInputBar/Core - -DEPENDENCIES: - - MessageInputBar (from `https://github.com/MessageKit/MessageInputBar.git`, branch `master`) - - MessageKit (from `../`) - -EXTERNAL SOURCES: - MessageInputBar: - :branch: master - :git: https://github.com/MessageKit/MessageInputBar.git - MessageKit: - :path: "../" - -CHECKOUT OPTIONS: - MessageInputBar: - :commit: faebe27f2dd8f39ea145e75b7296ef48133c099a - :git: https://github.com/MessageKit/MessageInputBar.git - -SPEC CHECKSUMS: - MessageInputBar: e81c7535347f1f7b923de7080409a535a004b6e4 - MessageKit: 2bbd13dd6a7c06f42f2d13ed8871d1fe5383b477 - -PODFILE CHECKSUM: 04c1a805e1997e83bacab1a34787e71e9fe4432b - -COCOAPODS: 1.5.3 diff --git a/Example/Sources/AppDelegate.swift b/Example/Sources/AppDelegate.swift index ea28caaae..604e3eb43 100644 --- a/Example/Sources/AppDelegate.swift +++ b/Example/Sources/AppDelegate.swift @@ -1,46 +1,52 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import UIKit -@UIApplicationMain +@main final internal class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? - var window: UIWindow? + func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + let masterViewController = UINavigationController(rootViewController: LaunchViewController()) + let splitViewController = UISplitViewController() + splitViewController.viewControllers = UIDevice.current.userInterfaceIdiom == .pad + ? [ + masterViewController, + UIViewController() + ] + : [masterViewController] + splitViewController.preferredDisplayMode = UISplitViewController.DisplayMode.oneBesideSecondary + masterViewController.navigationItem.largeTitleDisplayMode = .never - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - - window = UIWindow(frame: UIScreen.main.bounds) - window?.rootViewController = NavigationController(rootViewController: LaunchViewController()) - window?.makeKeyAndVisible() - - if UserDefaults.isFirstLaunch() { - // Enable Text Messages - UserDefaults.standard.set(true, forKey: "Text Messages") - } - - return true + window = UIWindow(frame: UIScreen.main.bounds) + window?.rootViewController = splitViewController + window?.makeKeyAndVisible() + + if UserDefaults.isFirstLaunch() { + // Enable Text Messages + UserDefaults.standard.set(true, forKey: "Text Messages") } + return true + } } diff --git a/Example/Sources/Assets.xcassets/Contents.json b/Example/Sources/Assets.xcassets/Contents.json deleted file mode 100644 index da4a164c9..000000000 --- a/Example/Sources/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Example/Sources/AudioController/BasicAudioController.swift b/Example/Sources/AudioController/BasicAudioController.swift new file mode 100644 index 000000000..9f239d3d8 --- /dev/null +++ b/Example/Sources/AudioController/BasicAudioController.swift @@ -0,0 +1,242 @@ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import AVFoundation +import MessageKit +import UIKit + +// MARK: - PlayerState + +/// The `PlayerState` indicates the current audio controller state +public enum PlayerState { + /// The audio controller is currently playing a sound + case playing + + /// The audio controller is currently in pause state + case pause + + /// The audio controller is not playing any sound and audioPlayer is nil + case stopped +} + +// MARK: - BasicAudioController + +/// The `BasicAudioController` update UI for current audio cell that is playing a sound +/// and also creates and manage an `AVAudioPlayer` states, play, pause and stop. +@MainActor +open class BasicAudioController: NSObject, @preconcurrency AVAudioPlayerDelegate { + // MARK: Lifecycle + + // MARK: - Init Methods + + public init(messageCollectionView: MessagesCollectionView) { + self.messageCollectionView = messageCollectionView + super.init() + } + + // MARK: Open + + /// The `AVAudioPlayer` that is playing the sound + open var audioPlayer: AVAudioPlayer? + + /// The `AudioMessageCell` that is currently playing sound + open weak var playingCell: AudioMessageCell? + + /// The `MessageType` that is currently playing sound + open var playingMessage: MessageType? + + /// Specify if current audio controller state: playing, in pause or none + open private(set) var state: PlayerState = .stopped + + // MARK: - Methods + + /// Used to configure the audio cell UI: + /// 1. play button selected state; + /// 2. progressView progress; + /// 3. durationLabel text; + /// + /// - Parameters: + /// - cell: The `AudioMessageCell` that needs to be configure. + /// - message: The `MessageType` that configures the cell. + /// + /// - Note: + /// This protocol method is called by MessageKit every time an audio cell needs to be configure + open func configureAudioCell(_ cell: AudioMessageCell, message: MessageType) { + if playingMessage?.messageId == message.messageId, let collectionView = messageCollectionView, let player = audioPlayer { + playingCell = cell + cell.progressView.progress = (player.duration == 0) ? 0 : Float(player.currentTime / player.duration) + cell.playButton.isSelected = (player.isPlaying == true) ? true : false + guard let displayDelegate = collectionView.messagesDisplayDelegate else { + fatalError("MessagesDisplayDelegate has not been set.") + } + cell.durationLabel.text = displayDelegate.audioProgressTextFormat( + Float(player.currentTime), + for: cell, + in: collectionView) + } + } + + /// Used to start play audio sound + /// + /// - Parameters: + /// - message: The `MessageType` that contain the audio item to be played. + /// - audioCell: The `AudioMessageCell` that needs to be updated while audio is playing. + open func playSound(for message: MessageType, in audioCell: AudioMessageCell) { + switch message.kind { + case .audio(let item): + playingCell = audioCell + playingMessage = message + guard let player = try? AVAudioPlayer(contentsOf: item.url) else { + print("Failed to create audio player for URL: \(item.url)") + return + } + audioPlayer = player + audioPlayer?.prepareToPlay() + audioPlayer?.delegate = self + audioPlayer?.play() + state = .playing + audioCell.playButton.isSelected = true // show pause button on audio cell + startProgressTimer() + audioCell.delegate?.didStartAudio(in: audioCell) + default: + print("BasicAudioPlayer failed play sound because given message kind is not Audio") + } + } + + /// Used to pause the audio sound + /// + /// - Parameters: + /// - message: The `MessageType` that contain the audio item to be pause. + /// - audioCell: The `AudioMessageCell` that needs to be updated by the pause action. + open func pauseSound(for _: MessageType, in audioCell: AudioMessageCell) { + audioPlayer?.pause() + state = .pause + audioCell.playButton.isSelected = false // show play button on audio cell + progressTimer?.invalidate() + if let cell = playingCell { + cell.delegate?.didPauseAudio(in: cell) + } + } + + /// Stops any ongoing audio playing if exists + open func stopAnyOngoingPlaying() { + guard + let player = audioPlayer, + let collectionView = messageCollectionView + else { return } // If the audio player is nil then we don't need to go through the stopping logic + player.stop() + state = .stopped + if let cell = playingCell { + cell.progressView.progress = 0.0 + cell.playButton.isSelected = false + guard let displayDelegate = collectionView.messagesDisplayDelegate else { + fatalError("MessagesDisplayDelegate has not been set.") + } + cell.durationLabel.text = displayDelegate.audioProgressTextFormat( + Float(player.duration), + for: cell, + in: collectionView) + cell.delegate?.didStopAudio(in: cell) + } + progressTimer?.invalidate() + progressTimer = nil + audioPlayer = nil + playingMessage = nil + playingCell = nil + } + + /// Resume a currently pause audio sound + open func resumeSound() { + guard let player = audioPlayer, let cell = playingCell else { + stopAnyOngoingPlaying() + return + } + player.prepareToPlay() + player.play() + state = .playing + startProgressTimer() + cell.playButton.isSelected = true // show pause button on audio cell + cell.delegate?.didStartAudio(in: cell) + } + + // MARK: - AVAudioPlayerDelegate + open func audioPlayerDidFinishPlaying(_: AVAudioPlayer, successfully _: Bool) { + stopAnyOngoingPlaying() + } + + open func audioPlayerDecodeErrorDidOccur(_: AVAudioPlayer, error _: Error?) { + stopAnyOngoingPlaying() + } + + // MARK: Public + + // The `MessagesCollectionView` where the playing cell exist + public weak var messageCollectionView: MessagesCollectionView? + + // MARK: Internal + + /// The `Timer` that update playing progress + internal var progressTimer: Timer? + + // MARK: Private + + // MARK: - Fire Methods + @objc + private func didFireProgressTimer(_: Timer) { + guard let player = audioPlayer, let collectionView = messageCollectionView, let cell = playingCell else { + return + } + // check if can update playing cell + if let playingCellIndexPath = collectionView.indexPath(for: cell) { + // 1. get the current message that decorates the playing cell + // 2. check if current message is the same with playing message, if so then update the cell content + // Note: Those messages differ in the case of cell reuse + let currentMessage = collectionView.messagesDataSource?.messageForItem(at: playingCellIndexPath, in: collectionView) + if currentMessage != nil, currentMessage?.messageId == playingMessage?.messageId { + // messages are the same update cell content + cell.progressView.progress = (player.duration == 0) ? 0 : Float(player.currentTime / player.duration) + guard let displayDelegate = collectionView.messagesDisplayDelegate else { + fatalError("MessagesDisplayDelegate has not been set.") + } + cell.durationLabel.text = displayDelegate.audioProgressTextFormat( + Float(player.currentTime), + for: cell, + in: collectionView) + } else { + // if the current message is not the same with playing message stop playing sound + stopAnyOngoingPlaying() + } + } + } + + // MARK: - Private Methods + private func startProgressTimer() { + progressTimer?.invalidate() + progressTimer = nil + progressTimer = Timer.scheduledTimer( + timeInterval: 0.1, + target: self, + selector: #selector(BasicAudioController.didFireProgressTimer(_:)), + userInfo: nil, + repeats: true) + } +} diff --git a/Example/Sources/Data Generation/Lorem.swift b/Example/Sources/Data Generation/Lorem.swift index b36e22b41..85e3a8d3b 100755 --- a/Example/Sources/Data Generation/Lorem.swift +++ b/Example/Sources/Data Generation/Lorem.swift @@ -1,317 +1,297 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation +// MARK: - Lorem + public class Lorem { - private static let wordList = [ - "alias", "consequatur", "aut", "perferendis", "sit", "voluptatem", - "accusantium", "doloremque", "aperiam", "eaque", "ipsa", "quae", "ab", - "illo", "inventore", "veritatis", "et", "quasi", "architecto", - "beatae", "vitae", "dicta", "sunt", "explicabo", "aspernatur", "aut", - "odit", "aut", "fugit", "sed", "quia", "consequuntur", "magni", - "dolores", "eos", "qui", "ratione", "voluptatem", "sequi", "nesciunt", - "neque", "dolorem", "ipsum", "quia", "dolor", "sit", "amet", - "consectetur", "adipisci", "velit", "sed", "quia", "non", "numquam", - "eius", "modi", "tempora", "incidunt", "ut", "labore", "et", "dolore", - "magnam", "aliquam", "quaerat", "voluptatem", "ut", "enim", "ad", - "minima", "veniam", "quis", "nostrum", "exercitationem", "ullam", - "corporis", "nemo", "enim", "ipsam", "voluptatem", "quia", "voluptas", - "sit", "suscipit", "laboriosam", "nisi", "ut", "aliquid", "ex", "ea", - "commodi", "consequatur", "quis", "autem", "vel", "eum", "iure", - "reprehenderit", "qui", "in", "ea", "voluptate", "velit", "esse", - "quam", "nihil", "molestiae", "et", "iusto", "odio", "dignissimos", - "ducimus", "qui", "blanditiis", "praesentium", "laudantium", "totam", - "rem", "voluptatum", "deleniti", "atque", "corrupti", "quos", - "dolores", "et", "quas", "molestias", "excepturi", "sint", - "occaecati", "cupiditate", "non", "provident", "sed", "ut", - "perspiciatis", "unde", "omnis", "iste", "natus", "error", - "similique", "sunt", "in", "culpa", "qui", "officia", "deserunt", - "mollitia", "animi", "id", "est", "laborum", "et", "dolorum", "fuga", - "et", "harum", "quidem", "rerum", "facilis", "est", "et", "expedita", - "distinctio", "nam", "libero", "tempore", "cum", "soluta", "nobis", - "est", "eligendi", "optio", "cumque", "nihil", "impedit", "quo", - "porro", "quisquam", "est", "qui", "minus", "id", "quod", "maxime", - "placeat", "facere", "possimus", "omnis", "voluptas", "assumenda", - "est", "omnis", "dolor", "repellendus", "temporibus", "autem", - "quibusdam", "et", "aut", "consequatur", "vel", "illum", "qui", - "dolorem", "eum", "fugiat", "quo", "voluptas", "nulla", "pariatur", - "at", "vero", "eos", "et", "accusamus", "officiis", "debitis", "aut", - "rerum", "necessitatibus", "saepe", "eveniet", "ut", "et", - "voluptates", "repudiandae", "sint", "et", "molestiae", "non", - "recusandae", "itaque", "earum", "rerum", "hic", "tenetur", "a", - "sapiente", "delectus", "ut", "aut", "reiciendis", "voluptatibus", - "maiores", "doloribus", "asperiores", "repellat" - ] - - /** - Return a random word. - - - returns: Returns a random word. - */ - public class func word() -> String { - return wordList.random()! - } - - /** - Return an array of `count` words. - - - parameter count: The number of words to return. - - - returns: Returns an array of `count` words. - */ - public class func words(nbWords: Int = 3) -> [String] { - return wordList.random(nbWords) + // MARK: Public + + /// Return a random word. + /// + /// - returns: Returns a random word. + public class func word() -> String { + wordList.random()! + } + + /// Return an array of `count` words. + /// + /// - parameter count: The number of words to return. + /// + /// - returns: Returns an array of `count` words. + public class func words(nbWords: Int = 3) -> [String] { + wordList.random(nbWords) + } + + /// Return a string of `count` words. + /// + /// - parameter count: The number of words the string should contain. + /// + /// - returns: Returns a string of `count` words. + public class func words(nbWords: Int = 3) -> String { + words(nbWords: nbWords).joined(separator: " ") + } + + /// Generate a sentence of `nbWords` words. + /// - parameter nbWords: The number of words the sentence should contain. + /// - parameter variable: If `true`, the number of words will vary between + /// +/- 40% of `nbWords`. + /// - returns: + public class func sentence(nbWords: Int = 6, variable: Bool = true) -> String { + if nbWords <= 0 { + return "" } - - /** - Return a string of `count` words. - - - parameter count: The number of words the string should contain. - - - returns: Returns a string of `count` words. - */ - public class func words(nbWords: Int = 3) -> String { - return words(nbWords: nbWords).joined(separator: " ") + + let result: String = words(nbWords: variable ? nbWords.randomize(variation: 40) : nbWords) + + return result.firstCapitalized + "." + } + + /// Generate an array of sentences. + /// - parameter nbSentences: The number of sentences to generate. + /// + /// - returns: Returns an array of random sentences. + public class func sentences(nbSentences: Int = 3) -> [String] { + (0 ..< nbSentences).map { _ in sentence() } + } + + /// Generate a paragraph with `nbSentences` random sentences. + /// - parameter nbSentences: The number of sentences the paragraph should + /// contain. + /// - parameter variable: If `true`, the number of sentences will vary + /// between +/- 40% of `nbSentences`. + /// - returns: Returns a paragraph with `nbSentences` random sentences. + public class func paragraph(nbSentences: Int = 3, variable: Bool = true) -> String { + if nbSentences <= 0 { + return "" } - - /** - Generate a sentence of `nbWords` words. - - parameter nbWords: The number of words the sentence should contain. - - parameter variable: If `true`, the number of words will vary between - +/- 40% of `nbWords`. - - returns: - */ - public class func sentence(nbWords: Int = 6, variable: Bool = true) -> String { - if nbWords <= 0 { - return "" + + return sentences(nbSentences: variable ? nbSentences.randomize(variation: 40) : nbSentences).joined(separator: " ") + } + + /// Generate an array of random paragraphs. + /// - parameter nbParagraphs: The number of paragraphs to generate. + /// - returns: Returns an array of `nbParagraphs` paragraphs. + public class func paragraphs(nbParagraphs: Int = 3) -> [String] { + (0 ..< nbParagraphs).map { _ in paragraph() } + } + + /// Generate a string of random paragraphs. + /// - parameter nbParagraphs: The number of paragraphs to generate. + /// - returns: Returns a string of random paragraphs. + public class func paragraphs(nbParagraphs: Int = 3) -> String { + paragraphs(nbParagraphs: nbParagraphs).joined(separator: "\n\n") + } + + /// Generate a string of at most `maxNbChars` characters. + /// - parameter maxNbChars: The maximum number of characters the string + /// should contain. + /// - returns: Returns a string of at most `maxNbChars` characters. + public class func text(maxNbChars: Int = 200) -> String { + var result: [String] = [] + + if maxNbChars < 5 { + return "" + } else if maxNbChars < 25 { + while result.count == 0 { + var size = 0 + + while size < maxNbChars { + let w = (size != 0 ? " " : "") + word() + result.append(w) + size += w.count } - - let result: String = self.words(nbWords: variable ? nbWords.randomize(variation: 40) : nbWords) - - return result.firstCapitalized + "." - } - - /** - Generate an array of sentences. - - parameter nbSentences: The number of sentences to generate. - - - returns: Returns an array of random sentences. - */ - public class func sentences(nbSentences: Int = 3) -> [String] { - return (0.. String { - if nbSentences <= 0 { - return "" + + _ = result.popLast() + } + } else if maxNbChars < 100 { + while result.count == 0 { + var size = 0 + + while size < maxNbChars { + let s = (size != 0 ? " " : "") + sentence() + result.append(s) + size += s.count } - - return sentences(nbSentences: variable ? nbSentences.randomize(variation: 40) : nbSentences).joined(separator: " ") - } - - /** - Generate an array of random paragraphs. - - parameter nbParagraphs: The number of paragraphs to generate. - - returns: Returns an array of `nbParagraphs` paragraphs. - */ - public class func paragraphs(nbParagraphs: Int = 3) -> [String] { - return (0.. String { - return paragraphs(nbParagraphs: nbParagraphs).joined(separator: "\n\n") - } - - /** - Generate a string of at most `maxNbChars` characters. - - parameter maxNbChars: The maximum number of characters the string - should contain. - - returns: Returns a string of at most `maxNbChars` characters. - */ - public class func text(maxNbChars: Int = 200) -> String { - var result: [String] = [] - - if maxNbChars < 5 { - return "" - } else if maxNbChars < 25 { - while result.count == 0 { - var size = 0 - - while size < maxNbChars { - let w = (size != 0 ? " " : "") + word() - result.append(w) - size += w.count - } - - _ = result.popLast() - } - } else if maxNbChars < 100 { - while result.count == 0 { - var size = 0 - - while size < maxNbChars { - let s = (size != 0 ? " " : "") + sentence() - result.append(s) - size += s.count - } - - _ = result.popLast() - } - } else { - while result.count == 0 { - var size = 0 - - while size < maxNbChars { - let p = (size != 0 ? "\n" : "") + paragraph() - result.append(p) - size += p.count - } - - _ = result.popLast() - } + + _ = result.popLast() + } + } else { + while result.count == 0 { + var size = 0 + + while size < maxNbChars { + let p = (size != 0 ? "\n" : "") + paragraph() + result.append(p) + size += p.count } - - return result.joined(separator: "") + + _ = result.popLast() + } } + + return result.joined(separator: "") + } + + // MARK: Private + + private static let wordList = [ + "alias", "consequatur", "aut", "perferendis", "sit", "voluptatem", + "accusantium", "doloremque", "aperiam", "eaque", "ipsa", "quae", "ab", + "illo", "inventore", "veritatis", "et", "quasi", "architecto", + "beatae", "vitae", "dicta", "sunt", "explicabo", "aspernatur", "aut", + "odit", "aut", "fugit", "sed", "quia", "consequuntur", "magni", + "dolores", "eos", "qui", "ratione", "voluptatem", "sequi", "nesciunt", + "neque", "dolorem", "ipsum", "quia", "dolor", "sit", "amet", + "consectetur", "adipisci", "velit", "sed", "quia", "non", "numquam", + "eius", "modi", "tempora", "incidunt", "ut", "labore", "et", "dolore", + "magnam", "aliquam", "quaerat", "voluptatem", "ut", "enim", "ad", + "minima", "veniam", "quis", "nostrum", "exercitationem", "ullam", + "corporis", "nemo", "enim", "ipsam", "voluptatem", "quia", "voluptas", + "sit", "suscipit", "laboriosam", "nisi", "ut", "aliquid", "ex", "ea", + "commodi", "consequatur", "quis", "autem", "vel", "eum", "iure", + "reprehenderit", "qui", "in", "ea", "voluptate", "velit", "esse", + "quam", "nihil", "molestiae", "et", "iusto", "odio", "dignissimos", + "ducimus", "qui", "blanditiis", "praesentium", "laudantium", "totam", + "rem", "voluptatum", "deleniti", "atque", "corrupti", "quos", + "dolores", "et", "quas", "molestias", "excepturi", "sint", + "occaecati", "cupiditate", "non", "provident", "sed", "ut", + "perspiciatis", "unde", "omnis", "iste", "natus", "error", + "similique", "sunt", "in", "culpa", "qui", "officia", "deserunt", + "mollitia", "animi", "id", "est", "laborum", "et", "dolorum", "fuga", + "et", "harum", "quidem", "rerum", "facilis", "est", "et", "expedita", + "distinctio", "nam", "libero", "tempore", "cum", "soluta", "nobis", + "est", "eligendi", "optio", "cumque", "nihil", "impedit", "quo", + "porro", "quisquam", "est", "qui", "minus", "id", "quod", "maxime", + "placeat", "facere", "possimus", "omnis", "voluptas", "assumenda", + "est", "omnis", "dolor", "repellendus", "temporibus", "autem", + "quibusdam", "et", "aut", "consequatur", "vel", "illum", "qui", + "dolorem", "eum", "fugiat", "quo", "voluptas", "nulla", "pariatur", + "at", "vero", "eos", "et", "accusamus", "officiis", "debitis", "aut", + "rerum", "necessitatibus", "saepe", "eveniet", "ut", "et", + "voluptates", "repudiandae", "sint", "et", "molestiae", "non", + "recusandae", "itaque", "earum", "rerum", "hic", "tenetur", "a", + "sapiente", "delectus", "ut", "aut", "reiciendis", "voluptatibus", + "maiores", "doloribus", "asperiores", "repellat", + ] } extension String { - var firstCapitalized: String { - var string = self - string.replaceSubrange(string.startIndex...string.startIndex, with: String(string[string.startIndex]).capitalized) - return string - } + var firstCapitalized: String { + var string = self + string.replaceSubrange(string.startIndex ... string.startIndex, with: String(string[string.startIndex]).capitalized) + return string + } } -public extension Array { - /** - Shuffle the array in-place using the Fisher-Yates algorithm. - */ - public mutating func shuffle() { - for i in 0..<(count - 1) { - let j = Int(arc4random_uniform(UInt32(count - i))) + i - if j != i { - self.swapAt(i, j) - } - } - } - - /** - Return a shuffled version of the array using the Fisher-Yates - algorithm. - - - returns: Returns a shuffled version of the array. - */ - public func shuffled() -> [Element] { - var list = self - list.shuffle() - - return list - } - - /** - Return a random element from the array. - - returns: Returns a random element from the array or `nil` if the - array is empty. - */ - public func random() -> Element? { - return (count > 0) ? self.shuffled()[0] : nil +extension Array { + /// Shuffle the array in-place using the Fisher-Yates algorithm. + public mutating func shuffle() { + if count == 0 { + return } - - /** - Return a random subset of `cnt` elements from the array. - - returns: Returns a random subset of `cnt` elements from the array. - */ - public func random(_ count: Int = 1) -> [Element] { - let result = shuffled() - - return (count > result.count) ? result : Array(result[0.. [Element] { + var list = self + list.shuffle() + + return list + } + + /// Return a random element from the array. + /// - returns: Returns a random element from the array or `nil` if the + /// array is empty. + public func random() -> Element? { + (count > 0) ? shuffled()[0] : nil + } + + /// Return a random subset of `cnt` elements from the array. + /// - returns: Returns a random subset of `cnt` elements from the array. + public func random(_ count: Int = 1) -> [Element] { + let result = shuffled() + + return (count > result.count) ? result : Array(result[0 ..< count]) + } } extension Int { - /** - Return a random number between `min` and `max`. - - note: The maximum value cannot be more than `UInt32.max - 1` - - - parameter min: The minimum value of the random value (defaults to `0`). - - parameter max: The maximum value of the random value (defaults to `UInt32.max - 1`) - - - returns: Returns a random value between `min` and `max`. - */ - public static func random(min: Int = 0, max: Int = Int.max) -> Int { - precondition(min <= max, "attempt to call random() with min > max") - - let diff = UInt(bitPattern: max &- min) - let result = UInt.random(min: 0, max: diff) - - return min + Int(bitPattern: result) - } - - public func randomize(variation: Int) -> Int { - let multiplier = Double(Int.random(min: 100 - variation, max: 100 + variation)) / 100 - let randomized = Double(self) * multiplier - - return Int(randomized) + 1 - } + /// Return a random number between `min` and `max`. + /// - note: The maximum value cannot be more than `UInt32.max - 1` + /// + /// - parameter min: The minimum value of the random value (defaults to `0`). + /// - parameter max: The maximum value of the random value (defaults to `UInt32.max - 1`) + /// + /// - returns: Returns a random value between `min` and `max`. + public static func random(min: Int = 0, max: Int = Int.max) -> Int { + precondition(min <= max, "attempt to call random() with min > max") + + let diff = UInt(bitPattern: max &- min) + let result = UInt.random(min: 0, max: diff) + + return min + Int(bitPattern: result) + } + + public func randomize(variation: Int) -> Int { + let multiplier = Double(Int.random(min: 100 - variation, max: 100 + variation)) / 100 + let randomized = Double(self) * multiplier + + return Int(randomized) + 1 + } } -private extension UInt { - static func random(min: UInt, max: UInt) -> UInt { - precondition(min <= max, "attempt to call random() with min > max") - - if min == UInt.min && max == UInt.max { - var result: UInt = 0 - arc4random_buf(&result, MemoryLayout.size(ofValue: result)) - - return result - } else { - let range = max - min + 1 - let limit = UInt.max - UInt.max % range - var result: UInt = 0 - - repeat { - arc4random_buf(&result, MemoryLayout.size(ofValue: result)) - } while result >= limit - - result = result % range - - return min + result - } +extension UInt { + fileprivate static func random(min: UInt, max: UInt) -> UInt { + precondition(min <= max, "attempt to call random() with min > max") + + if min == UInt.min, max == UInt.max { + var result: UInt = 0 + arc4random_buf(&result, MemoryLayout.size(ofValue: result)) + + return result + } else { + let range = max - min + 1 + let limit = UInt.max - UInt.max % range + var result: UInt = 0 + + repeat { + arc4random_buf(&result, MemoryLayout.size(ofValue: result)) + } while result >= limit + + result = result % range + + return min + result } + } } diff --git a/Example/Sources/Data Generation/SampleData.swift b/Example/Sources/Data Generation/SampleData.swift index 07b47218c..ffe348807 100644 --- a/Example/Sources/Data Generation/SampleData.swift +++ b/Example/Sources/Data Generation/SampleData.swift @@ -1,238 +1,315 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. -import MessageKit +import AVFoundation import CoreLocation +import MessageKit +import UIKit final internal class SampleData { + // MARK: Lifecycle - static let shared = SampleData() - - private init() {} - - enum MessageTypes: UInt32, CaseIterable { - case Text = 0 - case AttributedText = 1 - case Photo = 2 - case Video = 3 - case Emoji = 4 - case Location = 5 - case Url = 6 - case Phone = 7 - case Custom = 8 - - static func random() -> MessageTypes { - // Update as new enumerations are added - let maxValue = Custom.rawValue - - let rand = arc4random_uniform(maxValue+1) - return MessageTypes(rawValue: rand)! - } - } + private init() { } - let system = Sender(id: "000000", displayName: "System") - let nathan = Sender(id: "000001", displayName: "Nathan Tannar") - let steven = Sender(id: "000002", displayName: "Steven Deutsch") - let wu = Sender(id: "000003", displayName: "Wu Zhong") + // MARK: Internal - lazy var senders = [nathan, steven, wu] + enum MessageTypes: String, CaseIterable { + case Text + case AttributedText + case Photo + case PhotoFromURL = "Photo from URL" + case Video + case Audio + case Emoji + case Location + case Url + case Phone + case Custom + case ShareContact + } - var currentSender: Sender { - return nathan - } + static let shared = SampleData() - var now = Date() - - let messageImages: [UIImage] = [#imageLiteral(resourceName: "img1"), #imageLiteral(resourceName: "img2")] - - let emojis = [ - "πŸ‘", - "πŸ˜‚πŸ˜‚πŸ˜‚", - "πŸ‘‹πŸ‘‹πŸ‘‹", - "😱😱😱", - "πŸ˜ƒπŸ˜ƒπŸ˜ƒ", - "❀️" - ] - - let attributes = ["Font1", "Font2", "Font3", "Font4", "Color", "Combo"] - - let locations: [CLLocation] = [ - CLLocation(latitude: 37.3118, longitude: -122.0312), - CLLocation(latitude: 33.6318, longitude: -100.0386), - CLLocation(latitude: 29.3358, longitude: -108.8311), - CLLocation(latitude: 39.3218, longitude: -127.4312), - CLLocation(latitude: 35.3218, longitude: -127.4314), - CLLocation(latitude: 39.3218, longitude: -113.3317) - ] - - func attributedString(with text: String) -> NSAttributedString { - let nsString = NSString(string: text) - var mutableAttributedString = NSMutableAttributedString(string: text) - let randomAttribute = Int(arc4random_uniform(UInt32(attributes.count))) - let range = NSRange(location: 0, length: nsString.length) - - switch attributes[randomAttribute] { - case "Font1": - mutableAttributedString.addAttribute(NSAttributedString.Key.font, value: UIFont.preferredFont(forTextStyle: .body), range: range) - case "Font2": - mutableAttributedString.addAttributes([NSAttributedString.Key.font: UIFont.monospacedDigitSystemFont(ofSize: UIFont.systemFontSize, weight: UIFont.Weight.bold)], range: range) - case "Font3": - mutableAttributedString.addAttributes([NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: UIFont.systemFontSize)], range: range) - case "Font4": - mutableAttributedString.addAttributes([NSAttributedString.Key.font: UIFont.italicSystemFont(ofSize: UIFont.systemFontSize)], range: range) - case "Color": - mutableAttributedString.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.red], range: range) - case "Combo": - let msg9String = "Use .attributedText() to add bold, italic, colored text and more..." - let msg9Text = NSString(string: msg9String) - let msg9AttributedText = NSMutableAttributedString(string: String(msg9Text)) - - msg9AttributedText.addAttribute(NSAttributedString.Key.font, value: UIFont.preferredFont(forTextStyle: .body), range: NSRange(location: 0, length: msg9Text.length)) - msg9AttributedText.addAttributes([NSAttributedString.Key.font: UIFont.monospacedDigitSystemFont(ofSize: UIFont.systemFontSize, weight: UIFont.Weight.bold)], range: msg9Text.range(of: ".attributedText()")) - msg9AttributedText.addAttributes([NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: UIFont.systemFontSize)], range: msg9Text.range(of: "bold")) - msg9AttributedText.addAttributes([NSAttributedString.Key.font: UIFont.italicSystemFont(ofSize: UIFont.systemFontSize)], range: msg9Text.range(of: "italic")) - msg9AttributedText.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.red], range: msg9Text.range(of: "colored")) - mutableAttributedString = msg9AttributedText - default: - fatalError("Unrecognized attribute for mock message") - } - - return NSAttributedString(attributedString: mutableAttributedString) - } + let system = MockUser(senderId: "000000", displayName: "System") + let nathan = MockUser(senderId: "000001", displayName: "Nathan Tannar") + let steven = MockUser(senderId: "000002", displayName: "Steven Deutsch") + let wu = MockUser(senderId: "000003", displayName: "Wu Zhong") + + lazy var senders = [nathan, steven, wu] + + lazy var contactsToShare = [ + MockContactItem(name: "System", initials: "S"), + MockContactItem(name: "Nathan Tannar", initials: "NT", emails: ["test@test.com"]), + MockContactItem(name: "Steven Deutsch", initials: "SD", phoneNumbers: ["+1-202-555-0114", "+1-202-555-0145"]), + MockContactItem(name: "Wu Zhong", initials: "WZ", phoneNumbers: ["202-555-0158"]), + MockContactItem(name: "+40 123 123", initials: "#", phoneNumbers: ["+40 123 123"]), + MockContactItem(name: "test@test.com", initials: "#", emails: ["test@test.com"]), + ] + + var now = Date() + + let messageImages: [UIImage] = [#imageLiteral(resourceName: "img1"), #imageLiteral(resourceName: "img2")] + let messageImageURLs: [URL] = [ + URL(string: "https://placekitten.com/g/200/300")!, + URL(string: "https://placekitten.com/g/300/300")!, + URL(string: "https://placekitten.com/g/300/400")!, + URL(string: "https://placekitten.com/g/400/400")!, + ] + + let emojis = [ + "πŸ‘", + "πŸ˜‚πŸ˜‚πŸ˜‚", + "πŸ‘‹πŸ‘‹πŸ‘‹", + "😱😱😱", + "πŸ˜ƒπŸ˜ƒπŸ˜ƒ", + "❀️", + ] + + let attributes = ["Font1", "Font2", "Font3", "Font4", "Color", "Combo"] + + let locations: [CLLocation] = [ + CLLocation(latitude: 37.3118, longitude: -122.0312), + CLLocation(latitude: 33.6318, longitude: -100.0386), + CLLocation(latitude: 29.3358, longitude: -108.8311), + CLLocation(latitude: 39.3218, longitude: -127.4312), + CLLocation(latitude: 35.3218, longitude: -127.4314), + CLLocation(latitude: 39.3218, longitude: -113.3317), + ] + + let sounds: [URL] = [ + Bundle.main.url(forResource: "sound1", withExtension: "m4a")!, + Bundle.main.url(forResource: "sound2", withExtension: "m4a")!, + ] - func dateAddingRandomTime() -> Date { - let randomNumber = Int(arc4random_uniform(UInt32(10))) - if randomNumber % 2 == 0 { - let date = Calendar.current.date(byAdding: .hour, value: randomNumber, to: now)! - now = date - return date - } else { - let randomMinute = Int(arc4random_uniform(UInt32(59))) - let date = Calendar.current.date(byAdding: .minute, value: randomMinute, to: now)! - now = date - return date - } + let linkItem: (() -> MockLinkItem) = { + MockLinkItem( + text: "\(Lorem.sentence()) https://github.com/MessageKit", + attributedText: nil, + url: URL(string: "https://github.com/MessageKit")!, + title: "MessageKit", + teaser: "A community-driven replacement for JSQMessagesViewController - MessageKit", + thumbnailImage: UIImage(named: "mkorglogo")!) + } + + var currentSender: MockUser { + steven + } + + func attributedString(with text: String) -> NSAttributedString { + let nsString = NSString(string: text) + var mutableAttributedString = NSMutableAttributedString(string: text) + let randomAttribute = Int(arc4random_uniform(UInt32(attributes.count))) + let range = NSRange(location: 0, length: nsString.length) + + switch attributes[randomAttribute] { + case "Font1": + mutableAttributedString.addAttribute( + NSAttributedString.Key.font, + value: UIFont.preferredFont(forTextStyle: .body), + range: range) + case "Font2": + mutableAttributedString.addAttributes( + [ + NSAttributedString.Key.font: UIFont + .monospacedDigitSystemFont(ofSize: UIFont.systemFontSize, weight: UIFont.Weight.bold), + ], + range: range) + case "Font3": + mutableAttributedString.addAttributes( + [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: UIFont.systemFontSize)], + range: range) + case "Font4": + mutableAttributedString.addAttributes( + [NSAttributedString.Key.font: UIFont.italicSystemFont(ofSize: UIFont.systemFontSize)], + range: range) + case "Color": + mutableAttributedString.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.red], range: range) + case "Combo": + let msg9String = "Use .attributedText() to add bold, italic, colored text and more..." + let msg9Text = NSString(string: msg9String) + let msg9AttributedText = NSMutableAttributedString(string: String(msg9Text)) + + msg9AttributedText.addAttribute( + NSAttributedString.Key.font, + value: UIFont.preferredFont(forTextStyle: .body), + range: NSRange(location: 0, length: msg9Text.length)) + msg9AttributedText.addAttributes( + [ + NSAttributedString.Key.font: UIFont + .monospacedDigitSystemFont(ofSize: UIFont.systemFontSize, weight: UIFont.Weight.bold), + ], + range: msg9Text.range(of: ".attributedText()")) + msg9AttributedText.addAttributes( + [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: UIFont.systemFontSize)], + range: msg9Text.range(of: "bold")) + msg9AttributedText.addAttributes( + [NSAttributedString.Key.font: UIFont.italicSystemFont(ofSize: UIFont.systemFontSize)], + range: msg9Text.range(of: "italic")) + msg9AttributedText.addAttributes( + [NSAttributedString.Key.foregroundColor: UIColor.red], + range: msg9Text.range(of: "colored")) + mutableAttributedString = msg9AttributedText + default: + fatalError("Unrecognized attribute for mock message") } - - func randomMessageType() -> MessageTypes { - let messageType = MessageTypes.random() - - if !UserDefaults.standard.bool(forKey: "\(messageType)" + " Messages") { - return randomMessageType() - } - - return messageType + + return NSAttributedString(attributedString: mutableAttributedString) + } + + func dateAddingRandomTime() -> Date { + let randomNumber = Int(arc4random_uniform(UInt32(10))) + if randomNumber % 2 == 0 { + let date = Calendar.current.date(byAdding: .hour, value: randomNumber, to: now)! + now = date + return date + } else { + let randomMinute = Int(arc4random_uniform(UInt32(59))) + let date = Calendar.current.date(byAdding: .minute, value: randomMinute, to: now)! + now = date + return date } + } - func randomMessage(allowedSenders: [Sender]) -> MockMessage { - - let randomNumberSender = Int(arc4random_uniform(UInt32(allowedSenders.count))) - - let uniqueID = NSUUID().uuidString - let sender = allowedSenders[randomNumberSender] - let date = dateAddingRandomTime() - - switch randomMessageType() { - case .Text: - let randomSentence = Lorem.sentence() - return MockMessage(text: randomSentence, sender: sender, messageId: uniqueID, date: date) - case .AttributedText: - let randomSentence = Lorem.sentence() - let attributedText = attributedString(with: randomSentence) - return MockMessage(attributedText: attributedText, sender: senders[randomNumberSender], messageId: uniqueID, date: date) - case .Photo: - let randomNumberImage = Int(arc4random_uniform(UInt32(messageImages.count))) - let image = messageImages[randomNumberImage] - return MockMessage(image: image, sender: sender, messageId: uniqueID, date: date) - case .Video: - let randomNumberImage = Int(arc4random_uniform(UInt32(messageImages.count))) - let image = messageImages[randomNumberImage] - return MockMessage(thumbnail: image, sender: sender, messageId: uniqueID, date: date) - case .Emoji: - let randomNumberEmoji = Int(arc4random_uniform(UInt32(emojis.count))) - return MockMessage(emoji: emojis[randomNumberEmoji], sender: sender, messageId: uniqueID, date: date) - case .Location: - let randomNumberLocation = Int(arc4random_uniform(UInt32(locations.count))) - return MockMessage(location: locations[randomNumberLocation], sender: sender, messageId: uniqueID, date: date) - case .Url: - return MockMessage(text: "https://github.com/MessageKit", sender: sender, messageId: uniqueID, date: date) - case .Phone: - return MockMessage(text: "123-456-7890", sender: sender, messageId: uniqueID, date: date) - case .Custom: - return MockMessage(custom: "Someone left the conversation", sender: system, messageId: uniqueID, date: date) - } + func randomMessageType() -> MessageTypes { + MessageTypes.allCases.compactMap { + guard UserDefaults.standard.bool(forKey: "\($0.rawValue)" + " Messages") else { return nil } + return $0 + }.random()! + } + + // swiftlint:disable cyclomatic_complexity + func randomMessage(allowedSenders: [MockUser]) -> MockMessage { + let uniqueID = UUID().uuidString + let user = allowedSenders.random()! + let date = dateAddingRandomTime() + + switch randomMessageType() { + case .Text: + let randomSentence = Lorem.sentence() + return MockMessage(text: randomSentence, user: user, messageId: uniqueID, date: date) + case .AttributedText: + let randomSentence = Lorem.sentence() + let attributedText = attributedString(with: randomSentence) + return MockMessage(attributedText: attributedText, user: user, messageId: uniqueID, date: date) + case .Photo: + let image = messageImages.random()! + return MockMessage(image: image, user: user, messageId: uniqueID, date: date) + case .PhotoFromURL: + let imageURL: URL = messageImageURLs.random()! + return MockMessage(imageURL: imageURL, user: user, messageId: uniqueID, date: date) + case .Video: + let image = messageImages.random()! + return MockMessage(thumbnail: image, user: user, messageId: uniqueID, date: date) + case .Audio: + let soundURL = sounds.random()! + return MockMessage(audioURL: soundURL, user: user, messageId: uniqueID, date: date) + case .Emoji: + return MockMessage(emoji: emojis.random()!, user: user, messageId: uniqueID, date: date) + case .Location: + return MockMessage(location: locations.random()!, user: user, messageId: uniqueID, date: date) + case .Url: + return MockMessage(linkItem: linkItem(), user: user, messageId: uniqueID, date: date) + case .Phone: + return MockMessage(text: "123-456-7890", user: user, messageId: uniqueID, date: date) + case .Custom: + return MockMessage(custom: "Someone left the conversation", user: system, messageId: uniqueID, date: date) + case .ShareContact: + return MockMessage(contact: contactsToShare.random()!, user: user, messageId: uniqueID, date: date) } + } + + // swiftlint:enable cyclomatic_complexity - func getMessages(count: Int, completion: ([MockMessage]) -> Void) { - var messages: [MockMessage] = [] - // Disable Custom Messages - UserDefaults.standard.set(false, forKey: "Custom Messages") - for _ in 0.. Void) { + var messages: [MockMessage] = [] + // Disable Custom Messages + UserDefaults.standard.set(false, forKey: "Custom Messages") + for _ in 0 ..< count { + let uniqueID = UUID().uuidString + let user = senders.random()! + let date = dateAddingRandomTime() + let randomSentence = Lorem.sentence() + let message = MockMessage(text: randomSentence, user: user, messageId: uniqueID, date: date) + messages.append(message) } - - func getAdvancedMessages(count: Int, completion: ([MockMessage]) -> Void) { - var messages: [MockMessage] = [] - // Enable Custom Messages - UserDefaults.standard.set(true, forKey: "Custom Messages") - for _ in 0.. [MockMessage] { + var messages: [MockMessage] = [] + // Disable Custom Messages + UserDefaults.standard.set(false, forKey: "Custom Messages") + for _ in 0 ..< count { + let uniqueID = UUID().uuidString + let user = senders.random()! + let date = dateAddingRandomTime() + let randomSentence = Lorem.sentence() + let message = MockMessage(text: randomSentence, user: user, messageId: uniqueID, date: date) + messages.append(message) } - - func getMessages(count: Int, allowedSenders: [Sender], completion: ([MockMessage]) -> Void) { - var messages: [MockMessage] = [] - // Disable Custom Messages - UserDefaults.standard.set(false, forKey: "Custom Messages") - for _ in 0.. Void) { + var messages: [MockMessage] = [] + // Enable Custom Messages + UserDefaults.standard.set(true, forKey: "Custom Messages") + for _ in 0 ..< count { + let message = randomMessage(allowedSenders: senders) + messages.append(message) } + completion(messages) + } - func getAvatarFor(sender: Sender) -> Avatar { - let firstName = sender.displayName.components(separatedBy: " ").first - let lastName = sender.displayName.components(separatedBy: " ").first - let initials = "\(firstName?.first ?? "A")\(lastName?.first ?? "A")" - switch sender { - case nathan: - return Avatar(image: #imageLiteral(resourceName: "Nathan-Tannar"), initials: initials) - case steven: - return Avatar(image: #imageLiteral(resourceName: "Steven-Deutsch"), initials: initials) - case wu: - return Avatar(image: #imageLiteral(resourceName: "Wu-Zhong"), initials: initials) - case system: - return Avatar(image: nil, initials: "SS") - default: - return Avatar(image: nil, initials: initials) - } + func getMessages(count: Int, allowedSenders _: [MockUser], completion: ([MockMessage]) -> Void) { + var messages: [MockMessage] = [] + // Disable Custom Messages + UserDefaults.standard.set(false, forKey: "Custom Messages") + for _ in 0 ..< count { + let uniqueID = UUID().uuidString + let user = senders.random()! + let date = dateAddingRandomTime() + let randomSentence = Lorem.sentence() + let message = MockMessage(text: randomSentence, user: user, messageId: uniqueID, date: date) + messages.append(message) } + completion(messages) + } + func getAvatarFor(sender: SenderType) -> Avatar { + let firstName = sender.displayName.components(separatedBy: " ").first + let lastName = sender.displayName.components(separatedBy: " ").first + let initials = "\(firstName?.first ?? "A")\(lastName?.first ?? "A")" + switch sender.senderId { + case "000001": + return Avatar(image: #imageLiteral(resourceName: "Nathan-Tannar"), initials: initials) + case "000002": + return Avatar(image: #imageLiteral(resourceName: "Steven-Deutsch"), initials: initials) + case "000003": + return Avatar(image: #imageLiteral(resourceName: "Wu-Zhong"), initials: initials) + case "000000": + return Avatar(image: nil, initials: "SS") + default: + return Avatar(image: nil, initials: initials) + } + } } diff --git a/Example/Sources/Extensions/AlertService.swift b/Example/Sources/Extensions/AlertService.swift new file mode 100644 index 000000000..58d3b3c2c --- /dev/null +++ b/Example/Sources/Extensions/AlertService.swift @@ -0,0 +1,27 @@ +// +// AlertService.swift +// ChatExample +// +// Created by Mohannad on 12/25/20. +// Copyright Β© 2020 MessageKit. All rights reserved. +// + +import Foundation +import UIKit + +class AlertService { + static func showAlert( + style: UIAlertController.Style, + title: String?, + message: String?, + actions: [UIAlertAction] = [UIAlertAction(title: "Ok", style: .cancel, handler: nil)], + completion: (() -> Swift.Void)? = nil) + { + let alert = UIAlertController(title: title, message: message, preferredStyle: style) + for action in actions { + alert.addAction(action) + } + + UIApplication.shared.delegate?.window??.rootViewController?.present(alert, animated: true, completion: completion) + } +} diff --git a/Example/Sources/Extensions/Settings+UserDefaults.swift b/Example/Sources/Extensions/Settings+UserDefaults.swift new file mode 100644 index 000000000..86e2c60bb --- /dev/null +++ b/Example/Sources/Extensions/Settings+UserDefaults.swift @@ -0,0 +1,51 @@ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +extension UserDefaults { + static let messagesKey = "mockMessages" + + static func isFirstLaunch() -> Bool { + let hasBeenLaunchedBeforeFlag = "hasBeenLaunchedBeforeFlag" + let isFirstLaunch = !UserDefaults.standard.bool(forKey: hasBeenLaunchedBeforeFlag) + if isFirstLaunch { + UserDefaults.standard.set(true, forKey: hasBeenLaunchedBeforeFlag) + UserDefaults.standard.synchronize() + } + return isFirstLaunch + } + + // MARK: Mock Messages + + func setMockMessages(count: Int) { + set(count, forKey: UserDefaults.messagesKey) + synchronize() + } + + func mockMessagesCount() -> Int { + if let value = object(forKey: UserDefaults.messagesKey) as? Int { + return value + } + return 20 + } +} diff --git a/Example/Sources/Extensions/UIColor+Extensions.swift b/Example/Sources/Extensions/UIColor+Extensions.swift index f951c17c5..42df1a929 100644 --- a/Example/Sources/Extensions/UIColor+Extensions.swift +++ b/Example/Sources/Extensions/UIColor+Extensions.swift @@ -1,29 +1,27 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import UIKit extension UIColor { - static let primaryColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) + static let primaryColor = UIColor(red: 69 / 255, green: 193 / 255, blue: 89 / 255, alpha: 1) } diff --git a/Example/Sources/Extensions/UIViewController+Extensions.swift b/Example/Sources/Extensions/UIViewController+Extensions.swift index 2d7345498..d8f410c1c 100644 --- a/Example/Sources/Extensions/UIViewController+Extensions.swift +++ b/Example/Sources/Extensions/UIViewController+Extensions.swift @@ -1,67 +1,61 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import UIKit extension UIViewController { - - func updateTitleView(title: String, subtitle: String?, baseColor: UIColor = .white) { - - let titleLabel = UILabel(frame: CGRect(x: 0, y: -2, width: 0, height: 0)) - titleLabel.backgroundColor = UIColor.clear - titleLabel.textColor = baseColor - titleLabel.font = UIFont.systemFont(ofSize: 15) - titleLabel.text = title - titleLabel.textAlignment = .center - titleLabel.adjustsFontSizeToFitWidth = true - titleLabel.sizeToFit() - - let subtitleLabel = UILabel(frame: CGRect(x: 0, y: 18, width: 0, height: 0)) - subtitleLabel.textColor = baseColor.withAlphaComponent(0.95) - subtitleLabel.font = UIFont.systemFont(ofSize: 12) - subtitleLabel.text = subtitle - subtitleLabel.textAlignment = .center - subtitleLabel.adjustsFontSizeToFitWidth = true - subtitleLabel.sizeToFit() - - let titleView = UIView(frame: CGRect(x: 0, y: 0, width: max(titleLabel.frame.size.width, subtitleLabel.frame.size.width), height: 30)) - titleView.addSubview(titleLabel) - if subtitle != nil { - titleView.addSubview(subtitleLabel) - } else { - titleLabel.frame = titleView.frame - } - let widthDiff = subtitleLabel.frame.size.width - titleLabel.frame.size.width - if widthDiff < 0 { - let newX = widthDiff / 2 - subtitleLabel.frame.origin.x = abs(newX) - } else { - let newX = widthDiff / 2 - titleLabel.frame.origin.x = newX - } - - navigationItem.titleView = titleView + func updateTitleView(title: String, subtitle: String?) { + let titleLabel = UILabel(frame: CGRect(x: 0, y: -2, width: 0, height: 0)) + titleLabel.backgroundColor = UIColor.clear + titleLabel.font = UIFont.systemFont(ofSize: 15) + titleLabel.text = title + titleLabel.textAlignment = .center + titleLabel.adjustsFontSizeToFitWidth = true + titleLabel.sizeToFit() + + let subtitleLabel = UILabel(frame: CGRect(x: 0, y: 18, width: 0, height: 0)) + subtitleLabel.font = UIFont.systemFont(ofSize: 12) + subtitleLabel.text = subtitle + subtitleLabel.textAlignment = .center + subtitleLabel.adjustsFontSizeToFitWidth = true + subtitleLabel.sizeToFit() + + let titleView = + UIView(frame: CGRect(x: 0, y: 0, width: max(titleLabel.frame.size.width, subtitleLabel.frame.size.width), height: 30)) + titleView.addSubview(titleLabel) + if subtitle != nil { + titleView.addSubview(subtitleLabel) + } else { + titleLabel.frame = titleView.frame } - + let widthDiff = subtitleLabel.frame.size.width - titleLabel.frame.size.width + if widthDiff < 0 { + let newX = widthDiff / 2 + subtitleLabel.frame.origin.x = abs(newX) + } else { + let newX = widthDiff / 2 + titleLabel.frame.origin.x = newX + } + + navigationItem.titleView = titleView + } } diff --git a/Example/Sources/IMessageViewController.swift b/Example/Sources/IMessageViewController.swift deleted file mode 100644 index 4e29233c2..000000000 --- a/Example/Sources/IMessageViewController.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// IMessageViewController.swift -// ChatExample -// -// Created by Steven Deutsch on 12/17/17. -// Copyright Β© 2017 MessageKit. All rights reserved. -// - -import MessageKit - -internal class IMessageViewController: MessagesViewController { - - override func viewDidLoad() { - super.viewDidLoad() - iMessage() - } - - func defaultStyle() { - let newMessageInputBar = MessageInputBar() - newMessageInputBar.sendButton.tintColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) - //newMessageInputBar.delegate = self - messageInputBar = newMessageInputBar - reloadInputViews() - } - - func iMessage() { - defaultStyle() - messageInputBar.isTranslucent = false - messageInputBar.backgroundView.backgroundColor = .red - messageInputBar.separatorLine.isHidden = true - messageInputBar.inputTextView.backgroundColor = UIColor(red: 245/255, green: 245/255, blue: 245/255, alpha: 1) - messageInputBar.inputTextView.placeholderTextColor = UIColor(red: 0.6, green: 0.6, blue: 0.6, alpha: 1) - messageInputBar.inputTextView.textContainerInset = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 36) - messageInputBar.inputTextView.placeholderLabelInsets = UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 36) - messageInputBar.inputTextView.layer.borderColor = UIColor(red: 200/255, green: 200/255, blue: 200/255, alpha: 1).cgColor - messageInputBar.inputTextView.layer.borderWidth = 1.0 - messageInputBar.inputTextView.layer.cornerRadius = 16.0 - messageInputBar.inputTextView.layer.masksToBounds = true - messageInputBar.inputTextView.scrollIndicatorInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0) - messageInputBar.setRightStackViewWidthConstant(to: 36, animated: false) - messageInputBar.setStackViewItems([messageInputBar.sendButton], forStack: .right, animated: true) - messageInputBar.sendButton.imageView?.backgroundColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) - messageInputBar.sendButton.contentEdgeInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2) - messageInputBar.sendButton.setSize(CGSize(width: 36, height: 36), animated: true) - messageInputBar.sendButton.image = #imageLiteral(resourceName: "ic_up") - messageInputBar.sendButton.title = nil - messageInputBar.sendButton.imageView?.layer.cornerRadius = 16 - messageInputBar.sendButton.backgroundColor = .clear - messageInputBar.textViewPadding.right = -38 - messageInputBar.textViewPadding.top = 20 - } - -} diff --git a/Example/Sources/Layout/CustomMessageFlowLayout.swift b/Example/Sources/Layout/CustomMessageFlowLayout.swift index 004ec7035..e2d7d4526 100644 --- a/Example/Sources/Layout/CustomMessageFlowLayout.swift +++ b/Example/Sources/Layout/CustomMessageFlowLayout.swift @@ -1,67 +1,71 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation import MessageKit +import UIKit + +// MARK: - CustomMessagesFlowLayout open class CustomMessagesFlowLayout: MessagesCollectionViewFlowLayout { - - open lazy var customMessageSizeCalculator = CustomMessageSizeCalculator(layout: self) - - open override func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator { -// if isSectionReservedForTypingBubble(indexPath.section) { -// return typingMessageSizeCalculator -// } - let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - if case .custom = message.kind { - return customMessageSizeCalculator - } - return super.cellSizeCalculatorForItem(at: indexPath) + open lazy var customMessageSizeCalculator = CustomMessageSizeCalculator(layout: self) + + open override func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator { + if isSectionReservedForTypingIndicator(indexPath.section) { + return typingIndicatorSizeCalculator } - - open override func messageSizeCalculators() -> [MessageSizeCalculator] { - var superCalculators = super.messageSizeCalculators() - // Append any of your custom `MessageSizeCalculator` if you wish for the convenience - // functions to work such as `setMessageIncoming...` or `setMessageOutgoing...` - superCalculators.append(customMessageSizeCalculator) - return superCalculators + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + if case .custom = message.kind { + return customMessageSizeCalculator } + return super.cellSizeCalculatorForItem(at: indexPath) + } + + open override func messageSizeCalculators() -> [MessageSizeCalculator] { + var superCalculators = super.messageSizeCalculators() + // Append any of your custom `MessageSizeCalculator` if you wish for the convenience + // functions to work such as `setMessageIncoming...` or `setMessageOutgoing...` + superCalculators.append(customMessageSizeCalculator) + return superCalculators + } } +// MARK: - CustomMessageSizeCalculator + open class CustomMessageSizeCalculator: MessageSizeCalculator { - - public override init(layout: MessagesCollectionViewFlowLayout? = nil) { - super.init() - self.layout = layout - } - - open override func sizeForItem(at indexPath: IndexPath) -> CGSize { - guard let layout = layout else { return .zero } - let collectionViewWidth = layout.collectionView?.bounds.width ?? 0 - let contentInset = layout.collectionView?.contentInset ?? .zero - let inset = layout.sectionInset.left + layout.sectionInset.right + contentInset.left + contentInset.right - return CGSize(width: collectionViewWidth - inset, height: 44) - } - + // MARK: Lifecycle + + public override init(layout: MessagesCollectionViewFlowLayout? = nil) { + super.init() + self.layout = layout + } + + // MARK: Open + + open override func sizeForItem(at _: IndexPath) -> CGSize { + guard let layout = layout else { return .zero } + let collectionViewWidth = layout.collectionView?.bounds.width ?? 0 + let contentInset = layout.collectionView?.contentInset ?? .zero + let inset = layout.sectionInset.left + layout.sectionInset.right + contentInset.left + contentInset.right + return CGSize(width: collectionViewWidth - inset, height: 44) + } } diff --git a/Example/Sources/Models/CustomLayoutSizeCalculator.swift b/Example/Sources/Models/CustomLayoutSizeCalculator.swift new file mode 100644 index 000000000..06d417164 --- /dev/null +++ b/Example/Sources/Models/CustomLayoutSizeCalculator.swift @@ -0,0 +1,209 @@ +// +// CustomLayoutSizeCalculator.swift +// ChatExample +// +// Created by Vignesh J on 01/05/21. +// Copyright Β© 2021 MessageKit. All rights reserved. +// + +import MessageKit +import UIKit + +class CustomLayoutSizeCalculator: CellSizeCalculator { + // MARK: Lifecycle + + init(layout: MessagesCollectionViewFlowLayout? = nil) { + super.init() + + self.layout = layout + } + + // MARK: Internal + + var cellTopLabelVerticalPadding: CGFloat = 32 + var cellTopLabelHorizontalPadding: CGFloat = 32 + var cellMessageContainerHorizontalPadding: CGFloat = 48 + var cellMessageContainerExtraSpacing: CGFloat = 16 + var cellMessageContentVerticalPadding: CGFloat = 16 + var cellMessageContentHorizontalPadding: CGFloat = 16 + var cellDateLabelHorizontalPadding: CGFloat = 24 + var cellDateLabelBottomPadding: CGFloat = 8 + + var messagesLayout: MessagesCollectionViewFlowLayout { + layout as! MessagesCollectionViewFlowLayout + } + + var messageContainerMaxWidth: CGFloat { + messagesLayout.itemWidth - + cellMessageContainerHorizontalPadding - + cellMessageContainerExtraSpacing + } + + var messagesDataSource: MessagesDataSource { + self.messagesLayout.messagesDataSource + } + + override func sizeForItem(at indexPath: IndexPath) -> CGSize { + let dataSource = messagesDataSource + let message = dataSource.messageForItem( + at: indexPath, + in: messagesLayout.messagesCollectionView) + let itemHeight = cellContentHeight( + for: message, + at: indexPath) + return CGSize( + width: messagesLayout.itemWidth, + height: itemHeight) + } + + func cellContentHeight( + for message: MessageType, + at indexPath: IndexPath) + -> CGFloat + { + cellTopLabelSize( + for: message, + at: indexPath).height + + cellMessageBottomLabelSize( + for: message, + at: indexPath).height + + messageContainerSize( + for: message, + at: indexPath).height + } + + // MARK: - Top cell Label + + func cellTopLabelSize( + for message: MessageType, + at indexPath: IndexPath) + -> CGSize + { + guard + let attributedText = messagesDataSource.cellTopLabelAttributedText( + for: message, + at: indexPath) else + { + return .zero + } + + let maxWidth = messagesLayout.itemWidth - cellTopLabelHorizontalPadding + let size = attributedText.size(consideringWidth: maxWidth) + let height = size.height + cellTopLabelVerticalPadding + + return CGSize( + width: maxWidth, + height: height) + } + + func cellTopLabelFrame( + for message: MessageType, + at indexPath: IndexPath) + -> CGRect + { + let size = cellTopLabelSize( + for: message, + at: indexPath) + guard size != .zero else { + return .zero + } + + let origin = CGPoint( + x: cellTopLabelHorizontalPadding / 2, + y: 0) + + return CGRect( + origin: origin, + size: size) + } + + func cellMessageBottomLabelSize( + for message: MessageType, + at indexPath: IndexPath) + -> CGSize + { + guard + let attributedText = messagesDataSource.messageBottomLabelAttributedText( + for: message, + at: indexPath) else + { + return .zero + } + let maxWidth = messageContainerMaxWidth - cellDateLabelHorizontalPadding + + return attributedText.size(consideringWidth: maxWidth) + } + + func cellMessageBottomLabelFrame( + for message: MessageType, + at indexPath: IndexPath) + -> CGRect + { + let messageContainerSize = messageContainerSize( + for: message, + at: indexPath) + let labelSize = cellMessageBottomLabelSize( + for: message, + at: indexPath) + let x = messageContainerSize.width - labelSize.width - (cellDateLabelHorizontalPadding / 2) + let y = messageContainerSize.height - labelSize.height - cellDateLabelBottomPadding + let origin = CGPoint( + x: x, + y: y) + + return CGRect( + origin: origin, + size: labelSize) + } + + // MARK: - MessageContainer + + func messageContainerSize( + for message: MessageType, + at indexPath: IndexPath) + -> CGSize + { + let labelSize = cellMessageBottomLabelSize( + for: message, + at: indexPath) + let width = labelSize.width + + cellMessageContentHorizontalPadding + + cellDateLabelHorizontalPadding + let height = labelSize.height + + cellMessageContentVerticalPadding + + cellDateLabelBottomPadding + + return CGSize( + width: width, + height: height) + } + + func messageContainerFrame( + for message: MessageType, + at indexPath: IndexPath, + fromCurrentSender: Bool) + -> CGRect + { + let y = cellTopLabelSize( + for: message, + at: indexPath).height + let size = messageContainerSize( + for: message, + at: indexPath) + let origin: CGPoint + if fromCurrentSender { + let x = messagesLayout.itemWidth - + size.width - + (cellMessageContainerHorizontalPadding / 2) + origin = CGPoint(x: x, y: y) + } else { + origin = CGPoint( + x: cellMessageContainerHorizontalPadding / 2, + y: y) + } + + return CGRect( + origin: origin, + size: size) + } +} diff --git a/Example/Sources/Models/CustomTextLayoutSizeCalculator.swift b/Example/Sources/Models/CustomTextLayoutSizeCalculator.swift new file mode 100644 index 000000000..32f04b288 --- /dev/null +++ b/Example/Sources/Models/CustomTextLayoutSizeCalculator.swift @@ -0,0 +1,78 @@ +// +// CustomTextMessageSizeCalculator.swift +// ChatExample +// +// Created by Vignesh J on 30/04/21. +// Copyright Β© 2021 MessageKit. All rights reserved. +// + +import MessageKit +import UIKit + +class CustomTextLayoutSizeCalculator: CustomLayoutSizeCalculator { + var messageLabelFont = UIFont.preferredFont(forTextStyle: .body) + var cellMessageContainerRightSpacing: CGFloat = 16 + + override func messageContainerSize( + for message: MessageType, + at indexPath: IndexPath) + -> CGSize + { + let size = super.messageContainerSize( + for: message, + at: indexPath) + let labelSize = messageLabelSize( + for: message, + at: indexPath) + let selfWidth = labelSize.width + + cellMessageContentHorizontalPadding + + cellMessageContainerRightSpacing + let width = max(selfWidth, size.width) + let height = size.height + labelSize.height + + return CGSize( + width: width, + height: height) + } + + func messageLabelSize( + for message: MessageType, + at _: IndexPath) + -> CGSize + { + let attributedText: NSAttributedString + + let textMessageKind = message.kind + switch textMessageKind { + case .attributedText(let text): + attributedText = text + case .text(let text), .emoji(let text): + attributedText = NSAttributedString(string: text, attributes: [.font: messageLabelFont]) + default: + fatalError("messageLabelSize received unhandled MessageDataType: \(message.kind)") + } + + let maxWidth = messageContainerMaxWidth - + cellMessageContentHorizontalPadding - + cellMessageContainerRightSpacing + + return attributedText.size(consideringWidth: maxWidth) + } + + func messageLabelFrame( + for message: MessageType, + at indexPath: IndexPath) + -> CGRect + { + let origin = CGPoint( + x: cellMessageContentHorizontalPadding / 2, + y: cellMessageContentVerticalPadding / 2) + let size = messageLabelSize( + for: message, + at: indexPath) + + return CGRect( + origin: origin, + size: size) + } +} diff --git a/Example/Sources/Models/MockMessage.swift b/Example/Sources/Models/MockMessage.swift index 8a3203730..f47760936 100644 --- a/Example/Sources/Models/MockMessage.swift +++ b/Example/Sources/Models/MockMessage.swift @@ -1,101 +1,177 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import Foundation +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import AVFoundation import CoreLocation +import Foundation import MessageKit +import UIKit + +// MARK: - CoordinateItem private struct CoordinateItem: LocationItem { + var location: CLLocation + var size: CGSize - var location: CLLocation - var size: CGSize + init(location: CLLocation) { + self.location = location + size = CGSize(width: 240, height: 240) + } +} - init(location: CLLocation) { - self.location = location - self.size = CGSize(width: 240, height: 240) - } +// MARK: - ImageMediaItem +private struct ImageMediaItem: MediaItem { + var url: URL? + var image: UIImage? + var placeholderImage: UIImage + var size: CGSize + + init(image: UIImage) { + self.image = image + size = CGSize(width: 240, height: 240) + placeholderImage = UIImage() + } + + init(imageURL: URL) { + url = imageURL + size = CGSize(width: 240, height: 240) + placeholderImage = UIImage(imageLiteralResourceName: "image_message_placeholder") + } } -private struct ImageMediaItem: MediaItem { +// MARK: - MockAudioItem - var url: URL? - var image: UIImage? - var placeholderImage: UIImage - var size: CGSize +private struct MockAudioItem: AudioItem { + var url: URL + var size: CGSize + var duration: Float - init(image: UIImage) { - self.image = image - self.size = CGSize(width: 240, height: 240) - self.placeholderImage = UIImage() - } + init(url: URL) { + self.url = url + size = CGSize(width: 160, height: 35) + // compute duration + let audioAsset = AVURLAsset(url: url) + duration = Float(CMTimeGetSeconds(audioAsset.duration)) + } +} + +// MARK: - MockContactItem +struct MockContactItem: ContactItem { + var displayName: String + var initials: String + var phoneNumbers: [String] + var emails: [String] + + init(name: String, initials: String, phoneNumbers: [String] = [], emails: [String] = []) { + displayName = name + self.initials = initials + self.phoneNumbers = phoneNumbers + self.emails = emails + } } -internal struct MockMessage: MessageType { +// MARK: - MockLinkItem - var messageId: String - var sender: Sender - var sentDate: Date - var kind: MessageKind - - private init(kind: MessageKind, sender: Sender, messageId: String, date: Date) { - self.kind = kind - self.sender = sender - self.messageId = messageId - self.sentDate = date - } - - init(custom: Any?, sender: Sender, messageId: String, date: Date) { - self.init(kind: .custom(custom), sender: sender, messageId: messageId, date: date) - } - - init(text: String, sender: Sender, messageId: String, date: Date) { - self.init(kind: .text(text), sender: sender, messageId: messageId, date: date) - } - - init(attributedText: NSAttributedString, sender: Sender, messageId: String, date: Date) { - self.init(kind: .attributedText(attributedText), sender: sender, messageId: messageId, date: date) - } - - init(image: UIImage, sender: Sender, messageId: String, date: Date) { - let mediaItem = ImageMediaItem(image: image) - self.init(kind: .photo(mediaItem), sender: sender, messageId: messageId, date: date) - } - - init(thumbnail: UIImage, sender: Sender, messageId: String, date: Date) { - let mediaItem = ImageMediaItem(image: thumbnail) - self.init(kind: .video(mediaItem), sender: sender, messageId: messageId, date: date) - } - - init(location: CLLocation, sender: Sender, messageId: String, date: Date) { - let locationItem = CoordinateItem(location: location) - self.init(kind: .location(locationItem), sender: sender, messageId: messageId, date: date) - } - - init(emoji: String, sender: Sender, messageId: String, date: Date) { - self.init(kind: .emoji(emoji), sender: sender, messageId: messageId, date: date) - } +struct MockLinkItem: LinkItem { + let text: String? + let attributedText: NSAttributedString? + let url: URL + let title: String? + let teaser: String + let thumbnailImage: UIImage +} + +// MARK: - MockMessage +internal struct MockMessage: MessageType { + // MARK: Lifecycle + + private init(kind: MessageKind, user: MockUser, messageId: String, date: Date) { + self.kind = kind + self.user = user + self.messageId = messageId + sentDate = date + } + + init(custom: Any?, user: MockUser, messageId: String, date: Date) { + self.init(kind: .custom(custom), user: user, messageId: messageId, date: date) + } + + init(text: String, user: MockUser, messageId: String, date: Date) { + self.init(kind: .text(text), user: user, messageId: messageId, date: date) + } + + init(attributedText: NSAttributedString, user: MockUser, messageId: String, date: Date) { + self.init(kind: .attributedText(attributedText), user: user, messageId: messageId, date: date) + } + + init(image: UIImage, user: MockUser, messageId: String, date: Date) { + let mediaItem = ImageMediaItem(image: image) + self.init(kind: .photo(mediaItem), user: user, messageId: messageId, date: date) + } + + init(imageURL: URL, user: MockUser, messageId: String, date: Date) { + let mediaItem = ImageMediaItem(imageURL: imageURL) + self.init(kind: .photo(mediaItem), user: user, messageId: messageId, date: date) + } + + init(thumbnail: UIImage, user: MockUser, messageId: String, date: Date) { + let mediaItem = ImageMediaItem(image: thumbnail) + self.init(kind: .video(mediaItem), user: user, messageId: messageId, date: date) + } + + init(location: CLLocation, user: MockUser, messageId: String, date: Date) { + let locationItem = CoordinateItem(location: location) + self.init(kind: .location(locationItem), user: user, messageId: messageId, date: date) + } + + init(emoji: String, user: MockUser, messageId: String, date: Date) { + self.init(kind: .emoji(emoji), user: user, messageId: messageId, date: date) + } + + init(audioURL: URL, user: MockUser, messageId: String, date: Date) { + let audioItem = MockAudioItem(url: audioURL) + self.init(kind: .audio(audioItem), user: user, messageId: messageId, date: date) + } + + init(contact: MockContactItem, user: MockUser, messageId: String, date: Date) { + self.init(kind: .contact(contact), user: user, messageId: messageId, date: date) + } + + init(linkItem: LinkItem, user: MockUser, messageId: String, date: Date) { + self.init(kind: .linkPreview(linkItem), user: user, messageId: messageId, date: date) + } + + // MARK: Internal + + var messageId: String + var sentDate: Date + var kind: MessageKind + + var user: MockUser + + var sender: SenderType { + user + } } diff --git a/Example/Sources/Models/MockSocket.swift b/Example/Sources/Models/MockSocket.swift index 0585d9dc5..41c9f1b11 100644 --- a/Example/Sources/Models/MockSocket.swift +++ b/Example/Sources/Models/MockSocket.swift @@ -1,87 +1,94 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. -import UIKit import MessageKit +import UIKit +@MainActor final class MockSocket { - - static var shared = MockSocket() - - private var timer: Timer? - - private var queuedMessage: MockMessage? - - private var onNewMessageCode: ((MockMessage) -> Void)? - - private var onTypingStatusCode: (() -> Void)? - - private var connectedUsers: [Sender] = [] - - private init() {} - - @discardableResult - func connect(with senders: [Sender]) -> Self { - disconnect() - connectedUsers = senders - timer = Timer.scheduledTimer(timeInterval: 2.5, target: self, selector: #selector(handleTimer), userInfo: nil, repeats: true) - return self - } - - @discardableResult - func disconnect() -> Self { - timer?.invalidate() - timer = nil - onTypingStatusCode = nil - onNewMessageCode = nil - return self - } - - @discardableResult - func onNewMessage(code: @escaping (MockMessage) -> Void) -> Self { - onNewMessageCode = code - return self - } - - @discardableResult - func onTypingStatus(code: @escaping () -> Void) -> Self { - onTypingStatusCode = code - return self - } - - @objc - private func handleTimer() { - if let message = queuedMessage { - onNewMessageCode?(message) - queuedMessage = nil - } else { - let sender = arc4random_uniform(1) % 2 == 0 ? connectedUsers.first! : connectedUsers.last! - SampleData.shared.getMessages(count: 1, allowedSenders: [sender]) { (message) in - queuedMessage = message.first - } - onTypingStatusCode?() - } + // MARK: Lifecycle + + private init() { } + + // MARK: Internal + + static var shared = MockSocket() + + @discardableResult + func connect(with senders: [MockUser]) -> Self { + disconnect() + connectedUsers = senders + timer = Timer.scheduledTimer( + timeInterval: 2.5, + target: self, + selector: #selector(handleTimer), + userInfo: nil, + repeats: true) + return self + } + + @discardableResult + func disconnect() -> Self { + timer?.invalidate() + timer = nil + onTypingStatusCode = nil + onNewMessageCode = nil + return self + } + + @discardableResult + func onNewMessage(code: @escaping (MockMessage) -> Void) -> Self { + onNewMessageCode = code + return self + } + + @discardableResult + func onTypingStatus(code: @escaping () -> Void) -> Self { + onTypingStatusCode = code + return self + } + + // MARK: Private + + private var timer: Timer? + + private var queuedMessage: MockMessage? + + private var onNewMessageCode: ((MockMessage) -> Void)? + + private var onTypingStatusCode: (() -> Void)? + + private var connectedUsers: [MockUser] = [] + + @objc + private func handleTimer() { + if let message = queuedMessage { + onNewMessageCode?(message) + queuedMessage = nil + } else { + let sender = connectedUsers.random()! + let message = SampleData.shared.randomMessage(allowedSenders: [sender]) + queuedMessage = message + onTypingStatusCode?() } - + } } diff --git a/Example/Sources/Models/MockUser.swift b/Example/Sources/Models/MockUser.swift new file mode 100644 index 000000000..4274b2c2e --- /dev/null +++ b/Example/Sources/Models/MockUser.swift @@ -0,0 +1,29 @@ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import MessageKit + +struct MockUser: SenderType, Equatable { + var senderId: String + var displayName: String +} diff --git a/Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-1024.png b/Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-1024.png similarity index 100% rename from Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-1024.png rename to Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-1024.png diff --git a/Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-20.png b/Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-20.png similarity index 100% rename from Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-20.png rename to Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-20.png diff --git a/Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-20@2x.png b/Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-20@2x.png similarity index 100% rename from Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-20@2x.png rename to Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-20@2x.png diff --git a/Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-20@3x.png b/Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-20@3x.png similarity index 100% rename from Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-20@3x.png rename to Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-20@3x.png diff --git a/Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-29.png b/Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-29.png similarity index 100% rename from Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-29.png rename to Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-29.png diff --git a/Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-29@2x.png b/Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-29@2x.png similarity index 100% rename from Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-29@2x.png rename to Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-29@2x.png diff --git a/Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-29@3x.png b/Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-29@3x.png similarity index 100% rename from Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-29@3x.png rename to Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-29@3x.png diff --git a/Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-40.png b/Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-40.png similarity index 100% rename from Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-40.png rename to Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-40.png diff --git a/Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-40@2x.png b/Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-40@2x.png similarity index 100% rename from Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-40@2x.png rename to Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-40@2x.png diff --git a/Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-40@3x.png b/Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-40@3x.png similarity index 100% rename from Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-40@3x.png rename to Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-40@3x.png diff --git a/Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-60@2x.png b/Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-60@2x.png similarity index 100% rename from Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-60@2x.png rename to Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-60@2x.png diff --git a/Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-60@3x.png b/Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-60@3x.png similarity index 100% rename from Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-60@3x.png rename to Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-60@3x.png diff --git a/Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-76.png b/Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-76.png similarity index 100% rename from Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-76.png rename to Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-76.png diff --git a/Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-76@2x.png b/Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-76@2x.png similarity index 100% rename from Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-76@2x.png rename to Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-76@2x.png diff --git a/Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-83.5@2x.png b/Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-83.5@2x.png similarity index 100% rename from Example/Sources/Assets.xcassets/AppIcon.appiconset/30243544-83.5@2x.png rename to Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/30243544-83.5@2x.png diff --git a/Example/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Example/Sources/Resources/Assets.xcassets/Contents.json b/Example/Sources/Resources/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Example/Sources/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Sources/Assets.xcassets/Dan-Leonard.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/Dan-Leonard.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/Dan-Leonard.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/Dan-Leonard.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/Dan-Leonard.imageset/NiceSelfi.jpg b/Example/Sources/Resources/Assets.xcassets/Dan-Leonard.imageset/NiceSelfi.jpg similarity index 100% rename from Example/Sources/Assets.xcassets/Dan-Leonard.imageset/NiceSelfi.jpg rename to Example/Sources/Resources/Assets.xcassets/Dan-Leonard.imageset/NiceSelfi.jpg diff --git a/Example/Sources/Assets.xcassets/Nathan-Tannar.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/Nathan-Tannar.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/Nathan-Tannar.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/Nathan-Tannar.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/Nathan-Tannar.imageset/Nathan.jpg b/Example/Sources/Resources/Assets.xcassets/Nathan-Tannar.imageset/Nathan.jpg similarity index 100% rename from Example/Sources/Assets.xcassets/Nathan-Tannar.imageset/Nathan.jpg rename to Example/Sources/Resources/Assets.xcassets/Nathan-Tannar.imageset/Nathan.jpg diff --git a/Example/Sources/Assets.xcassets/Steve-Jobs.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/Steve-Jobs.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/Steve-Jobs.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/Steve-Jobs.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/Steve-Jobs.imageset/Steve-Jobs.jpeg b/Example/Sources/Resources/Assets.xcassets/Steve-Jobs.imageset/Steve-Jobs.jpeg similarity index 100% rename from Example/Sources/Assets.xcassets/Steve-Jobs.imageset/Steve-Jobs.jpeg rename to Example/Sources/Resources/Assets.xcassets/Steve-Jobs.imageset/Steve-Jobs.jpeg diff --git a/Example/Sources/Assets.xcassets/Steven-Deutsch.imageset/7445580.jpeg b/Example/Sources/Resources/Assets.xcassets/Steven-Deutsch.imageset/7445580.jpeg similarity index 100% rename from Example/Sources/Assets.xcassets/Steven-Deutsch.imageset/7445580.jpeg rename to Example/Sources/Resources/Assets.xcassets/Steven-Deutsch.imageset/7445580.jpeg diff --git a/Example/Sources/Assets.xcassets/Steven-Deutsch.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/Steven-Deutsch.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/Steven-Deutsch.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/Steven-Deutsch.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/Tim-Cook.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/Tim-Cook.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/Tim-Cook.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/Tim-Cook.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/Tim-Cook.imageset/Tim-Cook.jpeg b/Example/Sources/Resources/Assets.xcassets/Tim-Cook.imageset/Tim-Cook.jpeg similarity index 100% rename from Example/Sources/Assets.xcassets/Tim-Cook.imageset/Tim-Cook.jpeg rename to Example/Sources/Resources/Assets.xcassets/Tim-Cook.imageset/Tim-Cook.jpeg diff --git a/Example/Sources/Assets.xcassets/Wu-Zhong.imageset/5061845.png b/Example/Sources/Resources/Assets.xcassets/Wu-Zhong.imageset/5061845.png similarity index 100% rename from Example/Sources/Assets.xcassets/Wu-Zhong.imageset/5061845.png rename to Example/Sources/Resources/Assets.xcassets/Wu-Zhong.imageset/5061845.png diff --git a/Example/Sources/Assets.xcassets/Wu-Zhong.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/Wu-Zhong.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/Wu-Zhong.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/Wu-Zhong.imageset/Contents.json diff --git a/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Contents.json new file mode 100644 index 000000000..307a7038d --- /dev/null +++ b/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Group 1272628320.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group 1272628320@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group 1272628320@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320.png b/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320.png new file mode 100644 index 000000000..be5d8ca35 Binary files /dev/null and b/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320.png differ diff --git a/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320@2x.png b/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320@2x.png new file mode 100644 index 000000000..11eb26a18 Binary files /dev/null and b/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320@2x.png differ diff --git a/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320@3x.png b/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320@3x.png new file mode 100644 index 000000000..1bb9c5579 Binary files /dev/null and b/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320@3x.png differ diff --git a/Example/Sources/Assets.xcassets/ic_appstore.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/ic_appstore.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/ic_appstore.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/ic_appstore.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/ic_appstore.imageset/icons8-apple_app_store_filled.png b/Example/Sources/Resources/Assets.xcassets/ic_appstore.imageset/icons8-apple_app_store_filled.png similarity index 100% rename from Example/Sources/Assets.xcassets/ic_appstore.imageset/icons8-apple_app_store_filled.png rename to Example/Sources/Resources/Assets.xcassets/ic_appstore.imageset/icons8-apple_app_store_filled.png diff --git a/Example/Sources/Assets.xcassets/ic_at.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/ic_at.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/ic_at.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/ic_at.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/ic_at.imageset/icons8-email.png b/Example/Sources/Resources/Assets.xcassets/ic_at.imageset/icons8-email.png similarity index 100% rename from Example/Sources/Assets.xcassets/ic_at.imageset/icons8-email.png rename to Example/Sources/Resources/Assets.xcassets/ic_at.imageset/icons8-email.png diff --git a/Example/Sources/Assets.xcassets/ic_camera.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/ic_camera.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/ic_camera.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/ic_camera.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/ic_camera.imageset/icons8-camera.png b/Example/Sources/Resources/Assets.xcassets/ic_camera.imageset/icons8-camera.png similarity index 100% rename from Example/Sources/Assets.xcassets/ic_camera.imageset/icons8-camera.png rename to Example/Sources/Resources/Assets.xcassets/ic_camera.imageset/icons8-camera.png diff --git a/Example/Sources/Assets.xcassets/ic_hashtag.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/ic_hashtag.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/ic_hashtag.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/ic_hashtag.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/ic_hashtag.imageset/icons8-hashtag.png b/Example/Sources/Resources/Assets.xcassets/ic_hashtag.imageset/icons8-hashtag.png similarity index 100% rename from Example/Sources/Assets.xcassets/ic_hashtag.imageset/icons8-hashtag.png rename to Example/Sources/Resources/Assets.xcassets/ic_hashtag.imageset/icons8-hashtag.png diff --git a/Example/Sources/Assets.xcassets/ic_info.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/ic_info.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/ic_info.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/ic_info.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/ic_info.imageset/icons8-info.png b/Example/Sources/Resources/Assets.xcassets/ic_info.imageset/icons8-info.png similarity index 100% rename from Example/Sources/Assets.xcassets/ic_info.imageset/icons8-info.png rename to Example/Sources/Resources/Assets.xcassets/ic_info.imageset/icons8-info.png diff --git a/Example/Sources/Assets.xcassets/ic_keyboard.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/ic_keyboard.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/ic_keyboard.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/ic_keyboard.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/ic_keyboard.imageset/icons8-keyboard.png b/Example/Sources/Resources/Assets.xcassets/ic_keyboard.imageset/icons8-keyboard.png similarity index 100% rename from Example/Sources/Assets.xcassets/ic_keyboard.imageset/icons8-keyboard.png rename to Example/Sources/Resources/Assets.xcassets/ic_keyboard.imageset/icons8-keyboard.png diff --git a/Example/Sources/Assets.xcassets/ic_library.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/ic_library.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/ic_library.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/ic_library.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/ic_library.imageset/icons8-medium_icons.png b/Example/Sources/Resources/Assets.xcassets/ic_library.imageset/icons8-medium_icons.png similarity index 100% rename from Example/Sources/Assets.xcassets/ic_library.imageset/icons8-medium_icons.png rename to Example/Sources/Resources/Assets.xcassets/ic_library.imageset/icons8-medium_icons.png diff --git a/Example/Sources/Assets.xcassets/ic_like.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/ic_like.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/ic_like.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/ic_like.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/ic_like.imageset/icons8-like_it.png b/Example/Sources/Resources/Assets.xcassets/ic_like.imageset/icons8-like_it.png similarity index 100% rename from Example/Sources/Assets.xcassets/ic_like.imageset/icons8-like_it.png rename to Example/Sources/Resources/Assets.xcassets/ic_like.imageset/icons8-like_it.png diff --git a/Example/Sources/Assets.xcassets/ic_map_marker.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/ic_map_marker.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/ic_map_marker.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/ic_map_marker.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/ic_map_marker.imageset/icons8-map_pin-1.png b/Example/Sources/Resources/Assets.xcassets/ic_map_marker.imageset/icons8-map_pin-1.png similarity index 100% rename from Example/Sources/Assets.xcassets/ic_map_marker.imageset/icons8-map_pin-1.png rename to Example/Sources/Resources/Assets.xcassets/ic_map_marker.imageset/icons8-map_pin-1.png diff --git a/Example/Sources/Assets.xcassets/ic_mic.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/ic_mic.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/ic_mic.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/ic_mic.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/ic_mic.imageset/icons8-microphone.png b/Example/Sources/Resources/Assets.xcassets/ic_mic.imageset/icons8-microphone.png similarity index 100% rename from Example/Sources/Assets.xcassets/ic_mic.imageset/icons8-microphone.png rename to Example/Sources/Resources/Assets.xcassets/ic_mic.imageset/icons8-microphone.png diff --git a/Example/Sources/Assets.xcassets/ic_send.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/ic_send.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/ic_send.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/ic_send.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/ic_send.imageset/icons8-sent.png b/Example/Sources/Resources/Assets.xcassets/ic_send.imageset/icons8-sent.png similarity index 100% rename from Example/Sources/Assets.xcassets/ic_send.imageset/icons8-sent.png rename to Example/Sources/Resources/Assets.xcassets/ic_send.imageset/icons8-sent.png diff --git a/Example/Sources/Assets.xcassets/ic_typing.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/ic_typing.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/ic_typing.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/ic_typing.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/ic_typing.imageset/icons8-hand_with_pen.png b/Example/Sources/Resources/Assets.xcassets/ic_typing.imageset/icons8-hand_with_pen.png similarity index 100% rename from Example/Sources/Assets.xcassets/ic_typing.imageset/icons8-hand_with_pen.png rename to Example/Sources/Resources/Assets.xcassets/ic_typing.imageset/icons8-hand_with_pen.png diff --git a/Example/Sources/Assets.xcassets/ic_up.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/ic_up.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/ic_up.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/ic_up.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/ic_up.imageset/icons8-up_arrow.png b/Example/Sources/Resources/Assets.xcassets/ic_up.imageset/icons8-up_arrow.png similarity index 100% rename from Example/Sources/Assets.xcassets/ic_up.imageset/icons8-up_arrow.png rename to Example/Sources/Resources/Assets.xcassets/ic_up.imageset/icons8-up_arrow.png diff --git a/Example/Sources/Resources/Assets.xcassets/image_message_placeholder.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/image_message_placeholder.imageset/Contents.json new file mode 100644 index 000000000..ade8d418c --- /dev/null +++ b/Example/Sources/Resources/Assets.xcassets/image_message_placeholder.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "image_message_placeholder.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Sources/Resources/Assets.xcassets/image_message_placeholder.imageset/image_message_placeholder.pdf b/Example/Sources/Resources/Assets.xcassets/image_message_placeholder.imageset/image_message_placeholder.pdf new file mode 100644 index 000000000..69de86ee3 Binary files /dev/null and b/Example/Sources/Resources/Assets.xcassets/image_message_placeholder.imageset/image_message_placeholder.pdf differ diff --git a/Example/Sources/Assets.xcassets/img1.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/img1.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/img1.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/img1.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/img1.imageset/grey-crowned-crane-bird-crane-animal-45853.jpeg b/Example/Sources/Resources/Assets.xcassets/img1.imageset/grey-crowned-crane-bird-crane-animal-45853.jpeg similarity index 100% rename from Example/Sources/Assets.xcassets/img1.imageset/grey-crowned-crane-bird-crane-animal-45853.jpeg rename to Example/Sources/Resources/Assets.xcassets/img1.imageset/grey-crowned-crane-bird-crane-animal-45853.jpeg diff --git a/Example/Sources/Assets.xcassets/img2.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/img2.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/img2.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/img2.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/img2.imageset/pexels-photo-145939.jpeg b/Example/Sources/Resources/Assets.xcassets/img2.imageset/pexels-photo-145939.jpeg similarity index 100% rename from Example/Sources/Assets.xcassets/img2.imageset/pexels-photo-145939.jpeg rename to Example/Sources/Resources/Assets.xcassets/img2.imageset/pexels-photo-145939.jpeg diff --git a/Example/Sources/Assets.xcassets/mklogo.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/mklogo.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/mklogo.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/mklogo.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/mklogo.imageset/mklogo.png b/Example/Sources/Resources/Assets.xcassets/mklogo.imageset/mklogo.png similarity index 100% rename from Example/Sources/Assets.xcassets/mklogo.imageset/mklogo.png rename to Example/Sources/Resources/Assets.xcassets/mklogo.imageset/mklogo.png diff --git a/Example/Sources/Assets.xcassets/mkorglogo.imageset/30243544.png b/Example/Sources/Resources/Assets.xcassets/mkorglogo.imageset/30243544.png similarity index 100% rename from Example/Sources/Assets.xcassets/mkorglogo.imageset/30243544.png rename to Example/Sources/Resources/Assets.xcassets/mkorglogo.imageset/30243544.png diff --git a/Example/Sources/Assets.xcassets/mkorglogo.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/mkorglogo.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/mkorglogo.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/mkorglogo.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/pin.imageset/Contents.json b/Example/Sources/Resources/Assets.xcassets/pin.imageset/Contents.json similarity index 100% rename from Example/Sources/Assets.xcassets/pin.imageset/Contents.json rename to Example/Sources/Resources/Assets.xcassets/pin.imageset/Contents.json diff --git a/Example/Sources/Assets.xcassets/pin.imageset/pin.png b/Example/Sources/Resources/Assets.xcassets/pin.imageset/pin.png similarity index 100% rename from Example/Sources/Assets.xcassets/pin.imageset/pin.png rename to Example/Sources/Resources/Assets.xcassets/pin.imageset/pin.png diff --git a/Example/Sources/Assets.xcassets/pin.imageset/pin@2x.png b/Example/Sources/Resources/Assets.xcassets/pin.imageset/pin@2x.png similarity index 100% rename from Example/Sources/Assets.xcassets/pin.imageset/pin@2x.png rename to Example/Sources/Resources/Assets.xcassets/pin.imageset/pin@2x.png diff --git a/Example/Sources/Assets.xcassets/pin.imageset/pin@3x.png b/Example/Sources/Resources/Assets.xcassets/pin.imageset/pin@3x.png similarity index 100% rename from Example/Sources/Assets.xcassets/pin.imageset/pin@3x.png rename to Example/Sources/Resources/Assets.xcassets/pin.imageset/pin@3x.png diff --git a/Example/Sources/Base.lproj/LaunchScreen.storyboard b/Example/Sources/Resources/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from Example/Sources/Base.lproj/LaunchScreen.storyboard rename to Example/Sources/Resources/Base.lproj/LaunchScreen.storyboard diff --git a/Example/Sources/Info.plist b/Example/Sources/Resources/Info.plist similarity index 88% rename from Example/Sources/Info.plist rename to Example/Sources/Resources/Info.plist index aee82b188..a98712186 100644 --- a/Example/Sources/Info.plist +++ b/Example/Sources/Resources/Info.plist @@ -4,8 +4,14 @@ CFBundleDevelopmentRegion en + CFBundleDisplayName + MessageKit CFBundleExecutable $(EXECUTABLE_NAME) + CFBundleIcons + + CFBundleIcons~ipad + CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion @@ -28,6 +34,8 @@ armv7 + UIStatusBarStyle + UIStatusBarStyleLightContent UIStatusBarTintParameters UINavigationBar diff --git a/Example/Sources/Resources/sound1.m4a b/Example/Sources/Resources/sound1.m4a new file mode 100644 index 000000000..56b31a690 Binary files /dev/null and b/Example/Sources/Resources/sound1.m4a differ diff --git a/Example/Sources/Settings+UserDefaults.swift b/Example/Sources/Settings+UserDefaults.swift deleted file mode 100644 index edddcc724..000000000 --- a/Example/Sources/Settings+UserDefaults.swift +++ /dev/null @@ -1,54 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import Foundation - -extension UserDefaults { - - static let messagesKey = "mockMessages" - - // MARK: - Mock Messages - - func setMockMessages(count: Int) { - set(count, forKey: "mockMessages") - synchronize() - } - - func mockMessagesCount() -> Int { - if let value = object(forKey: "mockMessages") as? Int { - return value - } - return 20 - } - - static func isFirstLaunch() -> Bool { - let hasBeenLaunchedBeforeFlag = "hasBeenLaunchedBeforeFlag" - let isFirstLaunch = !UserDefaults.standard.bool(forKey: hasBeenLaunchedBeforeFlag) - if isFirstLaunch { - UserDefaults.standard.set(true, forKey: hasBeenLaunchedBeforeFlag) - UserDefaults.standard.synchronize() - } - return isFirstLaunch - } -} diff --git a/Example/Sources/Sounds/sound1.m4a b/Example/Sources/Sounds/sound1.m4a new file mode 100644 index 000000000..56b31a690 Binary files /dev/null and b/Example/Sources/Sounds/sound1.m4a differ diff --git a/Example/Sources/Sounds/sound2.m4a b/Example/Sources/Sounds/sound2.m4a new file mode 100644 index 000000000..ed80ce193 Binary files /dev/null and b/Example/Sources/Sounds/sound2.m4a differ diff --git a/Example/Sources/View Controllers/AdvancedExampleViewController.swift b/Example/Sources/View Controllers/AdvancedExampleViewController.swift index d55b07cd3..d575e64cc 100644 --- a/Example/Sources/View Controllers/AdvancedExampleViewController.swift +++ b/Example/Sources/View Controllers/AdvancedExampleViewController.swift @@ -1,388 +1,530 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. -import UIKit +import InputBarAccessoryView +import Kingfisher import MapKit import MessageKit -import MessageInputBar +import UIKit + +// MARK: - AdvancedExampleViewController final class AdvancedExampleViewController: ChatViewController { - - let outgoingAvatarOverlap: CGFloat = 17.5 - - override func viewDidLoad() { - messagesCollectionView = MessagesCollectionView(frame: .zero, collectionViewLayout: CustomMessagesFlowLayout()) - messagesCollectionView.register(CustomCell.self) - super.viewDidLoad() - - updateTitleView(title: "MessageKit", subtitle: "2 Online") - - // Customize the typing bubble! These are the default values -// typingBubbleBackgroundColor = UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1) -// typingBubbleDotColor = .lightGray + // MARK: Public + + // MARK: - UICollectionViewDataSource + + public override func collectionView( + _ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath) + -> UICollectionViewCell + { + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { + fatalError("Ouch. nil data source for messages") } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - MockSocket.shared.connect(with: [SampleData.shared.steven, SampleData.shared.wu]) - .onTypingStatus { [weak self] in - self?.setTypingIndicatorHidden(false) - }.onNewMessage { [weak self] message in - self?.setTypingIndicatorHidden(true, performUpdates: { -// self?.insertMessage(message) - }) - self?.insertMessage(message) - } + + // Very important to check this when overriding `cellForItemAt` + // Super method will handle returning the typing indicator cell + guard !isSectionReservedForTypingIndicator(indexPath.section) else { + return super.collectionView(collectionView, cellForItemAt: indexPath) } - - override func loadFirstMessages() { - DispatchQueue.global(qos: .userInitiated).async { - let count = UserDefaults.standard.mockMessagesCount() - SampleData.shared.getAdvancedMessages(count: count) { messages in - DispatchQueue.main.async { - self.messageList = messages - self.messagesCollectionView.reloadData() - self.messagesCollectionView.scrollToBottom() - } - } - } + + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + if case .custom = message.kind { + let cell = messagesCollectionView.dequeueReusableCell(CustomCell.self, for: indexPath) + cell.configure(with: message, at: indexPath, and: messagesCollectionView) + return cell } - - override func loadMoreMessages() { - DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1) { - SampleData.shared.getAdvancedMessages(count: 20) { messages in - DispatchQueue.main.async { - self.messageList.insert(contentsOf: messages, at: 0) - self.messagesCollectionView.reloadDataAndKeepOffset() - self.refreshControl.endRefreshing() - } - } + return super.collectionView(collectionView, cellForItemAt: indexPath) + } + + // MARK: Internal + + let outgoingAvatarOverlap: CGFloat = 17.5 + + override func viewDidLoad() { + messagesCollectionView = MessagesCollectionView(frame: .zero, collectionViewLayout: CustomMessagesFlowLayout()) + messagesCollectionView.register(CustomCell.self) + super.viewDidLoad() + + updateTitleView(title: "MessageKit", subtitle: "2 Online") + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + MockSocket.shared.connect(with: [SampleData.shared.nathan, SampleData.shared.wu]) + .onTypingStatus { [weak self] in + self?.setTypingIndicatorViewHidden(false, animated: true) + }.onNewMessage { [weak self] message in + self?.setTypingIndicatorViewHidden(true, animated: false, performUpdates: { + self?.insertMessage(message) + }) + } + } + + override func loadFirstMessages() { + DispatchQueue.global(qos: .userInitiated).async { + let count = UserDefaults.standard.mockMessagesCount() + SampleData.shared.getAdvancedMessages(count: count) { messages in + DispatchQueue.main.async { + self.messageList = messages + self.messagesCollectionView.reloadData() + self.messagesCollectionView.scrollToLastItem() } + } } - - override func configureMessageCollectionView() { - super.configureMessageCollectionView() - - let layout = messagesCollectionView.collectionViewLayout as? MessagesCollectionViewFlowLayout - layout?.sectionInset = UIEdgeInsets(top: 1, left: 8, bottom: 1, right: 8) - - // Hide the outgoing avatar and adjust the label alignment to line up with the messages - layout?.setMessageOutgoingAvatarSize(.zero) - layout?.setMessageOutgoingMessageTopLabelAlignment(LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8))) - layout?.setMessageOutgoingMessageBottomLabelAlignment(LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8))) - - // Set outgoing avatar to overlap with the message bubble - layout?.setMessageIncomingMessageTopLabelAlignment(LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(top: 0, left: 18, bottom: outgoingAvatarOverlap, right: 0))) - layout?.setMessageIncomingAvatarSize(CGSize(width: 30, height: 30)) - layout?.setMessageIncomingMessagePadding(UIEdgeInsets(top: -outgoingAvatarOverlap, left: -18, bottom: outgoingAvatarOverlap, right: 18)) - - layout?.setMessageIncomingAccessoryViewSize(CGSize(width: 30, height: 30)) - layout?.setMessageIncomingAccessoryViewPadding(HorizontalEdgeInsets(left: 8, right: 0)) - layout?.setMessageOutgoingAccessoryViewSize(CGSize(width: 30, height: 30)) - layout?.setMessageOutgoingAccessoryViewPadding(HorizontalEdgeInsets(left: 0, right: 8)) - - messagesCollectionView.messagesLayoutDelegate = self - messagesCollectionView.messagesDisplayDelegate = self - } - - override func configureMessageInputBar() { - super.configureMessageInputBar() - - messageInputBar.isTranslucent = true - messageInputBar.separatorLine.isHidden = true - messageInputBar.inputTextView.tintColor = .primaryColor - messageInputBar.inputTextView.backgroundColor = UIColor(red: 245/255, green: 245/255, blue: 245/255, alpha: 1) - messageInputBar.inputTextView.placeholderTextColor = UIColor(red: 0.6, green: 0.6, blue: 0.6, alpha: 1) - messageInputBar.inputTextView.textContainerInset = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 36) - messageInputBar.inputTextView.placeholderLabelInsets = UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 36) - messageInputBar.inputTextView.layer.borderColor = UIColor(red: 200/255, green: 200/255, blue: 200/255, alpha: 1).cgColor - messageInputBar.inputTextView.layer.borderWidth = 1.0 - messageInputBar.inputTextView.layer.cornerRadius = 16.0 - messageInputBar.inputTextView.layer.masksToBounds = true - messageInputBar.inputTextView.scrollIndicatorInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0) - configureInputBarItems() - } + } - private func configureInputBarItems() { - messageInputBar.setRightStackViewWidthConstant(to: 36, animated: false) - messageInputBar.sendButton.imageView?.backgroundColor = UIColor(white: 0.85, alpha: 1) - messageInputBar.sendButton.contentEdgeInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2) - messageInputBar.sendButton.setSize(CGSize(width: 36, height: 36), animated: false) - messageInputBar.sendButton.image = #imageLiteral(resourceName: "ic_up") - messageInputBar.sendButton.title = nil - messageInputBar.sendButton.imageView?.layer.cornerRadius = 16 - messageInputBar.textViewPadding.right = -38 - let charCountButton = InputBarButtonItem() - .configure { - $0.title = "0/140" - $0.contentHorizontalAlignment = .right - $0.setTitleColor(UIColor(white: 0.6, alpha: 1), for: .normal) - $0.titleLabel?.font = UIFont.systemFont(ofSize: 10, weight: .bold) - $0.setSize(CGSize(width: 50, height: 25), animated: false) - }.onTextViewDidChange { (item, textView) in - item.title = "\(textView.text.count)/140" - let isOverLimit = textView.text.count > 140 - item.messageInputBar?.shouldManageSendButtonEnabledState = !isOverLimit // Disable automated management when over limit - if isOverLimit { - item.messageInputBar?.sendButton.isEnabled = false - } - let color = isOverLimit ? .red : UIColor(white: 0.6, alpha: 1) - item.setTitleColor(color, for: .normal) + override func loadMoreMessages() { + DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1) { + SampleData.shared.getAdvancedMessages(count: 20) { messages in + DispatchQueue.main.async { + self.messageList.insert(contentsOf: messages, at: 0) + self.messagesCollectionView.reloadDataAndKeepOffset() + self.refreshControl.endRefreshing() } - let bottomItems = [makeButton(named: "ic_at"), makeButton(named: "ic_hashtag"), makeButton(named: "ic_library"), .flexibleSpace, charCountButton] - messageInputBar.textViewPadding.bottom = 8 - messageInputBar.setStackViewItems(bottomItems, forStack: .bottom, animated: false) - - // This just adds some more flare - messageInputBar.sendButton - .onEnabled { item in - UIView.animate(withDuration: 0.3, animations: { - item.imageView?.backgroundColor = .primaryColor - }) - }.onDisabled { item in - UIView.animate(withDuration: 0.3, animations: { - item.imageView?.backgroundColor = UIColor(white: 0.85, alpha: 1) - }) - } - } - - // MARK: - Helpers - - func isTimeLabelVisible(at indexPath: IndexPath) -> Bool { - return indexPath.section % 3 == 0 && !isPreviousMessageSameSender(at: indexPath) + } } - - func isPreviousMessageSameSender(at indexPath: IndexPath) -> Bool { - guard indexPath.section - 1 >= 0 else { return false } - return messageList[indexPath.section].sender == messageList[indexPath.section - 1].sender - } - - func isNextMessageSameSender(at indexPath: IndexPath) -> Bool { - guard indexPath.section + 1 < messageList.count else { return false } - return messageList[indexPath.section].sender == messageList[indexPath.section + 1].sender + } + + override func configureMessageCollectionView() { + super.configureMessageCollectionView() + + let layout = messagesCollectionView.collectionViewLayout as? MessagesCollectionViewFlowLayout + layout?.sectionInset = UIEdgeInsets(top: 1, left: 8, bottom: 1, right: 8) + + // Hide the outgoing avatar and adjust the label alignment to line up with the messages + layout?.setMessageOutgoingAvatarSize(.zero) + layout? + .setMessageOutgoingMessageTopLabelAlignment(LabelAlignment( + textAlignment: .right, + textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8))) + layout? + .setMessageOutgoingMessageBottomLabelAlignment(LabelAlignment( + textAlignment: .right, + textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8))) + + // Set outgoing avatar to overlap with the message bubble + layout? + .setMessageIncomingMessageTopLabelAlignment(LabelAlignment( + textAlignment: .left, + textInsets: UIEdgeInsets(top: 0, left: 18, bottom: outgoingAvatarOverlap, right: 0))) + layout?.setMessageIncomingAvatarSize(CGSize(width: 30, height: 30)) + layout? + .setMessageIncomingMessagePadding(UIEdgeInsets( + top: -outgoingAvatarOverlap, + left: -18, + bottom: outgoingAvatarOverlap, + right: 18)) + + layout?.setMessageIncomingAccessoryViewSize(CGSize(width: 30, height: 30)) + layout?.setMessageIncomingAccessoryViewPadding(HorizontalEdgeInsets(left: 8, right: 0)) + layout?.setMessageIncomingAccessoryViewPosition(.messageBottom) + layout?.setMessageOutgoingAccessoryViewSize(CGSize(width: 30, height: 30)) + layout?.setMessageOutgoingAccessoryViewPadding(HorizontalEdgeInsets(left: 0, right: 8)) + + messagesCollectionView.messagesLayoutDelegate = self + messagesCollectionView.messagesDisplayDelegate = self + } + + override func configureMessageInputBar() { + // super.configureMessageInputBar() + + messageInputBar = CameraInputBarAccessoryView() + messageInputBar.delegate = self + messageInputBar.inputTextView.tintColor = .primaryColor + messageInputBar.sendButton.setTitleColor(.primaryColor, for: .normal) + messageInputBar.sendButton.setTitleColor( + UIColor.primaryColor.withAlphaComponent(0.3), + for: .highlighted) + + messageInputBar.isTranslucent = true + messageInputBar.separatorLine.isHidden = true + messageInputBar.inputTextView.tintColor = .primaryColor + messageInputBar.inputTextView.backgroundColor = UIColor(red: 245 / 255, green: 245 / 255, blue: 245 / 255, alpha: 1) + messageInputBar.inputTextView.placeholderTextColor = UIColor(red: 0.6, green: 0.6, blue: 0.6, alpha: 1) + messageInputBar.inputTextView.textContainerInset = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 36) + messageInputBar.inputTextView.placeholderLabelInsets = UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 36) + messageInputBar.inputTextView.layer.borderColor = UIColor(red: 200 / 255, green: 200 / 255, blue: 200 / 255, alpha: 1).cgColor + messageInputBar.inputTextView.layer.borderWidth = 1.0 + messageInputBar.inputTextView.layer.cornerRadius = 16.0 + messageInputBar.inputTextView.layer.masksToBounds = true + messageInputBar.inputTextView.scrollIndicatorInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0) + configureInputBarItems() + inputBarType = .custom(messageInputBar) + } + + // MARK: - Helpers + + func isTimeLabelVisible(at indexPath: IndexPath) -> Bool { + indexPath.section % 3 == 0 && !isPreviousMessageSameSender(at: indexPath) + } + + func isPreviousMessageSameSender(at indexPath: IndexPath) -> Bool { + guard indexPath.section - 1 >= 0 else { return false } + return messageList[indexPath.section].user == messageList[indexPath.section - 1].user + } + + func isNextMessageSameSender(at indexPath: IndexPath) -> Bool { + guard indexPath.section + 1 < messageList.count else { return false } + return messageList[indexPath.section].user == messageList[indexPath.section + 1].user + } + + func setTypingIndicatorViewHidden(_ isHidden: Bool, animated: Bool, performUpdates updates: (() -> Void)? = nil) { + updateTitleView(title: "MessageKit", subtitle: isHidden ? "2 Online" : "Typing...") + setTypingIndicatorViewHidden(isHidden, animated: animated, whilePerforming: updates) { [weak self] success in + if success, self?.isLastSectionVisible() == true { + self?.messagesCollectionView.scrollToLastItem(animated: true) + } } - - func setTypingIndicatorHidden(_ isHidden: Bool, performUpdates updates: (() -> Void)? = nil) { - updateTitleView(title: "MessageKit", subtitle: isHidden ? "2 Online" : "Typing...") -// setTypingBubbleHidden(isHidden, animated: true, whilePerforming: updates) { [weak self] (_) in -// if self?.isLastSectionVisible() == true { -// self?.messagesCollectionView.scrollToBottom(animated: true) -// } -// } -// messagesCollectionView.scrollToBottom(animated: true) + } + + // MARK: - MessagesDataSource + + override func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + if isTimeLabelVisible(at: indexPath) { + return NSAttributedString( + string: MessageKitDateFormatter.shared.string(from: message.sentDate), + attributes: [ + NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10), + NSAttributedString.Key.foregroundColor: UIColor.darkGray, + ]) } - - private func makeButton(named: String) -> InputBarButtonItem { - return InputBarButtonItem() - .configure { - $0.spacing = .fixed(10) - $0.image = UIImage(named: named)?.withRenderingMode(.alwaysTemplate) - $0.setSize(CGSize(width: 25, height: 25), animated: false) - $0.tintColor = UIColor(white: 0.8, alpha: 1) - }.onSelected { - $0.tintColor = .primaryColor - }.onDeselected { - $0.tintColor = UIColor(white: 0.8, alpha: 1) - }.onTouchUpInside { _ in - print("Item Tapped") - } + return nil + } + + override func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + if !isPreviousMessageSameSender(at: indexPath) { + let name = message.sender.displayName + return NSAttributedString( + string: name, + attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)]) } - - // MARK: - UICollectionViewDataSource - - public override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - - guard let messagesDataSource = messagesCollectionView.messagesDataSource else { - fatalError("Ouch. nil data source for messages") - } - -// guard !isSectionReservedForTypingBubble(indexPath.section) else { -// return super.collectionView(collectionView, cellForItemAt: indexPath) -// } - - let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - if case .custom = message.kind { - let cell = messagesCollectionView.dequeueReusableCell(CustomCell.self, for: indexPath) - cell.configure(with: message, at: indexPath, and: messagesCollectionView) - return cell - } - return super.collectionView(collectionView, cellForItemAt: indexPath) + return nil + } + + override func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + if !isNextMessageSameSender(at: indexPath), isFromCurrentSender(message: message) { + return NSAttributedString( + string: "Delivered", + attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)]) } + return nil + } - // MARK: - MessagesDataSource + // MARK: Private - override func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { - if isTimeLabelVisible(at: indexPath) { - return NSAttributedString(string: MessageKitDateFormatter.shared.string(from: message.sentDate), attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10), NSAttributedString.Key.foregroundColor: UIColor.darkGray]) + private func configureInputBarItems() { + messageInputBar.setRightStackViewWidthConstant(to: 36, animated: false) + messageInputBar.sendButton.imageView?.backgroundColor = UIColor(white: 0.85, alpha: 1) + messageInputBar.sendButton.contentEdgeInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2) + messageInputBar.sendButton.setSize(CGSize(width: 36, height: 36), animated: false) + messageInputBar.sendButton.image = #imageLiteral(resourceName: "ic_up") + messageInputBar.sendButton.title = nil + messageInputBar.sendButton.imageView?.layer.cornerRadius = 16 + let charCountButton = InputBarButtonItem() + .configure { + $0.title = "0/140" + $0.contentHorizontalAlignment = .right + $0.setTitleColor(UIColor(white: 0.6, alpha: 1), for: .normal) + $0.titleLabel?.font = UIFont.systemFont(ofSize: 10, weight: .bold) + $0.setSize(CGSize(width: 50, height: 25), animated: false) + }.onTextViewDidChange { item, textView in + item.title = "\(textView.text.count)/140" + let isOverLimit = textView.text.count > 140 + item.inputBarAccessoryView? + .shouldManageSendButtonEnabledState = !isOverLimit // Disable automated management when over limit + if isOverLimit { + item.inputBarAccessoryView?.sendButton.isEnabled = false } - return nil - } - - override func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { - if !isPreviousMessageSameSender(at: indexPath) { - let name = message.sender.displayName - return NSAttributedString(string: name, attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)]) - } - return nil - } + let color = isOverLimit ? .red : UIColor(white: 0.6, alpha: 1) + item.setTitleColor(color, for: .normal) + } + let bottomItems = [.flexibleSpace, charCountButton] - override func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + configureInputBarPadding() - if !isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message) { - return NSAttributedString(string: "Delivered", attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)]) - } - return nil - } + messageInputBar.setStackViewItems(bottomItems, forStack: .bottom, animated: false) + + // This just adds some more flare + messageInputBar.sendButton + .onEnabled { item in + UIView.animate(withDuration: 0.3, animations: { + item.imageView?.backgroundColor = .primaryColor + }) + }.onDisabled { item in + UIView.animate(withDuration: 0.3, animations: { + item.imageView?.backgroundColor = UIColor(white: 0.85, alpha: 1) + }) + } + } + + /// The input bar will autosize based on the contained text, but we can add padding to adjust the height or width if necessary + /// See the InputBar diagram here to visualize how each of these would take effect: + /// https://raw.githubusercontent.com/MessageKit/MessageKit/master/Assets/InputBarAccessoryViewLayout.png + private func configureInputBarPadding() { + // Entire InputBar padding + messageInputBar.padding.bottom = 8 + + // or MiddleContentView padding + messageInputBar.middleContentViewPadding.right = -38 + // or InputTextView padding + messageInputBar.inputTextView.textContainerInset.bottom = 8 + } + + private func makeButton(named: String) -> InputBarButtonItem { + InputBarButtonItem() + .configure { + $0.spacing = .fixed(10) + $0.image = UIImage(named: named)?.withRenderingMode(.alwaysTemplate) + $0.setSize(CGSize(width: 25, height: 25), animated: false) + $0.tintColor = UIColor(white: 0.8, alpha: 1) + }.onSelected { + $0.tintColor = .primaryColor + }.onDeselected { + $0.tintColor = UIColor(white: 0.8, alpha: 1) + }.onTouchUpInside { + print("Item Tapped") + let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + let action = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + actionSheet.addAction(action) + if let popoverPresentationController = actionSheet.popoverPresentationController { + popoverPresentationController.sourceView = $0 + popoverPresentationController.sourceRect = $0.frame + } + self.navigationController?.present(actionSheet, animated: true, completion: nil) + } + } } -// MARK: - MessagesDisplayDelegate +// MARK: MessagesDisplayDelegate extension AdvancedExampleViewController: MessagesDisplayDelegate { + // MARK: - Text Messages - // MARK: - Text Messages + func textColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor { + isFromCurrentSender(message: message) ? .white : .darkText + } - func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { - return isFromCurrentSender(message: message) ? .white : .darkText + func detectorAttributes( + for detector: DetectorType, + and message: MessageType, + at _: IndexPath) -> [NSAttributedString.Key: Any] + { + switch detector { + case .hashtag, .mention: + if isFromCurrentSender(message: message) { + return [.foregroundColor: UIColor.white] + } else { + return [.foregroundColor: UIColor.primaryColor] + } + default: return MessageLabel.defaultAttributes } + } - func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any] { - return MessageLabel.defaultAttributes - } + func enabledDetectors(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> [DetectorType] { + [.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag] + } - func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] { - return [.url, .address, .phoneNumber, .date, .transitInformation] - } + // MARK: - All Messages - // MARK: - All Messages - - func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { - return isFromCurrentSender(message: message) ? .primaryColor : UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1) - } + func backgroundColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor { + isFromCurrentSender(message: message) ? .primaryColor : UIColor(red: 230 / 255, green: 230 / 255, blue: 230 / 255, alpha: 1) + } - func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle { - - var corners: UIRectCorner = [] - - if isFromCurrentSender(message: message) { - corners.formUnion(.topLeft) - corners.formUnion(.bottomLeft) - if !isPreviousMessageSameSender(at: indexPath) { - corners.formUnion(.topRight) - } - if !isNextMessageSameSender(at: indexPath) { - corners.formUnion(.bottomRight) - } - } else { - corners.formUnion(.topRight) - corners.formUnion(.bottomRight) - if !isPreviousMessageSameSender(at: indexPath) { - corners.formUnion(.topLeft) - } - if !isNextMessageSameSender(at: indexPath) { - corners.formUnion(.bottomLeft) - } - } - - return .custom { view in - let radius: CGFloat = 16 - let path = UIBezierPath(roundedRect: view.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)) - let mask = CAShapeLayer() - mask.path = path.cgPath - view.layer.mask = mask - } - } - - func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { - let avatar = SampleData.shared.getAvatarFor(sender: message.sender) - avatarView.set(avatar: avatar) - avatarView.isHidden = isNextMessageSameSender(at: indexPath) - avatarView.layer.borderWidth = 2 - avatarView.layer.borderColor = UIColor.primaryColor.cgColor - } - - func configureAccessoryView(_ accessoryView: UIView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { - // Cells are reused, so only add a button here once. For real use you would need to - // ensure any subviews are removed if not needed - guard accessoryView.subviews.isEmpty else { return } - let button = UIButton(type: .infoLight) - button.tintColor = .primaryColor - accessoryView.addSubview(button) - button.frame = accessoryView.bounds - button.isUserInteractionEnabled = false // respond to accessoryView tap through `MessageCellDelegate` - accessoryView.layer.cornerRadius = accessoryView.frame.height / 2 - accessoryView.backgroundColor = UIColor.primaryColor.withAlphaComponent(0.3) + func messageStyle(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> MessageStyle { + var corners: UIRectCorner = [] + + if isFromCurrentSender(message: message) { + corners.formUnion(.topLeft) + corners.formUnion(.bottomLeft) + if !isPreviousMessageSameSender(at: indexPath) { + corners.formUnion(.topRight) + } + if !isNextMessageSameSender(at: indexPath) { + corners.formUnion(.bottomRight) + } + } else { + corners.formUnion(.topRight) + corners.formUnion(.bottomRight) + if !isPreviousMessageSameSender(at: indexPath) { + corners.formUnion(.topLeft) + } + if !isNextMessageSameSender(at: indexPath) { + corners.formUnion(.bottomLeft) + } } - - // MARK: - Location Messages - - func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? { - let annotationView = MKAnnotationView(annotation: nil, reuseIdentifier: nil) - let pinImage = #imageLiteral(resourceName: "ic_map_marker") - annotationView.image = pinImage - annotationView.centerOffset = CGPoint(x: 0, y: -pinImage.size.height / 2) - return annotationView + + return .custom { view in + let radius: CGFloat = 16 + let path = UIBezierPath( + roundedRect: view.bounds, + byRoundingCorners: corners, + cornerRadii: CGSize(width: radius, height: radius)) + let mask = CAShapeLayer() + mask.path = path.cgPath + view.layer.mask = mask } - - func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)? { - return { view in - view.layer.transform = CATransform3DMakeScale(2, 2, 2) - UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0, options: [], animations: { - view.layer.transform = CATransform3DIdentity - }, completion: nil) - } + } + + func configureAvatarView( + _ avatarView: AvatarView, + for message: MessageType, + at indexPath: IndexPath, + in _: MessagesCollectionView) + { + let avatar = SampleData.shared.getAvatarFor(sender: message.sender) + avatarView.set(avatar: avatar) + avatarView.isHidden = isNextMessageSameSender(at: indexPath) + avatarView.layer.borderWidth = 2 + avatarView.layer.borderColor = UIColor.primaryColor.cgColor + } + + func configureAccessoryView(_ accessoryView: UIView, for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) { + // Cells are reused, so only add a button here once. For real use you would need to + // ensure any subviews are removed if not needed + accessoryView.subviews.forEach { $0.removeFromSuperview() } + accessoryView.backgroundColor = .clear + + let shouldShow = Int.random(in: 0 ... 10) == 0 + guard shouldShow else { return } + + let button = UIButton(type: .infoLight) + button.tintColor = .primaryColor + accessoryView.addSubview(button) + button.frame = accessoryView.bounds + button.isUserInteractionEnabled = false // respond to accessoryView tap through `MessageCellDelegate` + accessoryView.layer.cornerRadius = accessoryView.frame.height / 2 + accessoryView.backgroundColor = UIColor.primaryColor.withAlphaComponent(0.3) + } + + func configureMediaMessageImageView( + _ imageView: UIImageView, + for message: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) + { + if case MessageKind.photo(let media) = message.kind, let imageURL = media.url { + imageView.kf.setImage(with: imageURL) + } else { + imageView.kf.cancelDownloadTask() } - - func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions { - - return LocationMessageSnapshotOptions(showsBuildings: true, showsPointsOfInterest: true, span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)) + } + + // MARK: - Location Messages + + func annotationViewForLocation(message _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> MKAnnotationView? { + let annotationView = MKAnnotationView(annotation: nil, reuseIdentifier: nil) + let pinImage = #imageLiteral(resourceName: "ic_map_marker") + annotationView.image = pinImage + annotationView.centerOffset = CGPoint(x: 0, y: -pinImage.size.height / 2) + return annotationView + } + + func animationBlockForLocation( + message _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) -> ((UIImageView) -> Void)? + { + { view in + view.layer.transform = CATransform3DMakeScale(2, 2, 2) + UIView.animate( + withDuration: 0.6, + delay: 0, + usingSpringWithDamping: 0.9, + initialSpringVelocity: 0, + options: [], + animations: { + view.layer.transform = CATransform3DIdentity + }, + completion: nil) } + } + func snapshotOptionsForLocation( + message _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) + -> LocationMessageSnapshotOptions + { + LocationMessageSnapshotOptions( + showsBuildings: true, + showsPointsOfInterest: true, + span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)) + } + + // MARK: - Audio Messages + + func audioTintColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor { + isFromCurrentSender(message: message) ? .white : .primaryColor + } + + func configureAudioCell(_ cell: AudioMessageCell, message: MessageType) { + audioController + .configureAudioCell( + cell, + message: message) // this is needed especially when the cell is reconfigure while is playing sound + } } -// MARK: - MessagesLayoutDelegate +// MARK: MessagesLayoutDelegate extension AdvancedExampleViewController: MessagesLayoutDelegate { - - func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - if isTimeLabelVisible(at: indexPath) { - return 18 - } - return 0 + func cellTopLabelHeight(for _: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat { + if isTimeLabelVisible(at: indexPath) { + return 18 } - - func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - if isFromCurrentSender(message: message) { - return !isPreviousMessageSameSender(at: indexPath) ? 20 : 0 - } else { - return !isPreviousMessageSameSender(at: indexPath) ? (20 + outgoingAvatarOverlap) : 0 - } + return 0 + } + + func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat { + if isFromCurrentSender(message: message) { + return !isPreviousMessageSameSender(at: indexPath) ? 20 : 0 + } else { + return !isPreviousMessageSameSender(at: indexPath) ? (20 + outgoingAvatarOverlap) : 0 } + } + + func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat { + (!isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message)) ? 16 : 0 + } +} + +// MARK: CameraInputBarAccessoryViewDelegate - func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return (!isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message)) ? 16 : 0 +extension AdvancedExampleViewController: CameraInputBarAccessoryViewDelegate { + func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith attachments: [AttachmentManager.Attachment]) { + for item in attachments { + if case .image(let image) = item { + self.sendImageMessage(photo: image) + } } + inputBar.invalidatePlugins() + } + func sendImageMessage(photo: UIImage) { + let photoMessage = MockMessage(image: photo, user: currentSender as! MockUser, messageId: UUID().uuidString, date: Date()) + insertMessage(photoMessage) + } } diff --git a/Example/Sources/View Controllers/AutocompleteExampleViewController.swift b/Example/Sources/View Controllers/AutocompleteExampleViewController.swift new file mode 100644 index 000000000..c5b2de5da --- /dev/null +++ b/Example/Sources/View Controllers/AutocompleteExampleViewController.swift @@ -0,0 +1,432 @@ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import InputBarAccessoryView +import Kingfisher +import MessageKit +import UIKit + +// MARK: - AutocompleteExampleViewController + +final class AutocompleteExampleViewController: ChatViewController { + // MARK: Internal + + lazy var joinChatButton: UIButton = { + let button = UIButton() + button.layer.cornerRadius = 16 + button.backgroundColor = .primaryColor + button.setTitle("JOIN CHAT", for: .normal) + button.setTitleColor(.white, for: .normal) + button.setTitleColor(UIColor(white: 1, alpha: 0.3), for: .highlighted) + button.addTarget(self, action: #selector(joinChat), for: .touchUpInside) + return button + }() + + /// The object that manages autocomplete, from InputBarAccessoryView + lazy var autocompleteManager: AutocompleteManager = { [unowned self] in + let manager = AutocompleteManager(for: self.messageInputBar.inputTextView) + manager.delegate = self + manager.dataSource = self + return manager + }() + + var hashtagAutocompletes: [AutocompleteCompletion] = { + var array: [AutocompleteCompletion] = [] + for _ in 1 ... 100 { + array.append(AutocompleteCompletion(text: Lorem.word(), context: nil)) + } + return array + }() + + // Completions loaded async that get appended to local cached completions + var asyncCompletions: [AutocompleteCompletion] = [] + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + MockSocket.shared.connect(with: [SampleData.shared.nathan, SampleData.shared.wu]) + .onTypingStatus { [weak self] in + self?.setTypingIndicatorViewHidden(false) + }.onNewMessage { [weak self] message in + self?.setTypingIndicatorViewHidden(true, performUpdates: { + self?.insertMessage(message) + }) + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + messageInputBar.inputTextView.keyboardType = .twitter + + // Configure AutocompleteManager + autocompleteManager.register( + prefix: "@", + with: [ + .font: UIFont.preferredFont(forTextStyle: .body), + .foregroundColor: UIColor.primaryColor, + .backgroundColor: UIColor.primaryColor.withAlphaComponent(0.3), + ]) + autocompleteManager.register(prefix: "#") + autocompleteManager.maxSpaceCountDuringCompletion = 1 // Allow for autocompletes with a space + + // Set plugins + messageInputBar.inputPlugins = [autocompleteManager] + } + + override func configureMessageCollectionView() { + super.configureMessageCollectionView() + + let layout = messagesCollectionView.collectionViewLayout as? MessagesCollectionViewFlowLayout + layout?.sectionInset = UIEdgeInsets(top: 1, left: 8, bottom: 1, right: 8) + layout?.setMessageOutgoingCellBottomLabelAlignment(.init(textAlignment: .right, textInsets: .zero)) + layout?.setMessageOutgoingAvatarSize(.zero) + layout? + .setMessageOutgoingMessageTopLabelAlignment(LabelAlignment( + textAlignment: .right, + textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 12))) + layout? + .setMessageOutgoingMessageBottomLabelAlignment(LabelAlignment( + textAlignment: .right, + textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 12))) + + messagesCollectionView.messagesLayoutDelegate = self + messagesCollectionView.messagesDisplayDelegate = self + + additionalBottomInset = 30 + } + + override func configureMessageInputBar() { + super.configureMessageInputBar() + messageInputBar.layer.shadowColor = UIColor.black.cgColor + messageInputBar.layer.shadowRadius = 4 + messageInputBar.layer.shadowOpacity = 0.3 + messageInputBar.layer.shadowOffset = CGSize(width: 0, height: 0) + messageInputBar.separatorLine.isHidden = true + messageInputBar.setRightStackViewWidthConstant(to: 0, animated: false) + messageInputBar.setMiddleContentView(joinChatButton, animated: false) + } + + @objc + func joinChat() { + configureMessageInputBarForChat() + } + + // MARK: - Helpers + + func isTimeLabelVisible(at indexPath: IndexPath) -> Bool { + indexPath.section % 3 == 0 && !isPreviousMessageSameSender(at: indexPath) + } + + func isPreviousMessageSameSender(at indexPath: IndexPath) -> Bool { + guard indexPath.section - 1 >= 0 else { return false } + return messageList[indexPath.section].user == messageList[indexPath.section - 1].user + } + + func isNextMessageSameSender(at indexPath: IndexPath) -> Bool { + guard indexPath.section + 1 < messageList.count else { return false } + return messageList[indexPath.section].user == messageList[indexPath.section + 1].user + } + + func setTypingIndicatorViewHidden(_ isHidden: Bool, performUpdates updates: (() -> Void)? = nil) { + setTypingIndicatorViewHidden(isHidden, animated: true, whilePerforming: updates) { [weak self] success in + if success, self?.isLastSectionVisible() == true { + self?.messagesCollectionView.scrollToLastItem(animated: true) + } + } + } + + // MARK: - MessagesDataSource + + override func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + if isTimeLabelVisible(at: indexPath) { + return NSAttributedString( + string: MessageKitDateFormatter.shared.string(from: message.sentDate), + attributes: [ + NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10), + NSAttributedString.Key.foregroundColor: UIColor.darkGray, + ]) + } + return nil + } + + override func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + if !isPreviousMessageSameSender(at: indexPath) { + let name = message.sender.displayName + return NSAttributedString( + string: name, + attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)]) + } + return nil + } + + override func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + if !isNextMessageSameSender(at: indexPath), isFromCurrentSender(message: message) { + return NSAttributedString( + string: "Delivered", + attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)]) + } + return nil + } + + // Async autocomplete requires the manager to reload + func inputBar(_: InputBarAccessoryView, textViewTextDidChangeTo _: String) { + guard autocompleteManager.currentSession != nil, autocompleteManager.currentSession?.prefix == "#" else { return } + // Load some data asyncronously for the given session.prefix + DispatchQueue.global(qos: .default).async { + // fake background loading task + var array: [AutocompleteCompletion] = [] + for _ in 1 ... 10 { + array.append(AutocompleteCompletion(text: Lorem.word())) + } + sleep(1) + DispatchQueue.main.async { [weak self] in + self?.asyncCompletions = array + self?.autocompleteManager.reloadData() + } + } + } + + // MARK: Private + + private func configureMessageInputBarForChat() { + messageInputBar.setMiddleContentView(messageInputBar.inputTextView, animated: false) + messageInputBar.setRightStackViewWidthConstant(to: 52, animated: false) + let bottomItems = [makeButton(named: "ic_at"), makeButton(named: "ic_hashtag"), .flexibleSpace] + messageInputBar.setStackViewItems(bottomItems, forStack: .bottom, animated: false) + + messageInputBar.sendButton.activityViewColor = .white + messageInputBar.sendButton.backgroundColor = .primaryColor + messageInputBar.sendButton.layer.cornerRadius = 10 + messageInputBar.sendButton.setTitleColor(.white, for: .normal) + messageInputBar.sendButton.setTitleColor(UIColor(white: 1, alpha: 0.3), for: .highlighted) + messageInputBar.sendButton.setTitleColor(UIColor(white: 1, alpha: 0.3), for: .disabled) + messageInputBar.sendButton + .onSelected { item in + item.transform = CGAffineTransform(scaleX: 1.05, y: 1.05) + }.onDeselected { item in + item.transform = .identity + } + } + + private func makeButton(named: String) -> InputBarButtonItem { + InputBarButtonItem() + .configure { + $0.spacing = .fixed(10) + $0.image = UIImage(named: named)?.withRenderingMode(.alwaysTemplate) + $0.setSize(CGSize(width: 25, height: 25), animated: false) + $0.tintColor = UIColor(white: 0.8, alpha: 1) + }.onSelected { + $0.tintColor = .primaryColor + }.onDeselected { + $0.tintColor = UIColor(white: 0.8, alpha: 1) + }.onTouchUpInside { _ in + print("Item Tapped") + } + } +} + +// MARK: AutocompleteManagerDelegate, AutocompleteManagerDataSource + +extension AutocompleteExampleViewController: AutocompleteManagerDelegate, AutocompleteManagerDataSource { + // MARK: - AutocompleteManagerDataSource + + func autocompleteManager(_: AutocompleteManager, autocompleteSourceFor prefix: String) -> [AutocompleteCompletion] { + if prefix == "@" { + return SampleData.shared.senders + .map { user in + AutocompleteCompletion( + text: user.displayName, + context: ["id": user.senderId]) + } + } else if prefix == "#" { + return hashtagAutocompletes + asyncCompletions + } + return [] + } + + func autocompleteManager( + _ manager: AutocompleteManager, + tableView: UITableView, + cellForRowAt indexPath: IndexPath, + for session: AutocompleteSession) + -> UITableViewCell + { + guard + let cell = tableView + .dequeueReusableCell(withIdentifier: AutocompleteCell.reuseIdentifier, for: indexPath) as? AutocompleteCell else + { + fatalError("Oops, some unknown error occurred") + } + let users = SampleData.shared.senders + let id = session.completion?.context?["id"] as? String + let user = users.filter { $0.senderId == id }.first + if let sender = user { + cell.imageView?.image = SampleData.shared.getAvatarFor(sender: sender).image + } + cell.imageViewEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) + cell.imageView?.layer.cornerRadius = 14 + cell.imageView?.layer.borderColor = UIColor.primaryColor.cgColor + cell.imageView?.layer.borderWidth = 1 + cell.imageView?.clipsToBounds = true + cell.textLabel?.attributedText = manager.attributedText(matching: session, fontSize: 15) + return cell + } + + // MARK: - AutocompleteManagerDelegate + + func autocompleteManager(_: AutocompleteManager, shouldBecomeVisible: Bool) { + setAutocompleteManager(active: shouldBecomeVisible) + } + + // Optional + func autocompleteManager(_: AutocompleteManager, shouldRegister _: String, at _: NSRange) -> Bool { + true + } + + // Optional + func autocompleteManager(_: AutocompleteManager, shouldUnregister _: String) -> Bool { + true + } + + // Optional + func autocompleteManager(_: AutocompleteManager, shouldComplete _: String, with _: String) -> Bool { + true + } + + // MARK: - AutocompleteManagerDelegate Helper + + func setAutocompleteManager(active: Bool) { + let topStackView = messageInputBar.topStackView + if active, !topStackView.arrangedSubviews.contains(autocompleteManager.tableView) { + topStackView.insertArrangedSubview(autocompleteManager.tableView, at: topStackView.arrangedSubviews.count) + topStackView.layoutIfNeeded() + } else if !active, topStackView.arrangedSubviews.contains(autocompleteManager.tableView) { + topStackView.removeArrangedSubview(autocompleteManager.tableView) + topStackView.layoutIfNeeded() + } + messageInputBar.invalidateIntrinsicContentSize() + } +} + +// MARK: MessagesDisplayDelegate + +extension AutocompleteExampleViewController: MessagesDisplayDelegate { + // MARK: - Text Messages + + func textColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor { + isFromCurrentSender(message: message) ? .white : .darkText + } + + func detectorAttributes( + for detector: DetectorType, + and message: MessageType, + at _: IndexPath) -> [NSAttributedString.Key: Any] + { + switch detector { + case .hashtag, .mention: + if isFromCurrentSender(message: message) { + return [.foregroundColor: UIColor.white] + } else { + return [.foregroundColor: UIColor.primaryColor] + } + default: return MessageLabel.defaultAttributes + } + } + + func enabledDetectors(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> [DetectorType] { + [.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag] + } + + // MARK: - All Messages + + func backgroundColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor { + isFromCurrentSender(message: message) ? .primaryColor : UIColor(red: 230 / 255, green: 230 / 255, blue: 230 / 255, alpha: 1) + } + + func messageStyle(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> MessageStyle { + .bubble + } + + func configureAvatarView( + _ avatarView: AvatarView, + for message: MessageType, + at indexPath: IndexPath, + in _: MessagesCollectionView) + { + let avatar = SampleData.shared.getAvatarFor(sender: message.sender) + avatarView.set(avatar: avatar) + avatarView.isHidden = isNextMessageSameSender(at: indexPath) + avatarView.layer.borderWidth = 2 + avatarView.layer.borderColor = UIColor.primaryColor.cgColor + } + + func configureAccessoryView(_ accessoryView: UIView, for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) { + // Cells are reused, so only add a button here once. For real use you would need to + // ensure any subviews are removed if not needed + accessoryView.subviews.forEach { $0.removeFromSuperview() } + + let button = UIButton(type: .infoLight) + button.tintColor = .primaryColor + accessoryView.addSubview(button) + button.frame = accessoryView.bounds + button.isUserInteractionEnabled = false // respond to accessoryView tap through `MessageCellDelegate` + accessoryView.layer.cornerRadius = accessoryView.frame.height / 2 + accessoryView.backgroundColor = UIColor.primaryColor.withAlphaComponent(0.3) + } + + func configureMediaMessageImageView( + _ imageView: UIImageView, + for message: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) + { + if case MessageKind.photo(let media) = message.kind, let imageURL = media.url { + imageView.kf.setImage(with: imageURL) + } else { + imageView.kf.cancelDownloadTask() + } + } +} + +// MARK: MessagesLayoutDelegate + +extension AutocompleteExampleViewController: MessagesLayoutDelegate { + func cellTopLabelHeight(for _: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat { + if isTimeLabelVisible(at: indexPath) { + return 18 + } + return 0 + } + + func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat { + if isFromCurrentSender(message: message) { + return !isPreviousMessageSameSender(at: indexPath) ? 20 : 0 + } else { + return !isPreviousMessageSameSender(at: indexPath) ? 20 : 0 + } + } + + func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat { + (!isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message)) ? 16 : 0 + } +} diff --git a/Example/Sources/View Controllers/BasicExampleViewController.swift b/Example/Sources/View Controllers/BasicExampleViewController.swift index 0d7fc717b..9f1994faa 100644 --- a/Example/Sources/View Controllers/BasicExampleViewController.swift +++ b/Example/Sources/View Controllers/BasicExampleViewController.swift @@ -1,118 +1,169 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. -import UIKit +import Kingfisher import MapKit import MessageKit -import MessageInputBar - -final class BasicExampleViewController: ChatViewController { - - override func configureMessageCollectionView() { - super.configureMessageCollectionView() - - messagesCollectionView.messagesLayoutDelegate = self - messagesCollectionView.messagesDisplayDelegate = self - } +import UIKit + +// MARK: - BasicExampleViewController +class BasicExampleViewController: ChatViewController { + override func configureMessageCollectionView() { + super.configureMessageCollectionView() + messagesCollectionView.messagesLayoutDelegate = self + messagesCollectionView.messagesDisplayDelegate = self + } + + func textCellSizeCalculator(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CellSizeCalculator? { + nil + } } -// MARK: - MessagesDisplayDelegate +// MARK: MessagesDisplayDelegate extension BasicExampleViewController: MessagesDisplayDelegate { - - // MARK: - Text Messages - - func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { - return isFromCurrentSender(message: message) ? .white : .darkText - } - - func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any] { - return MessageLabel.defaultAttributes - } - - func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] { - return [.url, .address, .phoneNumber, .date, .transitInformation] - } - - // MARK: - All Messages - - func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { - return isFromCurrentSender(message: message) ? .primaryColor : UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1) + // MARK: - Text Messages + + func textColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor { + isFromCurrentSender(message: message) ? .white : .darkText + } + + func detectorAttributes(for detector: DetectorType, and _: MessageType, at _: IndexPath) -> [NSAttributedString.Key: Any] { + switch detector { + case .hashtag, .mention: return [.foregroundColor: UIColor.blue] + default: return MessageLabel.defaultAttributes } - - func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle { - - let tail: MessageStyle.TailCorner = isFromCurrentSender(message: message) ? .bottomRight : .bottomLeft + } + + func enabledDetectors(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> [DetectorType] { + [.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag] + } + + // MARK: - All Messages + + func backgroundColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor { + isFromCurrentSender(message: message) ? .primaryColor : UIColor(red: 230 / 255, green: 230 / 255, blue: 230 / 255, alpha: 1) + } + + func messageStyle(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> MessageStyle { + let tail: MessageStyle.TailCorner = isFromCurrentSender(message: message) ? .bottomRight : .bottomLeft + if let image = UIImage(named: "bobbly") { + return .customImageTail(image, tail) + } else { return .bubbleTail(tail, .curved) } - - func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { - let avatar = SampleData.shared.getAvatarFor(sender: message.sender) - avatarView.set(avatar: avatar) - } - - // MARK: - Location Messages - - func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? { - let annotationView = MKAnnotationView(annotation: nil, reuseIdentifier: nil) - let pinImage = #imageLiteral(resourceName: "ic_map_marker") - annotationView.image = pinImage - annotationView.centerOffset = CGPoint(x: 0, y: -pinImage.size.height / 2) - return annotationView - } - - func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)? { - return { view in - view.layer.transform = CATransform3DMakeScale(2, 2, 2) - UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0, options: [], animations: { - view.layer.transform = CATransform3DIdentity - }, completion: nil) - } + } + + func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) { + let avatar = SampleData.shared.getAvatarFor(sender: message.sender) + avatarView.set(avatar: avatar) + } + + func configureMediaMessageImageView( + _ imageView: UIImageView, + for message: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) + { + if case MessageKind.photo(let media) = message.kind, let imageURL = media.url { + imageView.kf.setImage(with: imageURL) + } else { + imageView.kf.cancelDownloadTask() } - - func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions { - - return LocationMessageSnapshotOptions(showsBuildings: true, showsPointsOfInterest: true, span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)) + } + + // MARK: - Location Messages + + func annotationViewForLocation(message _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> MKAnnotationView? { + let annotationView = MKAnnotationView(annotation: nil, reuseIdentifier: nil) + let pinImage = #imageLiteral(resourceName: "ic_map_marker") + annotationView.image = pinImage + annotationView.centerOffset = CGPoint(x: 0, y: -pinImage.size.height / 2) + return annotationView + } + + func animationBlockForLocation( + message _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) -> ((UIImageView) -> Void)? + { + { view in + view.layer.transform = CATransform3DMakeScale(2, 2, 2) + UIView.animate( + withDuration: 0.6, + delay: 0, + usingSpringWithDamping: 0.9, + initialSpringVelocity: 0, + options: [], + animations: { + view.layer.transform = CATransform3DIdentity + }, + completion: nil) } - + } + + func snapshotOptionsForLocation( + message _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) + -> LocationMessageSnapshotOptions + { + LocationMessageSnapshotOptions( + showsBuildings: true, + showsPointsOfInterest: true, + span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)) + } + + // MARK: - Audio Messages + + func audioTintColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor { + isFromCurrentSender(message: message) ? .white : UIColor(red: 15 / 255, green: 135 / 255, blue: 255 / 255, alpha: 1.0) + } + + func configureAudioCell(_ cell: AudioMessageCell, message: MessageType) { + audioController + .configureAudioCell( + cell, + message: message) // this is needed especially when the cell is reconfigure while is playing sound + } } -// MARK: - MessagesLayoutDelegate +// MARK: MessagesLayoutDelegate extension BasicExampleViewController: MessagesLayoutDelegate { - - func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return 18 - } - - func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return 20 - } - - func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return 16 - } - + func cellTopLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat { + 18 + } + + func cellBottomLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat { + 17 + } + + func messageTopLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat { + 20 + } + + func messageBottomLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat { + 16 + } } diff --git a/Example/Sources/View Controllers/ChatViewController.swift b/Example/Sources/View Controllers/ChatViewController.swift index a8de91665..aeb2171ae 100644 --- a/Example/Sources/View Controllers/ChatViewController.swift +++ b/Example/Sources/View Controllers/ChatViewController.swift @@ -1,250 +1,372 @@ -/* -MIT License - -Copyright (c) 2017-2018 MessageKit - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ +// +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. -import UIKit +import InputBarAccessoryView import MessageKit -import MessageInputBar +import UIKit + +// MARK: - ChatViewController /// A base class for the example controllers class ChatViewController: MessagesViewController, MessagesDataSource { - - override var preferredStatusBarStyle: UIStatusBarStyle { - return .lightContent - } - - var messageList: [MockMessage] = [] - - let refreshControl = UIRefreshControl() - - let formatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateStyle = .medium - return formatter - }() - - override func viewDidLoad() { - super.viewDidLoad() - - configureMessageCollectionView() - configureMessageInputBar() - loadFirstMessages() - title = "MessageKit" - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - MockSocket.shared.connect(with: [SampleData.shared.steven, SampleData.shared.wu]) - .onNewMessage { [weak self] message in - self?.insertMessage(message) - } - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - MockSocket.shared.disconnect() - } - - func loadFirstMessages() { - DispatchQueue.global(qos: .userInitiated).async { - let count = UserDefaults.standard.mockMessagesCount() - SampleData.shared.getMessages(count: count) { messages in - DispatchQueue.main.async { - self.messageList = messages - self.messagesCollectionView.reloadData() - self.messagesCollectionView.scrollToBottom() - } - } - } - } - - @objc - func loadMoreMessages() { - DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1) { - SampleData.shared.getMessages(count: 20) { messages in - DispatchQueue.main.async { - self.messageList.insert(contentsOf: messages, at: 0) - self.messagesCollectionView.reloadDataAndKeepOffset() - self.refreshControl.endRefreshing() - } - } + // MARK: Internal + + // MARK: - Public properties + + /// The `BasicAudioController` control the AVAudioPlayer state (play, pause, stop) and update audio cell UI accordingly. + lazy var audioController = BasicAudioController(messageCollectionView: messagesCollectionView) + + lazy var messageList: [MockMessage] = [] + + private(set) lazy var refreshControl: UIRefreshControl = { + let control = UIRefreshControl() + control.addTarget(self, action: #selector(loadMoreMessages), for: .valueChanged) + return control + }() + + // MARK: - MessagesDataSource + + var currentSender: SenderType { + SampleData.shared.currentSender + } + + override func viewDidLoad() { + super.viewDidLoad() + navigationItem.title = "MessageKit" + + configureMessageCollectionView() + configureMessageInputBar() + loadFirstMessages() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + MockSocket.shared.connect(with: [SampleData.shared.nathan, SampleData.shared.wu]) + .onNewMessage { [weak self] message in + self?.insertMessage(message) + } + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + MockSocket.shared.disconnect() + audioController.stopAnyOngoingPlaying() + } + + func loadFirstMessages() { + DispatchQueue.global(qos: .userInitiated).async { + let count = UserDefaults.standard.mockMessagesCount() + SampleData.shared.getMessages(count: count) { messages in + DispatchQueue.main.async { + self.messageList = messages + self.messagesCollectionView.reloadData() + self.messagesCollectionView.scrollToLastItem(animated: false) } + } } - - func configureMessageCollectionView() { - - messagesCollectionView.messagesDataSource = self - messagesCollectionView.messageCellDelegate = self - - scrollsToBottomOnKeyboardBeginsEditing = true // default false - maintainPositionOnKeyboardFrameChanged = true // default false - - messagesCollectionView.addSubview(refreshControl) - refreshControl.addTarget(self, action: #selector(loadMoreMessages), for: .valueChanged) - } - - func configureMessageInputBar() { - messageInputBar.delegate = self - messageInputBar.inputTextView.tintColor = .primaryColor - messageInputBar.sendButton.tintColor = .primaryColor - } - - // MARK: - Helpers - - func insertMessage(_ message: MockMessage) { - messageList.append(message) - // Reload last section to update header/footer labels and insert a new one - messagesCollectionView.performBatchUpdates({ - messagesCollectionView.insertSections([messageList.count - 1]) - if messageList.count >= 2 { - messagesCollectionView.reloadSections([messageList.count - 2]) - } - }, completion: { [weak self] _ in - if self?.isLastSectionVisible() == true { - self?.messagesCollectionView.scrollToBottom(animated: true) - } - }) - } - - func isLastSectionVisible() -> Bool { - - guard !messageList.isEmpty else { return false } - - let lastIndexPath = IndexPath(item: 0, section: messageList.count - 1) - - return messagesCollectionView.indexPathsForVisibleItems.contains(lastIndexPath) - } - - // MARK: - MessagesDataSource - - func currentSender() -> Sender { - return SampleData.shared.currentSender - } - - func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int { - return messageList.count - } - - func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType { - return messageList[indexPath.section] - } - - func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { - if indexPath.section % 3 == 0 { - return NSAttributedString(string: MessageKitDateFormatter.shared.string(from: message.sentDate), attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10), NSAttributedString.Key.foregroundColor: UIColor.darkGray]) + } + + @objc + func loadMoreMessages() { + DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1) { + SampleData.shared.getMessages(count: 20) { messages in + DispatchQueue.main.async { + self.messageList.insert(contentsOf: messages, at: 0) + self.messagesCollectionView.reloadDataAndKeepOffset() + self.refreshControl.endRefreshing() } - return nil + } } - - func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { - let name = message.sender.displayName - return NSAttributedString(string: name, attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)]) - } - - func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { - - let dateString = formatter.string(from: message.sentDate) - return NSAttributedString(string: dateString, attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption2)]) + } + + func configureMessageCollectionView() { + messagesCollectionView.messagesDataSource = self + messagesCollectionView.messageCellDelegate = self + + scrollsToLastItemOnKeyboardBeginsEditing = true // default false + maintainPositionOnInputBarHeightChanged = true // default false + showMessageTimestampOnSwipeLeft = true // default false + + messagesCollectionView.refreshControl = refreshControl + } + + func configureMessageInputBar() { + messageInputBar.delegate = self + messageInputBar.inputTextView.tintColor = .primaryColor + messageInputBar.sendButton.setTitleColor(.primaryColor, for: .normal) + messageInputBar.sendButton.setTitleColor( + UIColor.primaryColor.withAlphaComponent(0.3), + for: .highlighted) + } + + // MARK: - Helpers + + func insertMessage(_ message: MockMessage) { + messageList.append(message) + // Reload last section to update header/footer labels and insert a new one + messagesCollectionView.performBatchUpdates({ + messagesCollectionView.insertSections([messageList.count - 1]) + if messageList.count >= 2 { + messagesCollectionView.reloadSections([messageList.count - 2]) + } + }, completion: { [weak self] _ in + if self?.isLastSectionVisible() == true { + self?.messagesCollectionView.scrollToLastItem(animated: true) + } + }) + } + + func isLastSectionVisible() -> Bool { + guard !messageList.isEmpty else { return false } + + let lastIndexPath = IndexPath(item: 0, section: messageList.count - 1) + + return messagesCollectionView.indexPathsForVisibleItems.contains(lastIndexPath) + } + + func numberOfSections(in _: MessagesCollectionView) -> Int { + messageList.count + } + + func messageForItem(at indexPath: IndexPath, in _: MessagesCollectionView) -> MessageType { + messageList[indexPath.section] + } + + func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + if indexPath.section % 3 == 0 { + return NSAttributedString( + string: MessageKitDateFormatter.shared.string(from: message.sentDate), + attributes: [ + NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10), + NSAttributedString.Key.foregroundColor: UIColor.darkGray, + ]) } - + return nil + } + + func cellBottomLabelAttributedText(for _: MessageType, at _: IndexPath) -> NSAttributedString? { + NSAttributedString( + string: "Read", + attributes: [ + NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10), + NSAttributedString.Key.foregroundColor: UIColor.darkGray, + ]) + } + + func messageTopLabelAttributedText(for message: MessageType, at _: IndexPath) -> NSAttributedString? { + let name = message.sender.displayName + return NSAttributedString( + string: name, + attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)]) + } + + func messageBottomLabelAttributedText(for message: MessageType, at _: IndexPath) -> NSAttributedString? { + let dateString = formatter.string(from: message.sentDate) + return NSAttributedString( + string: dateString, + attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption2)]) + } + + func textCell(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UICollectionViewCell? { + nil + } + + // MARK: Private + + // MARK: - Private properties + + private let formatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + return formatter + }() } -// MARK: - MessageCellDelegate +// MARK: MessageCellDelegate extension ChatViewController: MessageCellDelegate { - - func didTapAvatar(in cell: MessageCollectionViewCell) { - print("Avatar tapped") - } - - func didTapMessage(in cell: MessageCollectionViewCell) { - print("Message tapped") - } - - func didTapCellTopLabel(in cell: MessageCollectionViewCell) { - print("Top cell label tapped") - } - - func didTapMessageTopLabel(in cell: MessageCollectionViewCell) { - print("Top message label tapped") + func didTapAvatar(in _: MessageCollectionViewCell) { + print("Avatar tapped") + } + + func didTapMessage(in _: MessageCollectionViewCell) { + print("Message tapped") + } + + func didTapImage(in _: MessageCollectionViewCell) { + print("Image tapped") + } + + func didTapCellTopLabel(in _: MessageCollectionViewCell) { + print("Top cell label tapped") + } + + func didTapCellBottomLabel(in _: MessageCollectionViewCell) { + print("Bottom cell label tapped") + } + + func didTapMessageTopLabel(in _: MessageCollectionViewCell) { + print("Top message label tapped") + } + + func didTapMessageBottomLabel(in _: MessageCollectionViewCell) { + print("Bottom label tapped") + } + + func didTapPlayButton(in cell: AudioMessageCell) { + guard + let indexPath = messagesCollectionView.indexPath(for: cell), + let message = messagesCollectionView.messagesDataSource?.messageForItem(at: indexPath, in: messagesCollectionView) + else { + print("Failed to identify message when audio cell receive tap gesture") + return } - - func didTapMessageBottomLabel(in cell: MessageCollectionViewCell) { - print("Bottom label tapped") + guard audioController.state != .stopped else { + // There is no audio sound playing - prepare to start playing for given audio message + audioController.playSound(for: message, in: cell) + return } - - func didTapAccessoryView(in cell: MessageCollectionViewCell) { - print("Accessory view tapped") + if audioController.playingMessage?.messageId == message.messageId { + // tap occur in the current cell that is playing audio sound + if audioController.state == .playing { + audioController.pauseSound(for: message, in: cell) + } else { + audioController.resumeSound() + } + } else { + // tap occur in a difference cell that the one is currently playing sound. First stop currently playing and start the sound for given message + audioController.stopAnyOngoingPlaying() + audioController.playSound(for: message, in: cell) } - + } + + func didStartAudio(in _: AudioMessageCell) { + print("Did start playing audio sound") + } + + func didPauseAudio(in _: AudioMessageCell) { + print("Did pause audio sound") + } + + func didStopAudio(in _: AudioMessageCell) { + print("Did stop audio sound") + } + + func didTapAccessoryView(in _: MessageCollectionViewCell) { + print("Accessory view tapped") + } } -// MARK: - MessageLabelDelegate +// MARK: MessageLabelDelegate extension ChatViewController: MessageLabelDelegate { - - func didSelectAddress(_ addressComponents: [String: String]) { - print("Address Selected: \(addressComponents)") - } - - func didSelectDate(_ date: Date) { - print("Date Selected: \(date)") - } - - func didSelectPhoneNumber(_ phoneNumber: String) { - print("Phone Number Selected: \(phoneNumber)") - } - - func didSelectURL(_ url: URL) { - print("URL Selected: \(url)") + func didSelectAddress(_ addressComponents: [String: String]) { + print("Address Selected: \(addressComponents)") + } + + func didSelectDate(_ date: Date) { + print("Date Selected: \(date)") + } + + func didSelectPhoneNumber(_ phoneNumber: String) { + print("Phone Number Selected: \(phoneNumber)") + } + + func didSelectURL(_ url: URL) { + print("URL Selected: \(url)") + } + + func didSelectTransitInformation(_ transitInformation: [String: String]) { + print("TransitInformation Selected: \(transitInformation)") + } + + func didSelectHashtag(_ hashtag: String) { + print("Hashtag selected: \(hashtag)") + } + + func didSelectMention(_ mention: String) { + print("Mention selected: \(mention)") + } + + func didSelectCustom(_ pattern: String, match _: String?) { + print("Custom data detector patter selected: \(pattern)") + } +} + +// MARK: InputBarAccessoryViewDelegate + +extension ChatViewController: InputBarAccessoryViewDelegate { + // MARK: Internal + + @objc + func inputBar(_: InputBarAccessoryView, didPressSendButtonWith _: String) { + processInputBar(messageInputBar) + } + + func processInputBar(_ inputBar: InputBarAccessoryView) { + // Here we can parse for which substrings were autocompleted + let attributedText = inputBar.inputTextView.attributedText! + let range = NSRange(location: 0, length: attributedText.length) + attributedText.enumerateAttribute(.autocompleted, in: range, options: []) { _, range, _ in + + let substring = attributedText.attributedSubstring(from: range) + let context = substring.attribute(.autocompletedContext, at: 0, effectiveRange: nil) + print("Autocompleted: `", substring, "` with context: ", context ?? "-") } - - func didSelectTransitInformation(_ transitInformation: [String: String]) { - print("TransitInformation Selected: \(transitInformation)") + + let components = inputBar.inputTextView.components + inputBar.inputTextView.text = String() + inputBar.invalidatePlugins() + // Send button activity animation + inputBar.sendButton.startAnimating() + inputBar.inputTextView.placeholder = "Sending..." + // Resign first responder for iPad split view + inputBar.inputTextView.resignFirstResponder() + DispatchQueue.global(qos: .default).async { + // fake send request task + sleep(1) + DispatchQueue.main.async { [weak self] in + inputBar.sendButton.stopAnimating() + inputBar.inputTextView.placeholder = "Aa" + self?.insertMessages(components) + self?.messagesCollectionView.scrollToLastItem(animated: true) + } } - -} + } -// MARK: - MessageInputBarDelegate - -extension ChatViewController: MessageInputBarDelegate { - - func messageInputBar(_ inputBar: MessageInputBar, didPressSendButtonWith text: String) { - - for component in inputBar.inputTextView.components { - - if let str = component as? String { - let message = MockMessage(text: str, sender: currentSender(), messageId: UUID().uuidString, date: Date()) - insertMessage(message) - } else if let img = component as? UIImage { - let message = MockMessage(image: img, sender: currentSender(), messageId: UUID().uuidString, date: Date()) - insertMessage(message) - } - - } - inputBar.inputTextView.text = String() - messagesCollectionView.scrollToBottom(animated: true) + // MARK: Private + + private func insertMessages(_ data: [Any]) { + for component in data { + let user = SampleData.shared.currentSender + if let str = component as? String { + let message = MockMessage(text: str, user: user, messageId: UUID().uuidString, date: Date()) + insertMessage(message) + } else if let img = component as? UIImage { + let message = MockMessage(image: img, user: user, messageId: UUID().uuidString, date: Date()) + insertMessage(message) + } } - + } } diff --git a/Example/Sources/View Controllers/CustomInputBarExampleViewController.swift b/Example/Sources/View Controllers/CustomInputBarExampleViewController.swift new file mode 100644 index 000000000..a3c148269 --- /dev/null +++ b/Example/Sources/View Controllers/CustomInputBarExampleViewController.swift @@ -0,0 +1,49 @@ +// MIT License +// +// Copyright (c) 2017-2022 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import UIKit + +final class CustomInputBarExampleViewController: BasicExampleViewController { + override func viewDidLoad() { + super.viewDidLoad() + + let customInputView = UIView() + customInputView.backgroundColor = .secondarySystemBackground + let customLabel = UILabel() + customLabel.translatesAutoresizingMaskIntoConstraints = false + customLabel.font = .preferredFont(forTextStyle: .headline) + customLabel.textAlignment = .center + customLabel.text = "This chat is read only." + customLabel.textColor = .primaryColor + customInputView.addSubview(customLabel) + + NSLayoutConstraint.activate([ + customLabel.topAnchor.constraint(equalTo: customInputView.topAnchor, constant: 16), + customLabel.bottomAnchor.constraint(equalTo: customInputView.safeAreaLayoutGuide.bottomAnchor, constant: -16), + customLabel.leadingAnchor.constraint(equalTo: customInputView.leadingAnchor), + customLabel.trailingAnchor.constraint(equalTo: customInputView.trailingAnchor), + ]) + + inputBarType = .custom(customInputView) + } +} diff --git a/Example/Sources/View Controllers/CustomLayoutExampleViewController.swift b/Example/Sources/View Controllers/CustomLayoutExampleViewController.swift new file mode 100644 index 000000000..bd5281109 --- /dev/null +++ b/Example/Sources/View Controllers/CustomLayoutExampleViewController.swift @@ -0,0 +1,62 @@ +// +// CustomLayoutExampleViewController.swift +// ChatExample +// +// Created by Vignesh J on 30/04/21. +// Copyright Β© 2021 MessageKit. All rights reserved. +// + +import Kingfisher +import MapKit +import MessageKit +import UIKit + +class CustomLayoutExampleViewController: BasicExampleViewController { + // MARK: Internal + + override func configureMessageCollectionView() { + super.configureMessageCollectionView() + messagesCollectionView.register(CustomTextMessageContentCell.self) + messagesCollectionView.messagesDataSource = self + messagesCollectionView.messagesLayoutDelegate = self + messagesCollectionView.messagesDisplayDelegate = self + } + + // MARK: - MessagesLayoutDelegate + + override func textCellSizeCalculator( + for _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) + -> CellSizeCalculator? + { + textMessageSizeCalculator + } + + // MARK: - MessagesDataSource + + override func textCell( + for message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) + -> UICollectionViewCell? + { + let cell = messagesCollectionView.dequeueReusableCell( + CustomTextMessageContentCell.self, + for: indexPath) + cell.configure( + with: message, + at: indexPath, + in: messagesCollectionView, + dataSource: self, + and: textMessageSizeCalculator) + + return cell + } + + // MARK: Private + + private lazy var textMessageSizeCalculator = CustomTextLayoutSizeCalculator( + layout: self.messagesCollectionView + .messagesCollectionViewFlowLayout) +} diff --git a/Example/Sources/View Controllers/LaunchViewController.swift b/Example/Sources/View Controllers/LaunchViewController.swift index 8340af1a8..90c42b894 100644 --- a/Example/Sources/View Controllers/LaunchViewController.swift +++ b/Example/Sources/View Controllers/LaunchViewController.swift @@ -1,107 +1,162 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2022 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. -import UIKit import MessageKit -import MessageInputBar import SafariServices +import SwiftUI +import UIKit final internal class LaunchViewController: UITableViewController { - - override var preferredStatusBarStyle: UIStatusBarStyle { - return .lightContent - } + // MARK: Lifecycle - let cells = ["Basic Example", "Advanced Example", "Embedded Example", "Settings", "Source Code", "Contributors"] - - // MARK: - View Life Cycle - - override func viewDidLoad() { - super.viewDidLoad() - title = "MessageKit" - navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) - tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") - tableView.tableFooterView = UIView() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - if #available(iOS 11.0, *) { - navigationController?.navigationBar.prefersLargeTitles = true - } - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - if #available(iOS 11.0, *) { - navigationController?.navigationBar.prefersLargeTitles = false - } - } - - // MARK: - UITableViewDataSource + // MARK: - View Life Cycle - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return cells.count - } + init() { + super.init(style: .insetGrouped) + } - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell() - cell.textLabel?.text = cells[indexPath.row] - cell.accessoryType = .disclosureIndicator - return cell - } - - // MARK: - UITableViewDelegate - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let cell = cells[indexPath.row] - switch cell { - case "Basic Example": - navigationController?.pushViewController(BasicExampleViewController(), animated: true) - case "Advanced Example": - navigationController?.pushViewController(AdvancedExampleViewController(), animated: true) - case "Embedded Example": - navigationController?.pushViewController(MessageContainerController(), animated: true) - case "Settings": - navigationController?.pushViewController(SettingsViewController(), animated: true) - case "Source Code": - guard let url = URL(string: "https://github.com/MessageKit/MessageKit") else { return } - openURL(url) - case "Contributors": - guard let url = URL(string: "https://github.com/orgs/MessageKit/teams/contributors/members") else { return } - openURL(url) - default: - assertionFailure("You need to impliment the action for this cell: \(cell)") - return - } + required init?(coder _: NSCoder) { nil } + + // MARK: Internal + + override func viewDidLoad() { + super.viewDidLoad() + title = "MessageKit" + navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) + navigationController?.navigationBar.tintColor = .primaryColor + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") + tableView.tableFooterView = UIView() + } + + // MARK: - UITableViewDataSource + + override func numberOfSections(in _: UITableView) -> Int { + sections.count + } + + override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { + sections[section].rows.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell() + cell.textLabel?.text = sections[indexPath.section].rows[indexPath.row].title + cell.accessoryType = .disclosureIndicator + return cell + } + + // MARK: - UITableViewDelegate + + // swiftlint:disable cyclomatic_complexity + override func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { + let cell = sections[indexPath.section].rows[indexPath.row] + switch cell { + case .basic: + let viewController = BasicExampleViewController() + let detailViewController = UINavigationController(rootViewController: viewController) + splitViewController?.showDetailViewController(detailViewController, sender: self) + case .advanced: + let viewController = AdvancedExampleViewController() + let detailViewController = UINavigationController(rootViewController: viewController) + splitViewController?.showDetailViewController(detailViewController, sender: self) + case .autocomplete: + let viewController = AutocompleteExampleViewController() + let detailViewController = UINavigationController(rootViewController: viewController) + splitViewController?.showDetailViewController(detailViewController, sender: self) + case .embedded: + splitViewController?.showDetailViewController(MessageContainerController(), sender: self) + case .customLayout: + splitViewController?.showDetailViewController(CustomLayoutExampleViewController(), sender: self) + case .customInputBar: + let detailViewController = UINavigationController(rootViewController: CustomInputBarExampleViewController()) + splitViewController?.showDetailViewController(detailViewController, sender: self) + case .swiftUI: + splitViewController?.showDetailViewController(UIHostingController(rootView: SwiftUIExampleView()), sender: self) + case .subview: + let viewController = MessageSubviewContainerViewController() + let detailViewController = UINavigationController(rootViewController: viewController) + splitViewController?.showDetailViewController(detailViewController, sender: self) + case .settings: + let viewController = SettingsViewController() + let detailViewController = UINavigationController(rootViewController: viewController) + splitViewController?.showDetailViewController(detailViewController, sender: self) + case .sourceCode: + openURL(URL(string: "https://github.com/MessageKit/MessageKit")!) + case .contributors: + openURL(URL(string: "https://github.com/MessageKit/MessageKit/graphs/contributors")!) } - - func openURL(_ url: URL) { - let webViewController = SFSafariViewController(url: url) - if #available(iOS 10.0, *) { - webViewController.preferredControlTintColor = .primaryColor - } - present(webViewController, animated: true, completion: nil) + } + + func openURL(_ url: URL) { + let webViewController = SFSafariViewController(url: url) + webViewController.preferredControlTintColor = .primaryColor + present(webViewController, animated: true) + } + + // MARK: Private + + private enum Row { + case basic, advanced, autocomplete, embedded, customLayout, subview, customInputBar, swiftUI + case settings, sourceCode, contributors + + // MARK: Internal + + var title: String { + switch self { + case .basic: + return "Basic Example" + case .advanced: + return "Advanced Example" + case .autocomplete: + return "Autocomplete Example" + case .embedded: + return "Embedded Example" + case .customLayout: + return "Custom Layout Example" + case .subview: + return "Subview Example" + case .customInputBar: + return "Custom InputBar Example" + case .swiftUI: + return "SwiftUI Example" + case .settings: + return "Settings" + case .sourceCode: + return "Source Code" + case .contributors: + return "Contributors" + } } + } + + private struct Section { + let title: String + let rows: [Row] + } + + private let sections: [Section] = [ + .init( + title: "Examples", + rows: [.basic, .advanced, .autocomplete, .embedded, .customLayout, .subview, .customInputBar, .swiftUI]), + .init(title: "Support", rows: [.settings, .sourceCode, .contributors]), + ] } diff --git a/Example/Sources/View Controllers/MessageContainerController.swift b/Example/Sources/View Controllers/MessageContainerController.swift index ec1ea24b2..3fd11c7e0 100644 --- a/Example/Sources/View Controllers/MessageContainerController.swift +++ b/Example/Sources/View Controllers/MessageContainerController.swift @@ -1,88 +1,71 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. -import UIKit import MapKit +import UIKit final class MessageContainerController: UIViewController { - - override var preferredStatusBarStyle: UIStatusBarStyle { - return .lightContent - } - - let mapView = MKMapView() - - let bannerView: UIView = { - let view = UIView() - view.backgroundColor = .primaryColor - view.alpha = 0.7 - return view - }() - - let conversationViewController = BasicExampleViewController() - - /// Required for the `MessageInputBar` to be visible - override var canBecomeFirstResponder: Bool { - return conversationViewController.canBecomeFirstResponder - } - - /// Required for the `MessageInputBar` to be visible - override var inputAccessoryView: UIView? { - return conversationViewController.inputAccessoryView - } - - override func viewDidLoad() { - super.viewDidLoad() - - /// Add the `ConversationViewController` as a child view controller - conversationViewController.willMove(toParent: self) - self.addChild(conversationViewController) - view.addSubview(conversationViewController.view) - conversationViewController.didMove(toParent: self) - - view.addSubview(mapView) - view.addSubview(bannerView) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationController?.navigationBar.isTranslucent = true - navigationController?.navigationBar.barTintColor = .clear - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - navigationController?.navigationBar.isTranslucent = false - navigationController?.navigationBar.barTintColor = .primaryColor - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - let headerHeight: CGFloat = 200 - mapView.frame = CGRect(origin: .zero, size: CGSize(width: view.bounds.width, height: headerHeight)) - bannerView.frame = CGRect(origin: .zero, size: CGSize(width: view.bounds.width, height: headerHeight)) - conversationViewController.view.frame = CGRect(x: 0, y: headerHeight, width: view.bounds.width, height: view.bounds.height - headerHeight) - } - + let mapView = MKMapView() + + let bannerView: UIView = { + let view = UIView() + view.backgroundColor = .primaryColor + view.alpha = 0.7 + return view + }() + + let conversationViewController = BasicExampleViewController() + + override var preferredStatusBarStyle: UIStatusBarStyle { + .lightContent + } + + /// Required for the `MessageInputBar` to be visible + override var canBecomeFirstResponder: Bool { + conversationViewController.canBecomeFirstResponder + } + + override func viewDidLoad() { + super.viewDidLoad() + + /// Add the `ConversationViewController` as a child view controller + conversationViewController.willMove(toParent: self) + addChild(conversationViewController) + view.addSubview(conversationViewController.view) + conversationViewController.didMove(toParent: self) + + view.addSubview(mapView) + view.addSubview(bannerView) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + let headerHeight: CGFloat = 200 + mapView.frame = CGRect(origin: .zero, size: CGSize(width: view.bounds.width, height: headerHeight)) + bannerView.frame = CGRect(origin: .zero, size: CGSize(width: view.bounds.width, height: headerHeight)) + conversationViewController.view.frame = CGRect( + x: 0, + y: headerHeight, + width: view.bounds.width, + height: view.bounds.height - headerHeight) + } } diff --git a/Example/Sources/View Controllers/MessageSubviewContainerViewController.swift b/Example/Sources/View Controllers/MessageSubviewContainerViewController.swift new file mode 100644 index 000000000..ed6e116c0 --- /dev/null +++ b/Example/Sources/View Controllers/MessageSubviewContainerViewController.swift @@ -0,0 +1,42 @@ +// +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import UIKit + +final class MessageSubviewContainerViewController: UIViewController { + let messageSubviewViewController = MessageSubviewViewController() + + override var preferredStatusBarStyle: UIStatusBarStyle { + .lightContent + } + + override func viewDidLoad() { + super.viewDidLoad() + + messageSubviewViewController.willMove(toParent: self) + addChild(messageSubviewViewController) + view.addSubview(messageSubviewViewController.view) + messageSubviewViewController.didMove(toParent: self) + } +} diff --git a/Example/Sources/View Controllers/MessageSubviewViewController.swift b/Example/Sources/View Controllers/MessageSubviewViewController.swift new file mode 100644 index 000000000..04d128891 --- /dev/null +++ b/Example/Sources/View Controllers/MessageSubviewViewController.swift @@ -0,0 +1,60 @@ +// +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import InputBarAccessoryView +import UIKit + +final class MessageSubviewViewController: BasicExampleViewController { + // MARK: Internal + + // In order to reach the subviewInputBar + override var inputAccessoryView: UIView? { + self.subviewInputBar + } + + override func viewDidLoad() { + super.viewDidLoad() + + subviewInputBar.delegate = self + // Take into account the height of the bottom input bar + additionalBottomInset = 88 + // Binding to the messagesCollectionView will enable interactive dismissal + keyboardManager.bind(to: messagesCollectionView) + } + + override func didMove(toParent parent: UIViewController?) { + super.didMove(toParent: parent) + parent?.view.addSubview(subviewInputBar) + // Binding the inputBar will set the needed callback actions to position the inputBar on top of the keyboard + keyboardManager.bind(inputAccessoryView: subviewInputBar) + } + + override func inputBar(_: InputBarAccessoryView, didPressSendButtonWith _: String) { + processInputBar(subviewInputBar) + } + + // MARK: Private + + private let subviewInputBar = InputBarAccessoryView() +} diff --git a/Example/Sources/View Controllers/NavigationController.swift b/Example/Sources/View Controllers/NavigationController.swift deleted file mode 100644 index cb41b0cb5..000000000 --- a/Example/Sources/View Controllers/NavigationController.swift +++ /dev/null @@ -1,67 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import UIKit - -final class NavigationController: UINavigationController { - - override var preferredStatusBarStyle: UIStatusBarStyle { - return viewControllers.last?.preferredStatusBarStyle ?? .lightContent - } - - override func viewDidLoad() { - super.viewDidLoad() - navigationBar.isTranslucent = false - navigationBar.tintColor = .white - navigationBar.barTintColor = .primaryColor - navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white] - if #available(iOS 11.0, *) { - navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.white] - } - navigationBar.shadowImage = UIImage() - navigationBar.setBackgroundImage(UIImage(), for: .default) - view.backgroundColor = .primaryColor - } - - func setAppearanceStyle(to style: UIStatusBarStyle) { - if style == .default { - navigationBar.shadowImage = UIImage() - navigationBar.barTintColor = .primaryColor - navigationBar.tintColor = .white - navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white] - if #available(iOS 11.0, *) { - navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.white] - } - } else if style == .lightContent { - navigationBar.shadowImage = nil - navigationBar.barTintColor = .white - navigationBar.tintColor = UIColor(red: 0, green: 0.5, blue: 1, alpha: 1) - navigationBar.titleTextAttributes = [.foregroundColor: UIColor.black] - if #available(iOS 11.0, *) { - navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.black] - } - } - } - -} diff --git a/Example/Sources/View Controllers/SettingsViewController.swift b/Example/Sources/View Controllers/SettingsViewController.swift index 882e0f9e3..9c8c5409f 100644 --- a/Example/Sources/View Controllers/SettingsViewController.swift +++ b/Example/Sources/View Controllers/SettingsViewController.swift @@ -1,158 +1,185 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. -import UIKit import MessageKit -import MessageInputBar +import UIKit + +// MARK: - SettingsViewController final internal class SettingsViewController: UITableViewController { + // MARK: Lifecycle - // MARK: - Properties - - override var preferredStatusBarStyle: UIStatusBarStyle { - return .lightContent - } - - let cells = ["Mock messages count", "Text Messages", "AttributedText Messages", "Photo Messages", "Video Messages", "Emoji Messages", "Location Messages", "Url Messages", "Phone Messages"] - - // MARK: - Picker - - var messagesPicker = UIPickerView() - - @objc func onDoneWithPickerView() { - let selectedMessagesCount = messagesPicker.selectedRow(inComponent: 0) - UserDefaults.standard.setMockMessages(count: selectedMessagesCount) - view.endEditing(false) - tableView.reloadData() - } - - @objc func dismissPickerView() { - view.endEditing(false) - } - - private func configurePickerView() { - messagesPicker.dataSource = self - messagesPicker.delegate = self - messagesPicker.backgroundColor = .white - - messagesPicker.selectRow(UserDefaults.standard.mockMessagesCount(), inComponent: 0, animated: false) - } - - // MARK: - Toolbar - - var messagesToolbar = UIToolbar() - - private func configureToolbar() { - let doneButton = UIBarButtonItem(title: "Done", style: .plain, target: self, action: #selector(onDoneWithPickerView)) - let spaceButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) - let cancelButton = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(dismissPickerView)) - messagesToolbar.items = [cancelButton, spaceButton, doneButton] - messagesToolbar.sizeToFit() - } - - // MARK: - View lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - tableView.register(TextFieldTableViewCell.self, forCellReuseIdentifier: TextFieldTableViewCell.identifier) - tableView.tableFooterView = UIView() - configurePickerView() - configureToolbar() - } - - // MARK: - TableViewDelegate & TableViewDataSource - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return cells.count - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cellValue = cells[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell() - cell.textLabel?.text = cells[indexPath.row] - - switch cellValue { - case "Mock messages count": - return configureTextFieldTableViewCell(at: indexPath) - default: - let switchView = UISwitch(frame: .zero) - switchView.isOn = UserDefaults.standard.bool(forKey: cellValue) - switchView.tag = indexPath.row - switchView.addTarget(self, action: #selector(self.switchChanged(_:)), for: .valueChanged) - cell.accessoryView = switchView - } - return cell - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - - let cell = tableView.cellForRow(at: indexPath) - - cell?.contentView.subviews.forEach { - if $0 is UITextField { - $0.becomeFirstResponder() - } - } + // MARK: - View lifecycle + + init() { + super.init(style: .insetGrouped) + } + + required init?(coder _: NSCoder) { nil } + + // MARK: Internal + + // MARK: - Properties + + let cells = [ + "Mock messages count", + "Text Messages", + "AttributedText Messages", + "Photo Messages", + "Photo from URL Messages", + "Video Messages", + "Audio Messages", + "Emoji Messages", + "Location Messages", + "Url Messages", + "Phone Messages", + "ShareContact Messages", + ] + + var messagesPicker = UIPickerView() + + // MARK: - Toolbar + + var messagesToolbar = UIToolbar() + + @objc + func onDoneWithPickerView() { + let selectedMessagesCount = messagesPicker.selectedRow(inComponent: 0) + UserDefaults.standard.setMockMessages(count: selectedMessagesCount) + view.endEditing(false) + tableView.reloadData() + } + + @objc + func dismissPickerView() { + view.endEditing(false) + } + + override func viewDidLoad() { + super.viewDidLoad() + navigationItem.title = "Settings" + tableView.register(TextFieldTableViewCell.self, forCellReuseIdentifier: TextFieldTableViewCell.identifier) + tableView.tableFooterView = UIView() + configurePickerView() + configureToolbar() + } + + // MARK: - TableViewDelegate & TableViewDataSource + + override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { + cells.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cellValue = cells[indexPath.row] + let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell() + cell.textLabel?.text = cells[indexPath.row] + + switch cellValue { + case "Mock messages count": + return configureTextFieldTableViewCell(at: indexPath) + default: + let switchView = UISwitch(frame: .zero) + switchView.isOn = UserDefaults.standard.bool(forKey: cellValue) + switchView.tag = indexPath.row + switchView.onTintColor = .primaryColor + switchView.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged) + cell.accessoryView = switchView } - - // MARK: - Helper - - private func configureTextFieldTableViewCell(at indexPath: IndexPath) -> TextFieldTableViewCell { - if let cell = tableView.dequeueReusableCell(withIdentifier: TextFieldTableViewCell.identifier, for: indexPath) as? TextFieldTableViewCell { - cell.mainLabel.text = "Mock messages count:" - - let messagesCount = UserDefaults.standard.mockMessagesCount() - cell.textField.text = "\(messagesCount)" - - cell.textField.inputView = messagesPicker - cell.textField.inputAccessoryView = messagesToolbar - - return cell - } - return TextFieldTableViewCell() + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + let cell = tableView.cellForRow(at: indexPath) + + cell?.contentView.subviews.forEach { + if $0 is UITextField { + $0.becomeFirstResponder() + } } - - @objc func switchChanged(_ sender: UISwitch!) { - let cell = cells[sender.tag] - - UserDefaults.standard.set(sender.isOn, forKey: cell) + } + + @objc + func switchChanged(_ sender: UISwitch!) { + let cell = cells[sender.tag] + + UserDefaults.standard.set(sender.isOn, forKey: cell) + UserDefaults.standard.synchronize() + } + + // MARK: Private + + private func configurePickerView() { + messagesPicker.dataSource = self + messagesPicker.delegate = self + messagesPicker.backgroundColor = .white + + messagesPicker.selectRow(UserDefaults.standard.mockMessagesCount(), inComponent: 0, animated: false) + } + + private func configureToolbar() { + let doneButton = UIBarButtonItem(title: "Done", style: .plain, target: self, action: #selector(onDoneWithPickerView)) + let spaceButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + let cancelButton = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(dismissPickerView)) + messagesToolbar.items = [cancelButton, spaceButton, doneButton] + messagesToolbar.sizeToFit() + } + + // MARK: - Helper + + private func configureTextFieldTableViewCell(at indexPath: IndexPath) -> TextFieldTableViewCell { + if + let cell = tableView.dequeueReusableCell( + withIdentifier: TextFieldTableViewCell.identifier, + for: indexPath) as? TextFieldTableViewCell + { + cell.mainLabel.text = "Mock messages count:" + + let messagesCount = UserDefaults.standard.mockMessagesCount() + cell.textField.text = "\(messagesCount)" + + cell.textField.inputView = messagesPicker + cell.textField.inputAccessoryView = messagesToolbar + + return cell } + return TextFieldTableViewCell() + } } -// MARK: - UIPickerViewDelegate, UIPickerViewDataSource +// MARK: UIPickerViewDelegate, UIPickerViewDataSource + extension SettingsViewController: UIPickerViewDelegate, UIPickerViewDataSource { - - func numberOfComponents(in pickerView: UIPickerView) -> Int { - return 1 - } - - func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { - return 100 - } - - func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { - return "\(row)" - } + func numberOfComponents(in _: UIPickerView) -> Int { + 1 + } + + func pickerView(_: UIPickerView, numberOfRowsInComponent _: Int) -> Int { + 100 + } + + func pickerView(_: UIPickerView, titleForRow row: Int, forComponent _: Int) -> String? { + "\(row)" + } } diff --git a/Example/Sources/Views/CameraInputBarAccessoryView.swift b/Example/Sources/Views/CameraInputBarAccessoryView.swift new file mode 100644 index 000000000..c672d9d95 --- /dev/null +++ b/Example/Sources/Views/CameraInputBarAccessoryView.swift @@ -0,0 +1,188 @@ +// +// CameraInput.swift +// ChatExample +// +// Created by Mohannad on 12/25/20. +// Copyright Β© 2020 MessageKit. All rights reserved. +// + +import InputBarAccessoryView +import UIKit + +// MARK: - CameraInputBarAccessoryViewDelegate + +protocol CameraInputBarAccessoryViewDelegate: InputBarAccessoryViewDelegate { + func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith attachments: [AttachmentManager.Attachment]) +} + +extension CameraInputBarAccessoryViewDelegate { + func inputBar(_: InputBarAccessoryView, didPressSendButtonWith _: [AttachmentManager.Attachment]) { } +} + +// MARK: - CameraInputBarAccessoryView + +class CameraInputBarAccessoryView: InputBarAccessoryView { + // MARK: Lifecycle + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Internal + + lazy var attachmentManager: AttachmentManager = { [unowned self] in + let manager = AttachmentManager() + manager.delegate = self + return manager + }() + + func configure() { + let camera = makeButton(named: "ic_camera") + camera.tintColor = .darkGray + camera.onTouchUpInside { [weak self] _ in + self?.showImagePickerControllerActionSheet() + } + setLeftStackViewWidthConstant(to: 35, animated: true) + setStackViewItems([camera], forStack: .left, animated: false) + inputPlugins = [attachmentManager] + } + + override func didSelectSendButton() { + if attachmentManager.attachments.count > 0 { + (delegate as? CameraInputBarAccessoryViewDelegate)? + .inputBar(self, didPressSendButtonWith: attachmentManager.attachments) + } + else { + delegate?.inputBar(self, didPressSendButtonWith: inputTextView.text) + } + } + + // MARK: Private + + private func makeButton(named _: String) -> InputBarButtonItem { + InputBarButtonItem() + .configure { + $0.spacing = .fixed(10) + $0.image = UIImage(systemName: "camera.fill")?.withRenderingMode(.alwaysTemplate) + $0.setSize(CGSize(width: 30, height: 30), animated: false) + }.onSelected { + $0.tintColor = .systemBlue + }.onDeselected { + $0.tintColor = UIColor.lightGray + }.onTouchUpInside { _ in + print("Item Tapped") + } + } +} + +// MARK: UIImagePickerControllerDelegate, UINavigationControllerDelegate + +extension CameraInputBarAccessoryView: UIImagePickerControllerDelegate, UINavigationControllerDelegate { + @objc + func showImagePickerControllerActionSheet() { + let photoLibraryAction = UIAlertAction(title: "Choose From Library", style: .default) { [weak self] _ in + self?.showImagePickerController(sourceType: .photoLibrary) + } + + let cameraAction = UIAlertAction(title: "Take From Camera", style: .default) { [weak self] _ in + self?.showImagePickerController(sourceType: .camera) + } + + let cancelAction = UIAlertAction(title: "Cancel", style: .default, handler: nil) + + AlertService.showAlert( + style: .actionSheet, + title: "Choose Your Image", + message: nil, + actions: [photoLibraryAction, cameraAction, cancelAction], + completion: nil) + } + + func showImagePickerController(sourceType: UIImagePickerController.SourceType) { + let imgPicker = UIImagePickerController() + imgPicker.delegate = self + imgPicker.allowsEditing = true + imgPicker.sourceType = sourceType + imgPicker.presentationController?.delegate = self + inputAccessoryView?.isHidden = true + getRootViewController()?.present(imgPicker, animated: true, completion: nil) + } + + func imagePickerController( + _: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) + { + if let editedImage = info[UIImagePickerController.InfoKey.editedImage] as? UIImage { + // self.sendImageMessage(photo: editedImage) + inputPlugins.forEach { _ = $0.handleInput(of: editedImage) } + } + else if let originImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { + inputPlugins.forEach { _ = $0.handleInput(of: originImage) } + // self.sendImageMessage(photo: originImage) + } + getRootViewController()?.dismiss(animated: true, completion: nil) + inputAccessoryView?.isHidden = false + } + + func imagePickerControllerDidCancel(_: UIImagePickerController) { + getRootViewController()?.dismiss(animated: true, completion: nil) + inputAccessoryView?.isHidden = false + } + + func getRootViewController() -> UIViewController? { + (UIApplication.shared.delegate as? AppDelegate)?.window?.rootViewController + } +} + +// MARK: AttachmentManagerDelegate + +extension CameraInputBarAccessoryView: AttachmentManagerDelegate { + // MARK: - AttachmentManagerDelegate + + func attachmentManager(_: AttachmentManager, shouldBecomeVisible: Bool) { + setAttachmentManager(active: shouldBecomeVisible) + } + + func attachmentManager(_ manager: AttachmentManager, didReloadTo _: [AttachmentManager.Attachment]) { + sendButton.isEnabled = manager.attachments.count > 0 + } + + func attachmentManager(_ manager: AttachmentManager, didInsert _: AttachmentManager.Attachment, at _: Int) { + sendButton.isEnabled = manager.attachments.count > 0 + } + + func attachmentManager(_ manager: AttachmentManager, didRemove _: AttachmentManager.Attachment, at _: Int) { + sendButton.isEnabled = manager.attachments.count > 0 + } + + func attachmentManager(_: AttachmentManager, didSelectAddAttachmentAt _: Int) { + showImagePickerControllerActionSheet() + } + + // MARK: - AttachmentManagerDelegate Helper + + func setAttachmentManager(active: Bool) { + let topStackView = topStackView + if active, !topStackView.arrangedSubviews.contains(attachmentManager.attachmentView) { + topStackView.insertArrangedSubview(attachmentManager.attachmentView, at: topStackView.arrangedSubviews.count) + topStackView.layoutIfNeeded() + } else if !active, topStackView.arrangedSubviews.contains(attachmentManager.attachmentView) { + topStackView.removeArrangedSubview(attachmentManager.attachmentView) + topStackView.layoutIfNeeded() + } + } +} + +// MARK: UIAdaptivePresentationControllerDelegate + +extension CameraInputBarAccessoryView: UIAdaptivePresentationControllerDelegate { + // Swipe to dismiss image modal + public func presentationControllerWillDismiss(_: UIPresentationController) { + isHidden = false + } +} diff --git a/Example/Sources/Views/CustomCell.swift b/Example/Sources/Views/CustomCell.swift index c23b0205a..55330ddb4 100644 --- a/Example/Sources/Views/CustomCell.swift +++ b/Example/Sources/Views/CustomCell.swift @@ -1,64 +1,66 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. -import UIKit import MessageKit +import UIKit open class CustomCell: UICollectionViewCell { - - let label = UILabel() - - public override init(frame: CGRect) { - super.init(frame: frame) - setupSubviews() - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setupSubviews() - } - - open func setupSubviews() { - contentView.addSubview(label) - label.textAlignment = .center - label.font = UIFont.italicSystemFont(ofSize: 13) - } - - open override func layoutSubviews() { - super.layoutSubviews() - label.frame = contentView.bounds - } - - open func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) { - // Do stuff - switch message.kind { - case .custom(let data): - guard let systemMessage = data as? String else { return } - label.text = systemMessage - default: - break - } + // MARK: Lifecycle + + public override init(frame: CGRect) { + super.init(frame: frame) + setupSubviews() + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupSubviews() + } + + // MARK: Open + + open func setupSubviews() { + contentView.addSubview(label) + label.textAlignment = .center + label.font = UIFont.italicSystemFont(ofSize: 13) + } + + open override func layoutSubviews() { + super.layoutSubviews() + label.frame = contentView.bounds + } + + open func configure(with message: MessageType, at _: IndexPath, and _: MessagesCollectionView) { + // Do stuff + switch message.kind { + case .custom(let data): + guard let systemMessage = data as? String else { return } + label.text = systemMessage + default: + break } - + } + + // MARK: Internal + + let label = UILabel() } diff --git a/Example/Sources/Views/CustomMessageContentCell.swift b/Example/Sources/Views/CustomMessageContentCell.swift new file mode 100644 index 000000000..7d64e1468 --- /dev/null +++ b/Example/Sources/Views/CustomMessageContentCell.swift @@ -0,0 +1,132 @@ +// +// CustomMessageContentCell.swift +// ChatExample +// +// Created by Vignesh J on 01/05/21. +// Copyright Β© 2021 MessageKit. All rights reserved. +// + +import MessageKit +import UIKit + +class CustomMessageContentCell: MessageCollectionViewCell { + // MARK: Lifecycle + + override init(frame: CGRect) { + super.init(frame: frame) + contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + setupSubviews() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + setupSubviews() + } + + // MARK: Internal + + /// The `MessageCellDelegate` for the cell. + weak var delegate: MessageCellDelegate? + + /// The container used for styling and holding the message's content view. + var messageContainerView: UIView = { + let containerView = UIView() + containerView.clipsToBounds = true + containerView.layer.masksToBounds = true + return containerView + }() + + /// The top label of the cell. + var cellTopLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.textAlignment = .center + return label + }() + + var cellDateLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.textAlignment = .right + return label + }() + + override func prepareForReuse() { + super.prepareForReuse() + cellTopLabel.text = nil + cellTopLabel.attributedText = nil + cellDateLabel.text = nil + cellDateLabel.attributedText = nil + } + + /// Handle tap gesture on contentView and its subviews. + override func handleTapGesture(_ gesture: UIGestureRecognizer) { + let touchLocation = gesture.location(in: self) + + switch true { + case messageContainerView.frame + .contains(touchLocation) && !cellContentView(canHandle: convert(touchLocation, to: messageContainerView)): + delegate?.didTapMessage(in: self) + case cellTopLabel.frame.contains(touchLocation): + delegate?.didTapCellTopLabel(in: self) + case cellDateLabel.frame.contains(touchLocation): + delegate?.didTapMessageBottomLabel(in: self) + default: + delegate?.didTapBackground(in: self) + } + } + + /// Handle long press gesture, return true when gestureRecognizer's touch point in `messageContainerView`'s frame + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + let touchPoint = gestureRecognizer.location(in: self) + guard gestureRecognizer.isKind(of: UILongPressGestureRecognizer.self) else { return false } + return messageContainerView.frame.contains(touchPoint) + } + + func setupSubviews() { + messageContainerView.layer.cornerRadius = 5 + + contentView.addSubview(cellTopLabel) + contentView.addSubview(messageContainerView) + messageContainerView.addSubview(cellDateLabel) + } + + func configure( + with message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView, + dataSource: MessagesDataSource, + and sizeCalculator: CustomLayoutSizeCalculator) + { + guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { + return + } + cellTopLabel.frame = sizeCalculator.cellTopLabelFrame( + for: message, + at: indexPath) + cellDateLabel.frame = sizeCalculator.cellMessageBottomLabelFrame( + for: message, + at: indexPath) + messageContainerView.frame = sizeCalculator.messageContainerFrame( + for: message, + at: indexPath, + fromCurrentSender: dataSource + .isFromCurrentSender(message: message)) + cellTopLabel.attributedText = dataSource.cellTopLabelAttributedText( + for: message, + at: indexPath) + cellDateLabel.attributedText = dataSource.messageBottomLabelAttributedText( + for: message, + at: indexPath) + messageContainerView.backgroundColor = displayDelegate.backgroundColor( + for: message, + at: indexPath, + in: messagesCollectionView) + } + + /// Handle `ContentView`'s tap gesture, return false when `ContentView` doesn't needs to handle gesture + func cellContentView(canHandle _: CGPoint) -> Bool { + false + } +} diff --git a/Example/Sources/Views/CustomTextMessageContentCell.swift b/Example/Sources/Views/CustomTextMessageContentCell.swift new file mode 100644 index 000000000..f4e2eb0fb --- /dev/null +++ b/Example/Sources/Views/CustomTextMessageContentCell.swift @@ -0,0 +1,70 @@ +// +// CustomTextMessageContentCell.swift +// ChatExample +// +// Created by Vignesh J on 01/05/21. +// Copyright Β© 2021 MessageKit. All rights reserved. +// + +import MessageKit +import UIKit + +class CustomTextMessageContentCell: CustomMessageContentCell { + /// The label used to display the message's text. + var messageLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.font = UIFont.preferredFont(forTextStyle: .body) + + return label + }() + + override func prepareForReuse() { + super.prepareForReuse() + + messageLabel.attributedText = nil + messageLabel.text = nil + } + + override func setupSubviews() { + super.setupSubviews() + + messageContainerView.addSubview(messageLabel) + } + + override func configure( + with message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView, + dataSource: MessagesDataSource, + and sizeCalculator: CustomLayoutSizeCalculator) + { + super.configure( + with: message, + at: indexPath, + in: messagesCollectionView, + dataSource: dataSource, + and: sizeCalculator) + + guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { + return + } + + let calculator = sizeCalculator as? CustomTextLayoutSizeCalculator + messageLabel.frame = calculator?.messageLabelFrame( + for: message, + at: indexPath) ?? .zero + + let textMessageKind = message.kind + switch textMessageKind { + case .text(let text), .emoji(let text): + let textColor = displayDelegate.textColor(for: message, at: indexPath, in: messagesCollectionView) + messageLabel.text = text + messageLabel.textColor = textColor + case .attributedText(let text): + messageLabel.attributedText = text + default: + break + } + } +} diff --git a/Example/Sources/Views/SwiftUI/MessagesView.swift b/Example/Sources/Views/SwiftUI/MessagesView.swift new file mode 100644 index 000000000..c6f6fcbae --- /dev/null +++ b/Example/Sources/Views/SwiftUI/MessagesView.swift @@ -0,0 +1,163 @@ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import InputBarAccessoryView +import MessageKit +import SwiftUI + +// MARK: - MessageSwiftUIVC + +final class MessageSwiftUIVC: MessagesViewController { + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + // Because SwiftUI wont automatically make our controller the first responder, we need to do it on viewDidAppear + becomeFirstResponder() + messagesCollectionView.scrollToLastItem(animated: true) + } +} + +// MARK: - MessagesView + +struct MessagesView: UIViewControllerRepresentable { + // MARK: Internal + + final class Coordinator { + // MARK: Lifecycle + + init(messages: Binding<[MessageType]>) { + self.messages = messages + } + + // MARK: Internal + + let formatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + return formatter + }() + + var messages: Binding<[MessageType]> + } + + @State var initialized = false + @Binding var messages: [MessageType] + + func makeUIViewController(context: Context) -> MessagesViewController { + let messagesVC = MessageSwiftUIVC() + + messagesVC.messagesCollectionView.messagesDisplayDelegate = context.coordinator + messagesVC.messagesCollectionView.messagesLayoutDelegate = context.coordinator + messagesVC.messagesCollectionView.messagesDataSource = context.coordinator + messagesVC.messageInputBar.delegate = context.coordinator + messagesVC.scrollsToLastItemOnKeyboardBeginsEditing = true // default false + messagesVC.maintainPositionOnInputBarHeightChanged = true // default false + messagesVC.showMessageTimestampOnSwipeLeft = true // default false + + return messagesVC + } + + func updateUIViewController(_ uiViewController: MessagesViewController, context _: Context) { + uiViewController.messagesCollectionView.reloadData() + scrollToBottom(uiViewController) + } + + func makeCoordinator() -> Coordinator { + Coordinator(messages: $messages) + } + + // MARK: Private + + private func scrollToBottom(_ uiViewController: MessagesViewController) { + DispatchQueue.main.async { + // The initialized state variable allows us to start at the bottom with the initial messages without seeing the initial scroll flash by + uiViewController.messagesCollectionView.scrollToLastItem(animated: self.initialized) + self.initialized = true + } + } +} + +// MARK: - MessagesView.Coordinator + MessagesDataSource + +extension MessagesView.Coordinator: MessagesDataSource { + var currentSender: SenderType { + SampleData.shared.currentSender + } + + func messageForItem(at indexPath: IndexPath, in _: MessagesCollectionView) -> MessageType { + messages.wrappedValue[indexPath.section] + } + + func numberOfSections(in _: MessagesCollectionView) -> Int { + messages.wrappedValue.count + } + + func messageTopLabelAttributedText(for message: MessageType, at _: IndexPath) -> NSAttributedString? { + let name = message.sender.displayName + return NSAttributedString( + string: name, + attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)]) + } + + func messageBottomLabelAttributedText(for message: MessageType, at _: IndexPath) -> NSAttributedString? { + let dateString = formatter.string(from: message.sentDate) + return NSAttributedString( + string: dateString, + attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption2)]) + } + + func messageTimestampLabelAttributedText(for message: MessageType, at _: IndexPath) -> NSAttributedString? { + let sentDate = message.sentDate + let sentDateString = MessageKitDateFormatter.shared.string(from: sentDate) + let timeLabelFont: UIFont = .boldSystemFont(ofSize: 10) + let timeLabelColor: UIColor = .systemGray + return NSAttributedString( + string: sentDateString, + attributes: [NSAttributedString.Key.font: timeLabelFont, NSAttributedString.Key.foregroundColor: timeLabelColor]) + } +} + +// MARK: - MessagesView.Coordinator + InputBarAccessoryViewDelegate + +extension MessagesView.Coordinator: InputBarAccessoryViewDelegate { + func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) { + let message = MockMessage(text: text, user: SampleData.shared.currentSender, messageId: UUID().uuidString, date: Date()) + messages.wrappedValue.append(message) + inputBar.inputTextView.text = "" + } +} + +// MARK: - MessagesView.Coordinator + MessagesLayoutDelegate, MessagesDisplayDelegate + +extension MessagesView.Coordinator: MessagesLayoutDelegate, MessagesDisplayDelegate { + func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) { + let avatar = SampleData.shared.getAvatarFor(sender: message.sender) + avatarView.set(avatar: avatar) + } + + func messageTopLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat { + 20 + } + + func messageBottomLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat { + 16 + } +} diff --git a/Example/Sources/Views/SwiftUI/SwiftUIExampleView.swift b/Example/Sources/Views/SwiftUI/SwiftUIExampleView.swift new file mode 100644 index 000000000..b4fd02bc0 --- /dev/null +++ b/Example/Sources/Views/SwiftUI/SwiftUIExampleView.swift @@ -0,0 +1,58 @@ +// +// ChatView.swift +// ChatExample +// +// Created by Kino Roy on 2020-07-18. +// Copyright Β© 2020 MessageKit. All rights reserved. +// + +import MessageKit +import SwiftUI + +// MARK: - SwiftUIExampleView + +struct SwiftUIExampleView: View { + // MARK: Internal + + @State var messages: [MessageType] = SampleData.shared.getMessages(count: 20) + + var body: some View { + MessagesView(messages: $messages).onAppear { + self.connectToMessageSocket() + }.onDisappear { + self.cleanupSocket() + } + .navigationBarTitle("SwiftUI Example", displayMode: .inline) + .modifier(IgnoresSafeArea()) //fixes issue with IBAV placement when keyboard appears + } + + // MARK: Private + + private struct IgnoresSafeArea: ViewModifier { + func body(content: Content) -> some View { + if #available(iOS 14.0, *) { + content.ignoresSafeArea(.keyboard, edges: .bottom) + } else { + content + } + } + } + + private func connectToMessageSocket() { + MockSocket.shared.connect(with: [SampleData.shared.nathan, SampleData.shared.wu]).onNewMessage { message in + self.messages.append(message) + } + } + + private func cleanupSocket() { + MockSocket.shared.disconnect() + } +} + +// MARK: - SwiftUIExampleView_Previews + +struct SwiftUIExampleView_Previews: PreviewProvider { + static var previews: some View { + SwiftUIExampleView() + } +} diff --git a/Example/Sources/Views/TableViewCells.swift b/Example/Sources/Views/TableViewCells.swift index 615ffc13c..fb4cc5d0a 100644 --- a/Example/Sources/Views/TableViewCells.swift +++ b/Example/Sources/Views/TableViewCells.swift @@ -1,62 +1,63 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import UIKit internal class TextFieldTableViewCell: UITableViewCell { + // MARK: Lifecycle - static let identifier = "TextFieldTableViewCellIdentifier" - - var mainLabel = UILabel() - var textField = UITextField() - - // MARK: - View lifecycle - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - mainLabel.translatesAutoresizingMaskIntoConstraints = false - textField.translatesAutoresizingMaskIntoConstraints = false - - contentView.addSubview(mainLabel) - contentView.addSubview(textField) - - NSLayoutConstraint.activate([ - mainLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), - mainLabel.widthAnchor.constraint(equalToConstant: 200), - mainLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - - textField.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - - textField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), - textField.widthAnchor.constraint(equalToConstant: 50) - ]) - - textField.textAlignment = .right - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + // MARK: - View lifecycle + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + mainLabel.translatesAutoresizingMaskIntoConstraints = false + textField.translatesAutoresizingMaskIntoConstraints = false + + contentView.addSubview(mainLabel) + contentView.addSubview(textField) + + NSLayoutConstraint.activate([ + mainLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + mainLabel.widthAnchor.constraint(equalToConstant: 200), + mainLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + + textField.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + + textField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + textField.widthAnchor.constraint(equalToConstant: 50), + ]) + + textField.textAlignment = .right + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Internal + + static let identifier = "TextFieldTableViewCellIdentifier" + + var mainLabel = UILabel() + var textField = UITextField() } diff --git a/Example/Tests/ChatExampleTests.swift b/Example/Tests/ChatExampleTests.swift index f29a4705f..6e376e447 100644 --- a/Example/Tests/ChatExampleTests.swift +++ b/Example/Tests/ChatExampleTests.swift @@ -20,18 +20,16 @@ import XCTest @testable import ChatExample final class ChatExampleTests: XCTestCase { + override func setUp() { + super.setUp() + } - override func setUp() { - super.setUp() - } - - override func tearDown() { - super.tearDown() - } - - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } + override func tearDown() { + super.tearDown() + } + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } } diff --git a/Example/UITests/ChatExampleUITests.swift b/Example/UITests/ChatExampleUITests.swift index d9548ce90..8c810b170 100644 --- a/Example/UITests/ChatExampleUITests.swift +++ b/Example/UITests/ChatExampleUITests.swift @@ -20,31 +20,25 @@ import XCTest @testable import ChatExample final class ChatExampleUITests: XCTestCase { + override func setUp() { + super.setUp() - override func setUp() { - super.setUp() + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + // UI tests must launch the application that they test. + // Doing this in setup will make sure it happens for each test method. + XCUIApplication().launch() - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - // UI tests must launch the application that they test. - // Doing this in setup will make sure it happens for each test method. - if #available(iOS 9.0, *) { - XCUIApplication().launch() - } else { - // Fallback on earlier versions - } - - // In UI tests it’s important to set the initial state - // - such as interface orientation - required for your tests before they run. - // The setUp method is a good place to do this. - } - - func testExampleRuns() { - // Extremely simple UI test which is designed to run and display the example project - // This should show if there are any very obvious crashes on render - let app = XCUIApplication() - app.tables.staticTexts["Test"].tap() - XCTAssertTrue(app.collectionViews.staticTexts["Check out this awesome UI library for Chat"].exists) - } + // In UI tests it’s important to set the initial state + // - such as interface orientation - required for your tests before they run. + // The setUp method is a good place to do this. + } + func testExampleRuns() { + // Extremely simple UI test which is designed to run and display the example project + // This should show if there are any very obvious crashes on render + let app = XCUIApplication() + app.tables.staticTexts["Test"].tap() + XCTAssertTrue(app.collectionViews.staticTexts["Check out this awesome UI library for Chat"].exists) + } } diff --git a/Gemfile b/Gemfile new file mode 100644 index 000000000..a8a0b8f14 --- /dev/null +++ b/Gemfile @@ -0,0 +1,26 @@ +# +# MIT License +# +# Copyright (c) 2017-2020 MessageKit +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +source 'https://rubygems.org' +gem 'danger', '~> 8.6' +gem 'danger-swiftlint', '~> 0.29' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 000000000..57adcd5ab --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,91 @@ +GEM + remote: https://rubygems.org/ + specs: + addressable (2.8.1) + public_suffix (>= 2.0.2, < 6.0) + claide (1.1.0) + claide-plugins (0.9.2) + cork + nap + open4 (~> 1.3) + colored2 (3.1.2) + cork (0.3.0) + colored2 (~> 3.1) + danger (8.6.1) + claide (~> 1.0) + claide-plugins (>= 0.9.2) + colored2 (~> 3.1) + cork (~> 0.1) + faraday (>= 0.9.0, < 2.0) + faraday-http-cache (~> 2.0) + git (~> 1.7) + kramdown (~> 2.3) + kramdown-parser-gfm (~> 1.0) + no_proxy_fix + octokit (~> 4.7) + terminal-table (>= 1, < 4) + danger-swiftlint (0.30.2) + danger + rake (> 10) + thor (~> 0.19) + faraday (1.10.2) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-http-cache (2.4.1) + faraday (>= 0.8) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + git (1.13.1) + addressable (~> 2.8) + rchardet (~> 1.8) + kramdown (2.4.0) + rexml + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + multipart-post (2.2.3) + nap (1.1.0) + no_proxy_fix (0.1.2) + octokit (4.25.1) + faraday (>= 1, < 3) + sawyer (~> 0.9) + open4 (1.3.4) + public_suffix (5.0.1) + rake (13.0.6) + rchardet (1.8.0) + rexml (3.3.9) + ruby2_keywords (0.0.5) + sawyer (0.9.2) + addressable (>= 2.3.5) + faraday (>= 0.17.3, < 3) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + thor (0.20.3) + unicode-display_width (2.3.0) + +PLATFORMS + ruby + +DEPENDENCIES + danger (~> 8.6) + danger-swiftlint (~> 0.29) + +BUNDLED WITH + 2.1.4 diff --git a/LICENSE.md b/LICENSE.md index edefe0f87..4731930ad 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017-2018 MessageKit +Copyright (c) 2017-2024 MessageKit Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..56e05c78f --- /dev/null +++ b/Makefile @@ -0,0 +1,48 @@ +# MIT License +# +# Copyright (c) 2017-2022 MessageKit +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +.SHELLFLAGS = -ec +.SHELL = /bin/bash + +test: + @echo "Running MessageKit tests." + @set -o pipefail && xcodebuild test -scheme MessageKit -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 16" | xcpretty -c + +framework: + @echo "Building MessageKit Framework." + @set -o pipefail && xcodebuild build -scheme MessageKit -destination "platform=iOS Simulator,name=iPhone 16" | xcpretty -c + +build_example: + @echo "Building & testing MessageKit Example app." + @cd Example && set -o pipefail && xcodebuild build analyze -scheme ChatExample -destination "platform=iOS Simulator,name=iPhone 16" CODE_SIGNING_REQUIRED=NO | xcpretty -c + +format: + @swift package --allow-writing-to-package-directory format-source-code --file . + +lint: + @swift package --disable-sandbox lint + +setup: + @mkdir -p .git/hooks + @rm -f .git/hooks/pre-commit + @cp ./Scripts/pre-commit ./.git/hooks + @chmod +x .git/hooks/pre-commit diff --git a/MessageKit.podspec b/MessageKit.podspec deleted file mode 100644 index 0d2e1b659..000000000 --- a/MessageKit.podspec +++ /dev/null @@ -1,25 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'MessageKit' - s.version = '2.0.0' - s.license = { :type => "MIT", :file => "LICENSE.md" } - - s.summary = 'An elegant messages UI library for iOS.' - s.homepage = 'https://github.com/MessageKit/MessageKit' - s.social_media_url = 'https://twitter.com/_SD10_' - s.author = { "Steven Deutsch" => "stevensdeutsch@yahoo.com" } - - s.source = { :git => 'https://github.com/MessageKit/MessageKit.git', :tag => s.version } - s.source_files = 'Sources/**/*.swift' - - s.pod_target_xcconfig = { - "SWIFT_VERSION" => "4.0", - } - - s.ios.deployment_target = '9.0' - s.ios.resource_bundle = { 'MessageKitAssets' => 'Assets/MessageKitAssets.bundle/Images' } - - s.requires_arc = true - - s.dependency 'MessageInputBar/Core' - -end diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj deleted file mode 100644 index 8e056eb65..000000000 --- a/MessageKit.xcodeproj/project.pbxproj +++ /dev/null @@ -1,883 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 0EE91E661FDEC888005420A2 /* CGRect+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE91E651FDEC887005420A2 /* CGRect+Extensions.swift */; }; - 0EF0888C206F7E83007F2F58 /* CellSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF0888B206F7E83007F2F58 /* CellSizeCalculator.swift */; }; - 1F066E131FD90BB600E11013 /* MessagesViewControllerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7FC8CB1FD2700B006CC979 /* MessagesViewControllerSpec.swift */; }; - 1F066E141FD90BB700E11013 /* MessageLabelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F066E101FD90A0600E11013 /* MessageLabelSpec.swift */; }; - 1F066E1D1FDA3C1700E11013 /* SenderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F066E1C1FDA3C1700E11013 /* SenderSpec.swift */; }; - 1F066E211FDA3DEA00E11013 /* AvatarSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F066E201FDA3DEA00E11013 /* AvatarSpec.swift */; }; - 1F066E231FDA3F0200E11013 /* DetectorTypeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F066E221FDA3F0200E11013 /* DetectorTypeSpec.swift */; }; - 1F087D451FD274FD00E95A45 /* Nimble.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 1F7FC8C71FD26F49006CC979 /* Nimble.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 1F087D461FD274FD00E95A45 /* Quick.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 1F7FC8C51FD26F33006CC979 /* Quick.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 1F6C040C206A2891007BDE44 /* MessageContentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6C040B206A2891007BDE44 /* MessageContentCell.swift */; }; - 1F6C040E206A2AF4007BDE44 /* MessageReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6C040D206A2AF4007BDE44 /* MessageReusableView.swift */; }; - 1F7FC8C61FD26F33006CC979 /* Quick.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F7FC8C51FD26F33006CC979 /* Quick.framework */; }; - 1F7FC8C81FD26F49006CC979 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F7FC8C71FD26F49006CC979 /* Nimble.framework */; }; - 1F82D1431FB1B75B00B81A88 /* AvatarPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */; }; - 1FCA6D30201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FCA6D2F201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift */; }; - 1FD589602064E08A004B5081 /* MediaItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD5895F2064E08A004B5081 /* MediaItem.swift */; }; - 1FD589612064E1B5004B5081 /* MockMessagesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC7A1F87AB230030B058 /* MockMessagesDataSource.swift */; }; - 1FD589622064E1B9004B5081 /* MockMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC7B1F87AB230030B058 /* MockMessage.swift */; }; - 1FD5896420660C1C004B5081 /* LocationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD5896320660C1C004B5081 /* LocationItem.swift */; }; - 1FE7839E20662835007FA024 /* MessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE7839D20662835007FA024 /* MessageSizeCalculator.swift */; }; - 1FE783A220662905007FA024 /* TextMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE783A120662905007FA024 /* TextMessageSizeCalculator.swift */; }; - 1FE783A4206629A5007FA024 /* MediaMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE783A3206629A5007FA024 /* MediaMessageSizeCalculator.swift */; }; - 1FE783A6206629C2007FA024 /* LocationMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE783A5206629C2007FA024 /* LocationMessageSizeCalculator.swift */; }; - 1FE783A8206633C0007FA024 /* InsetLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE783A7206633C0007FA024 /* InsetLabel.swift */; }; - 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A320087C82004FD648 /* MessageKitError.swift */; }; - 1FF377AA20087D78004FD648 /* MessagesViewController+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */; }; - 1FF377AC20087DA2004FD648 /* MessagesViewController+Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */; }; - 382C794221705D2000F4FAF5 /* HorizontalEdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382C794121705D2000F4FAF5 /* HorizontalEdgeInsets.swift */; }; - 38C2AE7C20D4878D00F8079E /* MessageInputBar.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38C2AE7B20D4878D00F8079E /* MessageInputBar.framework */; }; - 88916B2D1CF0DF2F00469F91 /* MessageKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 88916B221CF0DF2F00469F91 /* MessageKit.framework */; }; - 8962AC8A1F87AB7D0030B058 /* MessagesCollectionViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC831F87AB230030B058 /* MessagesCollectionViewTests.swift */; }; - 8962AC8C1F87AB7D0030B058 /* AvatarViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC851F87AB230030B058 /* AvatarViewTests.swift */; }; - 8962AC8D1F87AB7D0030B058 /* MessageCollectionViewCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC861F87AB230030B058 /* MessageCollectionViewCellTests.swift */; }; - 8962AC911F87AB860030B058 /* MessagesViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC7D1F87AB230030B058 /* MessagesViewControllerTests.swift */; }; - 8962AC941F87AB860030B058 /* MessageKitDateFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC741F87AB230030B058 /* MessageKitDateFormatterTests.swift */; }; - 8962AC951F87AB860030B058 /* MessageStyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC751F87AB230030B058 /* MessageStyleTests.swift */; }; - 8962AC991F87AB860030B058 /* MessagesDisplayDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC811F87AB230030B058 /* MessagesDisplayDelegateTests.swift */; }; - B0C8D99B1F73076B000A86E4 /* MessageKitAssets.bundle in Resources */ = {isa = PBXBuildFile; fileRef = B0C8D99A1F73076B000A86E4 /* MessageKitAssets.bundle */; }; - B7A03F181F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F161F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift */; }; - B7A03F191F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F171F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift */; }; - B7A03F251F866895006AEF79 /* NSConstraintLayoutSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F1A1F866895006AEF79 /* NSConstraintLayoutSet.swift */; }; - B7A03F261F866895006AEF79 /* MessageKitDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F1B1F866895006AEF79 /* MessageKitDateFormatter.swift */; }; - B7A03F271F866895006AEF79 /* Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F1C1F866895006AEF79 /* Avatar.swift */; }; - B7A03F281F866895006AEF79 /* LocationMessageSnapshotOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F1D1F866895006AEF79 /* LocationMessageSnapshotOptions.swift */; }; - B7A03F291F866895006AEF79 /* Sender.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F1E1F866895006AEF79 /* Sender.swift */; }; - B7A03F2A1F866895006AEF79 /* MessageStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F1F1F866895006AEF79 /* MessageStyle.swift */; }; - B7A03F2C1F866895006AEF79 /* DetectorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F211F866895006AEF79 /* DetectorType.swift */; }; - B7A03F2D1F866895006AEF79 /* LabelAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F221F866895006AEF79 /* LabelAlignment.swift */; }; - B7A03F2E1F866895006AEF79 /* MessageKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F231F866895006AEF79 /* MessageKind.swift */; }; - B7A03F3A1F866946006AEF79 /* TextMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F361F866946006AEF79 /* TextMessageCell.swift */; }; - B7A03F3C1F866946006AEF79 /* LocationMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F381F866946006AEF79 /* LocationMessageCell.swift */; }; - B7A03F3D1F866946006AEF79 /* MediaMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F391F866946006AEF79 /* MediaMessageCell.swift */; }; - B7A03F461F86694F006AEF79 /* AvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F3E1F86694F006AEF79 /* AvatarView.swift */; }; - B7A03F471F86694F006AEF79 /* MessageLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F3F1F86694F006AEF79 /* MessageLabel.swift */; }; - B7A03F4B1F86694F006AEF79 /* MessageContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F431F86694F006AEF79 /* MessageContainerView.swift */; }; - B7A03F4C1F86694F006AEF79 /* PlayButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F441F86694F006AEF79 /* PlayButtonView.swift */; }; - B7A03F4D1F86694F006AEF79 /* MessagesCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F451F86694F006AEF79 /* MessagesCollectionView.swift */; }; - B7A03F4F1F86697C006AEF79 /* MessagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F4E1F86697C006AEF79 /* MessagesViewController.swift */; }; - B7A03F5B1F8669CA006AEF79 /* MessageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F511F8669C9006AEF79 /* MessageType.swift */; }; - B7A03F5C1F8669CA006AEF79 /* MessageCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F521F8669C9006AEF79 /* MessageCellDelegate.swift */; }; - B7A03F5E1F8669CA006AEF79 /* MessagesLayoutDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F541F8669C9006AEF79 /* MessagesLayoutDelegate.swift */; }; - B7A03F5F1F8669CA006AEF79 /* MessageLabelDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F551F8669C9006AEF79 /* MessageLabelDelegate.swift */; }; - B7A03F601F8669CA006AEF79 /* MessagesDisplayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F561F8669C9006AEF79 /* MessagesDisplayDelegate.swift */; }; - B7A03F611F8669CA006AEF79 /* MessagesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F571F8669CA006AEF79 /* MessagesDataSource.swift */; }; - B7A03F6B1F8669EB006AEF79 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F651F8669EB006AEF79 /* UIColor+Extensions.swift */; }; - B7A03F6C1F8669EB006AEF79 /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F661F8669EB006AEF79 /* UIView+Extensions.swift */; }; - B7A03F6D1F8669EB006AEF79 /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F671F8669EB006AEF79 /* Bundle+Extensions.swift */; }; - B7A03F6E1F8669EB006AEF79 /* NSAttributedString+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F681F8669EB006AEF79 /* NSAttributedString+Extensions.swift */; }; - B7A03F731F866A06006AEF79 /* MessageKit+Availability.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F701F866A06006AEF79 /* MessageKit+Availability.swift */; }; - B7A03F751F866A06006AEF79 /* MessageKit.h in Headers */ = {isa = PBXBuildFile; fileRef = B7A03F721F866A06006AEF79 /* MessageKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; - B7A03F7B1F866B85006AEF79 /* MessageCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F7A1F866B85006AEF79 /* MessageCollectionViewCell.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 88916B2E1CF0DF2F00469F91 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 88916B191CF0DF2F00469F91 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 88916B211CF0DF2F00469F91; - remoteInfo = MessageKit; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 1F087D431FD274D300E95A45 /* CopyFiles */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 1F087D451FD274FD00E95A45 /* Nimble.framework in CopyFiles */, - 1F087D461FD274FD00E95A45 /* Quick.framework in CopyFiles */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 0EE91E651FDEC887005420A2 /* CGRect+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGRect+Extensions.swift"; sourceTree = ""; }; - 0EF0888B206F7E83007F2F58 /* CellSizeCalculator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellSizeCalculator.swift; sourceTree = ""; }; - 1F066E101FD90A0600E11013 /* MessageLabelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageLabelSpec.swift; sourceTree = ""; }; - 1F066E1C1FDA3C1700E11013 /* SenderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SenderSpec.swift; sourceTree = ""; }; - 1F066E201FDA3DEA00E11013 /* AvatarSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarSpec.swift; sourceTree = ""; }; - 1F066E221FDA3F0200E11013 /* DetectorTypeSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectorTypeSpec.swift; sourceTree = ""; }; - 1F6C040B206A2891007BDE44 /* MessageContentCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageContentCell.swift; sourceTree = ""; }; - 1F6C040D206A2AF4007BDE44 /* MessageReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReusableView.swift; sourceTree = ""; }; - 1F7FC8C31FD26F22006CC979 /* Carthage */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Carthage; sourceTree = ""; }; - 1F7FC8C51FD26F33006CC979 /* Quick.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quick.framework; path = Carthage/Build/iOS/Quick.framework; sourceTree = ""; }; - 1F7FC8C71FD26F49006CC979 /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Nimble.framework; path = Carthage/Build/iOS/Nimble.framework; sourceTree = ""; }; - 1F7FC8CB1FD2700B006CC979 /* MessagesViewControllerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesViewControllerSpec.swift; sourceTree = ""; }; - 1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarPosition.swift; sourceTree = ""; }; - 1FCA6D2F201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+Extensions.swift"; sourceTree = ""; }; - 1FD5895F2064E08A004B5081 /* MediaItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaItem.swift; sourceTree = ""; }; - 1FD5896320660C1C004B5081 /* LocationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationItem.swift; sourceTree = ""; }; - 1FE7839D20662835007FA024 /* MessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSizeCalculator.swift; sourceTree = ""; }; - 1FE783A120662905007FA024 /* TextMessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageSizeCalculator.swift; sourceTree = ""; }; - 1FE783A3206629A5007FA024 /* MediaMessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaMessageSizeCalculator.swift; sourceTree = ""; }; - 1FE783A5206629C2007FA024 /* LocationMessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationMessageSizeCalculator.swift; sourceTree = ""; }; - 1FE783A7206633C0007FA024 /* InsetLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsetLabel.swift; sourceTree = ""; }; - 1FF377A320087C82004FD648 /* MessageKitError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageKitError.swift; sourceTree = ""; }; - 1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Menu.swift"; sourceTree = ""; }; - 1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Keyboard.swift"; sourceTree = ""; }; - 382C794121705D2000F4FAF5 /* HorizontalEdgeInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalEdgeInsets.swift; sourceTree = ""; }; - 38C2AE7B20D4878D00F8079E /* MessageInputBar.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageInputBar.framework; path = Carthage/Build/iOS/MessageInputBar.framework; sourceTree = ""; }; - 88916B221CF0DF2F00469F91 /* MessageKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MessageKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 88916B2C1CF0DF2F00469F91 /* MessageKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MessageKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 8962AC741F87AB230030B058 /* MessageKitDateFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageKitDateFormatterTests.swift; sourceTree = ""; }; - 8962AC751F87AB230030B058 /* MessageStyleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageStyleTests.swift; sourceTree = ""; }; - 8962AC7A1F87AB230030B058 /* MockMessagesDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMessagesDataSource.swift; sourceTree = ""; }; - 8962AC7B1F87AB230030B058 /* MockMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMessage.swift; sourceTree = ""; }; - 8962AC7D1F87AB230030B058 /* MessagesViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesViewControllerTests.swift; sourceTree = ""; }; - 8962AC7F1F87AB230030B058 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 8962AC811F87AB230030B058 /* MessagesDisplayDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesDisplayDelegateTests.swift; sourceTree = ""; }; - 8962AC831F87AB230030B058 /* MessagesCollectionViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesCollectionViewTests.swift; sourceTree = ""; }; - 8962AC851F87AB230030B058 /* AvatarViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarViewTests.swift; sourceTree = ""; }; - 8962AC861F87AB230030B058 /* MessageCollectionViewCellTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewCellTests.swift; sourceTree = ""; }; - B0C8D99A1F73076B000A86E4 /* MessageKitAssets.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = MessageKitAssets.bundle; sourceTree = ""; }; - B7A03F161F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesCollectionViewFlowLayout.swift; sourceTree = ""; }; - B7A03F171F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesCollectionViewLayoutAttributes.swift; sourceTree = ""; }; - B7A03F1A1F866895006AEF79 /* NSConstraintLayoutSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSConstraintLayoutSet.swift; sourceTree = ""; }; - B7A03F1B1F866895006AEF79 /* MessageKitDateFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageKitDateFormatter.swift; sourceTree = ""; }; - B7A03F1C1F866895006AEF79 /* Avatar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Avatar.swift; sourceTree = ""; }; - B7A03F1D1F866895006AEF79 /* LocationMessageSnapshotOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationMessageSnapshotOptions.swift; sourceTree = ""; }; - B7A03F1E1F866895006AEF79 /* Sender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sender.swift; sourceTree = ""; }; - B7A03F1F1F866895006AEF79 /* MessageStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageStyle.swift; sourceTree = ""; }; - B7A03F211F866895006AEF79 /* DetectorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetectorType.swift; sourceTree = ""; }; - B7A03F221F866895006AEF79 /* LabelAlignment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelAlignment.swift; sourceTree = ""; }; - B7A03F231F866895006AEF79 /* MessageKind.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageKind.swift; sourceTree = ""; }; - B7A03F361F866946006AEF79 /* TextMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextMessageCell.swift; sourceTree = ""; }; - B7A03F381F866946006AEF79 /* LocationMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationMessageCell.swift; sourceTree = ""; }; - B7A03F391F866946006AEF79 /* MediaMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaMessageCell.swift; sourceTree = ""; }; - B7A03F3E1F86694F006AEF79 /* AvatarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarView.swift; sourceTree = ""; }; - B7A03F3F1F86694F006AEF79 /* MessageLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageLabel.swift; sourceTree = ""; }; - B7A03F431F86694F006AEF79 /* MessageContainerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageContainerView.swift; sourceTree = ""; }; - B7A03F441F86694F006AEF79 /* PlayButtonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayButtonView.swift; sourceTree = ""; }; - B7A03F451F86694F006AEF79 /* MessagesCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesCollectionView.swift; sourceTree = ""; }; - B7A03F4E1F86697C006AEF79 /* MessagesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesViewController.swift; sourceTree = ""; }; - B7A03F511F8669C9006AEF79 /* MessageType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageType.swift; sourceTree = ""; }; - B7A03F521F8669C9006AEF79 /* MessageCellDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCellDelegate.swift; sourceTree = ""; }; - B7A03F541F8669C9006AEF79 /* MessagesLayoutDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesLayoutDelegate.swift; sourceTree = ""; }; - B7A03F551F8669C9006AEF79 /* MessageLabelDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageLabelDelegate.swift; sourceTree = ""; }; - B7A03F561F8669C9006AEF79 /* MessagesDisplayDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDisplayDelegate.swift; sourceTree = ""; }; - B7A03F571F8669CA006AEF79 /* MessagesDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDataSource.swift; sourceTree = ""; }; - B7A03F651F8669EB006AEF79 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; - B7A03F661F8669EB006AEF79 /* UIView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = ""; }; - B7A03F671F8669EB006AEF79 /* Bundle+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+Extensions.swift"; sourceTree = ""; }; - B7A03F681F8669EB006AEF79 /* NSAttributedString+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Extensions.swift"; sourceTree = ""; }; - B7A03F701F866A06006AEF79 /* MessageKit+Availability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MessageKit+Availability.swift"; sourceTree = ""; }; - B7A03F711F866A06006AEF79 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B7A03F721F866A06006AEF79 /* MessageKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MessageKit.h; sourceTree = ""; }; - B7A03F7A1F866B85006AEF79 /* MessageCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewCell.swift; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 88916B1E1CF0DF2F00469F91 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 38C2AE7C20D4878D00F8079E /* MessageInputBar.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 88916B291CF0DF2F00469F91 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 1F7FC8C81FD26F49006CC979 /* Nimble.framework in Frameworks */, - 1F7FC8C61FD26F33006CC979 /* Quick.framework in Frameworks */, - 88916B2D1CF0DF2F00469F91 /* MessageKit.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 1F7FC8C21FD26F22006CC979 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 38C2AE7B20D4878D00F8079E /* MessageInputBar.framework */, - 1F7FC8C71FD26F49006CC979 /* Nimble.framework */, - 1F7FC8C51FD26F33006CC979 /* Quick.framework */, - 1F7FC8C31FD26F22006CC979 /* Carthage */, - ); - name = Frameworks; - sourceTree = ""; - }; - 2EB618F01F84676A007FBA0E /* Cells */ = { - isa = PBXGroup; - children = ( - B7A03F381F866946006AEF79 /* LocationMessageCell.swift */, - B7A03F391F866946006AEF79 /* MediaMessageCell.swift */, - B7A03F7A1F866B85006AEF79 /* MessageCollectionViewCell.swift */, - 1F6C040B206A2891007BDE44 /* MessageContentCell.swift */, - B7A03F361F866946006AEF79 /* TextMessageCell.swift */, - ); - path = Cells; - sourceTree = ""; - }; - 2EB618F11F846899007FBA0E /* Headers & Footers */ = { - isa = PBXGroup; - children = ( - 1F6C040D206A2AF4007BDE44 /* MessageReusableView.swift */, - ); - path = "Headers & Footers"; - sourceTree = ""; - }; - 88916B181CF0DF2F00469F91 = { - isa = PBXGroup; - children = ( - 88916B3C1CF0DF5100469F91 /* Sources */, - 88916B411CF0DF5900469F91 /* Tests */, - 88916B231CF0DF2F00469F91 /* Products */, - 1F7FC8C21FD26F22006CC979 /* Frameworks */, - ); - sourceTree = ""; - usesTabs = 0; - }; - 88916B231CF0DF2F00469F91 /* Products */ = { - isa = PBXGroup; - children = ( - 88916B221CF0DF2F00469F91 /* MessageKit.framework */, - 88916B2C1CF0DF2F00469F91 /* MessageKitTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 88916B3C1CF0DF5100469F91 /* Sources */ = { - isa = PBXGroup; - children = ( - B01E2DCC1F5BA34700E4FA1C /* Assets */, - B096439D1F295DC3004D0129 /* Layout */, - B09643991F295D58004D0129 /* Models */, - B096439A1F295D68004D0129 /* Views */, - B096439C1F295DA9004D0129 /* Controllers */, - B096439B1F295D82004D0129 /* Protocols */, - B09643981F295D43004D0129 /* Extensions */, - B096439E1F295DD7004D0129 /* Supporting */, - ); - path = Sources; - sourceTree = ""; - }; - 88916B411CF0DF5900469F91 /* Tests */ = { - isa = PBXGroup; - children = ( - 1F066E201FDA3DEA00E11013 /* AvatarSpec.swift */, - 1F066E221FDA3F0200E11013 /* DetectorTypeSpec.swift */, - 1F066E1C1FDA3C1700E11013 /* SenderSpec.swift */, - 8962AC7C1F87AB230030B058 /* ControllersTest */, - 8962AC791F87AB230030B058 /* Mocks */, - 8962AC731F87AB230030B058 /* ModelTests */, - 8962AC801F87AB230030B058 /* ProtocolsTests */, - 8962AC7E1F87AB230030B058 /* Supporting Files */, - 8962AC821F87AB230030B058 /* ViewsTests */, - ); - path = Tests; - sourceTree = SOURCE_ROOT; - }; - 8962AC731F87AB230030B058 /* ModelTests */ = { - isa = PBXGroup; - children = ( - 8962AC741F87AB230030B058 /* MessageKitDateFormatterTests.swift */, - 8962AC751F87AB230030B058 /* MessageStyleTests.swift */, - ); - path = ModelTests; - sourceTree = ""; - }; - 8962AC791F87AB230030B058 /* Mocks */ = { - isa = PBXGroup; - children = ( - 8962AC7B1F87AB230030B058 /* MockMessage.swift */, - 8962AC7A1F87AB230030B058 /* MockMessagesDataSource.swift */, - ); - path = Mocks; - sourceTree = ""; - }; - 8962AC7C1F87AB230030B058 /* ControllersTest */ = { - isa = PBXGroup; - children = ( - 1F066E101FD90A0600E11013 /* MessageLabelSpec.swift */, - 1F7FC8CB1FD2700B006CC979 /* MessagesViewControllerSpec.swift */, - 8962AC7D1F87AB230030B058 /* MessagesViewControllerTests.swift */, - ); - path = ControllersTest; - sourceTree = ""; - }; - 8962AC7E1F87AB230030B058 /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 8962AC7F1F87AB230030B058 /* Info.plist */, - ); - path = "Supporting Files"; - sourceTree = ""; - }; - 8962AC801F87AB230030B058 /* ProtocolsTests */ = { - isa = PBXGroup; - children = ( - 8962AC811F87AB230030B058 /* MessagesDisplayDelegateTests.swift */, - ); - path = ProtocolsTests; - sourceTree = ""; - }; - 8962AC821F87AB230030B058 /* ViewsTests */ = { - isa = PBXGroup; - children = ( - 8962AC831F87AB230030B058 /* MessagesCollectionViewTests.swift */, - 8962AC851F87AB230030B058 /* AvatarViewTests.swift */, - 8962AC861F87AB230030B058 /* MessageCollectionViewCellTests.swift */, - ); - path = ViewsTests; - sourceTree = ""; - }; - B01E2DCC1F5BA34700E4FA1C /* Assets */ = { - isa = PBXGroup; - children = ( - B0C8D99A1F73076B000A86E4 /* MessageKitAssets.bundle */, - ); - name = Assets; - path = ../Assets; - sourceTree = ""; - }; - B09643981F295D43004D0129 /* Extensions */ = { - isa = PBXGroup; - children = ( - 0EE91E651FDEC887005420A2 /* CGRect+Extensions.swift */, - B7A03F671F8669EB006AEF79 /* Bundle+Extensions.swift */, - B7A03F681F8669EB006AEF79 /* NSAttributedString+Extensions.swift */, - B7A03F651F8669EB006AEF79 /* UIColor+Extensions.swift */, - B7A03F661F8669EB006AEF79 /* UIView+Extensions.swift */, - 1FCA6D2F201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift */, - ); - path = Extensions; - sourceTree = ""; - }; - B09643991F295D58004D0129 /* Models */ = { - isa = PBXGroup; - children = ( - B7A03F1C1F866895006AEF79 /* Avatar.swift */, - 1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */, - B7A03F211F866895006AEF79 /* DetectorType.swift */, - 382C794121705D2000F4FAF5 /* HorizontalEdgeInsets.swift */, - B7A03F221F866895006AEF79 /* LabelAlignment.swift */, - B7A03F1D1F866895006AEF79 /* LocationMessageSnapshotOptions.swift */, - B7A03F231F866895006AEF79 /* MessageKind.swift */, - B7A03F1B1F866895006AEF79 /* MessageKitDateFormatter.swift */, - 1FF377A320087C82004FD648 /* MessageKitError.swift */, - B7A03F1F1F866895006AEF79 /* MessageStyle.swift */, - B7A03F1A1F866895006AEF79 /* NSConstraintLayoutSet.swift */, - B7A03F1E1F866895006AEF79 /* Sender.swift */, - ); - path = Models; - sourceTree = ""; - }; - B096439A1F295D68004D0129 /* Views */ = { - isa = PBXGroup; - children = ( - 2EB618F01F84676A007FBA0E /* Cells */, - 2EB618F11F846899007FBA0E /* Headers & Footers */, - B7A03F3E1F86694F006AEF79 /* AvatarView.swift */, - 1FE783A7206633C0007FA024 /* InsetLabel.swift */, - B7A03F431F86694F006AEF79 /* MessageContainerView.swift */, - B7A03F3F1F86694F006AEF79 /* MessageLabel.swift */, - B7A03F451F86694F006AEF79 /* MessagesCollectionView.swift */, - B7A03F441F86694F006AEF79 /* PlayButtonView.swift */, - ); - path = Views; - sourceTree = ""; - }; - B096439B1F295D82004D0129 /* Protocols */ = { - isa = PBXGroup; - children = ( - B7A03F521F8669C9006AEF79 /* MessageCellDelegate.swift */, - B7A03F551F8669C9006AEF79 /* MessageLabelDelegate.swift */, - B7A03F571F8669CA006AEF79 /* MessagesDataSource.swift */, - B7A03F561F8669C9006AEF79 /* MessagesDisplayDelegate.swift */, - B7A03F541F8669C9006AEF79 /* MessagesLayoutDelegate.swift */, - B7A03F511F8669C9006AEF79 /* MessageType.swift */, - 1FD5895F2064E08A004B5081 /* MediaItem.swift */, - 1FD5896320660C1C004B5081 /* LocationItem.swift */, - ); - path = Protocols; - sourceTree = ""; - }; - B096439C1F295DA9004D0129 /* Controllers */ = { - isa = PBXGroup; - children = ( - B7A03F4E1F86697C006AEF79 /* MessagesViewController.swift */, - 1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */, - 1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */, - ); - path = Controllers; - sourceTree = ""; - }; - B096439D1F295DC3004D0129 /* Layout */ = { - isa = PBXGroup; - children = ( - B7A03F161F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift */, - B7A03F171F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift */, - 1FE7839D20662835007FA024 /* MessageSizeCalculator.swift */, - 1FE783A120662905007FA024 /* TextMessageSizeCalculator.swift */, - 1FE783A3206629A5007FA024 /* MediaMessageSizeCalculator.swift */, - 1FE783A5206629C2007FA024 /* LocationMessageSizeCalculator.swift */, - 0EF0888B206F7E83007F2F58 /* CellSizeCalculator.swift */, - ); - path = Layout; - sourceTree = ""; - }; - B096439E1F295DD7004D0129 /* Supporting */ = { - isa = PBXGroup; - children = ( - B7A03F711F866A06006AEF79 /* Info.plist */, - B7A03F721F866A06006AEF79 /* MessageKit.h */, - B7A03F701F866A06006AEF79 /* MessageKit+Availability.swift */, - ); - path = Supporting; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXHeadersBuildPhase section */ - 88916B1F1CF0DF2F00469F91 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - B7A03F751F866A06006AEF79 /* MessageKit.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXHeadersBuildPhase section */ - -/* Begin PBXNativeTarget section */ - 88916B211CF0DF2F00469F91 /* MessageKit */ = { - isa = PBXNativeTarget; - buildConfigurationList = 88916B361CF0DF2F00469F91 /* Build configuration list for PBXNativeTarget "MessageKit" */; - buildPhases = ( - 88916B1D1CF0DF2F00469F91 /* Sources */, - 88916B1E1CF0DF2F00469F91 /* Frameworks */, - 88916B1F1CF0DF2F00469F91 /* Headers */, - 88916B201CF0DF2F00469F91 /* Resources */, - B03FF9A51F30398900754FE5 /* ShellScript */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = MessageKit; - productName = MessageKit; - productReference = 88916B221CF0DF2F00469F91 /* MessageKit.framework */; - productType = "com.apple.product-type.framework"; - }; - 88916B2B1CF0DF2F00469F91 /* MessageKitTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 88916B391CF0DF2F00469F91 /* Build configuration list for PBXNativeTarget "MessageKitTests" */; - buildPhases = ( - 88916B281CF0DF2F00469F91 /* Sources */, - 88916B291CF0DF2F00469F91 /* Frameworks */, - 88916B2A1CF0DF2F00469F91 /* Resources */, - 1F087D431FD274D300E95A45 /* CopyFiles */, - ); - buildRules = ( - ); - dependencies = ( - 88916B2F1CF0DF2F00469F91 /* PBXTargetDependency */, - ); - name = MessageKitTests; - productName = MessageKitTests; - productReference = 88916B2C1CF0DF2F00469F91 /* MessageKitTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 88916B191CF0DF2F00469F91 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0940; - ORGANIZATIONNAME = MessageKit; - TargetAttributes = { - 88916B211CF0DF2F00469F91 = { - CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 0800; - }; - 88916B2B1CF0DF2F00469F91 = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 88916B1C1CF0DF2F00469F91 /* Build configuration list for PBXProject "MessageKit" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; - hasScannedForEncodings = 0; - knownRegions = ( - en, - ); - mainGroup = 88916B181CF0DF2F00469F91; - productRefGroup = 88916B231CF0DF2F00469F91 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 88916B211CF0DF2F00469F91 /* MessageKit */, - 88916B2B1CF0DF2F00469F91 /* MessageKitTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 88916B201CF0DF2F00469F91 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B0C8D99B1F73076B000A86E4 /* MessageKitAssets.bundle in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 88916B2A1CF0DF2F00469F91 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - B03FF9A51F30398900754FE5 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 88916B1D1CF0DF2F00469F91 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 382C794221705D2000F4FAF5 /* HorizontalEdgeInsets.swift in Sources */, - B7A03F3C1F866946006AEF79 /* LocationMessageCell.swift in Sources */, - 1FF377AA20087D78004FD648 /* MessagesViewController+Menu.swift in Sources */, - B7A03F5B1F8669CA006AEF79 /* MessageType.swift in Sources */, - B7A03F601F8669CA006AEF79 /* MessagesDisplayDelegate.swift in Sources */, - 1FE783A8206633C0007FA024 /* InsetLabel.swift in Sources */, - B7A03F5C1F8669CA006AEF79 /* MessageCellDelegate.swift in Sources */, - 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */, - 1F6C040E206A2AF4007BDE44 /* MessageReusableView.swift in Sources */, - B7A03F4B1F86694F006AEF79 /* MessageContainerView.swift in Sources */, - B7A03F281F866895006AEF79 /* LocationMessageSnapshotOptions.swift in Sources */, - B7A03F6C1F8669EB006AEF79 /* UIView+Extensions.swift in Sources */, - B7A03F3A1F866946006AEF79 /* TextMessageCell.swift in Sources */, - B7A03F191F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift in Sources */, - B7A03F461F86694F006AEF79 /* AvatarView.swift in Sources */, - 1FCA6D30201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift in Sources */, - B7A03F3D1F866946006AEF79 /* MediaMessageCell.swift in Sources */, - 1FE783A220662905007FA024 /* TextMessageSizeCalculator.swift in Sources */, - B7A03F2E1F866895006AEF79 /* MessageKind.swift in Sources */, - B7A03F7B1F866B85006AEF79 /* MessageCollectionViewCell.swift in Sources */, - B7A03F6B1F8669EB006AEF79 /* UIColor+Extensions.swift in Sources */, - B7A03F5E1F8669CA006AEF79 /* MessagesLayoutDelegate.swift in Sources */, - B7A03F261F866895006AEF79 /* MessageKitDateFormatter.swift in Sources */, - B7A03F6D1F8669EB006AEF79 /* Bundle+Extensions.swift in Sources */, - 1FD589602064E08A004B5081 /* MediaItem.swift in Sources */, - B7A03F611F8669CA006AEF79 /* MessagesDataSource.swift in Sources */, - B7A03F6E1F8669EB006AEF79 /* NSAttributedString+Extensions.swift in Sources */, - B7A03F471F86694F006AEF79 /* MessageLabel.swift in Sources */, - B7A03F5F1F8669CA006AEF79 /* MessageLabelDelegate.swift in Sources */, - B7A03F4C1F86694F006AEF79 /* PlayButtonView.swift in Sources */, - 1FD5896420660C1C004B5081 /* LocationItem.swift in Sources */, - B7A03F291F866895006AEF79 /* Sender.swift in Sources */, - B7A03F4F1F86697C006AEF79 /* MessagesViewController.swift in Sources */, - B7A03F251F866895006AEF79 /* NSConstraintLayoutSet.swift in Sources */, - 0EE91E661FDEC888005420A2 /* CGRect+Extensions.swift in Sources */, - B7A03F181F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift in Sources */, - B7A03F2A1F866895006AEF79 /* MessageStyle.swift in Sources */, - B7A03F4D1F86694F006AEF79 /* MessagesCollectionView.swift in Sources */, - 0EF0888C206F7E83007F2F58 /* CellSizeCalculator.swift in Sources */, - 1FE783A4206629A5007FA024 /* MediaMessageSizeCalculator.swift in Sources */, - B7A03F731F866A06006AEF79 /* MessageKit+Availability.swift in Sources */, - 1FE7839E20662835007FA024 /* MessageSizeCalculator.swift in Sources */, - 1FE783A6206629C2007FA024 /* LocationMessageSizeCalculator.swift in Sources */, - B7A03F2D1F866895006AEF79 /* LabelAlignment.swift in Sources */, - 1F6C040C206A2891007BDE44 /* MessageContentCell.swift in Sources */, - B7A03F2C1F866895006AEF79 /* DetectorType.swift in Sources */, - B7A03F271F866895006AEF79 /* Avatar.swift in Sources */, - 1F82D1431FB1B75B00B81A88 /* AvatarPosition.swift in Sources */, - 1FF377AC20087DA2004FD648 /* MessagesViewController+Keyboard.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 88916B281CF0DF2F00469F91 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 8962AC8C1F87AB7D0030B058 /* AvatarViewTests.swift in Sources */, - 1F066E141FD90BB700E11013 /* MessageLabelSpec.swift in Sources */, - 8962AC941F87AB860030B058 /* MessageKitDateFormatterTests.swift in Sources */, - 8962AC911F87AB860030B058 /* MessagesViewControllerTests.swift in Sources */, - 1F066E231FDA3F0200E11013 /* DetectorTypeSpec.swift in Sources */, - 1F066E211FDA3DEA00E11013 /* AvatarSpec.swift in Sources */, - 1FD589612064E1B5004B5081 /* MockMessagesDataSource.swift in Sources */, - 1F066E131FD90BB600E11013 /* MessagesViewControllerSpec.swift in Sources */, - 1F066E1D1FDA3C1700E11013 /* SenderSpec.swift in Sources */, - 8962AC8A1F87AB7D0030B058 /* MessagesCollectionViewTests.swift in Sources */, - 8962AC8D1F87AB7D0030B058 /* MessageCollectionViewCellTests.swift in Sources */, - 8962AC991F87AB860030B058 /* MessagesDisplayDelegateTests.swift in Sources */, - 1FD589622064E1B9004B5081 /* MockMessage.swift in Sources */, - 8962AC951F87AB860030B058 /* MessageStyleTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 88916B2F1CF0DF2F00469F91 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 88916B211CF0DF2F00469F91 /* MessageKit */; - targetProxy = 88916B2E1CF0DF2F00469F91 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin XCBuildConfiguration section */ - 88916B341CF0DF2F00469F91 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - 88916B351CF0DF2F00469F91 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - 88916B371CF0DF2F00469F91 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Carthage/Build/iOS", - ); - INFOPLIST_FILE = "$(SRCROOT)/Sources/Supporting/Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks $(FRAMEWORK_SEARCH_PATHS)"; - PRODUCT_BUNDLE_IDENTIFIER = com.messagekit.MessageKit; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.2; - }; - name = Debug; - }; - 88916B381CF0DF2F00469F91 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Carthage/Build/iOS", - ); - INFOPLIST_FILE = "$(SRCROOT)/Sources/Supporting/Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks $(FRAMEWORK_SEARCH_PATHS)"; - PRODUCT_BUNDLE_IDENTIFIER = com.messagekit.MessageKit; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 4.2; - }; - name = Release; - }; - 88916B3A1CF0DF2F00469F91 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Carthage/Build/iOS", - ); - INFOPLIST_FILE = "Tests/Supporting Files/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks $(FRAMEWORK_SEARCH_PATHS)"; - PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.MessageKitTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 88916B3B1CF0DF2F00469F91 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Carthage/Build/iOS", - ); - INFOPLIST_FILE = "Tests/Supporting Files/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks $(FRAMEWORK_SEARCH_PATHS)"; - PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.MessageKitTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 88916B1C1CF0DF2F00469F91 /* Build configuration list for PBXProject "MessageKit" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 88916B341CF0DF2F00469F91 /* Debug */, - 88916B351CF0DF2F00469F91 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 88916B361CF0DF2F00469F91 /* Build configuration list for PBXNativeTarget "MessageKit" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 88916B371CF0DF2F00469F91 /* Debug */, - 88916B381CF0DF2F00469F91 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 88916B391CF0DF2F00469F91 /* Build configuration list for PBXNativeTarget "MessageKitTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 88916B3A1CF0DF2F00469F91 /* Debug */, - 88916B3B1CF0DF2F00469F91 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 88916B191CF0DF2F00469F91 /* Project object */; -} diff --git a/MessageKit.xcodeproj/project.pbxproj.orig b/MessageKit.xcodeproj/project.pbxproj.orig deleted file mode 100644 index 3db295189..000000000 --- a/MessageKit.xcodeproj/project.pbxproj.orig +++ /dev/null @@ -1,814 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 171D5AB91F36712B0053DF69 /* InputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171D5AB81F36712B0053DF69 /* InputTextView.swift */; }; - 1919E6121F850A3B00DE85BF /* AvatarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1919E60A1F850A1100DE85BF /* AvatarTests.swift */; }; - 1919E6131F850A3E00DE85BF /* DetectorTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1919E60B1F850A1100DE85BF /* DetectorTypeTests.swift */; }; - 1919E6141F850A3E00DE85BF /* MessageKitDateFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1919E60C1F850A1100DE85BF /* MessageKitDateFormatterTests.swift */; }; - 1919E6151F850A3E00DE85BF /* MessagesDisplayDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1919E60D1F850A1100DE85BF /* MessagesDisplayDelegateTests.swift */; }; - 1919E6161F850A3E00DE85BF /* MessageStyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1919E60E1F850A1100DE85BF /* MessageStyleTests.swift */; }; - 1919E6171F850A3E00DE85BF /* SenderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1919E60F1F850A1100DE85BF /* SenderTests.swift */; }; - 1919E61A1F850A6A00DE85BF /* AvatarViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1919E6001F850A1100DE85BF /* AvatarViewTests.swift */; }; - 1919E61B1F850A6A00DE85BF /* InputBarItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1919E6011F850A1100DE85BF /* InputBarItemTests.swift */; }; - 1919E61C1F850A6A00DE85BF /* InputTextViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1919E6021F850A1100DE85BF /* InputTextViewTests.swift */; }; - 1919E61D1F850A6A00DE85BF /* MessageCollectionViewCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1919E6031F850A1100DE85BF /* MessageCollectionViewCellTests.swift */; }; - 1919E61E1F850A6A00DE85BF /* MessageDateHeaderViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1919E6041F850A1100DE85BF /* MessageDateHeaderViewTests.swift */; }; - 1919E61F1F850A6A00DE85BF /* MessageInputBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1919E6051F850A1100DE85BF /* MessageInputBarTests.swift */; }; - 1919E6201F850A6A00DE85BF /* MessagesCollectionViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1919E6061F850A1100DE85BF /* MessagesCollectionViewTests.swift */; }; - 195F03DD1F850C7000066E32 /* MessagesViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 195F03DC1F850C7000066E32 /* MessagesViewControllerTests.swift */; }; - 195F03E11F865BD300066E32 /* MockMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 195F03E01F865BD300066E32 /* MockMessage.swift */; }; - 195F03E31F865C3200066E32 /* MockMessagesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 195F03E21F865C3200066E32 /* MockMessagesDataSource.swift */; }; - 372F6AEB1F36C15600B57FBD /* AvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372F6AEA1F36C15600B57FBD /* AvatarView.swift */; }; - 37C936981F38F6AC00853DF2 /* Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C936971F38F6AC00853DF2 /* Avatar.swift */; }; - 38C8679A1F50EA1000811974 /* NSConstraintLayoutSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C867981F50EA1000811974 /* NSConstraintLayoutSet.swift */; }; - 38C8679B1F50EA1000811974 /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C867991F50EA1000811974 /* UIView+Extensions.swift */; }; - 38C8679E1F50EA4A00811974 /* InputBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C8679D1F50EA4A00811974 /* InputBarItem.swift */; }; - 882D75841DE507320033F95F /* MessagesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882D75831DE507320033F95F /* MessagesDataSource.swift */; }; - 888CEBFC1D3FD525005178DE /* MessagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 888CEBFB1D3FD525005178DE /* MessagesViewController.swift */; }; - 88916B2D1CF0DF2F00469F91 /* MessageKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 88916B221CF0DF2F00469F91 /* MessageKit.framework */; }; - 88916B401CF0DF5100469F91 /* MessageKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 88916B3E1CF0DF5100469F91 /* MessageKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 88916B471CF0DFE600469F91 /* MessageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88916B461CF0DFE600469F91 /* MessageType.swift */; }; - B01049101F6F8684008DBA58 /* PlayButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B010490F1F6F8684008DBA58 /* PlayButtonView.swift */; }; - B01049121F6F86E8008DBA58 /* LocationMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B01049111F6F86E8008DBA58 /* LocationMessageCell.swift */; }; - B01280F31F4E8798004BCD3E /* MessageLabelDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B01280F21F4E8798004BCD3E /* MessageLabelDelegate.swift */; }; - B0147C831F5BE9220035B36E /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0147C821F5BE9220035B36E /* Bundle+Extensions.swift */; }; - B0147C901F5ED0810035B36E /* MessageDateHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0147C8F1F5ED0810035B36E /* MessageDateHeaderView.swift */; }; - B0147C921F6002150035B36E /* MessageKitDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0147C911F6002140035B36E /* MessageKitDateFormatter.swift */; }; - B0147C961F600E290035B36E /* MessageKit+Availability.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0147C951F600E290035B36E /* MessageKit+Availability.swift */; }; - B0147C981F61AF930035B36E /* LabelAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0147C971F61AF910035B36E /* LabelAlignment.swift */; }; - B015E8191F24623D007EDFB6 /* MessagesCollectionViewLayoutAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B015E8181F24623D007EDFB6 /* MessagesCollectionViewLayoutAttributes.swift */; }; - B015E81F1F259D8E007EDFB6 /* MessageInputBarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B015E81E1F259D8E007EDFB6 /* MessageInputBarDelegate.swift */; }; - B01E2DD81F5BBDB800E4FA1C /* MessageStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B01E2DD71F5BBDB800E4FA1C /* MessageStyle.swift */; }; - B0291DA91F6DBB9F00BEDF03 /* TextMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0291DA81F6DBB9F00BEDF03 /* TextMessageCell.swift */; }; - B03FF9AF1F31BB1200754FE5 /* MessageCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B03FF9AE1F31BB1200754FE5 /* MessageCellDelegate.swift */; }; - B05530B51F493CFA008BB420 /* DetectorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B05530B41F493CFA008BB420 /* DetectorType.swift */; }; - B0655A2A1F23D77200542A83 /* Sender.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0655A291F23D77200542A83 /* Sender.swift */; }; - B0655A2C1F23D81600542A83 /* MessageData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0655A2B1F23D81600542A83 /* MessageData.swift */; }; - B0655A2E1F23D8BC00542A83 /* MessagesCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0655A2D1F23D8BC00542A83 /* MessagesCollectionView.swift */; }; - B0655A381F23EE8B00542A83 /* MessageInputBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0655A371F23EE8B00542A83 /* MessageInputBar.swift */; }; - B0655A4D1F244C0600542A83 /* MessageCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0655A4C1F244C0600542A83 /* MessageCollectionViewCell.swift */; }; - B0655A4F1F245C5A00542A83 /* MessagesCollectionViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0655A4E1F245C5A00542A83 /* MessagesCollectionViewFlowLayout.swift */; }; - B06D19331F70D2CA0020E416 /* LocationMessageLayoutDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B06D19321F70D2CA0020E416 /* LocationMessageLayoutDelegate.swift */; }; - B06D19351F70D3170020E416 /* MediaMessageLayoutDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B06D19341F70D3170020E416 /* MediaMessageLayoutDelegate.swift */; }; - B06D19371F70D3580020E416 /* LocationMessageSnapshotOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B06D19361F70D3580020E416 /* LocationMessageSnapshotOptions.swift */; }; - B074EE931F35587100ABB8C8 /* MessageHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B074EE921F35587100ABB8C8 /* MessageHeaderView.swift */; }; - B074EE951F35588A00ABB8C8 /* MessageFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B074EE941F35588A00ABB8C8 /* MessageFooterView.swift */; }; - B074EE971F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B074EE961F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift */; }; - B074EEA81F3971A600ABB8C8 /* MessageLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B074EEA71F3971A600ABB8C8 /* MessageLabel.swift */; }; - B09643861F286C9E004D0129 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B09643851F286C9E004D0129 /* String+Extensions.swift */; }; - B096438E1F2890FB004D0129 /* MessagesDisplayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B096438D1F2890FB004D0129 /* MessagesDisplayDelegate.swift */; }; - B09643901F289142004D0129 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B096438F1F289142004D0129 /* UIColor+Extensions.swift */; }; - B0AA1F511F42E91A00BAE583 /* AvatarAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0AA1F501F42E91A00BAE583 /* AvatarAlignment.swift */; }; - B0AA1F531F44388C00BAE583 /* NSAttributedString+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0AA1F521F44388900BAE583 /* NSAttributedString+Extensions.swift */; }; - B0C8D99B1F73076B000A86E4 /* MessageKitAssets.bundle in Resources */ = {isa = PBXBuildFile; fileRef = B0C8D99A1F73076B000A86E4 /* MessageKitAssets.bundle */; }; - B0D943BE1F6DC9AB008B7BFD /* MediaMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0D943BD1F6DC9AB008B7BFD /* MediaMessageCell.swift */; }; - B0E1756F1F655A1600F0DEF6 /* AvatarHorizontalAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0E1756E1F655A1600F0DEF6 /* AvatarHorizontalAlignment.swift */; }; - D88EBBE01F85252E00F63AD2 /* MockMessagesDisplayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88EBBDF1F85252E00F63AD2 /* MockMessagesDisplayDelegate.swift */; }; - E8586DB51F59A1C300C9BE9D /* MessageContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8586DB41F59A1C300C9BE9D /* MessageContainerView.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 88916B2E1CF0DF2F00469F91 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 88916B191CF0DF2F00469F91 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 88916B211CF0DF2F00469F91; - remoteInfo = MessageKit; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXFileReference section */ - 171D5AB81F36712B0053DF69 /* InputTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTextView.swift; sourceTree = ""; }; - 1919E6001F850A1100DE85BF /* AvatarViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarViewTests.swift; sourceTree = ""; }; - 1919E6011F850A1100DE85BF /* InputBarItemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputBarItemTests.swift; sourceTree = ""; }; - 1919E6021F850A1100DE85BF /* InputTextViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputTextViewTests.swift; sourceTree = ""; }; - 1919E6031F850A1100DE85BF /* MessageCollectionViewCellTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewCellTests.swift; sourceTree = ""; }; - 1919E6041F850A1100DE85BF /* MessageDateHeaderViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDateHeaderViewTests.swift; sourceTree = ""; }; - 1919E6051F850A1100DE85BF /* MessageInputBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageInputBarTests.swift; sourceTree = ""; }; - 1919E6061F850A1100DE85BF /* MessagesCollectionViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesCollectionViewTests.swift; sourceTree = ""; }; - 1919E60A1F850A1100DE85BF /* AvatarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarTests.swift; sourceTree = ""; }; - 1919E60B1F850A1100DE85BF /* DetectorTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectorTypeTests.swift; sourceTree = ""; }; - 1919E60C1F850A1100DE85BF /* MessageKitDateFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageKitDateFormatterTests.swift; sourceTree = ""; }; - 1919E60D1F850A1100DE85BF /* MessagesDisplayDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesDisplayDelegateTests.swift; sourceTree = ""; }; - 1919E60E1F850A1100DE85BF /* MessageStyleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageStyleTests.swift; sourceTree = ""; }; - 1919E60F1F850A1100DE85BF /* SenderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SenderTests.swift; sourceTree = ""; }; - 1919E6111F850A1100DE85BF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 195F03DC1F850C7000066E32 /* MessagesViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesViewControllerTests.swift; sourceTree = ""; }; - 195F03E01F865BD300066E32 /* MockMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMessage.swift; sourceTree = ""; }; - 195F03E21F865C3200066E32 /* MockMessagesDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMessagesDataSource.swift; sourceTree = ""; }; - 372F6AEA1F36C15600B57FBD /* AvatarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarView.swift; sourceTree = ""; }; - 37C936971F38F6AC00853DF2 /* Avatar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Avatar.swift; sourceTree = ""; }; - 38C867981F50EA1000811974 /* NSConstraintLayoutSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSConstraintLayoutSet.swift; sourceTree = ""; }; - 38C867991F50EA1000811974 /* UIView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = ""; }; - 38C8679D1F50EA4A00811974 /* InputBarItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputBarItem.swift; sourceTree = ""; }; - 882D75831DE507320033F95F /* MessagesDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDataSource.swift; sourceTree = ""; }; - 888CEBFB1D3FD525005178DE /* MessagesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesViewController.swift; sourceTree = ""; }; - 88916B221CF0DF2F00469F91 /* MessageKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MessageKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 88916B2C1CF0DF2F00469F91 /* MessageKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MessageKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 88916B3D1CF0DF5100469F91 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 88916B3E1CF0DF5100469F91 /* MessageKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MessageKit.h; sourceTree = ""; }; - 88916B461CF0DFE600469F91 /* MessageType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageType.swift; sourceTree = ""; }; - B010490F1F6F8684008DBA58 /* PlayButtonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayButtonView.swift; sourceTree = ""; }; - B01049111F6F86E8008DBA58 /* LocationMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationMessageCell.swift; sourceTree = ""; }; - B01280F21F4E8798004BCD3E /* MessageLabelDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageLabelDelegate.swift; sourceTree = ""; }; - B0147C821F5BE9220035B36E /* Bundle+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+Extensions.swift"; sourceTree = ""; }; - B0147C8F1F5ED0810035B36E /* MessageDateHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageDateHeaderView.swift; sourceTree = ""; }; - B0147C911F6002140035B36E /* MessageKitDateFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageKitDateFormatter.swift; sourceTree = ""; }; - B0147C951F600E290035B36E /* MessageKit+Availability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MessageKit+Availability.swift"; sourceTree = ""; }; - B0147C971F61AF910035B36E /* LabelAlignment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelAlignment.swift; sourceTree = ""; }; - B015E8181F24623D007EDFB6 /* MessagesCollectionViewLayoutAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesCollectionViewLayoutAttributes.swift; sourceTree = ""; }; - B015E81E1F259D8E007EDFB6 /* MessageInputBarDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageInputBarDelegate.swift; sourceTree = ""; }; - B01E2DD71F5BBDB800E4FA1C /* MessageStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageStyle.swift; sourceTree = ""; }; - B0291DA81F6DBB9F00BEDF03 /* TextMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextMessageCell.swift; sourceTree = ""; }; - B03FF9AE1F31BB1200754FE5 /* MessageCellDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCellDelegate.swift; sourceTree = ""; }; - B05530B41F493CFA008BB420 /* DetectorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetectorType.swift; sourceTree = ""; }; - B0655A291F23D77200542A83 /* Sender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sender.swift; sourceTree = ""; }; - B0655A2B1F23D81600542A83 /* MessageData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageData.swift; sourceTree = ""; }; - B0655A2D1F23D8BC00542A83 /* MessagesCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesCollectionView.swift; sourceTree = ""; }; - B0655A371F23EE8B00542A83 /* MessageInputBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageInputBar.swift; sourceTree = ""; }; - B0655A4C1F244C0600542A83 /* MessageCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewCell.swift; sourceTree = ""; }; - B0655A4E1F245C5A00542A83 /* MessagesCollectionViewFlowLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesCollectionViewFlowLayout.swift; sourceTree = ""; }; - B06D19321F70D2CA0020E416 /* LocationMessageLayoutDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationMessageLayoutDelegate.swift; sourceTree = ""; }; - B06D19341F70D3170020E416 /* MediaMessageLayoutDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaMessageLayoutDelegate.swift; sourceTree = ""; }; - B06D19361F70D3580020E416 /* LocationMessageSnapshotOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationMessageSnapshotOptions.swift; sourceTree = ""; }; - B074EE921F35587100ABB8C8 /* MessageHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHeaderView.swift; sourceTree = ""; }; - B074EE941F35588A00ABB8C8 /* MessageFooterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageFooterView.swift; sourceTree = ""; }; - B074EE961F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesLayoutDelegate.swift; sourceTree = ""; }; - B074EEA71F3971A600ABB8C8 /* MessageLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageLabel.swift; sourceTree = ""; }; - B09643851F286C9E004D0129 /* String+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; - B096438D1F2890FB004D0129 /* MessagesDisplayDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDisplayDelegate.swift; sourceTree = ""; }; - B096438F1F289142004D0129 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; - B0AA1F501F42E91A00BAE583 /* AvatarAlignment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarAlignment.swift; sourceTree = ""; }; - B0AA1F521F44388900BAE583 /* NSAttributedString+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Extensions.swift"; sourceTree = ""; }; - B0C8D99A1F73076B000A86E4 /* MessageKitAssets.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = MessageKitAssets.bundle; sourceTree = ""; }; - B0D943BD1F6DC9AB008B7BFD /* MediaMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaMessageCell.swift; sourceTree = ""; }; - B0E1756E1F655A1600F0DEF6 /* AvatarHorizontalAlignment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarHorizontalAlignment.swift; sourceTree = ""; }; - D88EBBDF1F85252E00F63AD2 /* MockMessagesDisplayDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMessagesDisplayDelegate.swift; sourceTree = ""; }; - E8586DB41F59A1C300C9BE9D /* MessageContainerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageContainerView.swift; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 88916B1E1CF0DF2F00469F91 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 88916B291CF0DF2F00469F91 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 88916B2D1CF0DF2F00469F91 /* MessageKit.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 1919E5FF1F850A1100DE85BF /* ViewsTests */ = { - isa = PBXGroup; - children = ( - 1919E6001F850A1100DE85BF /* AvatarViewTests.swift */, - 1919E6011F850A1100DE85BF /* InputBarItemTests.swift */, - 1919E6021F850A1100DE85BF /* InputTextViewTests.swift */, - 1919E6031F850A1100DE85BF /* MessageCollectionViewCellTests.swift */, - 1919E6041F850A1100DE85BF /* MessageDateHeaderViewTests.swift */, - 1919E6051F850A1100DE85BF /* MessageInputBarTests.swift */, - 1919E6061F850A1100DE85BF /* MessagesCollectionViewTests.swift */, - ); - path = ViewsTests; - sourceTree = ""; - }; - 1919E6091F850A1100DE85BF /* ModelTests */ = { - isa = PBXGroup; - children = ( - 1919E60A1F850A1100DE85BF /* AvatarTests.swift */, - 1919E60B1F850A1100DE85BF /* DetectorTypeTests.swift */, - 1919E60C1F850A1100DE85BF /* MessageKitDateFormatterTests.swift */, - 1919E60E1F850A1100DE85BF /* MessageStyleTests.swift */, - 1919E60F1F850A1100DE85BF /* SenderTests.swift */, - ); - path = ModelTests; - sourceTree = ""; - }; - 1919E6101F850A1100DE85BF /* Supporting Files */ = { - isa = PBXGroup; - children = ( -<<<<<<< HEAD -||||||| merged common ancestors - 1919E6071F850A1100DE85BF /* TestMessageModel.swift */, - 1919E6081F850A1100DE85BF /* TestMessagesViewControllerModel.swift */, - D88EBBDF1F85252E00F63AD2 /* MockMessagesDisplayDelegate.swift */, -======= -<<<<<<< HEAD - 1919E6071F850A1100DE85BF /* TestMessageModel.swift */, - 1919E6081F850A1100DE85BF /* TestMessagesViewControllerModel.swift */, - D88EBBDF1F85252E00F63AD2 /* MockMessagesDisplayDelegate.swift */, -||||||| merged common ancestors - 1919E6071F850A1100DE85BF /* TestMessageModel.swift */, - 1919E6081F850A1100DE85BF /* TestMessagesViewControllerModel.swift */, -======= ->>>>>>> 1eedca87d925fc7c1a75859037a9fc2f422d4908 ->>>>>>> Add several tests - 1919E6111F850A1100DE85BF /* Info.plist */, - ); - path = "Supporting Files"; - sourceTree = ""; - }; - 195F03DB1F850C6200066E32 /* ControllersTest */ = { - isa = PBXGroup; - children = ( - 195F03DC1F850C7000066E32 /* MessagesViewControllerTests.swift */, - ); - path = ControllersTest; - sourceTree = ""; - }; - 195F03DF1F865B9D00066E32 /* Mocks */ = { - isa = PBXGroup; - children = ( - 195F03E01F865BD300066E32 /* MockMessage.swift */, - 195F03E21F865C3200066E32 /* MockMessagesDataSource.swift */, - ); - path = Mocks; - sourceTree = ""; - }; - 88916B181CF0DF2F00469F91 = { - isa = PBXGroup; - children = ( - 88916B3C1CF0DF5100469F91 /* Sources */, - 88916B411CF0DF5900469F91 /* Tests */, - 88916B231CF0DF2F00469F91 /* Products */, - ); - sourceTree = ""; - }; - 88916B231CF0DF2F00469F91 /* Products */ = { - isa = PBXGroup; - children = ( - 88916B221CF0DF2F00469F91 /* MessageKit.framework */, - 88916B2C1CF0DF2F00469F91 /* MessageKitTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 88916B3C1CF0DF5100469F91 /* Sources */ = { - isa = PBXGroup; - children = ( - B01E2DCC1F5BA34700E4FA1C /* Assets */, - B096439D1F295DC3004D0129 /* Layout */, - B09643991F295D58004D0129 /* Models */, - B096439A1F295D68004D0129 /* Views */, - B096439C1F295DA9004D0129 /* Controllers */, - B096439B1F295D82004D0129 /* Protocols */, - B09643981F295D43004D0129 /* Extensions */, - B096439E1F295DD7004D0129 /* Supporting */, - ); - path = Sources; - sourceTree = ""; - }; - 88916B411CF0DF5900469F91 /* Tests */ = { - isa = PBXGroup; - children = ( - 195F03DB1F850C6200066E32 /* ControllersTest */, - 195F03DF1F865B9D00066E32 /* Mocks */, - 1919E6091F850A1100DE85BF /* ModelTests */, - 1919E6101F850A1100DE85BF /* Supporting Files */, - D88EBBDC1F85249E00F63AD2 /* ProtocolsTests */, - 1919E5FF1F850A1100DE85BF /* ViewsTests */, - ); - path = Tests; - sourceTree = SOURCE_ROOT; - }; - B01E2DCC1F5BA34700E4FA1C /* Assets */ = { - isa = PBXGroup; - children = ( - B0C8D99A1F73076B000A86E4 /* MessageKitAssets.bundle */, - ); - name = Assets; - path = ../Assets; - sourceTree = ""; - }; - B09643981F295D43004D0129 /* Extensions */ = { - isa = PBXGroup; - children = ( - 38C867991F50EA1000811974 /* UIView+Extensions.swift */, - B09643851F286C9E004D0129 /* String+Extensions.swift */, - B096438F1F289142004D0129 /* UIColor+Extensions.swift */, - B0AA1F521F44388900BAE583 /* NSAttributedString+Extensions.swift */, - B0147C821F5BE9220035B36E /* Bundle+Extensions.swift */, - ); - name = Extensions; - sourceTree = ""; - }; - B09643991F295D58004D0129 /* Models */ = { - isa = PBXGroup; - children = ( - B0655A291F23D77200542A83 /* Sender.swift */, - B0655A2B1F23D81600542A83 /* MessageData.swift */, - 37C936971F38F6AC00853DF2 /* Avatar.swift */, - B0AA1F501F42E91A00BAE583 /* AvatarAlignment.swift */, - B05530B41F493CFA008BB420 /* DetectorType.swift */, - B01E2DD71F5BBDB800E4FA1C /* MessageStyle.swift */, - 38C867981F50EA1000811974 /* NSConstraintLayoutSet.swift */, - B0147C911F6002140035B36E /* MessageKitDateFormatter.swift */, - B0147C971F61AF910035B36E /* LabelAlignment.swift */, - B0E1756E1F655A1600F0DEF6 /* AvatarHorizontalAlignment.swift */, - B06D19361F70D3580020E416 /* LocationMessageSnapshotOptions.swift */, - ); - name = Models; - sourceTree = ""; - }; - B096439A1F295D68004D0129 /* Views */ = { - isa = PBXGroup; - children = ( - B0655A2D1F23D8BC00542A83 /* MessagesCollectionView.swift */, - B0655A4C1F244C0600542A83 /* MessageCollectionViewCell.swift */, - 372F6AEA1F36C15600B57FBD /* AvatarView.swift */, - B0655A371F23EE8B00542A83 /* MessageInputBar.swift */, - 38C8679D1F50EA4A00811974 /* InputBarItem.swift */, - 171D5AB81F36712B0053DF69 /* InputTextView.swift */, - B074EE921F35587100ABB8C8 /* MessageHeaderView.swift */, - B074EE941F35588A00ABB8C8 /* MessageFooterView.swift */, - B074EEA71F3971A600ABB8C8 /* MessageLabel.swift */, - E8586DB41F59A1C300C9BE9D /* MessageContainerView.swift */, - B0147C8F1F5ED0810035B36E /* MessageDateHeaderView.swift */, - B0291DA81F6DBB9F00BEDF03 /* TextMessageCell.swift */, - B0D943BD1F6DC9AB008B7BFD /* MediaMessageCell.swift */, - B010490F1F6F8684008DBA58 /* PlayButtonView.swift */, - B01049111F6F86E8008DBA58 /* LocationMessageCell.swift */, - ); - name = Views; - sourceTree = ""; - }; - B096439B1F295D82004D0129 /* Protocols */ = { - isa = PBXGroup; - children = ( - B015E81E1F259D8E007EDFB6 /* MessageInputBarDelegate.swift */, - 88916B461CF0DFE600469F91 /* MessageType.swift */, - 882D75831DE507320033F95F /* MessagesDataSource.swift */, - B096438D1F2890FB004D0129 /* MessagesDisplayDelegate.swift */, - B03FF9AE1F31BB1200754FE5 /* MessageCellDelegate.swift */, - B074EE961F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift */, - B01280F21F4E8798004BCD3E /* MessageLabelDelegate.swift */, - B06D19321F70D2CA0020E416 /* LocationMessageLayoutDelegate.swift */, - B06D19341F70D3170020E416 /* MediaMessageLayoutDelegate.swift */, - ); - name = Protocols; - sourceTree = ""; - }; - B096439C1F295DA9004D0129 /* Controllers */ = { - isa = PBXGroup; - children = ( - 888CEBFB1D3FD525005178DE /* MessagesViewController.swift */, - ); - name = Controllers; - sourceTree = ""; - }; - B096439D1F295DC3004D0129 /* Layout */ = { - isa = PBXGroup; - children = ( - B0655A4E1F245C5A00542A83 /* MessagesCollectionViewFlowLayout.swift */, - B015E8181F24623D007EDFB6 /* MessagesCollectionViewLayoutAttributes.swift */, - ); - name = Layout; - sourceTree = ""; - }; - B096439E1F295DD7004D0129 /* Supporting */ = { - isa = PBXGroup; - children = ( - 88916B3D1CF0DF5100469F91 /* Info.plist */, - 88916B3E1CF0DF5100469F91 /* MessageKit.h */, - B0147C951F600E290035B36E /* MessageKit+Availability.swift */, - ); - name = Supporting; - sourceTree = ""; - }; - D88EBBDC1F85249E00F63AD2 /* ProtocolsTests */ = { - isa = PBXGroup; - children = ( - 1919E60D1F850A1100DE85BF /* MessagesDisplayDelegateTests.swift */, - ); - path = ProtocolsTests; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXHeadersBuildPhase section */ - 88916B1F1CF0DF2F00469F91 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - 88916B401CF0DF5100469F91 /* MessageKit.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXHeadersBuildPhase section */ - -/* Begin PBXNativeTarget section */ - 88916B211CF0DF2F00469F91 /* MessageKit */ = { - isa = PBXNativeTarget; - buildConfigurationList = 88916B361CF0DF2F00469F91 /* Build configuration list for PBXNativeTarget "MessageKit" */; - buildPhases = ( - 88916B1D1CF0DF2F00469F91 /* Sources */, - 88916B1E1CF0DF2F00469F91 /* Frameworks */, - 88916B1F1CF0DF2F00469F91 /* Headers */, - 88916B201CF0DF2F00469F91 /* Resources */, - B03FF9A51F30398900754FE5 /* ShellScript */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = MessageKit; - productName = MessageKit; - productReference = 88916B221CF0DF2F00469F91 /* MessageKit.framework */; - productType = "com.apple.product-type.framework"; - }; - 88916B2B1CF0DF2F00469F91 /* MessageKitTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 88916B391CF0DF2F00469F91 /* Build configuration list for PBXNativeTarget "MessageKitTests" */; - buildPhases = ( - 88916B281CF0DF2F00469F91 /* Sources */, - 88916B291CF0DF2F00469F91 /* Frameworks */, - 88916B2A1CF0DF2F00469F91 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 88916B2F1CF0DF2F00469F91 /* PBXTargetDependency */, - ); - name = MessageKitTests; - productName = MessageKitTests; - productReference = 88916B2C1CF0DF2F00469F91 /* MessageKitTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 88916B191CF0DF2F00469F91 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0900; - ORGANIZATIONNAME = MessageKit; - TargetAttributes = { - 88916B211CF0DF2F00469F91 = { - CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 0800; - }; - 88916B2B1CF0DF2F00469F91 = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 88916B1C1CF0DF2F00469F91 /* Build configuration list for PBXProject "MessageKit" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; - hasScannedForEncodings = 0; - knownRegions = ( - en, - ); - mainGroup = 88916B181CF0DF2F00469F91; - productRefGroup = 88916B231CF0DF2F00469F91 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 88916B211CF0DF2F00469F91 /* MessageKit */, - 88916B2B1CF0DF2F00469F91 /* MessageKitTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 88916B201CF0DF2F00469F91 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B0C8D99B1F73076B000A86E4 /* MessageKitAssets.bundle in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 88916B2A1CF0DF2F00469F91 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - B03FF9A51F30398900754FE5 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 88916B1D1CF0DF2F00469F91 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 171D5AB91F36712B0053DF69 /* InputTextView.swift in Sources */, - B06D19331F70D2CA0020E416 /* LocationMessageLayoutDelegate.swift in Sources */, - B0147C901F5ED0810035B36E /* MessageDateHeaderView.swift in Sources */, - 38C8679E1F50EA4A00811974 /* InputBarItem.swift in Sources */, - B01049121F6F86E8008DBA58 /* LocationMessageCell.swift in Sources */, - B074EE971F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift in Sources */, - 38C8679A1F50EA1000811974 /* NSConstraintLayoutSet.swift in Sources */, - B06D19351F70D3170020E416 /* MediaMessageLayoutDelegate.swift in Sources */, - 195F03E11F865BD300066E32 /* MockMessage.swift in Sources */, - 882D75841DE507320033F95F /* MessagesDataSource.swift in Sources */, - 888CEBFC1D3FD525005178DE /* MessagesViewController.swift in Sources */, - B0147C831F5BE9220035B36E /* Bundle+Extensions.swift in Sources */, - B06D19371F70D3580020E416 /* LocationMessageSnapshotOptions.swift in Sources */, - E8586DB51F59A1C300C9BE9D /* MessageContainerView.swift in Sources */, - B0655A4F1F245C5A00542A83 /* MessagesCollectionViewFlowLayout.swift in Sources */, - B0655A2C1F23D81600542A83 /* MessageData.swift in Sources */, - B0E1756F1F655A1600F0DEF6 /* AvatarHorizontalAlignment.swift in Sources */, - B01049101F6F8684008DBA58 /* PlayButtonView.swift in Sources */, - B09643861F286C9E004D0129 /* String+Extensions.swift in Sources */, - B015E81F1F259D8E007EDFB6 /* MessageInputBarDelegate.swift in Sources */, - 195F03E31F865C3200066E32 /* MockMessagesDataSource.swift in Sources */, - B01280F31F4E8798004BCD3E /* MessageLabelDelegate.swift in Sources */, - B0147C981F61AF930035B36E /* LabelAlignment.swift in Sources */, - B0655A2A1F23D77200542A83 /* Sender.swift in Sources */, - B0147C961F600E290035B36E /* MessageKit+Availability.swift in Sources */, - B074EE931F35587100ABB8C8 /* MessageHeaderView.swift in Sources */, - B0655A4D1F244C0600542A83 /* MessageCollectionViewCell.swift in Sources */, - B0147C921F6002150035B36E /* MessageKitDateFormatter.swift in Sources */, - B0655A2E1F23D8BC00542A83 /* MessagesCollectionView.swift in Sources */, - B0AA1F531F44388C00BAE583 /* NSAttributedString+Extensions.swift in Sources */, - B074EE951F35588A00ABB8C8 /* MessageFooterView.swift in Sources */, - B09643901F289142004D0129 /* UIColor+Extensions.swift in Sources */, - B0655A381F23EE8B00542A83 /* MessageInputBar.swift in Sources */, - 38C8679B1F50EA1000811974 /* UIView+Extensions.swift in Sources */, - B074EEA81F3971A600ABB8C8 /* MessageLabel.swift in Sources */, - 372F6AEB1F36C15600B57FBD /* AvatarView.swift in Sources */, - D88EBBE01F85252E00F63AD2 /* MockMessagesDisplayDelegate.swift in Sources */, - B05530B51F493CFA008BB420 /* DetectorType.swift in Sources */, - B01E2DD81F5BBDB800E4FA1C /* MessageStyle.swift in Sources */, - 88916B471CF0DFE600469F91 /* MessageType.swift in Sources */, - B0D943BE1F6DC9AB008B7BFD /* MediaMessageCell.swift in Sources */, - B0291DA91F6DBB9F00BEDF03 /* TextMessageCell.swift in Sources */, - B0AA1F511F42E91A00BAE583 /* AvatarAlignment.swift in Sources */, - B03FF9AF1F31BB1200754FE5 /* MessageCellDelegate.swift in Sources */, - 37C936981F38F6AC00853DF2 /* Avatar.swift in Sources */, - B096438E1F2890FB004D0129 /* MessagesDisplayDelegate.swift in Sources */, - B015E8191F24623D007EDFB6 /* MessagesCollectionViewLayoutAttributes.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 88916B281CF0DF2F00469F91 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 1919E61F1F850A6A00DE85BF /* MessageInputBarTests.swift in Sources */, - 1919E6161F850A3E00DE85BF /* MessageStyleTests.swift in Sources */, - 1919E6121F850A3B00DE85BF /* AvatarTests.swift in Sources */, - 1919E61A1F850A6A00DE85BF /* AvatarViewTests.swift in Sources */, - 1919E61C1F850A6A00DE85BF /* InputTextViewTests.swift in Sources */, - 1919E6171F850A3E00DE85BF /* SenderTests.swift in Sources */, - 1919E6141F850A3E00DE85BF /* MessageKitDateFormatterTests.swift in Sources */, - 1919E61B1F850A6A00DE85BF /* InputBarItemTests.swift in Sources */, - 195F03DD1F850C7000066E32 /* MessagesViewControllerTests.swift in Sources */, - 1919E61E1F850A6A00DE85BF /* MessageDateHeaderViewTests.swift in Sources */, - 1919E6131F850A3E00DE85BF /* DetectorTypeTests.swift in Sources */, - 1919E61D1F850A6A00DE85BF /* MessageCollectionViewCellTests.swift in Sources */, - 1919E6151F850A3E00DE85BF /* MessagesDisplayDelegateTests.swift in Sources */, - 1919E6201F850A6A00DE85BF /* MessagesCollectionViewTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 88916B2F1CF0DF2F00469F91 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 88916B211CF0DF2F00469F91 /* MessageKit */; - targetProxy = 88916B2E1CF0DF2F00469F91 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin XCBuildConfiguration section */ - 88916B341CF0DF2F00469F91 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - 88916B351CF0DF2F00469F91 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - 88916B371CF0DF2F00469F91 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.messagekit.MessageKit; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; - }; - name = Debug; - }; - 88916B381CF0DF2F00469F91 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.messagekit.MessageKit; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 4.0; - }; - name = Release; - }; - 88916B3A1CF0DF2F00469F91 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - INFOPLIST_FILE = "Tests/Supporting Files/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.MessageKitTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 88916B3B1CF0DF2F00469F91 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - INFOPLIST_FILE = "Tests/Supporting Files/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.MessageKitTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 88916B1C1CF0DF2F00469F91 /* Build configuration list for PBXProject "MessageKit" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 88916B341CF0DF2F00469F91 /* Debug */, - 88916B351CF0DF2F00469F91 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 88916B361CF0DF2F00469F91 /* Build configuration list for PBXNativeTarget "MessageKit" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 88916B371CF0DF2F00469F91 /* Debug */, - 88916B381CF0DF2F00469F91 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 88916B391CF0DF2F00469F91 /* Build configuration list for PBXNativeTarget "MessageKitTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 88916B3A1CF0DF2F00469F91 /* Debug */, - 88916B3B1CF0DF2F00469F91 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 88916B191CF0DF2F00469F91 /* Project object */; -} diff --git a/MessageKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/MessageKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 3ac286aca..000000000 --- a/MessageKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/MessageKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/MessageKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/MessageKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKit.xcscheme b/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKit.xcscheme deleted file mode 100644 index 0bb9eb20c..000000000 --- a/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKit.xcscheme +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKitTests.xcscheme b/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKitTests.xcscheme deleted file mode 100644 index 1569e1b64..000000000 --- a/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKitTests.xcscheme +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Package.swift b/Package.swift new file mode 100644 index 000000000..ca0bcbd50 --- /dev/null +++ b/Package.swift @@ -0,0 +1,47 @@ +// swift-tools-version:6.0 + +// MIT License +// +// Copyright (c) 2017-2022 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import PackageDescription + +let package = Package( + name: "MessageKit", + platforms: [.iOS(.v14)], + products: [ + .library(name: "MessageKit", targets: ["MessageKit"]) + ], + dependencies: [ + .package(url: "https://github.com/nathantannar4/InputBarAccessoryView", .upToNextMajor(from: "7.0.0")), + ], + targets: [ + // MARK: - MessageKit + + .target( + name: "MessageKit", + dependencies: ["InputBarAccessoryView"], + path: "Sources", + exclude: ["Supporting/Info.plist", "Supporting/MessageKit.h"], + swiftSettings: [SwiftSetting.define("IS_SPM")] + ), + .testTarget(name: "MessageKitTests", dependencies: ["MessageKit"]), + ], + swiftLanguageModes: [.v6] +) diff --git a/README.md b/README.md index 65544b432..260d48555 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,28 @@ -

- +

+

-

- +

+ A community-driven replacement for JSQMessagesViewController +

+

+ + + + + + Xcode + + + MIT + + + Contributions Welcome + +

+
+

+

- -[![CircleCI](https://circleci.com/gh/MessageKit/MessageKit.svg?style=svg)](https://circleci.com/gh/MessageKit/MessageKit) -[![codecov](https://codecov.io/gh/MessageKit/MessageKit/branch/master/graph/badge.svg)](https://codecov.io/gh/MessageKit/MessageKit) -[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) - - Swift - - - CocoaPods - - - Xcode - - - MIT - - - Contributions Welcome - ## Goals @@ -37,65 +32,66 @@ - Provide an awesome Open Source project for the iOS open source community. - Help others learn. -## Vision -See [VISION.md](https://github.com/MessageKit/MessageKit/blob/master/VISION.md) for Goals, Scope, & Technical Considerations. - ## Installation -### [CocoaPods](https://cocoapods.org/) **Recommended** -````ruby -# Swift 4.2 -pod 'MessageKit' -```` -> If you are already using Swift 5, use the `3.0.0-swift5` branch until the offical release is made +### [Swift Package Manager](https://swift.org/package-manager/) - **Recommended** -### [Carthage](https://github.com/Carthage/Carthage) +Swift 5.3 in Xcode 12 [added support](https://github.com/apple/swift-evolution/blob/master/proposals/0271-package-manager-resources.md) for assets in Swift Packages. +You can [just add](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app) MessageKit package to your project by entering it's repository URL -To integrate MessageKit using Carthage, add the following to your `Cartfile`: - -```` -github "MessageKit/MessageKit" -```` +``` +https://github.com/MessageKit/MessageKit +``` +Older versions of Swift and Xcode don't support MessageKit via SPM. -### [Manual]([https://github.com/MessageKit/MessageKit/blob/master/Documentation/MANUAL_INSTALLATION.md) +### [Manual](https://github.com/MessageKit/MessageKit/blob/master/Documentation/MANUAL_INSTALLATION.md) ## Requirements -- **iOS9** or later -- **Swift 4.2** or later +- **iOS 14** or later +- **Swift 6** or later + +> For iOS 13 or Swift 5.x please use version 4.3.0 + +> For iOS 12 or CocoaPods please use version 3.8.0 + +> For iOS 11 please use version 3.3.0 +> For iOS 9 and iOS 10 please use version 3.1.1 ## Getting Started -### Cell Structure -

- -

+Please have a look at the [Quick Start guide](https://github.com/MessageKit/MessageKit/blob/master/Documentation/QuickStart.md) and the [FAQs](https://github.com/MessageKit/MessageKit/blob/master/Documentation/FAQs.md). -Each default cell is a subclass of [`MessageContentCell`](https://github.com/MessageKit/MessageKit/blob/master/Sources/Views/Cells/MessageContentCell.swift) which has 7 parts. From top down we have a: `cellTopLabel`, `messageTopLabel`, `messageContainerView`, `messageBottomLabel`, `cellBottomLabel` with the `avatarView` and `accessoryView` on either side respectively. Above we see the basic [`TextMessageCell`](https://github.com/MessageKit/MessageKit/blob/master/Sources/Views/Cells/TextMessageCell.swift) which uses a `MessageLabel` as its main content. +We recommend you start by looking at the [Example](https://github.com/MessageKit/MessageKit/tree/master/Example) project or write a question with the "messagekit" tag on [Stack Overflow](https://stackoverflow.com/questions/tagged/messagekit). You can also look at previous issues here on GitHub with the **"Question"** tag. -This structure will allow you to create a layout that suits your needs as you can customize the size, appearance and padding of each. If you need something more advanced you can implement a custom cell, which we show how to do in the [Example](https://github.com/MessageKit/MessageKit/tree/master/Example) project. +For more on how to use the MessageInputBar, see the dependency it is based on [InputBarAccessoryView](https://github.com/nathantannar4/InputBarAccessoryView). You can also see this [short guide]([https://github.com/MessageKit/MessageKit/blob/master/Documentation/MessageInputBar.md) + +Check out the full documentation [here](https://messagekit.github.io/MessageKit/documentation/messagekit). + +### Cell Structure -### MessageInputBar Structure

- +

-The `MessageInputBar`, derrived from [InputBarAccessoryView](https://github.com/nathantannar4/InputBarAccessoryView) is a flexible and robust way of creating any kind of input layout you wish. It is self-sizing which means as the user types it will grow to fill available space. It is centered around the `middleContentView` which by default holds the `InputTextView`. This is surrounded by `InputStackView`'s that will also grow in high based on the needs of their subviews `intrinsicContentSize`. See the [Example](https://github.com/MessageKit/MessageKit/tree/master/Example) project for examples on how to taylor the layout for your own needs. +Each default cell is a subclass of [`MessageContentCell`](https://github.com/MessageKit/MessageKit/blob/master/Sources/Views/Cells/MessageContentCell.swift) which has 7 parts. From top down we have a: `cellTopLabel`, `messageTopLabel`, `messageContainerView`, `messageBottomLabel`, `cellBottomLabel` with the `avatarView` and `accessoryView` on either side respectively. Above we see the basic [`TextMessageCell`](https://github.com/MessageKit/MessageKit/blob/master/Sources/Views/Cells/TextMessageCell.swift) which uses a `MessageLabel` as its main content. -### Guides +This structure will allow you to create a layout that suits your needs as you can customize the size, appearance and padding of each. If you need something more advanced you can implement a custom cell, which we show how to do in the [Example](https://github.com/MessageKit/MessageKit/tree/master/Example) project. -Please have a look at the [Quick Start guide](https://github.com/MessageKit/MessageKit/blob/master/Documentation/QuickStart.md) and the [FAQs](https://github.com/MessageKit/MessageKit/blob/master/Documentation/FAQs.md). +### InputBarAccessoryView Structure -We recommend you start by looking at the [Example](https://github.com/MessageKit/MessageKit/tree/master/Example) project or write a question with the "messagekit" tag on [Stack Overflow](https://stackoverflow.com/questions/tagged/messagekit). You can also look at previous issues here on GitHib with the **"Question"** tag. +

+ +

-For more on how to use the MessageInputBar, see the dependency it is based on [InputBarAccessoryView](https://github.com/nathantannar4/InputBarAccessoryView). You can also see this [short guide]([https://github.com/MessageKit/MessageKit/blob/master/Documentation/MessageInputBar.md) +The `InputBarAccessoryView`, 3rd party dependency from [InputBarAccessoryView](https://github.com/nathantannar4/InputBarAccessoryView) is a flexible and robust way of creating any kind of input layout you wish. Check the repo and examples there for more info. ## Default Cells

- - + +

The type of cell rendered for a given message is based on the `MessageKind` @@ -110,6 +106,7 @@ public enum MessageKind { case emoji(String) // TextMessageCell case audio(AudioItem) // AudioMessageCell case contact(ContactItem) // ContactMessageCell + case linkPreview(LinkItem) // LinkPreviewMessageCell /// A custom message. /// - Note: Using this case requires that you implement the following methods and handle this case: @@ -120,19 +117,27 @@ public enum MessageKind { ``` If you choose to use the `.custom` kind you are responsible for all of the cells layout. Any `UICollectionViewCell` can be returned for custom cells which means any of the styling you provide from the `MessageDisplayDelegate` will not effect your custom cell. Even if you subclass your cell from `MessageContentCell`. +[Read more about custom cells](https://github.com/MessageKit/MessageKit/blob/master/Documentation/CUSTOM_CELLS.md) +[Read more about the cases on the Quick Start guide.](https://github.com/MessageKit/MessageKit/blob/master/Documentation/QuickStart.md#messagekind) ## Contributing +[![Tests](https://github.com/MessageKit/MessageKit/workflows/Tests/badge.svg)](https://github.com/MessageKit/MessageKit/actions?query=workflow%3A%22Tests%22) +[![Build framework](https://github.com/MessageKit/MessageKit/workflows/Build%20Framework/badge.svg)](https://github.com/MessageKit/MessageKit/actions?query=workflow%3A%22Build+Framework%22) +[![Build example app](https://github.com/MessageKit/MessageKit/workflows/Build%20Example%20app/badge.svg)](https://github.com/MessageKit/MessageKit/actions?query=workflow%3A%22PR+Example+app%22) +[![Danger](https://github.com/MessageKit/MessageKit/workflows/Danger/badge.svg)](https://github.com/MessageKit/MessageKit/actions?query=workflow%3A%22Danger%22) + Great! Look over these things first. + - Please read our [Code of Conduct](https://github.com/MessageKit/MessageKit/blob/master/CODE_OF_CONDUCT.md) - Check the [Contributing Guide Lines](https://github.com/MessageKit/MessageKit/blob/master/CONTRIBUTING.md). -- Come join us on [Slack](https://join.slack.com/t/messagekit/shared_invite/MjI4NzIzNzMyMzU0LTE1MDMwODIzMDUtYzllYzIyNTU4MA) and πŸ—£ don't be a stranger. -- Check out the [current issues](https://github.com/MessageKit/MessageKit/issues) and see if you can tackle any of those. -- Download the project and check out the current code base. Suggest any improvements by opening a new issue. +- Come join us on [Slack](https://join.slack.com/t/messagekit/shared_invite/zt-2484ymok0-O82~1EtnHALSngQvn6Xwyw) and πŸ—£ don't be a stranger. +- Check out the [current issues](https://github.com/MessageKit/MessageKit/issues) and see if you can tackle any of those. +- Download the project and check out the current code base. Suggest any improvements by opening a new issue. - Check out the [What's Next](#whats-next) section :point_down: to see where we are headed. - Check [StackOverflow](https://stackoverflow.com/questions/tagged/messagekit) -- Install [SwiftLint](https://github.com/realm/SwiftLint) too keep yourself in :neckbeard: style. +- Install [SwiftLint](https://github.com/realm/SwiftLint) to keep yourself in :neckbeard: style. - Be kind and helpful. @@ -150,26 +155,48 @@ Interested in contributing to MessageKit? Click here to join our [Slack](https:/ Add your app to the list of apps using this library and make a pull request. +- [ClassDojo](https://www.classdojo.com) +- [Coursicle](https://apps.apple.com/us/app/coursicle/id1187418307) +- [Connect Messaging](https://apps.apple.com/app/id1607268774) +- [Ring4](https://www.ring4.com) - [Formacar](https://itunes.apple.com/ru/app/id1180117334) - [HopUp](https://itunes.apple.com/us/app/hopup-airsoft-community/id1128903141?mt=8) - [MediQuo](https://www.mediquo.com) - [RappresentaMe](https://itunes.apple.com/it/app/rappresentame/id1330914443) - [WiseEyes](https://itunes.apple.com/us/app/wiseeyes/id1391408511?mt=8) - -*Please provide attribution, it is greatly appreciated.* +- [SwiftHub](https://github.com/khoren93/SwiftHub) +- [Studievenn](https://studievenn.no) +- [SmooveText](https://apps.apple.com/np/app/smoove-text/id1362792811) +- [COYO Engage](https://apps.apple.com/app/coyo-engage/id1341588804) +- [HitchPin](https://www.hitchpin.com) +- [Charge Running](https://apps.apple.com/app/charge-running-live-coaching/id1204578360) +- [HER](https://apps.apple.com/us/app/id573328837) +- [Girlfriend Plus](https://apps.apple.com/us/app/girlfriend-plus/id1011637655) +- [Noon Happen](https://apps.apple.com/app/id1477310602) +- [XPASS](https://apps.apple.com/cz/app/id1596773834) +- [HeiaHeia](https://www.heiaheia.com) +- [Starstruck AI](https://apps.apple.com/au/app/starstruck-message-anyone/id6446234281) +- [OutyPlay](https://apps.apple.com/app/id6450551793) + +_Please provide attribution, it is greatly appreciated._ ## Core Team - [@SD10](https://github.com/sd10), Steven Deutsch - [@nathantannar4](https://github.com/nathantannar4), Nathan Tannar - [@zhongwuzw](https://github.com/zhongwuzw), Wu Zhong +- [@austinwright](https://github.com/austinwright), Austin Wright +- [@kaspik](https://github.com/kaspik), Jakub Kaspar +- [@martinpucik](https://github.com/martinpucik), Martin Pucik ## Thanks Many thanks to [**the contributors**](https://github.com/MessageKit/MessageKit/graphs/contributors) of this project. ## License + MessageKit is released under the [MIT License](https://github.com/MessageKit/MessageKit/blob/master/LICENSE.md). ## Inspiration + Inspired by [JSQMessagesViewController](https://github.com/jessesquires/JSQMessagesViewController) :point_left: :100: diff --git a/Scripts/pre-commit b/Scripts/pre-commit new file mode 100644 index 000000000..ad5572c4d --- /dev/null +++ b/Scripts/pre-commit @@ -0,0 +1,6 @@ +#!/bin/sh + +git diff --diff-filter=d --staged --name-only | grep -e '\(.*\).swift$' | while read line; do + swift package --allow-writing-to-package-directory format-source-code --file "${line}"; + git add "$line"; +done \ No newline at end of file diff --git a/Sources/Assets.xcassets/Colors/Contents.json b/Sources/Assets.xcassets/Colors/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Sources/Assets.xcassets/Colors/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Assets.xcassets/Colors/avatarViewBackground.colorset/Contents.json b/Sources/Assets.xcassets/Colors/avatarViewBackground.colorset/Contents.json new file mode 100644 index 000000000..c3ed2a2e7 --- /dev/null +++ b/Sources/Assets.xcassets/Colors/avatarViewBackground.colorset/Contents.json @@ -0,0 +1,28 @@ +{ + "colors" : [ + { + "color" : { + "platform" : "ios", + "reference" : "systemGrayColor" + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "platform" : "ios", + "reference" : "systemGrayColor" + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Assets.xcassets/Colors/collectionViewBackground.colorset/Contents.json b/Sources/Assets.xcassets/Colors/collectionViewBackground.colorset/Contents.json new file mode 100644 index 000000000..2afe943ef --- /dev/null +++ b/Sources/Assets.xcassets/Colors/collectionViewBackground.colorset/Contents.json @@ -0,0 +1,58 @@ +{ + "colors" : [ + { + "color" : { + "platform" : "ios", + "reference" : "systemBackgroundColor" + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "platform" : "ios", + "reference" : "systemBackgroundColor" + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "platform" : "ios", + "reference" : "systemBackgroundColor" + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + }, + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "platform" : "ios", + "reference" : "systemBackgroundColor" + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Assets.xcassets/Colors/incomingAudioMessageTint.colorset/Contents.json b/Sources/Assets.xcassets/Colors/incomingAudioMessageTint.colorset/Contents.json new file mode 100644 index 000000000..90a7a7c4a --- /dev/null +++ b/Sources/Assets.xcassets/Colors/incomingAudioMessageTint.colorset/Contents.json @@ -0,0 +1,58 @@ +{ + "colors" : [ + { + "color" : { + "platform" : "ios", + "reference" : "systemBlueColor" + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "platform" : "ios", + "reference" : "systemBlueColor" + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "platform" : "ios", + "reference" : "systemBlueColor" + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + }, + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "platform" : "ios", + "reference" : "systemBlueColor" + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Assets.xcassets/Colors/incomingMessageBackground.colorset/Contents.json b/Sources/Assets.xcassets/Colors/incomingMessageBackground.colorset/Contents.json new file mode 100644 index 000000000..093cb4fb6 --- /dev/null +++ b/Sources/Assets.xcassets/Colors/incomingMessageBackground.colorset/Contents.json @@ -0,0 +1,58 @@ +{ + "colors" : [ + { + "color" : { + "platform" : "ios", + "reference" : "systemGray5Color" + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "platform" : "ios", + "reference" : "systemGray5Color" + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "platform" : "ios", + "reference" : "systemGray5Color" + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + }, + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "platform" : "ios", + "reference" : "systemGray5Color" + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Assets.xcassets/Colors/incomingMessageLabel.colorset/Contents.json b/Sources/Assets.xcassets/Colors/incomingMessageLabel.colorset/Contents.json new file mode 100644 index 000000000..f59aa157e --- /dev/null +++ b/Sources/Assets.xcassets/Colors/incomingMessageLabel.colorset/Contents.json @@ -0,0 +1,68 @@ +{ + "colors" : [ + { + "color" : { + "platform" : "ios", + "reference" : "labelColor" + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "platform" : "ios", + "reference" : "labelColor" + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + }, + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Assets.xcassets/Colors/label.colorset/Contents.json b/Sources/Assets.xcassets/Colors/label.colorset/Contents.json new file mode 100644 index 000000000..b6604150a --- /dev/null +++ b/Sources/Assets.xcassets/Colors/label.colorset/Contents.json @@ -0,0 +1,28 @@ +{ + "colors" : [ + { + "color" : { + "platform" : "ios", + "reference" : "labelColor" + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "platform" : "ios", + "reference" : "labelColor" + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Assets.xcassets/Colors/outgoingAudioMessageTint.colorset/Contents.json b/Sources/Assets.xcassets/Colors/outgoingAudioMessageTint.colorset/Contents.json new file mode 100644 index 000000000..39c6f564f --- /dev/null +++ b/Sources/Assets.xcassets/Colors/outgoingAudioMessageTint.colorset/Contents.json @@ -0,0 +1,78 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + }, + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Assets.xcassets/Colors/outgoingMessageBackground.colorset/Contents.json b/Sources/Assets.xcassets/Colors/outgoingMessageBackground.colorset/Contents.json new file mode 100644 index 000000000..989aff194 --- /dev/null +++ b/Sources/Assets.xcassets/Colors/outgoingMessageBackground.colorset/Contents.json @@ -0,0 +1,58 @@ +{ + "colors" : [ + { + "color" : { + "platform" : "ios", + "reference" : "systemGreenColor" + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "platform" : "ios", + "reference" : "systemGreenColor" + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "platform" : "ios", + "reference" : "systemGreenColor" + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + }, + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "platform" : "ios", + "reference" : "systemGreenColor" + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Assets.xcassets/Colors/outgoingMessageLabel.colorset/Contents.json b/Sources/Assets.xcassets/Colors/outgoingMessageLabel.colorset/Contents.json new file mode 100644 index 000000000..30343633a --- /dev/null +++ b/Sources/Assets.xcassets/Colors/outgoingMessageLabel.colorset/Contents.json @@ -0,0 +1,68 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "platform" : "ios", + "reference" : "labelColor" + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + }, + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "platform" : "ios", + "reference" : "labelColor" + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Assets.xcassets/Colors/typingIndicatorDot.colorset/Contents.json b/Sources/Assets.xcassets/Colors/typingIndicatorDot.colorset/Contents.json new file mode 100644 index 000000000..291cad52a --- /dev/null +++ b/Sources/Assets.xcassets/Colors/typingIndicatorDot.colorset/Contents.json @@ -0,0 +1,58 @@ +{ + "colors" : [ + { + "color" : { + "platform" : "ios", + "reference" : "systemGray2Color" + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "platform" : "ios", + "reference" : "systemGray2Color" + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "platform" : "ios", + "reference" : "systemGray2Color" + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + }, + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "platform" : "ios", + "reference" : "systemGray2Color" + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Assets.xcassets/Contents.json b/Sources/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Sources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Assets.xcassets/Images/Contents.json b/Sources/Assets.xcassets/Images/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Sources/Assets.xcassets/Images/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Assets.xcassets/Images/bubble_full.imageset/Contents.json b/Sources/Assets.xcassets/Images/bubble_full.imageset/Contents.json new file mode 100644 index 000000000..2ade93161 --- /dev/null +++ b/Sources/Assets.xcassets/Images/bubble_full.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "bubble_full.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "bubble_full@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "bubble_full@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_full.png b/Sources/Assets.xcassets/Images/bubble_full.imageset/bubble_full.png similarity index 100% rename from Assets/MessageKitAssets.bundle/Images/bubble_full.png rename to Sources/Assets.xcassets/Images/bubble_full.imageset/bubble_full.png diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_full@2x.png b/Sources/Assets.xcassets/Images/bubble_full.imageset/bubble_full@2x.png similarity index 100% rename from Assets/MessageKitAssets.bundle/Images/bubble_full@2x.png rename to Sources/Assets.xcassets/Images/bubble_full.imageset/bubble_full@2x.png diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_full@3x.png b/Sources/Assets.xcassets/Images/bubble_full.imageset/bubble_full@3x.png similarity index 100% rename from Assets/MessageKitAssets.bundle/Images/bubble_full@3x.png rename to Sources/Assets.xcassets/Images/bubble_full.imageset/bubble_full@3x.png diff --git a/Sources/Assets.xcassets/Images/bubble_full_tail_v1.imageset/Contents.json b/Sources/Assets.xcassets/Images/bubble_full_tail_v1.imageset/Contents.json new file mode 100644 index 000000000..27524111f --- /dev/null +++ b/Sources/Assets.xcassets/Images/bubble_full_tail_v1.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "bubble_full_tail_v1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "bubble_full_tail_v1@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "bubble_full_tail_v1@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v1.png b/Sources/Assets.xcassets/Images/bubble_full_tail_v1.imageset/bubble_full_tail_v1.png similarity index 100% rename from Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v1.png rename to Sources/Assets.xcassets/Images/bubble_full_tail_v1.imageset/bubble_full_tail_v1.png diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v1@2x.png b/Sources/Assets.xcassets/Images/bubble_full_tail_v1.imageset/bubble_full_tail_v1@2x.png similarity index 100% rename from Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v1@2x.png rename to Sources/Assets.xcassets/Images/bubble_full_tail_v1.imageset/bubble_full_tail_v1@2x.png diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v1@3x.png b/Sources/Assets.xcassets/Images/bubble_full_tail_v1.imageset/bubble_full_tail_v1@3x.png similarity index 100% rename from Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v1@3x.png rename to Sources/Assets.xcassets/Images/bubble_full_tail_v1.imageset/bubble_full_tail_v1@3x.png diff --git a/Sources/Assets.xcassets/Images/bubble_full_tail_v2.imageset/Contents.json b/Sources/Assets.xcassets/Images/bubble_full_tail_v2.imageset/Contents.json new file mode 100644 index 000000000..7b135c5f4 --- /dev/null +++ b/Sources/Assets.xcassets/Images/bubble_full_tail_v2.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "bubble_full_tail_v2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "bubble_full_tail_v2@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "bubble_full_tail_v2@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v2.png b/Sources/Assets.xcassets/Images/bubble_full_tail_v2.imageset/bubble_full_tail_v2.png similarity index 100% rename from Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v2.png rename to Sources/Assets.xcassets/Images/bubble_full_tail_v2.imageset/bubble_full_tail_v2.png diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v2@2x.png b/Sources/Assets.xcassets/Images/bubble_full_tail_v2.imageset/bubble_full_tail_v2@2x.png similarity index 100% rename from Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v2@2x.png rename to Sources/Assets.xcassets/Images/bubble_full_tail_v2.imageset/bubble_full_tail_v2@2x.png diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v2@3x.png b/Sources/Assets.xcassets/Images/bubble_full_tail_v2.imageset/bubble_full_tail_v2@3x.png similarity index 100% rename from Assets/MessageKitAssets.bundle/Images/bubble_full_tail_v2@3x.png rename to Sources/Assets.xcassets/Images/bubble_full_tail_v2.imageset/bubble_full_tail_v2@3x.png diff --git a/Sources/Assets.xcassets/Images/bubble_outlined.imageset/Contents.json b/Sources/Assets.xcassets/Images/bubble_outlined.imageset/Contents.json new file mode 100644 index 000000000..cb3ef6832 --- /dev/null +++ b/Sources/Assets.xcassets/Images/bubble_outlined.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "bubble_outlined.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "bubble_outlined@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "bubble_outlined@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_outlined.png b/Sources/Assets.xcassets/Images/bubble_outlined.imageset/bubble_outlined.png similarity index 100% rename from Assets/MessageKitAssets.bundle/Images/bubble_outlined.png rename to Sources/Assets.xcassets/Images/bubble_outlined.imageset/bubble_outlined.png diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_outlined@2x.png b/Sources/Assets.xcassets/Images/bubble_outlined.imageset/bubble_outlined@2x.png similarity index 100% rename from Assets/MessageKitAssets.bundle/Images/bubble_outlined@2x.png rename to Sources/Assets.xcassets/Images/bubble_outlined.imageset/bubble_outlined@2x.png diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_outlined@3x.png b/Sources/Assets.xcassets/Images/bubble_outlined.imageset/bubble_outlined@3x.png similarity index 100% rename from Assets/MessageKitAssets.bundle/Images/bubble_outlined@3x.png rename to Sources/Assets.xcassets/Images/bubble_outlined.imageset/bubble_outlined@3x.png diff --git a/Sources/Assets.xcassets/Images/bubble_outlined_tail_v1.imageset/Contents.json b/Sources/Assets.xcassets/Images/bubble_outlined_tail_v1.imageset/Contents.json new file mode 100644 index 000000000..c12e393f9 --- /dev/null +++ b/Sources/Assets.xcassets/Images/bubble_outlined_tail_v1.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "bubble_outlined_tail_v1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "bubble_outlined_tail_v1@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "bubble_outlined_tail_v1@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v1.png b/Sources/Assets.xcassets/Images/bubble_outlined_tail_v1.imageset/bubble_outlined_tail_v1.png similarity index 100% rename from Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v1.png rename to Sources/Assets.xcassets/Images/bubble_outlined_tail_v1.imageset/bubble_outlined_tail_v1.png diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v1@2x.png b/Sources/Assets.xcassets/Images/bubble_outlined_tail_v1.imageset/bubble_outlined_tail_v1@2x.png similarity index 100% rename from Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v1@2x.png rename to Sources/Assets.xcassets/Images/bubble_outlined_tail_v1.imageset/bubble_outlined_tail_v1@2x.png diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v1@3x.png b/Sources/Assets.xcassets/Images/bubble_outlined_tail_v1.imageset/bubble_outlined_tail_v1@3x.png similarity index 100% rename from Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v1@3x.png rename to Sources/Assets.xcassets/Images/bubble_outlined_tail_v1.imageset/bubble_outlined_tail_v1@3x.png diff --git a/Sources/Assets.xcassets/Images/bubble_outlined_tail_v2.imageset/Contents.json b/Sources/Assets.xcassets/Images/bubble_outlined_tail_v2.imageset/Contents.json new file mode 100644 index 000000000..e2b1619c2 --- /dev/null +++ b/Sources/Assets.xcassets/Images/bubble_outlined_tail_v2.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "bubble_outlined_tail_v2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "bubble_outlined_tail_v2@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "bubble_outlined_tail_v2@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v2.png b/Sources/Assets.xcassets/Images/bubble_outlined_tail_v2.imageset/bubble_outlined_tail_v2.png similarity index 100% rename from Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v2.png rename to Sources/Assets.xcassets/Images/bubble_outlined_tail_v2.imageset/bubble_outlined_tail_v2.png diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v2@2x.png b/Sources/Assets.xcassets/Images/bubble_outlined_tail_v2.imageset/bubble_outlined_tail_v2@2x.png similarity index 100% rename from Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v2@2x.png rename to Sources/Assets.xcassets/Images/bubble_outlined_tail_v2.imageset/bubble_outlined_tail_v2@2x.png diff --git a/Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v2@3x.png b/Sources/Assets.xcassets/Images/bubble_outlined_tail_v2.imageset/bubble_outlined_tail_v2@3x.png similarity index 100% rename from Assets/MessageKitAssets.bundle/Images/bubble_outlined_tail_v2@3x.png rename to Sources/Assets.xcassets/Images/bubble_outlined_tail_v2.imageset/bubble_outlined_tail_v2@3x.png diff --git a/Sources/Assets.xcassets/Images/disclouser.imageset/Contents.json b/Sources/Assets.xcassets/Images/disclouser.imageset/Contents.json new file mode 100644 index 000000000..2e16c9978 --- /dev/null +++ b/Sources/Assets.xcassets/Images/disclouser.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "disclouser.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "disclouser@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "disclouser@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Assets.xcassets/Images/disclouser.imageset/disclouser.png b/Sources/Assets.xcassets/Images/disclouser.imageset/disclouser.png new file mode 100644 index 000000000..1c5a8c9ed Binary files /dev/null and b/Sources/Assets.xcassets/Images/disclouser.imageset/disclouser.png differ diff --git a/Sources/Assets.xcassets/Images/disclouser.imageset/disclouser@2x.png b/Sources/Assets.xcassets/Images/disclouser.imageset/disclouser@2x.png new file mode 100644 index 000000000..8ef8385c5 Binary files /dev/null and b/Sources/Assets.xcassets/Images/disclouser.imageset/disclouser@2x.png differ diff --git a/Sources/Assets.xcassets/Images/disclouser.imageset/disclouser@3x.png b/Sources/Assets.xcassets/Images/disclouser.imageset/disclouser@3x.png new file mode 100644 index 000000000..cc09ac13f Binary files /dev/null and b/Sources/Assets.xcassets/Images/disclouser.imageset/disclouser@3x.png differ diff --git a/Sources/Assets.xcassets/Images/pause.imageset/Contents.json b/Sources/Assets.xcassets/Images/pause.imageset/Contents.json new file mode 100644 index 000000000..cb22a98c4 --- /dev/null +++ b/Sources/Assets.xcassets/Images/pause.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "pause.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "pause@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "pause@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Assets.xcassets/Images/pause.imageset/pause.png b/Sources/Assets.xcassets/Images/pause.imageset/pause.png new file mode 100644 index 000000000..487a308fb Binary files /dev/null and b/Sources/Assets.xcassets/Images/pause.imageset/pause.png differ diff --git a/Sources/Assets.xcassets/Images/pause.imageset/pause@2x.png b/Sources/Assets.xcassets/Images/pause.imageset/pause@2x.png new file mode 100644 index 000000000..8f5a3c2f4 Binary files /dev/null and b/Sources/Assets.xcassets/Images/pause.imageset/pause@2x.png differ diff --git a/Sources/Assets.xcassets/Images/pause.imageset/pause@3x.png b/Sources/Assets.xcassets/Images/pause.imageset/pause@3x.png new file mode 100644 index 000000000..a15cadda6 Binary files /dev/null and b/Sources/Assets.xcassets/Images/pause.imageset/pause@3x.png differ diff --git a/Sources/Assets.xcassets/Images/play.imageset/Contents.json b/Sources/Assets.xcassets/Images/play.imageset/Contents.json new file mode 100644 index 000000000..2d3099a40 --- /dev/null +++ b/Sources/Assets.xcassets/Images/play.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "play.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "play@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "play@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Assets.xcassets/Images/play.imageset/play.png b/Sources/Assets.xcassets/Images/play.imageset/play.png new file mode 100644 index 000000000..9ae693277 Binary files /dev/null and b/Sources/Assets.xcassets/Images/play.imageset/play.png differ diff --git a/Sources/Assets.xcassets/Images/play.imageset/play@2x.png b/Sources/Assets.xcassets/Images/play.imageset/play@2x.png new file mode 100644 index 000000000..c769e68f6 Binary files /dev/null and b/Sources/Assets.xcassets/Images/play.imageset/play@2x.png differ diff --git a/Sources/Assets.xcassets/Images/play.imageset/play@3x.png b/Sources/Assets.xcassets/Images/play.imageset/play@3x.png new file mode 100644 index 000000000..53fb82a0d Binary files /dev/null and b/Sources/Assets.xcassets/Images/play.imageset/play@3x.png differ diff --git a/Sources/Controllers/MessagesViewController+Keyboard.swift b/Sources/Controllers/MessagesViewController+Keyboard.swift index bdd333e17..974fa1077 100644 --- a/Sources/Controllers/MessagesViewController+Keyboard.swift +++ b/Sources/Controllers/MessagesViewController+Keyboard.swift @@ -1,141 +1,152 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - +// MIT License +// +// Copyright (c) 2017-2022 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Combine import Foundation -import MessageInputBar - -extension MessagesViewController { // swiftlint:disable:this explicit_acl explicit_top_level_acl - - // MARK: - Register / Unregister Observers - - internal func addKeyboardObservers() { - NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.handleKeyboardDidChangeState(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.handleTextViewDidBeginEditing(_:)), name: UITextView.textDidBeginEditingNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.adjustScrollViewTopInset), name: UIDevice.orientationDidChangeNotification, object: nil) - } - - internal func removeKeyboardObservers() { - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UITextView.textDidBeginEditingNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil) - } - - // MARK: - Notification Handlers - - @objc - private func handleTextViewDidBeginEditing(_ notification: Notification) { - if scrollsToBottomOnKeyboardBeginsEditing { - guard let inputTextView = notification.object as? InputTextView, inputTextView === messageInputBar.inputTextView else { return } - messagesCollectionView.scrollToBottom(animated: true) +import InputBarAccessoryView +import UIKit + +extension MessagesViewController { + // MARK: Internal + + // MARK: - Register Observers + + internal func addKeyboardObservers() { + keyboardManager.bind( + inputAccessoryView: inputContainerView, + withAdditionalBottomSpace: { [weak self] in self?.inputBarAdditionalBottomSpace() ?? 0 } + ) + keyboardManager.bind(to: messagesCollectionView) + + /// Observe didBeginEditing to scroll content to last item if necessary + NotificationCenter.default + .publisher(for: UITextView.textDidBeginEditingNotification) + .subscribe(on: DispatchQueue.global()) + /// Wait for inputBar frame change animation to end + .delay(for: .milliseconds(200), scheduler: DispatchQueue.main) + .receive(on: DispatchQueue.main) + .sink { [weak self] notification in + self?.handleTextViewDidBeginEditing(notification) + } + .store(in: &disposeBag) + + NotificationCenter.default + .publisher(for: UITextView.textDidChangeNotification) + .subscribe(on: DispatchQueue.global()) + .receive(on: DispatchQueue.main) + .compactMap { $0.object as? InputTextView } + .filter { [weak self] textView in + textView == self?.messageInputBar.inputTextView + } + .map(\.text) + .removeDuplicates() + .delay(for: .milliseconds(50), scheduler: DispatchQueue.main) /// Wait for next runloop to lay out inputView properly + .sink { [weak self] _ in + self?.updateMessageCollectionViewBottomInset() + + if !(self?.maintainPositionOnInputBarHeightChanged ?? false) { + self?.messagesCollectionView.scrollToLastItem() } + } + .store(in: &disposeBag) + + NotificationCenter.default + .publisher(for: UITextInputMode.currentInputModeDidChangeNotification) + .subscribe(on: DispatchQueue.global()) + .receive(on: DispatchQueue.main) + .removeDuplicates() + .delay(for: .milliseconds(50), scheduler: DispatchQueue.main) /// Wait for next runloop to lay out inputView properly + .sink { [weak self] _ in + self?.updateMessageCollectionViewBottomInset() + + if !(self?.maintainPositionOnInputBarHeightChanged ?? false) { + self?.messagesCollectionView.scrollToLastItem() + } + } + .store(in: &disposeBag) + + /// Observe frame change of the input bar container to update collectioView bottom inset + inputContainerView.publisher(for: \.center) + .receive(on: DispatchQueue.main) + .removeDuplicates() + .sink(receiveValue: { [weak self] _ in + self?.updateMessageCollectionViewBottomInset() + }) + .store(in: &disposeBag) + } + + // MARK: - Updating insets + + /// Updates bottom messagesCollectionView inset based on the position of inputContainerView + internal func updateMessageCollectionViewBottomInset() { + let collectionViewHeight = messagesCollectionView.frame.maxY + let newBottomInset = collectionViewHeight - (inputContainerView.frame.minY - additionalBottomInset) - + automaticallyAddedBottomInset + let normalizedNewBottomInset = max(0, newBottomInset) + let differenceOfBottomInset = newBottomInset - messageCollectionViewBottomInset + + UIView.performWithoutAnimation { + guard differenceOfBottomInset != 0 else { return } + messagesCollectionView.contentInset.bottom = normalizedNewBottomInset + messagesCollectionView.verticalScrollIndicatorInsets.bottom = newBottomInset } - - @objc - private func handleKeyboardDidChangeState(_ notification: Notification) { - guard !isMessagesControllerBeingDismissed else { return } - - guard let keyboardStartFrameInScreenCoords = notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? CGRect else { return } - guard !keyboardStartFrameInScreenCoords.isEmpty else { - // WORKAROUND for what seems to be a bug in iPad's keyboard handling in iOS 11: we receive an extra spurious frame change - // notification when undocking the keyboard, with a zero starting frame and an incorrect end frame. The workaround is to - // ignore this notification. - return - } - - // Note that the check above does not exclude all notifications from an undocked keyboard, only the weird ones. - // - // We've tried following Apple's recommended approach of tracking UIKeyboardWillShow / UIKeyboardDidHide and ignoring frame - // change notifications while the keyboard is hidden or undocked (undocked keyboard is considered hidden by those events). - // Unfortunately, we do care about the difference between hidden and undocked, because we have an input bar which is at the - // bottom when the keyboard is hidden, and is tied to the keyboard when it's undocked. - // - // If we follow what Apple recommends and ignore notifications while the keyboard is hidden/undocked, we get an extra inset - // at the bottom when the undocked keyboard is visible (the inset that tries to compensate for the missing input bar). - // (Alternatives like setting newBottomInset to 0 or to the height of the input bar don't work either.) - // - // We could make it work by adding extra checks for the state of the keyboard and compensating accordingly, but it seems easier - // to simply check whether the current keyboard frame, whatever it is (even when undocked), covers the bottom of the collection - // view. - - guard let keyboardEndFrameInScreenCoords = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } - let keyboardEndFrame = view.convert(keyboardEndFrameInScreenCoords, from: view.window) - - let newBottomInset = requiredScrollViewBottomInset(forKeyboardFrame: keyboardEndFrame) - let differenceOfBottomInset = newBottomInset - messageCollectionViewBottomInset - - if maintainPositionOnKeyboardFrameChanged && differenceOfBottomInset != 0 { - let contentOffset = CGPoint(x: messagesCollectionView.contentOffset.x, y: messagesCollectionView.contentOffset.y + differenceOfBottomInset) - messagesCollectionView.setContentOffset(contentOffset, animated: false) - } - - messageCollectionViewBottomInset = newBottomInset + } + + // MARK: Private + + /// UIScrollView can automatically add safe area insets to its contentInset, + /// which needs to be accounted for when setting the contentInset based on screen coordinates. + /// + /// - Returns: The distance automatically added to contentInset.bottom, if any. + private var automaticallyAddedBottomInset: CGFloat { + messagesCollectionView.adjustedContentInset.bottom - messageCollectionViewBottomInset + } + + private var messageCollectionViewBottomInset: CGFloat { + messagesCollectionView.contentInset.bottom + } + + /// UIScrollView can automatically add safe area insets to its contentInset, + /// which needs to be accounted for when setting the contentInset based on screen coordinates. + /// + /// - Returns: The distance automatically added to contentInset.top, if any. + private var automaticallyAddedTopInset: CGFloat { + messagesCollectionView.adjustedContentInset.top - messageCollectionViewTopInset + } + + private var messageCollectionViewTopInset: CGFloat { + messagesCollectionView.contentInset.top + } + + // MARK: - Private methods + + private func handleTextViewDidBeginEditing(_ notification: Notification) { + guard + scrollsToLastItemOnKeyboardBeginsEditing, + let inputTextView = notification.object as? InputTextView, + inputTextView === messageInputBar.inputTextView + else { + return } - - // MARK: - Inset Computation - - @objc - internal func adjustScrollViewTopInset() { - if #available(iOS 11.0, *) { - // No need to add to the top contentInset - } else { - let navigationBarInset = navigationController?.navigationBar.frame.height ?? 0 - let statusBarInset: CGFloat = UIApplication.shared.isStatusBarHidden ? 0 : 20 - let topInset = navigationBarInset + statusBarInset - messagesCollectionView.contentInset.top = topInset - messagesCollectionView.scrollIndicatorInsets.top = topInset - } - } - - private func requiredScrollViewBottomInset(forKeyboardFrame keyboardFrame: CGRect) -> CGFloat { - // we only need to adjust for the part of the keyboard that covers (i.e. intersects) our collection view; - // see https://developer.apple.com/videos/play/wwdc2017/242/ for more details - let intersection = messagesCollectionView.frame.intersection(keyboardFrame) - - if intersection.isNull || intersection.maxY < messagesCollectionView.frame.maxY { - // The keyboard is hidden, is a hardware one, or is undocked and does not cover the bottom of the collection view. - // Note: intersection.maxY may be less than messagesCollectionView.frame.maxY when dealing with undocked keyboards. - return max(0, additionalBottomInset - automaticallyAddedBottomInset) - } else { - return max(0, intersection.height + additionalBottomInset - automaticallyAddedBottomInset) - } - } - - internal func requiredInitialScrollViewBottomInset() -> CGFloat { - guard let inputAccessoryView = inputAccessoryView else { return 0 } - return max(0, inputAccessoryView.frame.height + additionalBottomInset - automaticallyAddedBottomInset) - } - - /// iOS 11's UIScrollView can automatically add safe area insets to its contentInset, - /// which needs to be accounted for when setting the contentInset based on screen coordinates. - /// - /// - Returns: The distance automatically added to contentInset.bottom, if any. - private var automaticallyAddedBottomInset: CGFloat { - if #available(iOS 11.0, *) { - return messagesCollectionView.adjustedContentInset.bottom - messagesCollectionView.contentInset.bottom - } else { - return 0 - } - } - + messagesCollectionView.scrollToLastItem() + } } diff --git a/Sources/Controllers/MessagesViewController+Menu.swift b/Sources/Controllers/MessagesViewController+Menu.swift index 361b5ab64..7aa07a36d 100644 --- a/Sources/Controllers/MessagesViewController+Menu.swift +++ b/Sources/Controllers/MessagesViewController+Menu.swift @@ -1,100 +1,113 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2022 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation -import MessageInputBar +import UIKit -extension MessagesViewController { // swiftlint:disable:this explicit_acl explicit_top_level_acl +extension MessagesViewController { + // MARK: Internal - // MARK: - Register / Unregister Observers + // MARK: - Register / Unregister Observers - internal func addMenuControllerObservers() { - NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(_:)), name: UIMenuController.willShowMenuNotification, object: nil) - } - - internal func removeMenuControllerObservers() { - NotificationCenter.default.removeObserver(self, name: UIMenuController.willShowMenuNotification, object: nil) - } - - // MARK: - Notification Handlers - - /// Show menuController and set target rect to selected bubble - @objc - private func menuControllerWillShow(_ notification: Notification) { - - guard let currentMenuController = notification.object as? UIMenuController, - let selectedIndexPath = selectedIndexPathForMenu else { return } - - NotificationCenter.default.removeObserver(self, name: UIMenuController.willShowMenuNotification, object: nil) - defer { - NotificationCenter.default.addObserver(self, - selector: #selector(MessagesViewController.menuControllerWillShow(_:)), - name: UIMenuController.willShowMenuNotification, object: nil) - selectedIndexPathForMenu = nil - } - - currentMenuController.setMenuVisible(false, animated: false) + internal func addMenuControllerObservers() { + NotificationCenter.default.addObserver( + self, + selector: #selector(MessagesViewController.menuControllerWillShow(_:)), + name: UIMenuController.willShowMenuNotification, + object: nil) + } - guard let selectedCell = messagesCollectionView.cellForItem(at: selectedIndexPath) as? MessageContentCell else { return } - let selectedCellMessageBubbleFrame = selectedCell.convert(selectedCell.messageContainerView.frame, to: view) + // MARK: Private - var messageInputBarFrame: CGRect = .zero - if let messageInputBarSuperview = messageInputBar.superview { - messageInputBarFrame = view.convert(messageInputBar.frame, from: messageInputBarSuperview) - } + // MARK: - Helpers - var topNavigationBarFrame: CGRect = navigationBarFrame - if navigationBarFrame != .zero, let navigationBarSuperview = navigationController?.navigationBar.superview { - topNavigationBarFrame = view.convert(navigationController!.navigationBar.frame, from: navigationBarSuperview) - } - - let menuHeight = currentMenuController.menuFrame.height - - let selectedCellMessageBubblePlusMenuFrame = CGRect(selectedCellMessageBubbleFrame.origin.x, selectedCellMessageBubbleFrame.origin.y - menuHeight, selectedCellMessageBubbleFrame.size.width, selectedCellMessageBubbleFrame.size.height + 2 * menuHeight) + private var navigationBarFrame: CGRect { + guard let navigationController = navigationController, !navigationController.navigationBar.isHidden else { + return .zero + } + return navigationController.navigationBar.frame + } + + // MARK: - Notification Handlers + + /// Show menuController and set target rect to selected bubble + @objc + private func menuControllerWillShow(_ notification: Notification) { + guard + let currentMenuController = notification.object as? UIMenuController, + let selectedIndexPath = selectedIndexPathForMenu else { return } + + NotificationCenter.default.removeObserver(self, name: UIMenuController.willShowMenuNotification, object: nil) + defer { + NotificationCenter.default.addObserver( + self, + selector: #selector(MessagesViewController.menuControllerWillShow(_:)), + name: UIMenuController.willShowMenuNotification, + object: nil) + selectedIndexPathForMenu = nil + } - var targetRect: CGRect = selectedCellMessageBubbleFrame - currentMenuController.arrowDirection = .default + currentMenuController.hideMenu() - /// Message bubble intersects with navigationBar and keyboard - if selectedCellMessageBubblePlusMenuFrame.intersects(topNavigationBarFrame) && selectedCellMessageBubblePlusMenuFrame.intersects(messageInputBarFrame) { - let centerY = (selectedCellMessageBubblePlusMenuFrame.intersection(messageInputBarFrame).minY + selectedCellMessageBubblePlusMenuFrame.intersection(topNavigationBarFrame).maxY) / 2 - targetRect = CGRect(selectedCellMessageBubblePlusMenuFrame.midX, centerY, 1, 1) - } /// Message bubble only intersects with navigationBar - else if selectedCellMessageBubblePlusMenuFrame.intersects(topNavigationBarFrame) { - currentMenuController.arrowDirection = .up - } + guard let selectedCell = messagesCollectionView.cellForItem(at: selectedIndexPath) as? MessageContentCell else { + return + } + let selectedCellMessageBubbleFrame = selectedCell.convert(selectedCell.messageContainerView.frame, to: view) - currentMenuController.setTargetRect(targetRect, in: view) - currentMenuController.setMenuVisible(true, animated: true) + var messageInputBarFrame: CGRect = .zero + if let messageInputBarSuperview = messageInputBar.superview { + messageInputBarFrame = view.convert(messageInputBar.frame, from: messageInputBarSuperview) } - // MARK: - Helpers + var topNavigationBarFrame: CGRect = navigationBarFrame + if navigationBarFrame != .zero, let navigationBarSuperview = navigationController?.navigationBar.superview { + topNavigationBarFrame = view.convert(navigationController!.navigationBar.frame, from: navigationBarSuperview) + } - private var navigationBarFrame: CGRect { - guard let navigationController = navigationController, !navigationController.navigationBar.isHidden else { - return .zero - } - return navigationController.navigationBar.frame + let menuHeight = currentMenuController.menuFrame.height + + let selectedCellMessageBubblePlusMenuFrame = CGRect( + selectedCellMessageBubbleFrame.origin.x, + selectedCellMessageBubbleFrame.origin.y - menuHeight, + selectedCellMessageBubbleFrame.size.width, + selectedCellMessageBubbleFrame.size.height + 2 * menuHeight) + + var targetRect: CGRect = selectedCellMessageBubbleFrame + currentMenuController.arrowDirection = .default + + /// Message bubble intersects with navigationBar and keyboard + if + selectedCellMessageBubblePlusMenuFrame.intersects(topNavigationBarFrame), + selectedCellMessageBubblePlusMenuFrame.intersects(messageInputBarFrame) + { + let centerY = ( + selectedCellMessageBubblePlusMenuFrame.intersection(messageInputBarFrame) + .minY + selectedCellMessageBubblePlusMenuFrame.intersection(topNavigationBarFrame).maxY) / 2 + targetRect = CGRect(selectedCellMessageBubblePlusMenuFrame.midX, centerY, 1, 1) + } /// Message bubble only intersects with navigationBar + else if selectedCellMessageBubblePlusMenuFrame.intersects(topNavigationBarFrame) { + currentMenuController.arrowDirection = .up } + + currentMenuController.showMenu(from: view, rect: targetRect) + } } diff --git a/Sources/Controllers/MessagesViewController+PanGesture.swift b/Sources/Controllers/MessagesViewController+PanGesture.swift new file mode 100644 index 000000000..dc81391d7 --- /dev/null +++ b/Sources/Controllers/MessagesViewController+PanGesture.swift @@ -0,0 +1,97 @@ +// MIT License +// +// Copyright (c) 2017-2022 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import UIKit + +extension MessagesViewController { + // MARK: Internal + + /// Display time of message by swiping the cell + func addPanGesture() { + panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) + guard let panGesture = panGesture else { + return + } + panGesture.delegate = self + messagesCollectionView.addGestureRecognizer(panGesture) + messagesCollectionView.clipsToBounds = false + } + + func removePanGesture() { + guard let panGesture = panGesture else { + return + } + panGesture.delegate = nil + self.panGesture = nil + messagesCollectionView.removeGestureRecognizer(panGesture) + messagesCollectionView.clipsToBounds = true + } + + // MARK: Private + + @objc + private func handlePanGesture(_ gesture: UIPanGestureRecognizer) { + guard let parentView = gesture.view else { + return + } + + switch gesture.state { + case .began, .changed: + messagesCollectionView.showsVerticalScrollIndicator = false + let translation = gesture.translation(in: view) + let minX = -(view.frame.size.width * 0.35) + let maxX: CGFloat = 0 + var offsetValue = translation.x + offsetValue = max(offsetValue, minX) + offsetValue = min(offsetValue, maxX) + parentView.frame.origin.x = offsetValue + case .ended: + messagesCollectionView.showsVerticalScrollIndicator = true + UIView.animate( + withDuration: 0.5, + delay: 0, + usingSpringWithDamping: 0.7, + initialSpringVelocity: 0.8, + options: .curveEaseOut, + animations: { + parentView.frame.origin.x = 0 + }, + completion: nil) + default: + break + } + } +} + +// MARK: - MessagesViewController + UIGestureRecognizerDelegate + +extension MessagesViewController: UIGestureRecognizerDelegate { + /// Check Pan Gesture Direction: + open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else { + return false + } + let velocity = panGesture.velocity(in: messagesCollectionView) + return abs(velocity.x) > abs(velocity.y) + } +} diff --git a/Sources/Controllers/MessagesViewController+State.swift b/Sources/Controllers/MessagesViewController+State.swift new file mode 100644 index 000000000..62d2d9cdb --- /dev/null +++ b/Sources/Controllers/MessagesViewController+State.swift @@ -0,0 +1,100 @@ +// MIT License +// +// Copyright (c) 2017-2022 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Combine +import Foundation +import InputBarAccessoryView +import UIKit + +extension MessagesViewController { + @MainActor + final class State { + /// Pan gesture for display the date of message by swiping left. + var panGesture: UIPanGestureRecognizer? + var maintainPositionOnInputBarHeightChanged = false + var scrollsToLastItemOnKeyboardBeginsEditing = false + + let inputContainerView: MessagesInputContainerView = .init() + @Published var inputBarType: MessageInputBarKind = .messageInputBar + let keyboardManager = KeyboardManager() + var disposeBag: Set = .init() + } + + // MARK: - Getters + + public var keyboardManager: KeyboardManager { state.keyboardManager } + + var panGesture: UIPanGestureRecognizer? { + get { state.panGesture } + set { state.panGesture = newValue } + } + + var disposeBag: Set { + get { state.disposeBag } + set { state.disposeBag = newValue } + } +} + +extension MessagesViewController { + /// Container holding `messageInputBar` view. To change type of input bar, set `inputBarType` to desired kind. + public var inputContainerView: MessagesInputContainerView { state.inputContainerView } + + /// Kind of `messageInputBar` to be added into `inputContainerView` + public var inputBarType: MessageInputBarKind { + get { state.inputBarType } + set { state.inputBarType = newValue } + } + + /// A Boolean value that determines whether the `MessagesCollectionView` + /// maintains it's current position when the height of the `MessageInputBar` changes. + /// + /// The default value of this property is `false`. + @available( + *, + deprecated, + renamed: "maintainPositionOnInputBarHeightChanged", + message: "Please use new property - maintainPositionOnInputBarHeightChanged") + public var maintainPositionOnKeyboardFrameChanged: Bool { + get { state.maintainPositionOnInputBarHeightChanged } + set { state.maintainPositionOnInputBarHeightChanged = newValue } + } + + /// A Boolean value that determines whether the `MessagesCollectionView` + /// maintains it's current position when the height of the `MessageInputBar` changes. + /// + /// The default value of this property is `false` and the `MessagesCollectionView` will scroll to bottom after the + /// height of the `MessageInputBar` changes. + public var maintainPositionOnInputBarHeightChanged: Bool { + get { state.maintainPositionOnInputBarHeightChanged } + set { state.maintainPositionOnInputBarHeightChanged = newValue } + } + + /// A Boolean value that determines whether the `MessagesCollectionView` scrolls to the + /// last item whenever the `InputTextView` begins editing. + /// + /// The default value of this property is `false`. + /// NOTE: This is related to `scrollToLastItem` whereas the below flag is related to `scrollToBottom` - check each function for differences + public var scrollsToLastItemOnKeyboardBeginsEditing: Bool { + get { state.scrollsToLastItemOnKeyboardBeginsEditing } + set { state.scrollsToLastItemOnKeyboardBeginsEditing = newValue } + } +} diff --git a/Sources/Controllers/MessagesViewController+TypingIndicator.swift b/Sources/Controllers/MessagesViewController+TypingIndicator.swift new file mode 100644 index 000000000..434a66bb2 --- /dev/null +++ b/Sources/Controllers/MessagesViewController+TypingIndicator.swift @@ -0,0 +1,96 @@ +// MIT License +// +// Copyright (c) 2017-2022 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import UIKit + +// MARK: - Typing Indicator API + +extension MessagesViewController { + // MARK: Open + + /// Sets the typing indicator sate by inserting/deleting the `TypingBubbleCell` + /// + /// - Parameters: + /// - isHidden: A Boolean value that is to be the new state of the typing indicator + /// - animated: A Boolean value determining if the insertion is to be animated + /// - updates: A block of code that will be executed during `performBatchUpdates` + /// when `animated` is `TRUE` or before the `completion` block executes + /// when `animated` is `FALSE` + /// - completion: A completion block to execute after the insertion/deletion + @objc + open func setTypingIndicatorViewHidden( + _ isHidden: Bool, + animated: Bool, + whilePerforming updates: (() -> Void)? = nil, + completion: ((Bool) -> Void)? = nil) + { + guard isTypingIndicatorHidden != isHidden else { + completion?(false) + return + } + + let section = messagesCollectionView.numberOfSections + + if animated { + messagesCollectionView.performBatchUpdates({ [weak self] in + self?.messagesCollectionView.setTypingIndicatorViewHidden(isHidden) + self?.performUpdatesForTypingIndicatorVisability(at: section) + updates?() + }, completion: completion) + } else { + messagesCollectionView.setTypingIndicatorViewHidden(isHidden) + performUpdatesForTypingIndicatorVisability(at: section) + updates?() + completion?(true) + } + } + + // MARK: Public + + public var isTypingIndicatorHidden: Bool { + messagesCollectionView.isTypingIndicatorHidden + } + + /// A method that by default checks if the section is the last in the + /// `messagesCollectionView` and that `isTypingIndicatorViewHidden` + /// is FALSE + /// + /// - Parameter section + /// - Returns: A Boolean indicating if the TypingIndicator should be presented at the given section + public func isSectionReservedForTypingIndicator(_ section: Int) -> Bool { + !messagesCollectionView.isTypingIndicatorHidden && section == numberOfSections(in: messagesCollectionView) - 1 + } + + // MARK: Private + + /// Performs a delete or insert on the `MessagesCollectionView` on the provided section + /// + /// - Parameter section: The index to modify + private func performUpdatesForTypingIndicatorVisability(at section: Int) { + if isTypingIndicatorHidden { + messagesCollectionView.deleteSections([section - 1]) + } else { + messagesCollectionView.insertSections([section]) + } + } +} diff --git a/Sources/Controllers/MessagesViewController+UIScrollViewDelegate.swift b/Sources/Controllers/MessagesViewController+UIScrollViewDelegate.swift new file mode 100644 index 000000000..e524215b4 --- /dev/null +++ b/Sources/Controllers/MessagesViewController+UIScrollViewDelegate.swift @@ -0,0 +1,39 @@ +// MIT License +// +// Copyright (c) 2017-2022 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import UIKit + +extension MessagesViewController: UIScrollViewDelegate { + open func scrollViewDidScroll(_: UIScrollView) { } + open func scrollViewWillBeginDragging(_: UIScrollView) { } + open func scrollViewWillEndDragging( + _: UIScrollView, + withVelocity _: CGPoint, + targetContentOffset _: UnsafeMutablePointer) { } + open func scrollViewDidEndDragging(_: UIScrollView, willDecelerate _: Bool) { } + open func scrollViewWillBeginDecelerating(_: UIScrollView) { } + open func scrollViewDidEndDecelerating(_: UIScrollView) { } + open func scrollViewDidEndScrollingAnimation(_: UIScrollView) { } + open func scrollViewDidScrollToTop(_: UIScrollView) { } + open func scrollViewDidChangeAdjustedContentInset(_: UIScrollView) { } +} diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift index ff43ffeb1..41965d5a4 100644 --- a/Sources/Controllers/MessagesViewController.swift +++ b/Sources/Controllers/MessagesViewController.swift @@ -1,314 +1,403 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - +// MIT License +// +// Copyright (c) 2017-2022 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Combine +import Foundation +import InputBarAccessoryView import UIKit -import MessageInputBar /// A subclass of `UIViewController` with a `MessagesCollectionView` object /// that is used to display conversation interfaces. -open class MessagesViewController: UIViewController, -UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { - - /// The `MessagesCollectionView` managed by the messages view controller object. - open var messagesCollectionView = MessagesCollectionView() - - /// The `MessageInputBar` used as the `inputAccessoryView` in the view controller. - open var messageInputBar = MessageInputBar() - - /// A Boolean value that determines whether the `MessagesCollectionView` scrolls to the - /// bottom whenever the `InputTextView` begins editing. - /// - /// The default value of this property is `false`. - open var scrollsToBottomOnKeyboardBeginsEditing: Bool = false - - /// A Boolean value that determines whether the `MessagesCollectionView` - /// maintains it's current position when the height of the `MessageInputBar` changes. - /// - /// The default value of this property is `false`. - open var maintainPositionOnKeyboardFrameChanged: Bool = false - - open override var canBecomeFirstResponder: Bool { - return true +open class MessagesViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { + // MARK: Lifecycle + + deinit { + NotificationCenter.default.removeObserver(self, name: UIMenuController.willShowMenuNotification, object: nil) + MessageStyle.bubbleImageCache.removeAllObjects() + } + + // MARK: Open + + /// The `MessagesCollectionView` managed by the messages view controller object. + open var messagesCollectionView = MessagesCollectionView() + + /// The `InputBarAccessoryView` used as the `inputAccessoryView` in the view controller. + open lazy var messageInputBar = InputBarAccessoryView() + + /// Display the date of message by swiping left. + /// The default value of this property is `false`. + open var showMessageTimestampOnSwipeLeft = false { + didSet { + messagesCollectionView.showMessageTimestampOnSwipeLeft = showMessageTimestampOnSwipeLeft + if showMessageTimestampOnSwipeLeft { + addPanGesture() + } else { + removePanGesture() + } } - - open override var inputAccessoryView: UIView? { - return messageInputBar + } + + /// A CGFloat value that adds to (or, if negative, subtracts from) the automatically + /// computed value of `messagesCollectionView.contentInset.bottom`. Meant to be used + /// as a measure of last resort when the built-in algorithm does not produce the right + /// value for your app. Please let us know when you end up having to use this property. + open var additionalBottomInset: CGFloat = 0 { + didSet { + updateMessageCollectionViewBottomInset() } - - open override var shouldAutorotate: Bool { - return false + } + + /// withAdditionalBottomSpace parameter for InputBarAccessoryView's KeyboardManager + open func inputBarAdditionalBottomSpace() -> CGFloat { + 0 + } + + open override func viewDidLoad() { + super.viewDidLoad() + setupDefaults() + setupSubviews() + setupConstraints() + setupInputBar(for: inputBarType) + setupDelegates() + addObservers() + addKeyboardObservers() + addMenuControllerObservers() + /// Layout input container view and update messagesCollectionViewInsets + view.layoutIfNeeded() + } + + open override func viewSafeAreaInsetsDidChange() { + super.viewSafeAreaInsetsDidChange() + updateMessageCollectionViewBottomInset() + } + + // MARK: - UICollectionViewDataSource + + open func numberOfSections(in collectionView: UICollectionView) -> Int { + guard let collectionView = collectionView as? MessagesCollectionView else { + fatalError(MessageKitError.notMessagesCollectionView) } + let sections = collectionView.messagesDataSource?.numberOfSections(in: collectionView) ?? 0 + return collectionView.isTypingIndicatorHidden ? sections : sections + 1 + } - /// A CGFloat value that adds to (or, if negative, subtracts from) the automatically - /// computed value of `messagesCollectionView.contentInset.bottom`. Meant to be used - /// as a measure of last resort when the built-in algorithm does not produce the right - /// value for your app. Please let us know when you end up having to use this property. - open var additionalBottomInset: CGFloat = 0 { - didSet { - let delta = additionalBottomInset - oldValue - messageCollectionViewBottomInset += delta - } + open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + guard let collectionView = collectionView as? MessagesCollectionView else { + fatalError(MessageKitError.notMessagesCollectionView) + } + if isSectionReservedForTypingIndicator(section) { + return 1 + } + return collectionView.messagesDataSource?.numberOfItems(inSection: section, in: collectionView) ?? 0 + } + + /// Notes: + /// - If you override this method, remember to call MessagesDataSource's customCell(for:at:in:) + /// for MessageKind.custom messages, if necessary. + /// + /// - If you are using the typing indicator you will need to ensure that the section is not + /// reserved for it with `isSectionReservedForTypingIndicator` defined in + /// `MessagesCollectionViewFlowLayout` + open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let messagesCollectionView = collectionView as? MessagesCollectionView else { + fatalError(MessageKitError.notMessagesCollectionView) } - private var isFirstLayout: Bool = true - - internal var isMessagesControllerBeingDismissed: Bool = false + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) + } - internal var selectedIndexPathForMenu: IndexPath? + if isSectionReservedForTypingIndicator(indexPath.section) { + return messagesDataSource.typingIndicator(at: indexPath, in: messagesCollectionView) + } - internal var messageCollectionViewBottomInset: CGFloat = 0 { - didSet { - messagesCollectionView.contentInset.bottom = messageCollectionViewBottomInset - messagesCollectionView.scrollIndicatorInsets.bottom = messageCollectionViewBottomInset - } + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + + switch message.kind { + case .text, .attributedText, .emoji: + if let cell = messagesDataSource.textCell(for: message, at: indexPath, in: messagesCollectionView) { + return cell + } else { + let cell = messagesCollectionView.dequeueReusableCell(TextMessageCell.self, for: indexPath) + cell.configure(with: message, at: indexPath, and: messagesCollectionView) + return cell + } + case .photo, .video: + if let cell = messagesDataSource.photoCell(for: message, at: indexPath, in: messagesCollectionView) { + return cell + } else { + let cell = messagesCollectionView.dequeueReusableCell(MediaMessageCell.self, for: indexPath) + cell.configure(with: message, at: indexPath, and: messagesCollectionView) + return cell + } + case .location: + if let cell = messagesDataSource.locationCell(for: message, at: indexPath, in: messagesCollectionView) { + return cell + } else { + let cell = messagesCollectionView.dequeueReusableCell(LocationMessageCell.self, for: indexPath) + cell.configure(with: message, at: indexPath, and: messagesCollectionView) + return cell + } + case .audio: + if let cell = messagesDataSource.audioCell(for: message, at: indexPath, in: messagesCollectionView) { + return cell + } else { + let cell = messagesCollectionView.dequeueReusableCell(AudioMessageCell.self, for: indexPath) + cell.configure(with: message, at: indexPath, and: messagesCollectionView) + return cell + } + case .contact: + if let cell = messagesDataSource.contactCell(for: message, at: indexPath, in: messagesCollectionView) { + return cell + } else { + let cell = messagesCollectionView.dequeueReusableCell(ContactMessageCell.self, for: indexPath) + cell.configure(with: message, at: indexPath, and: messagesCollectionView) + return cell + } + case .linkPreview: + let cell = messagesCollectionView.dequeueReusableCell(LinkPreviewMessageCell.self, for: indexPath) + cell.configure(with: message, at: indexPath, and: messagesCollectionView) + return cell + case .custom: + return messagesDataSource.customCell(for: message, at: indexPath, in: messagesCollectionView) + } + } + + open func collectionView( + _ collectionView: UICollectionView, + viewForSupplementaryElementOfKind kind: String, + at indexPath: IndexPath) + -> UICollectionReusableView + { + guard let messagesCollectionView = collectionView as? MessagesCollectionView else { + fatalError(MessageKitError.notMessagesCollectionView) } - // MARK: - View Life Cycle + guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { + fatalError(MessageKitError.nilMessagesDisplayDelegate) + } - open override func viewDidLoad() { - super.viewDidLoad() - setupDefaults() - setupSubviews() - setupConstraints() - setupDelegates() - addMenuControllerObservers() - addObservers() + switch kind { + case UICollectionView.elementKindSectionHeader: + return displayDelegate.messageHeaderView(for: indexPath, in: messagesCollectionView) + case UICollectionView.elementKindSectionFooter: + return displayDelegate.messageFooterView(for: indexPath, in: messagesCollectionView) + default: + fatalError(MessageKitError.unrecognizedSectionKind) + } + } + + // MARK: - UICollectionViewDelegateFlowLayout + + open func collectionView( + _: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath) + -> CGSize + { + guard let messagesFlowLayout = collectionViewLayout as? MessagesCollectionViewFlowLayout else { return .zero } + return messagesFlowLayout.sizeForItem(at: indexPath) + } + + open func collectionView( + _ collectionView: UICollectionView, + layout _: UICollectionViewLayout, + referenceSizeForHeaderInSection section: Int) + -> CGSize + { + guard let messagesCollectionView = collectionView as? MessagesCollectionView else { + fatalError(MessageKitError.notMessagesCollectionView) } - - open override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - isMessagesControllerBeingDismissed = false + guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { + fatalError(MessageKitError.nilMessagesLayoutDelegate) } - - open override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - isMessagesControllerBeingDismissed = true + if isSectionReservedForTypingIndicator(section) { + return .zero } - - open override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - isMessagesControllerBeingDismissed = false + return layoutDelegate.headerViewSize(for: section, in: messagesCollectionView) + } + + open func collectionView(_: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt _: IndexPath) { + guard let cell = cell as? TypingIndicatorCell else { return } + cell.typingBubble.startAnimating() + } + + open func collectionView( + _ collectionView: UICollectionView, + layout _: UICollectionViewLayout, + referenceSizeForFooterInSection section: Int) + -> CGSize + { + guard let messagesCollectionView = collectionView as? MessagesCollectionView else { + fatalError(MessageKitError.notMessagesCollectionView) } - - open override func viewDidLayoutSubviews() { - // Hack to prevent animation of the contentInset after viewDidAppear - if isFirstLayout { - defer { isFirstLayout = false } - addKeyboardObservers() - messageCollectionViewBottomInset = requiredInitialScrollViewBottomInset() - } - adjustScrollViewTopInset() + guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { + fatalError(MessageKitError.nilMessagesLayoutDelegate) } + if isSectionReservedForTypingIndicator(section) { + return .zero + } + return layoutDelegate.footerViewSize(for: section, in: messagesCollectionView) + } - // MARK: - Initializers + open func collectionView(_: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool { + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return false } - deinit { - removeKeyboardObservers() - removeMenuControllerObservers() - removeObservers() - clearMemoryCache() + if isSectionReservedForTypingIndicator(indexPath.section) { + return false } - // MARK: - Methods [Private] + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - private func setupDefaults() { - extendedLayoutIncludesOpaqueBars = true - automaticallyAdjustsScrollViewInsets = false - view.backgroundColor = .white - messagesCollectionView.keyboardDismissMode = .interactive - messagesCollectionView.alwaysBounceVertical = true + switch message.kind { + case .text, .attributedText, .emoji, .photo: + selectedIndexPathForMenu = indexPath + return true + default: + return false } - - private func setupDelegates() { - messagesCollectionView.delegate = self - messagesCollectionView.dataSource = self + } + + open func collectionView( + _: UICollectionView, + canPerformAction action: Selector, + forItemAt indexPath: IndexPath, + withSender _: Any?) + -> Bool + { + if isSectionReservedForTypingIndicator(indexPath.section) { + return false } + return (action == NSSelectorFromString("copy:")) + } - private func setupSubviews() { - view.addSubview(messagesCollectionView) + open func collectionView(_: UICollectionView, performAction _: Selector, forItemAt indexPath: IndexPath, withSender _: Any?) { + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) } - - private func setupConstraints() { - messagesCollectionView.translatesAutoresizingMaskIntoConstraints = false - - let top = messagesCollectionView.topAnchor.constraint(equalTo: view.topAnchor, constant: topLayoutGuide.length) - let bottom = messagesCollectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor) - if #available(iOS 11.0, *) { - let leading = messagesCollectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor) - let trailing = messagesCollectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) - NSLayoutConstraint.activate([top, bottom, trailing, leading]) - } else { - let leading = messagesCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor) - let trailing = messagesCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor) - NSLayoutConstraint.activate([top, bottom, trailing, leading]) - } + let pasteBoard = UIPasteboard.general + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + + switch message.kind { + case .text(let text), .emoji(let text): + pasteBoard.string = text + case .attributedText(let attributedText): + pasteBoard.string = attributedText.string + case .photo(let mediaItem): + pasteBoard.image = mediaItem.image ?? mediaItem.placeholderImage + default: + break } + } - // MARK: - UICollectionViewDataSource + // MARK: Public - open func numberOfSections(in collectionView: UICollectionView) -> Int { - guard let collectionView = collectionView as? MessagesCollectionView else { - fatalError(MessageKitError.notMessagesCollectionView) - } - return collectionView.messagesDataSource?.numberOfSections(in: collectionView) ?? 0 - } - - open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - guard let collectionView = collectionView as? MessagesCollectionView else { - fatalError(MessageKitError.notMessagesCollectionView) - } - return collectionView.messagesDataSource?.numberOfItems(inSection: section, in: collectionView) ?? 0 - } + public var selectedIndexPathForMenu: IndexPath? - /// Note: - /// If you override this method, remember to call MessagesDataSource's customCell(for:at:in:) for MessageKind.custom messages, if necessary - open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - - guard let messagesCollectionView = collectionView as? MessagesCollectionView else { - fatalError(MessageKitError.notMessagesCollectionView) - } - - guard let messagesDataSource = messagesCollectionView.messagesDataSource else { - fatalError(MessageKitError.nilMessagesDataSource) - } - - let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - - switch message.kind { - case .text, .attributedText, .emoji: - let cell = messagesCollectionView.dequeueReusableCell(TextMessageCell.self, for: indexPath) - cell.configure(with: message, at: indexPath, and: messagesCollectionView) - return cell - case .photo, .video: - let cell = messagesCollectionView.dequeueReusableCell(MediaMessageCell.self, for: indexPath) - cell.configure(with: message, at: indexPath, and: messagesCollectionView) - return cell - case .location: - let cell = messagesCollectionView.dequeueReusableCell(LocationMessageCell.self, for: indexPath) - cell.configure(with: message, at: indexPath, and: messagesCollectionView) - return cell - case .custom: - return messagesDataSource.customCell(for: message, at: indexPath, in: messagesCollectionView) - } - } + // MARK: Internal - open func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + // MARK: - Internal properties - guard let messagesCollectionView = collectionView as? MessagesCollectionView else { - fatalError(MessageKitError.notMessagesCollectionView) - } + internal let state: State = .init() - guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { - fatalError(MessageKitError.nilMessagesDisplayDelegate) - } + // MARK: Private - switch kind { - case UICollectionView.elementKindSectionHeader: - return displayDelegate.messageHeaderView(for: indexPath, in: messagesCollectionView) - case UICollectionView.elementKindSectionFooter: - return displayDelegate.messageFooterView(for: indexPath, in: messagesCollectionView) - default: - fatalError(MessageKitError.unrecognizedSectionKind) - } - } + // MARK: - Private methods - // MARK: - UICollectionViewDelegateFlowLayout + private func setupDefaults() { + extendedLayoutIncludesOpaqueBars = true + view.backgroundColor = .collectionViewBackground + messagesCollectionView.keyboardDismissMode = .interactive + messagesCollectionView.alwaysBounceVertical = true + messagesCollectionView.backgroundColor = .collectionViewBackground + } - open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - guard let messagesFlowLayout = collectionViewLayout as? MessagesCollectionViewFlowLayout else { return .zero } - return messagesFlowLayout.sizeForItem(at: indexPath) - } + private func setupSubviews() { + view.addSubviews(messagesCollectionView, inputContainerView) + } - open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { + private func setupConstraints() { + messagesCollectionView.translatesAutoresizingMaskIntoConstraints = false + /// Constraints of inputContainerView are managed by keyboardManager + inputContainerView.translatesAutoresizingMaskIntoConstraints = false - guard let messagesCollectionView = collectionView as? MessagesCollectionView else { - fatalError(MessageKitError.notMessagesCollectionView) - } - guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { - fatalError(MessageKitError.nilMessagesLayoutDelegate) - } - return layoutDelegate.headerViewSize(for: section, in: messagesCollectionView) - } + NSLayoutConstraint.activate([ + messagesCollectionView.topAnchor.constraint(equalTo: view.topAnchor), + messagesCollectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + messagesCollectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + messagesCollectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + ]) + } - open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { - guard let messagesCollectionView = collectionView as? MessagesCollectionView else { - fatalError(MessageKitError.notMessagesCollectionView) - } - guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { - fatalError(MessageKitError.nilMessagesLayoutDelegate) - } - return layoutDelegate.footerViewSize(for: section, in: messagesCollectionView) - } + private func setupDelegates() { + messagesCollectionView.delegate = self + messagesCollectionView.dataSource = self + } - open func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool { - guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return false } - let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - - switch message.kind { - case .text, .attributedText, .emoji, .photo: - selectedIndexPathForMenu = indexPath - return true - default: - return false - } - } + private func setupInputBar(for kind: MessageInputBarKind) { + inputContainerView.subviews.forEach { $0.removeFromSuperview() } - open func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool { - return (action == NSSelectorFromString("copy:")) - } + func pinViewToInputContainer(_ view: UIView) { + view.translatesAutoresizingMaskIntoConstraints = false + inputContainerView.addSubviews(view) - open func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) { - guard let messagesDataSource = messagesCollectionView.messagesDataSource else { - fatalError(MessageKitError.nilMessagesDataSource) - } - let pasteBoard = UIPasteboard.general - let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - - switch message.kind { - case .text(let text), .emoji(let text): - pasteBoard.string = text - case .attributedText(let attributedText): - pasteBoard.string = attributedText.string - case .photo(let mediaItem): - pasteBoard.image = mediaItem.image ?? mediaItem.placeholderImage - default: - break - } + NSLayoutConstraint.activate([ + view.topAnchor.constraint(equalTo: inputContainerView.topAnchor), + view.bottomAnchor.constraint(equalTo: inputContainerView.bottomAnchor), + view.leadingAnchor.constraint(equalTo: inputContainerView.leadingAnchor), + view.trailingAnchor.constraint(equalTo: inputContainerView.trailingAnchor), + ]) } - // MARK: - Helpers - - private func addObservers() { - NotificationCenter.default.addObserver( - self, selector: #selector(clearMemoryCache), name: UIApplication.didReceiveMemoryWarningNotification, object: nil) - } - - private func removeObservers() { - NotificationCenter.default.removeObserver(self, name: UIApplication.didReceiveMemoryWarningNotification, object: nil) - } - - @objc private func clearMemoryCache() { - MessageStyle.bubbleImageCache.removeAllObjects() + switch kind { + case .messageInputBar: + pinViewToInputContainer(messageInputBar) + case .custom(let view): + pinViewToInputContainer(view) } + } + + private func addObservers() { + NotificationCenter.default + .publisher(for: UIApplication.didReceiveMemoryWarningNotification) + .subscribe(on: DispatchQueue.global()) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.clearMemoryCache() + } + .store(in: &disposeBag) + + state.$inputBarType + .subscribe(on: DispatchQueue.global()) + .dropFirst() + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] newType in + self?.setupInputBar(for: newType) + }) + .store(in: &disposeBag) + } + + private func clearMemoryCache() { + MessageStyle.bubbleImageCache.removeAllObjects() + } } diff --git a/Sources/Extensions/Bundle+Extensions.swift b/Sources/Extensions/Bundle+Extensions.swift index 35dc394dd..aa4ca7d4f 100644 --- a/Sources/Extensions/Bundle+Extensions.swift +++ b/Sources/Extensions/Bundle+Extensions.swift @@ -1,43 +1,33 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation -internal extension Bundle { - - static func messageKitAssetBundle() -> Bundle { // swiftlint:disable:this explicit_acl - let podBundle = Bundle(for: MessagesViewController.self) - - guard let resourceBundleUrl = podBundle.url(forResource: "MessageKitAssets", withExtension: "bundle") else { - fatalError(MessageKitError.couldNotCreateAssetsPath) - } - - guard let resourceBundle = Bundle(url: resourceBundleUrl) else { - fatalError(MessageKitError.couldNotLoadAssetsBundle) - } - - return resourceBundle - } - +extension Bundle { + #if IS_SPM + nonisolated internal static let messageKitAssetBundle = Bundle.module + #else + nonisolated internal static var messageKitAssetBundle: Bundle { + Bundle(for: MessagesViewController.self) + } + #endif } diff --git a/Sources/Extensions/CGRect+Extensions.swift b/Sources/Extensions/CGRect+Extensions.swift index 59675a078..2ead04b23 100644 --- a/Sources/Extensions/CGRect+Extensions.swift +++ b/Sources/Extensions/CGRect+Extensions.swift @@ -1,33 +1,30 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation +import UIKit -internal extension CGRect { - - init(_ x: CGFloat, _ y: CGFloat, _ w: CGFloat, _ h: CGFloat) { // swiftlint:disable:this explicit_acl - self.init(x: x, y: y, width: w, height: h) - } - +extension CGRect { + internal init(_ x: CGFloat, _ y: CGFloat, _ w: CGFloat, _ h: CGFloat) { + self.init(x: x, y: y, width: w, height: h) + } } diff --git a/Sources/Extensions/MessageKind+textMessageKind.swift b/Sources/Extensions/MessageKind+textMessageKind.swift new file mode 100644 index 000000000..bd9a31b21 --- /dev/null +++ b/Sources/Extensions/MessageKind+textMessageKind.swift @@ -0,0 +1,36 @@ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +extension MessageKind { + internal var textMessageKind: MessageKind { + switch self { + case .linkPreview(let linkItem): + return linkItem.textKind + case .text, .emoji, .attributedText: + return self + default: + fatalError("textMessageKind not supported for messageKind: \(self)") + } + } +} diff --git a/Sources/Extensions/MessageKit+Availability.swift b/Sources/Extensions/MessageKit+Availability.swift new file mode 100644 index 000000000..07c051214 --- /dev/null +++ b/Sources/Extensions/MessageKit+Availability.swift @@ -0,0 +1,88 @@ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import UIKit + +extension MessagesLayoutDelegate { + public func avatarSize(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGSize { + fatalError("avatarSize(for:at:in) has been removed in MessageKit 1.0.") + } + + public func avatarPosition(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> AvatarPosition { + fatalError("avatarPosition(for:at:in) has been removed in MessageKit 1.0.") + } + + public func messageLabelInset(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIEdgeInsets { + fatalError("messageLabelInset(for:at:in) has been removed in MessageKit 1.0") + } + + public func messagePadding(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIEdgeInsets { + fatalError("messagePadding(for:at:in) has been removed in MessageKit 1.0.") + } + + public func cellTopLabelAlignment(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> LabelAlignment { + fatalError("cellTopLabelAlignment(for:at:in) has been removed in MessageKit 1.0.") + } + + public func cellBottomLabelAlignment(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> LabelAlignment { + fatalError("cellBottomLabelAlignment(for:at:in) has been removed in MessageKit 1.0.") + } + + public func widthForMedia(message _: MessageType, at _: IndexPath, with _: CGFloat, in _: MessagesCollectionView) -> CGFloat { + fatalError("widthForMedia(message:at:with:in) has been removed in MessageKit 1.0.") + } + + public func heightForMedia( + message _: MessageType, + at _: IndexPath, + with _: CGFloat, + in _: MessagesCollectionView) + -> CGFloat + { + fatalError("heightForMedia(message:at:with:in) has been removed in MessageKit 1.0.") + } + + public func widthForLocation( + message _: MessageType, + at _: IndexPath, + with _: CGFloat, + in _: MessagesCollectionView) + -> CGFloat + { + fatalError("widthForLocation(message:at:with:in) has been removed in MessageKit 1.0.") + } + + public func heightForLocation( + message _: MessageType, + at _: IndexPath, + with _: CGFloat, + in _: MessagesCollectionView) + -> CGFloat + { + fatalError("heightForLocation(message:at:with:in) has been removed in MessageKit 1.0.") + } + + public func shouldCacheLayoutAttributes(for _: MessageType) -> Bool { + fatalError("shouldCacheLayoutAttributes(for:) has been removed in MessageKit 1.0.") + } +} diff --git a/Sources/Extensions/NSAttributedString+Extensions.swift b/Sources/Extensions/NSAttributedString+Extensions.swift index 7b49ac65d..87b15671f 100644 --- a/Sources/Extensions/NSAttributedString+Extensions.swift +++ b/Sources/Extensions/NSAttributedString+Extensions.swift @@ -1,36 +1,51 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation - -internal extension NSAttributedString { - - func width(considering height: CGFloat) -> CGFloat { // swiftlint:disable:this explicit_acl - - let constraintBox = CGSize(width: .greatestFiniteMagnitude, height: height) - let rect = self.boundingRect(with: constraintBox, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil) - return rect.width - - } +import UIKit + +extension NSAttributedString { + public func width(considering height: CGFloat) -> CGFloat { + let size = size(consideringHeight: height) + return size.width + } + + public func height(considering width: CGFloat) -> CGFloat { + let size = size(consideringWidth: width) + return size.height + } + + public func size(consideringHeight height: CGFloat) -> CGSize { + let constraintBox = CGSize(width: .greatestFiniteMagnitude, height: height) + return size(considering: constraintBox) + } + + public func size(consideringWidth width: CGFloat) -> CGSize { + let constraintBox = CGSize(width: width, height: .greatestFiniteMagnitude) + return size(considering: constraintBox) + } + + public func size(considering size: CGSize) -> CGSize { + let rect = boundingRect(with: size, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil) + return rect.size + } } diff --git a/Sources/Extensions/UIColor+Extensions.swift b/Sources/Extensions/UIColor+Extensions.swift index 73f1d357b..e5df7ad80 100644 --- a/Sources/Extensions/UIColor+Extensions.swift +++ b/Sources/Extensions/UIColor+Extensions.swift @@ -1,41 +1,58 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation +import UIKit -// swiftlint:disable explicit_acl +@MainActor +extension UIColor { + // MARK: Internal -internal extension UIColor { + internal static var incomingMessageBackground: UIColor { colorFromAssetBundle(named: "incomingMessageBackground") } - static let incomingGray = UIColor(red: 230/255, green: 230/255, blue: 235/255, alpha: 1.0) + internal static var outgoingMessageBackground: UIColor { colorFromAssetBundle(named: "outgoingMessageBackground") } - static let outgoingGreen = UIColor(red: 69/255, green: 214/255, blue: 93/255, alpha: 1.0) + internal static var incomingMessageLabel: UIColor { colorFromAssetBundle(named: "incomingMessageLabel") } - static let inputBarGray = UIColor(red: 247/255, green: 247/255, blue: 247/255, alpha: 1.0) + internal static var outgoingMessageLabel: UIColor { colorFromAssetBundle(named: "outgoingMessageLabel") } - static let playButtonLightGray = UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1.0) + internal static var incomingAudioMessageTint: UIColor { colorFromAssetBundle(named: "incomingAudioMessageTint") } - static let sendButtonBlue = UIColor(red: 15/255, green: 135/255, blue: 255/255, alpha: 1.0) + internal static var outgoingAudioMessageTint: UIColor { colorFromAssetBundle(named: "outgoingAudioMessageTint") } + internal static var collectionViewBackground: UIColor { colorFromAssetBundle(named: "collectionViewBackground") } + + internal static var typingIndicatorDot: UIColor { colorFromAssetBundle(named: "typingIndicatorDot") } + + internal static var label: UIColor { colorFromAssetBundle(named: "label") } + + internal static var avatarViewBackground: UIColor { colorFromAssetBundle(named: "avatarViewBackground") } + + // MARK: Private + + private static func colorFromAssetBundle(named: String) -> UIColor { + guard let color = UIColor(named: named, in: Bundle.messageKitAssetBundle, compatibleWith: nil) else { + fatalError(MessageKitError.couldNotFindColorAsset) + } + return color + } } diff --git a/Sources/Extensions/UIEdgeInsets+Extensions.swift b/Sources/Extensions/UIEdgeInsets+Extensions.swift index fc0f9d494..c26740de5 100644 --- a/Sources/Extensions/UIEdgeInsets+Extensions.swift +++ b/Sources/Extensions/UIEdgeInsets+Extensions.swift @@ -1,39 +1,34 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation +import UIKit -// swiftlint:disable explicit_acl - -internal extension UIEdgeInsets { - - var vertical: CGFloat { - return top + bottom - } - - var horizontal: CGFloat { - return left + right - } +extension UIEdgeInsets { + internal var vertical: CGFloat { + top + bottom + } + internal var horizontal: CGFloat { + left + right + } } diff --git a/Sources/Extensions/UIImage+Extensions.swift b/Sources/Extensions/UIImage+Extensions.swift new file mode 100644 index 000000000..aa371f67e --- /dev/null +++ b/Sources/Extensions/UIImage+Extensions.swift @@ -0,0 +1,38 @@ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit + +// MARK: - ImageType + +public enum ImageType: String { + case play + case pause + case disclosure +} + +/// This extension provide a way to access image resources with in framework +extension UIImage { + internal static func messageKitImageWith(type: ImageType) -> UIImage? { + UIImage(named: type.rawValue, in: Bundle.messageKitAssetBundle, compatibleWith: nil) + } +} diff --git a/Sources/Extensions/UIView+Extensions.swift b/Sources/Extensions/UIView+Extensions.swift index 2fc7a80b9..d741de44b 100644 --- a/Sources/Extensions/UIView+Extensions.swift +++ b/Sources/Extensions/UIView+Extensions.swift @@ -1,118 +1,142 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2022 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import UIKit -// swiftlint:disable explicit_acl - -internal extension UIView { - - func fillSuperview() { - guard let superview = self.superview else { - return - } - translatesAutoresizingMaskIntoConstraints = false - - let constraints: [NSLayoutConstraint] = [ - leftAnchor.constraint(equalTo: superview.leftAnchor), - rightAnchor.constraint(equalTo: superview.rightAnchor), - topAnchor.constraint(equalTo: superview.topAnchor), - bottomAnchor.constraint(equalTo: superview.bottomAnchor) - ] - NSLayoutConstraint.activate(constraints) +extension UIView { + internal func fillSuperview() { + guard let superview = superview else { + return } + translatesAutoresizingMaskIntoConstraints = false - func centerInSuperview() { - guard let superview = self.superview else { - return - } - translatesAutoresizingMaskIntoConstraints = false - let constraints: [NSLayoutConstraint] = [ - centerXAnchor.constraint(equalTo: superview.centerXAnchor), - centerYAnchor.constraint(equalTo: superview.centerYAnchor) - ] - NSLayoutConstraint.activate(constraints) + let constraints: [NSLayoutConstraint] = [ + leftAnchor.constraint(equalTo: superview.leftAnchor), + rightAnchor.constraint(equalTo: superview.rightAnchor), + topAnchor.constraint(equalTo: superview.topAnchor), + bottomAnchor.constraint(equalTo: superview.bottomAnchor), + ] + NSLayoutConstraint.activate(constraints) + } + + internal func centerInSuperview() { + guard let superview = superview else { + return + } + translatesAutoresizingMaskIntoConstraints = false + let constraints: [NSLayoutConstraint] = [ + centerXAnchor.constraint(equalTo: superview.centerXAnchor), + centerYAnchor.constraint(equalTo: superview.centerYAnchor), + ] + NSLayoutConstraint.activate(constraints) + } + + internal func constraint(equalTo size: CGSize) { + guard superview != nil else { return } + translatesAutoresizingMaskIntoConstraints = false + let constraints: [NSLayoutConstraint] = [ + widthAnchor.constraint(equalToConstant: size.width), + heightAnchor.constraint(equalToConstant: size.height), + ] + NSLayoutConstraint.activate(constraints) + } + + @discardableResult + internal func addConstraints( + _ top: NSLayoutYAxisAnchor? = nil, + left: NSLayoutXAxisAnchor? = nil, + bottom: NSLayoutYAxisAnchor? = nil, + right: NSLayoutXAxisAnchor? = nil, + centerY: NSLayoutYAxisAnchor? = nil, + centerX: NSLayoutXAxisAnchor? = nil, + topConstant: CGFloat = 0, + leftConstant: CGFloat = 0, + bottomConstant: CGFloat = 0, + rightConstant: CGFloat = 0, + centerYConstant: CGFloat = 0, + centerXConstant: CGFloat = 0, + widthConstant: CGFloat = 0, + heightConstant: CGFloat = 0) -> [NSLayoutConstraint] + { + if superview == nil { + return [] + } + translatesAutoresizingMaskIntoConstraints = false + + var constraints = [NSLayoutConstraint]() + + if let top = top { + let constraint = topAnchor.constraint(equalTo: top, constant: topConstant) + constraint.identifier = "top" + constraints.append(constraint) + } + + if let left = left { + let constraint = leftAnchor.constraint(equalTo: left, constant: leftConstant) + constraint.identifier = "left" + constraints.append(constraint) + } + + if let bottom = bottom { + let constraint = bottomAnchor.constraint(equalTo: bottom, constant: -bottomConstant) + constraint.identifier = "bottom" + constraints.append(constraint) + } + + if let right = right { + let constraint = rightAnchor.constraint(equalTo: right, constant: -rightConstant) + constraint.identifier = "right" + constraints.append(constraint) + } + + if let centerY = centerY { + let constraint = centerYAnchor.constraint(equalTo: centerY, constant: centerYConstant) + constraint.identifier = "centerY" + constraints.append(constraint) } - - func constraint(equalTo size: CGSize) { - guard superview != nil else { return } - translatesAutoresizingMaskIntoConstraints = false - let constraints: [NSLayoutConstraint] = [ - widthAnchor.constraint(equalToConstant: size.width), - heightAnchor.constraint(equalToConstant: size.height) - ] - NSLayoutConstraint.activate(constraints) - + + if let centerX = centerX { + let constraint = centerXAnchor.constraint(equalTo: centerX, constant: centerXConstant) + constraint.identifier = "centerX" + constraints.append(constraint) + } + + if widthConstant > 0 { + let constraint = widthAnchor.constraint(equalToConstant: widthConstant) + constraint.identifier = "width" + constraints.append(constraint) } - @discardableResult - func addConstraints(_ top: NSLayoutYAxisAnchor? = nil, left: NSLayoutXAxisAnchor? = nil, bottom: NSLayoutYAxisAnchor? = nil, right: NSLayoutXAxisAnchor? = nil, topConstant: CGFloat = 0, leftConstant: CGFloat = 0, bottomConstant: CGFloat = 0, rightConstant: CGFloat = 0, widthConstant: CGFloat = 0, heightConstant: CGFloat = 0) -> [NSLayoutConstraint] { - - if self.superview == nil { - return [] - } - translatesAutoresizingMaskIntoConstraints = false - - var constraints = [NSLayoutConstraint]() - - if let top = top { - let constraint = topAnchor.constraint(equalTo: top, constant: topConstant) - constraint.identifier = "top" - constraints.append(constraint) - } - - if let left = left { - let constraint = leftAnchor.constraint(equalTo: left, constant: leftConstant) - constraint.identifier = "left" - constraints.append(constraint) - } - - if let bottom = bottom { - let constraint = bottomAnchor.constraint(equalTo: bottom, constant: -bottomConstant) - constraint.identifier = "bottom" - constraints.append(constraint) - } - - if let right = right { - let constraint = rightAnchor.constraint(equalTo: right, constant: -rightConstant) - constraint.identifier = "right" - constraints.append(constraint) - } - - if widthConstant > 0 { - let constraint = widthAnchor.constraint(equalToConstant: widthConstant) - constraint.identifier = "width" - constraints.append(constraint) - } - - if heightConstant > 0 { - let constraint = heightAnchor.constraint(equalToConstant: heightConstant) - constraint.identifier = "height" - constraints.append(constraint) - } - - NSLayoutConstraint.activate(constraints) - return constraints + if heightConstant > 0 { + let constraint = heightAnchor.constraint(equalToConstant: heightConstant) + constraint.identifier = "height" + constraints.append(constraint) } + + NSLayoutConstraint.activate(constraints) + return constraints + } + + internal func addSubviews(_ subviews: UIView...) { + subviews.forEach { addSubview($0) } + } } diff --git a/Sources/Layout/AudioMessageSizeCalculator.swift b/Sources/Layout/AudioMessageSizeCalculator.swift new file mode 100644 index 000000000..b08ed2995 --- /dev/null +++ b/Sources/Layout/AudioMessageSizeCalculator.swift @@ -0,0 +1,41 @@ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import UIKit + +open class AudioMessageSizeCalculator: MessageSizeCalculator { + open override func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + switch message.kind { + case .audio(let item): + let maxWidth = messageContainerMaxWidth(for: message, at: indexPath) + if maxWidth < item.size.width { + // Maintain the ratio if width is too great + let height = maxWidth * item.size.height / item.size.width + return CGSize(width: maxWidth, height: height) + } + return item.size + default: + fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)") + } + } +} diff --git a/Sources/Layout/CellSizeCalculator.swift b/Sources/Layout/CellSizeCalculator.swift index 7037b5715..85ffbffed 100644 --- a/Sources/Layout/CellSizeCalculator.swift +++ b/Sources/Layout/CellSizeCalculator.swift @@ -1,50 +1,53 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import UIKit /// An object is responsible for /// sizing and configuring cells for given `IndexPath`s. +@MainActor open class CellSizeCalculator { + // MARK: Lifecycle - /// The layout object for which the cell size calculator is used. - public weak var layout: UICollectionViewFlowLayout? - - /// Used to configure the layout attributes for a given cell. - /// - /// - Parameters: - /// - attributes: The attributes of the cell. - /// The default does nothing - open func configure(attributes: UICollectionViewLayoutAttributes) {} - - /// Used to size an item at a given `IndexPath`. - /// - /// - Parameters: - /// - indexPath: The `IndexPath` of the item to be displayed. - /// The default return .zero - open func sizeForItem(at indexPath: IndexPath) -> CGSize { return .zero } - - public init() {} + public init() { } + // MARK: Open + + /// Used to configure the layout attributes for a given cell. + /// + /// - Parameters: + /// - attributes: The attributes of the cell. + /// The default does nothing + open func configure(attributes _: UICollectionViewLayoutAttributes) { } + + /// Used to size an item at a given `IndexPath`. + /// + /// - Parameters: + /// - indexPath: The `IndexPath` of the item to be displayed. + /// The default return .zero + open func sizeForItem(at _: IndexPath) -> CGSize { .zero } + + // MARK: Public + + /// The layout object for which the cell size calculator is used. + public weak var layout: UICollectionViewFlowLayout? } diff --git a/Sources/Layout/ContactMessageSizeCalculator.swift b/Sources/Layout/ContactMessageSizeCalculator.swift new file mode 100644 index 000000000..9d2166b29 --- /dev/null +++ b/Sources/Layout/ContactMessageSizeCalculator.swift @@ -0,0 +1,76 @@ +// MIT License +// +// Copyright (c) 2017-2018 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import UIKit + +open class ContactMessageSizeCalculator: MessageSizeCalculator { + // MARK: Open + + open override func messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + let maxWidth = super.messageContainerMaxWidth(for: message, at: indexPath) + let textInsets = contactLabelInsets(for: message) + return maxWidth - textInsets.horizontal + } + + open override func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + let maxWidth = messageContainerMaxWidth(for: message, at: indexPath) + + var messageContainerSize: CGSize + let attributedText: NSAttributedString + + switch message.kind { + case .contact(let item): + attributedText = NSAttributedString(string: item.displayName, attributes: [.font: contactLabelFont]) + default: + fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)") + } + + messageContainerSize = labelSize(for: attributedText, considering: maxWidth) + + let messageInsets = contactLabelInsets(for: message) + messageContainerSize.width += messageInsets.horizontal + messageContainerSize.height += messageInsets.vertical + + return messageContainerSize + } + + open override func configure(attributes: UICollectionViewLayoutAttributes) { + super.configure(attributes: attributes) + guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return } + attributes.messageLabelFont = contactLabelFont + } + + // MARK: Public + + public var incomingMessageNameLabelInsets = UIEdgeInsets(top: 7, left: 46, bottom: 7, right: 30) + public var outgoingMessageNameLabelInsets = UIEdgeInsets(top: 7, left: 41, bottom: 7, right: 35) + public var contactLabelFont = UIFont.preferredFont(forTextStyle: .body) + + // MARK: Internal + + internal func contactLabelInsets(for message: MessageType) -> UIEdgeInsets { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingMessageNameLabelInsets : incomingMessageNameLabelInsets + } +} diff --git a/Sources/Layout/LinkPreviewMessageSizeCalculator.swift b/Sources/Layout/LinkPreviewMessageSizeCalculator.swift new file mode 100644 index 000000000..b18f241a0 --- /dev/null +++ b/Sources/Layout/LinkPreviewMessageSizeCalculator.swift @@ -0,0 +1,120 @@ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import UIKit + +// MARK: - LinkPreviewMessageSizeCalculator + +open class LinkPreviewMessageSizeCalculator: TextMessageSizeCalculator { + // MARK: Lifecycle + + public override init(layout: MessagesCollectionViewFlowLayout?) { + let titleFont = UIFont.systemFont(ofSize: 13, weight: .semibold) + let titleFontMetrics = UIFontMetrics(forTextStyle: .footnote) + self.titleFont = titleFontMetrics.scaledFont(for: titleFont) + + let domainFont = UIFont.systemFont(ofSize: 12, weight: .semibold) + let domainFontMetrics = UIFontMetrics(forTextStyle: .caption1) + self.domainFont = domainFontMetrics.scaledFont(for: domainFont) + + super.init(layout: layout) + } + + // MARK: Open + + open override func messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + switch message.kind { + case .linkPreview: + let maxWidth = super.messageContainerMaxWidth(for: message, at: indexPath) + return max(maxWidth, (layout?.collectionView?.bounds.width ?? 0) * 0.75) + default: + return super.messageContainerMaxWidth(for: message, at: indexPath) + } + } + + open override func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + guard case MessageKind.linkPreview(let linkItem) = message.kind else { + fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)") + } + + var containerSize = super.messageContainerSize(for: message, at: indexPath) + containerSize.width = max(containerSize.width, messageContainerMaxWidth(for: message, at: indexPath)) + + let labelInsets: UIEdgeInsets = messageLabelInsets(for: message) + + let minHeight = containerSize.height + LinkPreviewMessageSizeCalculator.imageViewSize + let previewMaxWidth = containerSize + .width - + ( + LinkPreviewMessageSizeCalculator.imageViewSize + LinkPreviewMessageSizeCalculator.imageViewMargin + labelInsets + .horizontal) + + calculateContainerSize( + with: NSAttributedString(string: linkItem.title ?? "", attributes: [.font: titleFont]), + containerSize: &containerSize, + maxWidth: previewMaxWidth) + + calculateContainerSize( + with: NSAttributedString(string: linkItem.teaser, attributes: [.font: teaserFont]), + containerSize: &containerSize, + maxWidth: previewMaxWidth) + + calculateContainerSize( + with: NSAttributedString(string: linkItem.url.host ?? "", attributes: [.font: domainFont]), + containerSize: &containerSize, + maxWidth: previewMaxWidth) + + containerSize.height = max(minHeight, containerSize.height) + labelInsets.vertical + + return containerSize + } + + open override func configure(attributes: UICollectionViewLayoutAttributes) { + super.configure(attributes: attributes) + guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return } + attributes.linkPreviewFonts = LinkPreviewFonts(titleFont: titleFont, teaserFont: teaserFont, domainFont: domainFont) + } + + // MARK: Public + + public var titleFont: UIFont + public var teaserFont: UIFont = .preferredFont(forTextStyle: .caption2) + public var domainFont: UIFont + + // MARK: Internal + + static let imageViewSize: CGFloat = 60 + static let imageViewMargin: CGFloat = 8 +} + +extension LinkPreviewMessageSizeCalculator { + private func calculateContainerSize( + with attributedString: NSAttributedString, + containerSize: inout CGSize, + maxWidth: CGFloat) + { + guard !attributedString.string.isEmpty else { return } + let size = labelSize(for: attributedString, considering: maxWidth) + containerSize.height += size.height + } +} diff --git a/Sources/Layout/LocationMessageSizeCalculator.swift b/Sources/Layout/LocationMessageSizeCalculator.swift index 0efc0260b..56ce3329c 100644 --- a/Sources/Layout/LocationMessageSizeCalculator.swift +++ b/Sources/Layout/LocationMessageSizeCalculator.swift @@ -1,43 +1,41 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation +import UIKit open class LocationMessageSizeCalculator: MessageSizeCalculator { - - open override func messageContainerSize(for message: MessageType) -> CGSize { - switch message.kind { - case .location(let item): - let maxWidth = messageContainerMaxWidth(for: message) - if maxWidth < item.size.width { - // Maintain the ratio if width is too great - let height = maxWidth * item.size.height / item.size.width - return CGSize(width: maxWidth, height: height) - } - return item.size - default: - fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)") - } + open override func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + switch message.kind { + case .location(let item): + let maxWidth = messageContainerMaxWidth(for: message, at: indexPath) + if maxWidth < item.size.width { + // Maintain the ratio if width is too great + let height = maxWidth * item.size.height / item.size.width + return CGSize(width: maxWidth, height: height) + } + return item.size + default: + fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)") } + } } diff --git a/Sources/Layout/MediaMessageSizeCalculator.swift b/Sources/Layout/MediaMessageSizeCalculator.swift index 020bbd624..c6eebaf91 100644 --- a/Sources/Layout/MediaMessageSizeCalculator.swift +++ b/Sources/Layout/MediaMessageSizeCalculator.swift @@ -1,48 +1,46 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation +import UIKit open class MediaMessageSizeCalculator: MessageSizeCalculator { - - open override func messageContainerSize(for message: MessageType) -> CGSize { - let maxWidth = messageContainerMaxWidth(for: message) - let sizeForMediaItem = { (maxWidth: CGFloat, item: MediaItem) -> CGSize in - if maxWidth < item.size.width { - // Maintain the ratio if width is too great - let height = maxWidth * item.size.height / item.size.width - return CGSize(width: maxWidth, height: height) - } - return item.size - } - switch message.kind { - case .photo(let item): - return sizeForMediaItem(maxWidth, item) - case .video(let item): - return sizeForMediaItem(maxWidth, item) - default: - fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)") - } + open override func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + let maxWidth = messageContainerMaxWidth(for: message, at: indexPath) + let sizeForMediaItem = { (maxWidth: CGFloat, item: MediaItem) -> CGSize in + if maxWidth < item.size.width { + // Maintain the ratio if width is too great + let height = maxWidth * item.size.height / item.size.width + return CGSize(width: maxWidth, height: height) + } + return item.size + } + switch message.kind { + case .photo(let item): + return sizeForMediaItem(maxWidth, item) + case .video(let item): + return sizeForMediaItem(maxWidth, item) + default: + fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)") } + } } diff --git a/Sources/Layout/MessageSizeCalculator.swift b/Sources/Layout/MessageSizeCalculator.swift index f83f826e9..099487215 100644 --- a/Sources/Layout/MessageSizeCalculator.swift +++ b/Sources/Layout/MessageSizeCalculator.swift @@ -1,256 +1,353 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2022 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation +import UIKit -open class MessageSizeCalculator: CellSizeCalculator { +// MARK: - MessageSizeCalculator - public init(layout: MessagesCollectionViewFlowLayout? = nil) { - super.init() - - self.layout = layout +open class MessageSizeCalculator: CellSizeCalculator { + // MARK: Lifecycle + + public init(layout: MessagesCollectionViewFlowLayout? = nil) { + super.init() + + self.layout = layout + } + + // MARK: Open + + open override func configure(attributes: UICollectionViewLayoutAttributes) { + guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return } + + let dataSource = messagesLayout.messagesDataSource + let indexPath = attributes.indexPath + let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView) + + attributes.avatarSize = avatarSize(for: message, at: indexPath) + attributes.avatarPosition = avatarPosition(for: message) + attributes.avatarLeadingTrailingPadding = avatarLeadingTrailingPadding + + attributes.messageContainerPadding = messageContainerPadding(for: message) + attributes.messageContainerSize = messageContainerSize(for: message, at: indexPath) + attributes.cellTopLabelSize = cellTopLabelSize(for: message, at: indexPath) + attributes.cellTopLabelAlignment = cellTopLabelAlignment(for: message) + attributes.cellBottomLabelSize = cellBottomLabelSize(for: message, at: indexPath) + attributes.messageTimeLabelSize = messageTimeLabelSize(for: message, at: indexPath) + attributes.cellBottomLabelAlignment = cellBottomLabelAlignment(for: message) + attributes.messageTopLabelSize = messageTopLabelSize(for: message, at: indexPath) + attributes.messageTopLabelAlignment = messageTopLabelAlignment(for: message, at: indexPath) + + attributes.messageBottomLabelAlignment = messageBottomLabelAlignment(for: message, at: indexPath) + attributes.messageBottomLabelSize = messageBottomLabelSize(for: message, at: indexPath) + + attributes.accessoryViewSize = accessoryViewSize(for: message) + attributes.accessoryViewPadding = accessoryViewPadding(for: message) + attributes.accessoryViewPosition = accessoryViewPosition(for: message) + } + + open override func sizeForItem(at indexPath: IndexPath) -> CGSize { + let dataSource = messagesLayout.messagesDataSource + let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView) + let itemHeight = cellContentHeight(for: message, at: indexPath) + return CGSize(width: messagesLayout.itemWidth, height: itemHeight) + } + + open func cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + let messageContainerHeight = messageContainerSize(for: message, at: indexPath).height + let cellBottomLabelHeight = cellBottomLabelSize(for: message, at: indexPath).height + let messageBottomLabelHeight = messageBottomLabelSize(for: message, at: indexPath).height + let cellTopLabelHeight = cellTopLabelSize(for: message, at: indexPath).height + let messageTopLabelHeight = messageTopLabelSize(for: message, at: indexPath).height + let messageVerticalPadding = messageContainerPadding(for: message).vertical + let avatarHeight = avatarSize(for: message, at: indexPath).height + let avatarVerticalPosition = avatarPosition(for: message).vertical + let accessoryViewHeight = accessoryViewSize(for: message).height + + switch avatarVerticalPosition { + case .messageCenter: + let totalLabelHeight: CGFloat = cellTopLabelHeight + messageTopLabelHeight + + messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight + cellBottomLabelHeight + let cellHeight = max(avatarHeight, totalLabelHeight) + return max(cellHeight, accessoryViewHeight) + case .messageBottom: + var cellHeight: CGFloat = 0 + cellHeight += messageBottomLabelHeight + cellHeight += cellBottomLabelHeight + let labelsHeight = messageContainerHeight + messageVerticalPadding + cellTopLabelHeight + messageTopLabelHeight + cellHeight += max(labelsHeight, avatarHeight) + return max(cellHeight, accessoryViewHeight) + case .messageTop: + var cellHeight: CGFloat = 0 + cellHeight += cellTopLabelHeight + cellHeight += messageTopLabelHeight + let labelsHeight = messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight + cellBottomLabelHeight + cellHeight += max(labelsHeight, avatarHeight) + return max(cellHeight, accessoryViewHeight) + case .messageLabelTop: + var cellHeight: CGFloat = 0 + cellHeight += cellTopLabelHeight + let messageLabelsHeight = messageContainerHeight + messageBottomLabelHeight + messageVerticalPadding + + messageTopLabelHeight + cellBottomLabelHeight + cellHeight += max(messageLabelsHeight, avatarHeight) + return max(cellHeight, accessoryViewHeight) + case .cellTop, .cellBottom: + let totalLabelHeight: CGFloat = cellTopLabelHeight + messageTopLabelHeight + + messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight + cellBottomLabelHeight + let cellHeight = max(avatarHeight, totalLabelHeight) + return max(cellHeight, accessoryViewHeight) } + } - public var incomingAvatarSize = CGSize(width: 30, height: 30) - public var outgoingAvatarSize = CGSize(width: 30, height: 30) - - public var incomingAvatarPosition = AvatarPosition(vertical: .cellBottom) - public var outgoingAvatarPosition = AvatarPosition(vertical: .cellBottom) - - public var incomingMessagePadding = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30) - public var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) - - public var incomingCellTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) - public var outgoingCellTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) - - public var incomingMessageTopLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42)) - public var outgoingMessageTopLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42)) - - public var incomingMessageBottomLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42)) - public var outgoingMessageBottomLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42)) - - public var incomingAccessoryViewSize = CGSize.zero - public var outgoingAccessoryViewSize = CGSize.zero + // MARK: - Avatar - public var incomingAccessoryViewPadding = HorizontalEdgeInsets.zero - public var outgoingAccessoryViewPadding = HorizontalEdgeInsets.zero + open func avatarPosition(for message: MessageType) -> AvatarPosition { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + var position = isFromCurrentSender ? outgoingAvatarPosition : incomingAvatarPosition - open override func configure(attributes: UICollectionViewLayoutAttributes) { - guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return } - - let dataSource = messagesLayout.messagesDataSource - let indexPath = attributes.indexPath - let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView) - - attributes.avatarSize = avatarSize(for: message) - attributes.avatarPosition = avatarPosition(for: message) - - attributes.messageContainerPadding = messageContainerPadding(for: message) - attributes.messageContainerSize = messageContainerSize(for: message) - attributes.cellTopLabelSize = cellTopLabelSize(for: message, at: indexPath) - attributes.messageTopLabelSize = messageTopLabelSize(for: message, at: indexPath) - attributes.messageTopLabelAlignment = messageTopLabelAlignment(for: message) - - attributes.messageBottomLabelAlignment = messageBottomLabelAlignment(for: message) - attributes.messageBottomLabelSize = messageBottomLabelSize(for: message, at: indexPath) - - attributes.accessoryViewSize = accessoryViewSize(for: message) - attributes.accessoryViewPadding = accessoryViewPadding(for: message) + switch position.horizontal { + case .cellTrailing, .cellLeading: + break + case .natural: + position.horizontal = isFromCurrentSender ? .cellTrailing : .cellLeading } - - open override func sizeForItem(at indexPath: IndexPath) -> CGSize { - let dataSource = messagesLayout.messagesDataSource - let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView) - let itemHeight = cellContentHeight(for: message, at: indexPath) - return CGSize(width: messagesLayout.itemWidth, height: itemHeight) + return position + } + + open func avatarSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + let layoutDelegate = messagesLayout.messagesLayoutDelegate + let collectionView = messagesLayout.messagesCollectionView + if let size = layoutDelegate.avatarSize(for: message, at: indexPath, in: collectionView) { + return size } - - open func cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - - let messageContainerHeight = messageContainerSize(for: message).height - let messageBottomLabelHeight = messageBottomLabelSize(for: message, at: indexPath).height - let cellTopLabelHeight = cellTopLabelSize(for: message, at: indexPath).height - let messageTopLabelHeight = messageTopLabelSize(for: message, at: indexPath).height - let messageVerticalPadding = messageContainerPadding(for: message).vertical - let avatarHeight = avatarSize(for: message).height - let avatarVerticalPosition = avatarPosition(for: message).vertical - let accessoryViewHeight = accessoryViewSize(for: message).height - - switch avatarVerticalPosition { - case .messageCenter: - let totalLabelHeight: CGFloat = cellTopLabelHeight + messageTopLabelHeight - + messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight - let cellHeight = max(avatarHeight, totalLabelHeight) - return max(cellHeight, accessoryViewHeight) - case .messageBottom: - var cellHeight: CGFloat = 0 - cellHeight += messageBottomLabelHeight - let labelsHeight = messageContainerHeight + messageVerticalPadding + cellTopLabelHeight + messageTopLabelHeight - cellHeight += max(labelsHeight, avatarHeight) - return max(cellHeight, accessoryViewHeight) - case .messageTop: - var cellHeight: CGFloat = 0 - cellHeight += cellTopLabelHeight - cellHeight += messageTopLabelHeight - let labelsHeight = messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight - cellHeight += max(labelsHeight, avatarHeight) - return max(cellHeight, accessoryViewHeight) - case .messageLabelTop: - var cellHeight: CGFloat = 0 - cellHeight += cellTopLabelHeight - let messageLabelsHeight = messageContainerHeight + messageBottomLabelHeight + messageVerticalPadding + messageTopLabelHeight - cellHeight += max(messageLabelsHeight, avatarHeight) - return max(cellHeight, accessoryViewHeight) - case .cellTop, .cellBottom: - let totalLabelHeight: CGFloat = cellTopLabelHeight + messageTopLabelHeight - + messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight - let cellHeight = max(avatarHeight, totalLabelHeight) - return max(cellHeight, accessoryViewHeight) - } + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingAvatarSize : incomingAvatarSize + } + + // MARK: - Top cell Label + + open func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + let layoutDelegate = messagesLayout.messagesLayoutDelegate + let collectionView = messagesLayout.messagesCollectionView + let height = layoutDelegate.cellTopLabelHeight(for: message, at: indexPath, in: collectionView) + return CGSize(width: messagesLayout.itemWidth, height: height) + } + + open func cellTopLabelAlignment(for message: MessageType) -> LabelAlignment { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingCellTopLabelAlignment : incomingCellTopLabelAlignment + } + + // MARK: - Top message Label + + open func messageTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + let layoutDelegate = messagesLayout.messagesLayoutDelegate + let collectionView = messagesLayout.messagesCollectionView + let height = layoutDelegate.messageTopLabelHeight(for: message, at: indexPath, in: collectionView) + return CGSize(width: messagesLayout.itemWidth, height: height) + } + + open func messageTopLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { + let collectionView = messagesLayout.messagesCollectionView + let layoutDelegate = messagesLayout.messagesLayoutDelegate + + if let alignment = layoutDelegate.messageTopLabelAlignment(for: message, at: indexPath, in: collectionView) { + return alignment } - // MARK: - Avatar + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingMessageTopLabelAlignment : incomingMessageTopLabelAlignment + } - open func avatarPosition(for message: MessageType) -> AvatarPosition { - let dataSource = messagesLayout.messagesDataSource - let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) - var position = isFromCurrentSender ? outgoingAvatarPosition : incomingAvatarPosition + // MARK: - Message time label - switch position.horizontal { - case .cellTrailing, .cellLeading: - break - case .natural: - position.horizontal = isFromCurrentSender ? .cellTrailing : .cellLeading - } - return position + open func messageTimeLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + let dataSource = messagesLayout.messagesDataSource + guard let attributedText = dataSource.messageTimestampLabelAttributedText(for: message, at: indexPath) else { + return .zero } - - open func avatarSize(for message: MessageType) -> CGSize { - let dataSource = messagesLayout.messagesDataSource - let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? outgoingAvatarSize : incomingAvatarSize + let size = attributedText.size() + return CGSize(width: size.width, height: size.height) + } + + // MARK: - Bottom cell Label + + open func cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + let layoutDelegate = messagesLayout.messagesLayoutDelegate + let collectionView = messagesLayout.messagesCollectionView + let height = layoutDelegate.cellBottomLabelHeight(for: message, at: indexPath, in: collectionView) + return CGSize(width: messagesLayout.itemWidth, height: height) + } + + open func cellBottomLabelAlignment(for message: MessageType) -> LabelAlignment { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingCellBottomLabelAlignment : incomingCellBottomLabelAlignment + } + + // MARK: - Bottom Message Label + + open func messageBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + let layoutDelegate = messagesLayout.messagesLayoutDelegate + let collectionView = messagesLayout.messagesCollectionView + let height = layoutDelegate.messageBottomLabelHeight(for: message, at: indexPath, in: collectionView) + return CGSize(width: messagesLayout.itemWidth, height: height) + } + + open func messageBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { + let collectionView = messagesLayout.messagesCollectionView + let layoutDelegate = messagesLayout.messagesLayoutDelegate + + if let alignment = layoutDelegate.messageBottomLabelAlignment(for: message, at: indexPath, in: collectionView) { + return alignment } - // MARK: - Top cell Label + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingMessageBottomLabelAlignment : incomingMessageBottomLabelAlignment + } - open func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - let layoutDelegate = messagesLayout.messagesLayoutDelegate - let collectionView = messagesLayout.messagesCollectionView - let height = layoutDelegate.cellTopLabelHeight(for: message, at: indexPath, in: collectionView) - return CGSize(width: messagesLayout.itemWidth, height: height) - } + // MARK: - MessageContainer - open func cellTopLabelAlignment(for message: MessageType) -> LabelAlignment { - let dataSource = messagesLayout.messagesDataSource - let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? outgoingCellTopLabelAlignment : incomingCellTopLabelAlignment - } - - // MARK: - Top message Label - - open func messageTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - let layoutDelegate = messagesLayout.messagesLayoutDelegate - let collectionView = messagesLayout.messagesCollectionView - let height = layoutDelegate.messageTopLabelHeight(for: message, at: indexPath, in: collectionView) - return CGSize(width: messagesLayout.itemWidth, height: height) - } - - open func messageTopLabelAlignment(for message: MessageType) -> LabelAlignment { - let dataSource = messagesLayout.messagesDataSource - let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? outgoingMessageTopLabelAlignment : incomingMessageTopLabelAlignment - } + open func messageContainerPadding(for message: MessageType) -> UIEdgeInsets { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingMessagePadding : incomingMessagePadding + } - // MARK: - Bottom Label + open func messageContainerSize(for _: MessageType, at _: IndexPath) -> CGSize { + // Returns .zero by default + .zero + } - open func messageBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - let layoutDelegate = messagesLayout.messagesLayoutDelegate - let collectionView = messagesLayout.messagesCollectionView - let height = layoutDelegate.messageBottomLabelHeight(for: message, at: indexPath, in: collectionView) - return CGSize(width: messagesLayout.itemWidth, height: height) - } + open func messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + let avatarWidth: CGFloat = avatarSize(for: message, at: indexPath).width + let messagePadding = messageContainerPadding(for: message) + let accessoryWidth = accessoryViewSize(for: message).width + let accessoryPadding = accessoryViewPadding(for: message) + return messagesLayout.itemWidth - avatarWidth - messagePadding.horizontal - accessoryWidth - accessoryPadding + .horizontal - avatarLeadingTrailingPadding + } - open func messageBottomLabelAlignment(for message: MessageType) -> LabelAlignment { - let dataSource = messagesLayout.messagesDataSource - let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? outgoingMessageBottomLabelAlignment : incomingMessageBottomLabelAlignment - } + // MARK: Public - // MARK: - Accessory View + public var incomingAvatarSize = CGSize(width: 30, height: 30) + public var outgoingAvatarSize = CGSize(width: 30, height: 30) - public func accessoryViewSize(for message: MessageType) -> CGSize { - let dataSource = messagesLayout.messagesDataSource - let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? outgoingAccessoryViewSize : incomingAccessoryViewSize - } + public var incomingAvatarPosition = AvatarPosition(vertical: .cellBottom) + public var outgoingAvatarPosition = AvatarPosition(vertical: .cellBottom) - public func accessoryViewPadding(for message: MessageType) -> HorizontalEdgeInsets { - let dataSource = messagesLayout.messagesDataSource - let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? outgoingAccessoryViewPadding : incomingAccessoryViewPadding - } + public var avatarLeadingTrailingPadding: CGFloat = 0 - // MARK: - MessageContainer + public var incomingMessagePadding = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30) + public var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) - open func messageContainerPadding(for message: MessageType) -> UIEdgeInsets { - let dataSource = messagesLayout.messagesDataSource - let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? outgoingMessagePadding : incomingMessagePadding - } + public var incomingCellTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) + public var outgoingCellTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) - open func messageContainerSize(for message: MessageType) -> CGSize { - // Returns .zero by default - return .zero - } + public var incomingCellBottomLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42)) + public var outgoingCellBottomLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42)) - open func messageContainerMaxWidth(for message: MessageType) -> CGFloat { - let avatarWidth = avatarSize(for: message).width - let messagePadding = messageContainerPadding(for: message) - let accessoryWidth = accessoryViewSize(for: message).width - let accessoryPadding = accessoryViewPadding(for: message) - return messagesLayout.itemWidth - avatarWidth - messagePadding.horizontal - accessoryWidth - accessoryPadding.horizontal - } + public var incomingMessageTopLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42)) + public var outgoingMessageTopLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42)) - // MARK: - Helpers + public var incomingMessageBottomLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42)) + public var outgoingMessageBottomLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42)) - public var messagesLayout: MessagesCollectionViewFlowLayout { - guard let layout = layout as? MessagesCollectionViewFlowLayout else { - fatalError("Layout object is missing or is not a MessagesCollectionViewFlowLayout") - } - return layout - } + public var incomingAccessoryViewSize = CGSize.zero + public var outgoingAccessoryViewSize = CGSize.zero - internal func labelSize(for attributedText: NSAttributedString, considering maxWidth: CGFloat) -> CGSize { - let constraintBox = CGSize(width: maxWidth, height: .greatestFiniteMagnitude) - let rect = attributedText.boundingRect(with: constraintBox, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil).integral + public var incomingAccessoryViewPadding = HorizontalEdgeInsets.zero + public var outgoingAccessoryViewPadding = HorizontalEdgeInsets.zero - return rect.size + public var incomingAccessoryViewPosition: AccessoryPosition = .messageCenter + public var outgoingAccessoryViewPosition: AccessoryPosition = .messageCenter + + // MARK: - Helpers + + public var messagesLayout: MessagesCollectionViewFlowLayout { + guard let layout = layout as? MessagesCollectionViewFlowLayout else { + fatalError("Layout object is missing or is not a MessagesCollectionViewFlowLayout") } + return layout + } + + // MARK: - Accessory View + + public func accessoryViewSize(for message: MessageType) -> CGSize { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingAccessoryViewSize : incomingAccessoryViewSize + } + + public func accessoryViewPadding(for message: MessageType) -> HorizontalEdgeInsets { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingAccessoryViewPadding : incomingAccessoryViewPadding + } + + public func accessoryViewPosition(for message: MessageType) -> AccessoryPosition { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingAccessoryViewPosition : incomingAccessoryViewPosition + } + + // MARK: Internal + internal lazy var textContainer: NSTextContainer = { + let textContainer = NSTextContainer() + textContainer.maximumNumberOfLines = 0 + textContainer.lineFragmentPadding = 0 + return textContainer + }() + internal lazy var layoutManager: NSLayoutManager = { + let layoutManager = NSLayoutManager() + layoutManager.addTextContainer(textContainer) + return layoutManager + }() + internal lazy var textStorage: NSTextStorage = { + let textStorage = NSTextStorage() + textStorage.addLayoutManager(layoutManager) + return textStorage + }() + + internal func labelSize(for attributedText: NSAttributedString, considering maxWidth: CGFloat) -> CGSize { + let constraintBox = CGSize(width: maxWidth, height: .greatestFiniteMagnitude) + + textContainer.size = constraintBox + textStorage.replaceCharacters(in: NSRange(location: 0, length: textStorage.length), with: attributedText) + layoutManager.ensureLayout(for: textContainer) + + let size = layoutManager.usedRect(for: textContainer).size + + return CGSize(width: size.width.rounded(.up), height: size.height.rounded(.up)) + } } -fileprivate extension UIEdgeInsets { - init(top: CGFloat = 0, bottom: CGFloat = 0, left: CGFloat = 0, right: CGFloat = 0) { - self.init(top: top, left: left, bottom: bottom, right: right) - } +extension UIEdgeInsets { + fileprivate init(top: CGFloat = 0, bottom: CGFloat = 0, left: CGFloat = 0, right: CGFloat = 0) { + self.init(top: top, left: left, bottom: bottom, right: right) + } } diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index cd5fe5af8..fa280b932 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -1,262 +1,357 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. -import UIKit import AVFoundation +import Foundation +import UIKit /// The layout object used by `MessagesCollectionView` to determine the size of all /// framework provided `MessageCollectionViewCell` subclasses. open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { + // MARK: Lifecycle + + // MARK: - Initializers + + public override init() { + super.init() + setupView() + setupObserver() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupView() + setupObserver() + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: Open + + open override class var layoutAttributesClass: AnyClass { + MessagesCollectionViewLayoutAttributes.self + } - open override class var layoutAttributesClass: AnyClass { - return MessagesCollectionViewLayoutAttributes.self + // MARK: - Cell Sizing + + lazy open var textMessageSizeCalculator = TextMessageSizeCalculator(layout: self) + lazy open var attributedTextMessageSizeCalculator = TextMessageSizeCalculator(layout: self) + lazy open var emojiMessageSizeCalculator: TextMessageSizeCalculator = { + let sizeCalculator = TextMessageSizeCalculator(layout: self) + sizeCalculator.messageLabelFont = UIFont.systemFont(ofSize: sizeCalculator.messageLabelFont.pointSize * 2) + return sizeCalculator + }() + + lazy open var photoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self) + lazy open var videoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self) + lazy open var locationMessageSizeCalculator = LocationMessageSizeCalculator(layout: self) + lazy open var audioMessageSizeCalculator = AudioMessageSizeCalculator(layout: self) + lazy open var contactMessageSizeCalculator = ContactMessageSizeCalculator(layout: self) + lazy open var typingIndicatorSizeCalculator = TypingCellSizeCalculator(layout: self) + lazy open var linkPreviewMessageSizeCalculator = LinkPreviewMessageSizeCalculator(layout: self) + + /// A method that by default checks if the section is the last in the + /// `messagesCollectionView` and that `isTypingIndicatorViewHidden` + /// is FALSE + /// + /// - Parameter section + /// - Returns: A Boolean indicating if the TypingIndicator should be presented at the given section + open func isSectionReservedForTypingIndicator(_ section: Int) -> Bool { + !isTypingIndicatorViewHidden && section == messagesCollectionView.numberOfSections - 1 + } + + // MARK: - Attributes + + open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { + guard let attributesArray = super.layoutAttributesForElements(in: rect) as? [MessagesCollectionViewLayoutAttributes] + else { + return nil } - - /// The `MessagesCollectionView` that owns this layout object. - public var messagesCollectionView: MessagesCollectionView { - guard let messagesCollectionView = collectionView as? MessagesCollectionView else { - fatalError(MessageKitError.layoutUsedOnForeignType) - } - return messagesCollectionView + for attributes in attributesArray where attributes.representedElementCategory == .cell { + let cellSizeCalculator = cellSizeCalculatorForItem(at: attributes.indexPath) + cellSizeCalculator.configure(attributes: attributes) } - - /// The `MessagesDataSource` for the layout's collection view. - public var messagesDataSource: MessagesDataSource { - guard let messagesDataSource = messagesCollectionView.messagesDataSource else { - fatalError(MessageKitError.nilMessagesDataSource) - } - return messagesDataSource + return attributesArray + } + + open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { + guard let attributes = super.layoutAttributesForItem(at: indexPath) as? MessagesCollectionViewLayoutAttributes else { + return nil } - - /// The `MessagesLayoutDelegate` for the layout's collection view. - public var messagesLayoutDelegate: MessagesLayoutDelegate { - guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { - fatalError(MessageKitError.nilMessagesLayoutDelegate) - } - return messagesLayoutDelegate + if attributes.representedElementCategory == .cell { + let cellSizeCalculator = cellSizeCalculatorForItem(at: attributes.indexPath) + cellSizeCalculator.configure(attributes: attributes) } + return attributes + } - public var itemWidth: CGFloat { - guard let collectionView = collectionView else { return 0 } - return collectionView.frame.width - sectionInset.left - sectionInset.right - } + // MARK: - Layout Invalidation - // MARK: - Initializers + open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { + collectionView?.bounds.width != newBounds.width + } - public override init() { - super.init() - - setupView() - setupObserver() - } + open override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext { + let context = super.invalidationContext(forBoundsChange: newBounds) + guard let flowLayoutContext = context as? UICollectionViewFlowLayoutInvalidationContext else { return context } + flowLayoutContext.invalidateFlowLayoutDelegateMetrics = shouldInvalidateLayout(forBoundsChange: newBounds) + return flowLayoutContext + } - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - - setupView() - setupObserver() - } + /// Note: + /// - If you override this method, remember to call MessageLayoutDelegate's + /// customCellSizeCalculator(for:at:in:) method for MessageKind.custom messages, if necessary + /// - If you are using the typing indicator be sure to return the `typingIndicatorSizeCalculator` + /// when the section is reserved for it, indicated by `isSectionReservedForTypingIndicator` + open func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator { + if isSectionReservedForTypingIndicator(indexPath.section) { + return typingIndicatorSizeCalculator + } + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + switch message.kind { + case .text: + return messagesLayoutDelegate + .textCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? textMessageSizeCalculator + case .attributedText: + return messagesLayoutDelegate.attributedTextCellSizeCalculator( + for: message, + at: indexPath, + in: messagesCollectionView) ?? attributedTextMessageSizeCalculator + case .emoji: + return messagesLayoutDelegate + .emojiCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? emojiMessageSizeCalculator + case .photo: + return messagesLayoutDelegate + .photoCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? photoMessageSizeCalculator + case .video: + return messagesLayoutDelegate + .videoCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? videoMessageSizeCalculator + case .location: + return messagesLayoutDelegate + .locationCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? + locationMessageSizeCalculator + case .audio: + return messagesLayoutDelegate + .audioCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? audioMessageSizeCalculator + case .contact: + return messagesLayoutDelegate + .contactCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? contactMessageSizeCalculator + case .linkPreview: + return linkPreviewMessageSizeCalculator + case .custom: + return messagesLayoutDelegate.customCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) + } + } - deinit { - NotificationCenter.default.removeObserver(self) - } - - // MARK: - Methods - - private func setupView() { - sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) - } - - private func setupObserver() { - NotificationCenter.default.addObserver(self, selector: #selector(MessagesCollectionViewFlowLayout.handleOrientationChange(_:)), name: UIDevice.orientationDidChangeNotification, object: nil) - } + open func sizeForItem(at indexPath: IndexPath) -> CGSize { + let calculator = cellSizeCalculatorForItem(at: indexPath) + return calculator.sizeForItem(at: indexPath) + } - // MARK: - Attributes - - open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { - guard let attributesArray = super.layoutAttributesForElements(in: rect) as? [MessagesCollectionViewLayoutAttributes] else { - return nil - } - for attributes in attributesArray where attributes.representedElementCategory == .cell { - let cellSizeCalculator = cellSizeCalculatorForItem(at: attributes.indexPath) - cellSizeCalculator.configure(attributes: attributes) - } - return attributesArray - } + /// Get all `MessageSizeCalculator`s + open func messageSizeCalculators() -> [MessageSizeCalculator] { + [ + textMessageSizeCalculator, + attributedTextMessageSizeCalculator, + emojiMessageSizeCalculator, + photoMessageSizeCalculator, + videoMessageSizeCalculator, + locationMessageSizeCalculator, + audioMessageSizeCalculator, + contactMessageSizeCalculator, + linkPreviewMessageSizeCalculator, + ] + } - open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { - guard let attributes = super.layoutAttributesForItem(at: indexPath) as? MessagesCollectionViewLayoutAttributes else { - return nil - } - if attributes.representedElementCategory == .cell { - let cellSizeCalculator = cellSizeCalculatorForItem(at: attributes.indexPath) - cellSizeCalculator.configure(attributes: attributes) - } - return attributes - } + // MARK: Public - // MARK: - Layout Invalidation + public private(set) var isTypingIndicatorViewHidden = true - open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { - return collectionView?.bounds.width != newBounds.width + /// The `MessagesCollectionView` that owns this layout object. + public var messagesCollectionView: MessagesCollectionView { + guard let messagesCollectionView = collectionView as? MessagesCollectionView else { + fatalError(MessageKitError.layoutUsedOnForeignType) } + return messagesCollectionView + } - open override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext { - let context = super.invalidationContext(forBoundsChange: newBounds) - guard let flowLayoutContext = context as? UICollectionViewFlowLayoutInvalidationContext else { return context } - flowLayoutContext.invalidateFlowLayoutDelegateMetrics = shouldInvalidateLayout(forBoundsChange: newBounds) - return flowLayoutContext + /// The `MessagesDataSource` for the layout's collection view. + public var messagesDataSource: MessagesDataSource { + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) } + return messagesDataSource + } - @objc - private func handleOrientationChange(_ notification: Notification) { - invalidateLayout() + /// The `MessagesLayoutDelegate` for the layout's collection view. + public var messagesLayoutDelegate: MessagesLayoutDelegate { + guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { + fatalError(MessageKitError.nilMessagesLayoutDelegate) } + return messagesLayoutDelegate + } - // MARK: - Cell Sizing - - lazy open var textMessageSizeCalculator = TextMessageSizeCalculator(layout: self) - lazy open var attributedTextMessageSizeCalculator = TextMessageSizeCalculator(layout: self) - lazy open var emojiMessageSizeCalculator: TextMessageSizeCalculator = { - let sizeCalculator = TextMessageSizeCalculator(layout: self) - sizeCalculator.messageLabelFont = UIFont.systemFont(ofSize: sizeCalculator.messageLabelFont.pointSize * 2) - return sizeCalculator - }() - lazy open var photoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self) - lazy open var videoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self) - lazy open var locationMessageSizeCalculator = LocationMessageSizeCalculator(layout: self) - - /// - Note: - /// If you override this method, remember to call MessageLayoutDelegate's customCellSizeCalculator(for:at:in:) method for MessageKind.custom messages, if necessary - open func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator { - let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - switch message.kind { - case .text: - return textMessageSizeCalculator - case .attributedText: - return attributedTextMessageSizeCalculator - case .emoji: - return emojiMessageSizeCalculator - case .photo: - return photoMessageSizeCalculator - case .video: - return videoMessageSizeCalculator - case .location: - return locationMessageSizeCalculator - case .custom: - return messagesLayoutDelegate.customCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) - } - } + public var itemWidth: CGFloat { + guard let collectionView = collectionView else { return 0 } + return collectionView.frame.width - sectionInset.left - sectionInset.right + } - open func sizeForItem(at indexPath: IndexPath) -> CGSize { - let calculator = cellSizeCalculatorForItem(at: indexPath) - return calculator.sizeForItem(at: indexPath) - } - - /// Set `incomingAvatarSize` of all `MessageSizeCalculator`s - public func setMessageIncomingAvatarSize(_ newSize: CGSize) { - messageSizeCalculators().forEach { $0.incomingAvatarSize = newSize } - } - - /// Set `outgoingAvatarSize` of all `MessageSizeCalculator`s - public func setMessageOutgoingAvatarSize(_ newSize: CGSize) { - messageSizeCalculators().forEach { $0.outgoingAvatarSize = newSize } - } - - /// Set `incomingAvatarPosition` of all `MessageSizeCalculator`s - public func setMessageIncomingAvatarPosition(_ newPosition: AvatarPosition) { - messageSizeCalculators().forEach { $0.incomingAvatarPosition = newPosition } - } - - /// Set `outgoingAvatarPosition` of all `MessageSizeCalculator`s - public func setMessageOutgoingAvatarPosition(_ newPosition: AvatarPosition) { - messageSizeCalculators().forEach { $0.outgoingAvatarPosition = newPosition } - } - - /// Set `incomingMessagePadding` of all `MessageSizeCalculator`s - public func setMessageIncomingMessagePadding(_ newPadding: UIEdgeInsets) { - messageSizeCalculators().forEach { $0.incomingMessagePadding = newPadding } - } - - /// Set `outgoingMessagePadding` of all `MessageSizeCalculator`s - public func setMessageOutgoingMessagePadding(_ newPadding: UIEdgeInsets) { - messageSizeCalculators().forEach { $0.outgoingMessagePadding = newPadding } - } - - /// Set `incomingCellTopLabelAlignment` of all `MessageSizeCalculator`s - public func setMessageIncomingCellTopLabelAlignment(_ newAlignment: LabelAlignment) { - messageSizeCalculators().forEach { $0.incomingCellTopLabelAlignment = newAlignment } - } - - /// Set `outgoingCellTopLabelAlignment` of all `MessageSizeCalculator`s - public func setMessageOutgoingCellTopLabelAlignment(_ newAlignment: LabelAlignment) { - messageSizeCalculators().forEach { $0.outgoingCellTopLabelAlignment = newAlignment } - } - - /// Set `incomingMessageTopLabelAlignment` of all `MessageSizeCalculator`s - public func setMessageIncomingMessageTopLabelAlignment(_ newAlignment: LabelAlignment) { - messageSizeCalculators().forEach { $0.incomingMessageTopLabelAlignment = newAlignment } - } - - /// Set `outgoingMessageTopLabelAlignment` of all `MessageSizeCalculator`s - public func setMessageOutgoingMessageTopLabelAlignment(_ newAlignment: LabelAlignment) { - messageSizeCalculators().forEach { $0.outgoingMessageTopLabelAlignment = newAlignment } - } - - /// Set `incomingMessageBottomLabelAlignment` of all `MessageSizeCalculator`s - public func setMessageIncomingMessageBottomLabelAlignment(_ newAlignment: LabelAlignment) { - messageSizeCalculators().forEach { $0.incomingMessageBottomLabelAlignment = newAlignment } - } - - /// Set `outgoingMessageBottomLabelAlignment` of all `MessageSizeCalculator`s - public func setMessageOutgoingMessageBottomLabelAlignment(_ newAlignment: LabelAlignment) { - messageSizeCalculators().forEach { $0.outgoingMessageBottomLabelAlignment = newAlignment } - } + /// Set `incomingAvatarSize` of all `MessageSizeCalculator`s + public func setMessageIncomingAvatarSize(_ newSize: CGSize) { + messageSizeCalculators().forEach { $0.incomingAvatarSize = newSize } + } - /// Set `incomingAccessoryViewSize` of all `MessageSizeCalculator`s - public func setMessageIncomingAccessoryViewSize(_ newSize: CGSize) { - messageSizeCalculators().forEach { $0.incomingAccessoryViewSize = newSize } - } + /// Set `outgoingAvatarSize` of all `MessageSizeCalculator`s + public func setMessageOutgoingAvatarSize(_ newSize: CGSize) { + messageSizeCalculators().forEach { $0.outgoingAvatarSize = newSize } + } - /// Set `outgoingAvatarSize` of all `MessageSizeCalculator`s - public func setMessageOutgoingAccessoryViewSize(_ newSize: CGSize) { - messageSizeCalculators().forEach { $0.outgoingAccessoryViewSize = newSize } - } + /// Set `incomingAvatarPosition` of all `MessageSizeCalculator`s + public func setMessageIncomingAvatarPosition(_ newPosition: AvatarPosition) { + messageSizeCalculators().forEach { $0.incomingAvatarPosition = newPosition } + } - /// Set `incomingAccessoryViewSize` of all `MessageSizeCalculator`s - public func setMessageIncomingAccessoryViewPadding(_ newPadding: HorizontalEdgeInsets) { - messageSizeCalculators().forEach { $0.incomingAccessoryViewPadding = newPadding } - } + /// Set `outgoingAvatarPosition` of all `MessageSizeCalculator`s + public func setMessageOutgoingAvatarPosition(_ newPosition: AvatarPosition) { + messageSizeCalculators().forEach { $0.outgoingAvatarPosition = newPosition } + } - /// Set `outgoingAvatarSize` of all `MessageSizeCalculator`s - public func setMessageOutgoingAccessoryViewPadding(_ newPadding: HorizontalEdgeInsets) { - messageSizeCalculators().forEach { $0.outgoingAccessoryViewPadding = newPadding } - } + /// Set `avatarLeadingTrailingPadding` of all `MessageSizeCalculator`s + public func setAvatarLeadingTrailingPadding(_ newPadding: CGFloat) { + messageSizeCalculators().forEach { $0.avatarLeadingTrailingPadding = newPadding } + } - /// Get all `MessageSizeCalculator`s - open func messageSizeCalculators() -> [MessageSizeCalculator] { - return [textMessageSizeCalculator, attributedTextMessageSizeCalculator, emojiMessageSizeCalculator, photoMessageSizeCalculator, videoMessageSizeCalculator, locationMessageSizeCalculator] - } - + /// Set `incomingMessagePadding` of all `MessageSizeCalculator`s + public func setMessageIncomingMessagePadding(_ newPadding: UIEdgeInsets) { + messageSizeCalculators().forEach { $0.incomingMessagePadding = newPadding } + } + + /// Set `outgoingMessagePadding` of all `MessageSizeCalculator`s + public func setMessageOutgoingMessagePadding(_ newPadding: UIEdgeInsets) { + messageSizeCalculators().forEach { $0.outgoingMessagePadding = newPadding } + } + + /// Set `incomingCellTopLabelAlignment` of all `MessageSizeCalculator`s + public func setMessageIncomingCellTopLabelAlignment(_ newAlignment: LabelAlignment) { + messageSizeCalculators().forEach { $0.incomingCellTopLabelAlignment = newAlignment } + } + + /// Set `outgoingCellTopLabelAlignment` of all `MessageSizeCalculator`s + public func setMessageOutgoingCellTopLabelAlignment(_ newAlignment: LabelAlignment) { + messageSizeCalculators().forEach { $0.outgoingCellTopLabelAlignment = newAlignment } + } + + /// Set `incomingCellBottomLabelAlignment` of all `MessageSizeCalculator`s + public func setMessageIncomingCellBottomLabelAlignment(_ newAlignment: LabelAlignment) { + messageSizeCalculators().forEach { $0.incomingCellBottomLabelAlignment = newAlignment } + } + + /// Set `outgoingCellBottomLabelAlignment` of all `MessageSizeCalculator`s + public func setMessageOutgoingCellBottomLabelAlignment(_ newAlignment: LabelAlignment) { + messageSizeCalculators().forEach { $0.outgoingCellBottomLabelAlignment = newAlignment } + } + + /// Set `incomingMessageTopLabelAlignment` of all `MessageSizeCalculator`s + public func setMessageIncomingMessageTopLabelAlignment(_ newAlignment: LabelAlignment) { + messageSizeCalculators().forEach { $0.incomingMessageTopLabelAlignment = newAlignment } + } + + /// Set `outgoingMessageTopLabelAlignment` of all `MessageSizeCalculator`s + public func setMessageOutgoingMessageTopLabelAlignment(_ newAlignment: LabelAlignment) { + messageSizeCalculators().forEach { $0.outgoingMessageTopLabelAlignment = newAlignment } + } + + /// Set `incomingMessageBottomLabelAlignment` of all `MessageSizeCalculator`s + public func setMessageIncomingMessageBottomLabelAlignment(_ newAlignment: LabelAlignment) { + messageSizeCalculators().forEach { $0.incomingMessageBottomLabelAlignment = newAlignment } + } + + /// Set `outgoingMessageBottomLabelAlignment` of all `MessageSizeCalculator`s + public func setMessageOutgoingMessageBottomLabelAlignment(_ newAlignment: LabelAlignment) { + messageSizeCalculators().forEach { $0.outgoingMessageBottomLabelAlignment = newAlignment } + } + + /// Set `incomingAccessoryViewSize` of all `MessageSizeCalculator`s + public func setMessageIncomingAccessoryViewSize(_ newSize: CGSize) { + messageSizeCalculators().forEach { $0.incomingAccessoryViewSize = newSize } + } + + /// Set `outgoingAccessoryViewSize` of all `MessageSizeCalculator`s + public func setMessageOutgoingAccessoryViewSize(_ newSize: CGSize) { + messageSizeCalculators().forEach { $0.outgoingAccessoryViewSize = newSize } + } + + /// Set `incomingAccessoryViewPadding` of all `MessageSizeCalculator`s + public func setMessageIncomingAccessoryViewPadding(_ newPadding: HorizontalEdgeInsets) { + messageSizeCalculators().forEach { $0.incomingAccessoryViewPadding = newPadding } + } + + /// Set `outgoingAccessoryViewPadding` of all `MessageSizeCalculator`s + public func setMessageOutgoingAccessoryViewPadding(_ newPadding: HorizontalEdgeInsets) { + messageSizeCalculators().forEach { $0.outgoingAccessoryViewPadding = newPadding } + } + + /// Set `incomingAccessoryViewPosition` of all `MessageSizeCalculator`s + public func setMessageIncomingAccessoryViewPosition(_ newPosition: AccessoryPosition) { + messageSizeCalculators().forEach { $0.incomingAccessoryViewPosition = newPosition } + } + + /// Set `outgoingAccessoryViewPosition` of all `MessageSizeCalculator`s + public func setMessageOutgoingAccessoryViewPosition(_ newPosition: AccessoryPosition) { + messageSizeCalculators().forEach { $0.outgoingAccessoryViewPosition = newPosition } + } + + // MARK: Internal + + // MARK: - Typing Indicator API + + /// Notifies the layout that the typing indicator will change state + /// + /// - Parameters: + /// - isHidden: A Boolean value that is to be the new state of the typing indicator + internal func setTypingIndicatorViewHidden(_ isHidden: Bool) { + isTypingIndicatorViewHidden = isHidden + } + + // MARK: Private + + // MARK: - Methods + + private func setupView() { + sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) + } + + private func setupObserver() { + NotificationCenter.default.addObserver( + self, + selector: #selector(MessagesCollectionViewFlowLayout.handleOrientationChange(_:)), + name: UIDevice.orientationDidChangeNotification, + object: nil) + } + + @objc + private func handleOrientationChange(_: Notification) { + invalidateLayout() + } } diff --git a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift index e055ac570..ff8fefad6 100644 --- a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift +++ b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift @@ -1,96 +1,91 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import UIKit /// The layout attributes used by a `MessageCollectionViewCell` to layout its subviews. -open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes { +open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes { + // MARK: Open - // MARK: - Properties + // MARK: - Methods - public var avatarSize: CGSize = .zero - public var avatarPosition = AvatarPosition(vertical: .cellBottom) + open override func copy(with zone: NSZone? = nil) -> Any { + // swiftlint:disable force_cast + let copy = super.copy(with: zone) as! MessagesCollectionViewLayoutAttributes + copy.avatarSize = avatarSize + copy.avatarPosition = avatarPosition + copy.avatarLeadingTrailingPadding = avatarLeadingTrailingPadding + copy.messageContainerSize = messageContainerSize + copy.messageContainerPadding = messageContainerPadding + copy.messageLabelFont = messageLabelFont + copy.messageLabelInsets = messageLabelInsets + copy.cellTopLabelAlignment = cellTopLabelAlignment + copy.cellTopLabelSize = cellTopLabelSize + copy.cellBottomLabelAlignment = cellBottomLabelAlignment + copy.cellBottomLabelSize = cellBottomLabelSize + copy.messageTimeLabelSize = messageTimeLabelSize + copy.messageTopLabelAlignment = messageTopLabelAlignment + copy.messageTopLabelSize = messageTopLabelSize + copy.messageBottomLabelAlignment = messageBottomLabelAlignment + copy.messageBottomLabelSize = messageBottomLabelSize + copy.accessoryViewSize = accessoryViewSize + copy.accessoryViewPadding = accessoryViewPadding + copy.accessoryViewPosition = accessoryViewPosition + copy.linkPreviewFonts = linkPreviewFonts + return copy + // swiftlint:enable force_cast + } - public var messageContainerSize: CGSize = .zero - public var messageContainerPadding: UIEdgeInsets = .zero - public var messageLabelFont: UIFont = UIFont.preferredFont(forTextStyle: .body) - public var messageLabelInsets: UIEdgeInsets = .zero + // MARK: - Properties - public var cellTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) - public var cellTopLabelSize: CGSize = .zero - - public var messageTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) - public var messageTopLabelSize: CGSize = .zero + public var avatarSize: CGSize = .zero + public var avatarPosition = AvatarPosition(vertical: .cellBottom) + public var avatarLeadingTrailingPadding: CGFloat = 0 - public var messageBottomLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) - public var messageBottomLabelSize: CGSize = .zero + public var messageContainerSize: CGSize = .zero + public var messageContainerPadding: UIEdgeInsets = .zero + public var messageLabelFont = UIFont.preferredFont(forTextStyle: .body) + public var messageLabelInsets: UIEdgeInsets = .zero - public var accessoryViewSize: CGSize = .zero - public var accessoryViewPadding: HorizontalEdgeInsets = .zero + public var cellTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) + public var cellTopLabelSize: CGSize = .zero - // MARK: - Methods + public var cellBottomLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) + public var cellBottomLabelSize: CGSize = .zero - open override func copy(with zone: NSZone? = nil) -> Any { - // swiftlint:disable force_cast - let copy = super.copy(with: zone) as! MessagesCollectionViewLayoutAttributes - copy.avatarSize = avatarSize - copy.avatarPosition = avatarPosition - copy.messageContainerSize = messageContainerSize - copy.messageContainerPadding = messageContainerPadding - copy.messageLabelFont = messageLabelFont - copy.messageLabelInsets = messageLabelInsets - copy.cellTopLabelAlignment = cellTopLabelAlignment - copy.cellTopLabelSize = cellTopLabelSize - copy.messageTopLabelAlignment = messageTopLabelAlignment - copy.messageTopLabelSize = messageTopLabelSize - copy.messageBottomLabelAlignment = messageBottomLabelAlignment - copy.messageBottomLabelSize = messageBottomLabelSize - copy.accessoryViewSize = accessoryViewSize - copy.accessoryViewPadding = accessoryViewPadding - return copy - // swiftlint:enable force_cast - } + public var messageTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) + public var messageTopLabelSize: CGSize = .zero - open override func isEqual(_ object: Any?) -> Bool { - // MARK: - LEAVE this as is - if let attributes = object as? MessagesCollectionViewLayoutAttributes { - return super.isEqual(object) && attributes.avatarSize == avatarSize - && attributes.avatarPosition == attributes.avatarPosition - && attributes.messageContainerSize == messageContainerSize - && attributes.messageContainerPadding == messageContainerPadding - && attributes.messageLabelFont == messageLabelFont - && attributes.messageLabelInsets == messageLabelInsets - && attributes.cellTopLabelAlignment == cellTopLabelAlignment - && attributes.cellTopLabelSize == cellTopLabelSize - && attributes.messageTopLabelAlignment == messageTopLabelAlignment - && attributes.messageTopLabelSize == messageTopLabelSize - && attributes.messageBottomLabelAlignment == messageBottomLabelAlignment - && attributes.messageBottomLabelSize == messageBottomLabelSize - && attributes.accessoryViewSize == accessoryViewSize - && attributes.accessoryViewPadding == accessoryViewPadding - } else { - return false - } - } + public var messageBottomLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) + public var messageBottomLabelSize: CGSize = .zero + + public var messageTimeLabelSize: CGSize = .zero + + public var accessoryViewSize: CGSize = .zero + public var accessoryViewPadding: HorizontalEdgeInsets = .zero + public var accessoryViewPosition: AccessoryPosition = .messageCenter + + public var linkPreviewFonts = LinkPreviewFonts( + titleFont: .preferredFont(forTextStyle: .footnote), + teaserFont: .preferredFont(forTextStyle: .caption2), + domainFont: .preferredFont(forTextStyle: .caption1)) } diff --git a/Sources/Layout/TextMessageSizeCalculator.swift b/Sources/Layout/TextMessageSizeCalculator.swift index 9f2b5cc16..85cb3890f 100644 --- a/Sources/Layout/TextMessageSizeCalculator.swift +++ b/Sources/Layout/TextMessageSizeCalculator.swift @@ -1,90 +1,95 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation +import UIKit open class TextMessageSizeCalculator: MessageSizeCalculator { + // MARK: Open + + open override func messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + let maxWidth = super.messageContainerMaxWidth(for: message, at: indexPath) + let textInsets = messageLabelInsets(for: message) + return maxWidth - textInsets.horizontal + } + + open override func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + let maxWidth = messageContainerMaxWidth(for: message, at: indexPath) + + var messageContainerSize: CGSize + let attributedText: NSAttributedString + + let textMessageKind = message.kind.textMessageKind + switch textMessageKind { + case .attributedText(let text): + attributedText = text + case .text(let text), .emoji(let text): + attributedText = NSAttributedString(string: text, attributes: [.font: messageLabelFont]) + default: + fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)") + } - public var incomingMessageLabelInsets = UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14) - public var outgoingMessageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18) + messageContainerSize = labelSize(for: attributedText, considering: maxWidth) - public var messageLabelFont = UIFont.preferredFont(forTextStyle: .body) + let messageInsets = messageLabelInsets(for: message) + messageContainerSize.width += messageInsets.horizontal + messageContainerSize.height += messageInsets.vertical - internal func messageLabelInsets(for message: MessageType) -> UIEdgeInsets { - let dataSource = messagesLayout.messagesDataSource - let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? outgoingMessageLabelInsets : incomingMessageLabelInsets - } + return messageContainerSize + } - open override func messageContainerMaxWidth(for message: MessageType) -> CGFloat { - let maxWidth = super.messageContainerMaxWidth(for: message) - let textInsets = messageLabelInsets(for: message) - return maxWidth - textInsets.horizontal - } + open override func configure(attributes: UICollectionViewLayoutAttributes) { + super.configure(attributes: attributes) + guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return } - open override func messageContainerSize(for message: MessageType) -> CGSize { - let maxWidth = messageContainerMaxWidth(for: message) + let dataSource = messagesLayout.messagesDataSource + let indexPath = attributes.indexPath + let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView) - var messageContainerSize: CGSize - let attributedText: NSAttributedString + attributes.messageLabelInsets = messageLabelInsets(for: message) + attributes.messageLabelFont = messageLabelFont - switch message.kind { - case .attributedText(let text): - attributedText = text - case .text(let text), .emoji(let text): - attributedText = NSAttributedString(string: text, attributes: [.font: messageLabelFont]) - default: - fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)") - } + switch message.kind { + case .attributedText(let text): + guard !text.string.isEmpty else { return } + guard let font = text.attribute(.font, at: 0, effectiveRange: nil) as? UIFont else { return } + attributes.messageLabelFont = font + default: + break + } + } - messageContainerSize = labelSize(for: attributedText, considering: maxWidth) + // MARK: Public - let messageInsets = messageLabelInsets(for: message) - messageContainerSize.width += messageInsets.horizontal - messageContainerSize.height += messageInsets.vertical + public var incomingMessageLabelInsets = UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14) + public var outgoingMessageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18) - return messageContainerSize - } + public var messageLabelFont = UIFont.preferredFont(forTextStyle: .body) - open override func configure(attributes: UICollectionViewLayoutAttributes) { - super.configure(attributes: attributes) - guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return } - - let dataSource = messagesLayout.messagesDataSource - let indexPath = attributes.indexPath - let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView) - - attributes.messageLabelInsets = messageLabelInsets(for: message) - attributes.messageLabelFont = messageLabelFont - - switch message.kind { - case .attributedText(let text): - guard !text.string.isEmpty else { return } - guard let font = text.attribute(.font, at: 0, effectiveRange: nil) as? UIFont else { return } - attributes.messageLabelFont = font - default: - break - } - } + // MARK: Internal + + internal func messageLabelInsets(for message: MessageType) -> UIEdgeInsets { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingMessageLabelInsets : incomingMessageLabelInsets + } } diff --git a/Sources/Layout/TypingIndicatorCellSizeCalculator.swift b/Sources/Layout/TypingIndicatorCellSizeCalculator.swift new file mode 100644 index 000000000..f8d687956 --- /dev/null +++ b/Sources/Layout/TypingIndicatorCellSizeCalculator.swift @@ -0,0 +1,39 @@ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit + +open class TypingCellSizeCalculator: CellSizeCalculator { + // MARK: Lifecycle + + public init(layout: MessagesCollectionViewFlowLayout? = nil) { + super.init() + self.layout = layout + } + + // MARK: Open + + open override func sizeForItem(at _: IndexPath) -> CGSize { + guard let layout = layout as? MessagesCollectionViewFlowLayout else { return .zero } + return layout.messagesLayoutDelegate.typingIndicatorViewSize(for: layout) + } +} diff --git a/Sources/Models/AccessoryPosition.swift b/Sources/Models/AccessoryPosition.swift new file mode 100644 index 000000000..59c09c5ce --- /dev/null +++ b/Sources/Models/AccessoryPosition.swift @@ -0,0 +1,45 @@ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +/// Used to determine the `Horizontal` and `Vertical` position of +/// an `AccessoryView` in a `MessageCollectionViewCell`. +public enum AccessoryPosition: Equatable { + /// Aligns the `AccessoryView`'s top edge to the cell's top edge. + case cellTop + + /// Aligns the `AccessoryView`'s top edge to the `messageTopLabel`'s top edge. + case messageLabelTop + + /// Aligns the `AccessoryView`'s top edge to the `MessageContainerView`'s top edge. + case messageTop + + /// Aligns the `AccessoryView` center to the `MessageContainerView` center. + case messageCenter + + /// Aligns the `AccessoryView`'s bottom edge to the `MessageContainerView`s bottom edge. + case messageBottom + + /// Aligns the `AccessoryView`'s bottom edge to the cell's bottom edge. + case cellBottom +} diff --git a/Sources/Models/Avatar.swift b/Sources/Models/Avatar.swift index 29fa16943..42872a755 100644 --- a/Sources/Models/Avatar.swift +++ b/Sources/Models/Avatar.swift @@ -1,47 +1,44 @@ -/* - MIT License +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. - Copyright (c) 2017-2018 MessageKit +import Foundation +import UIKit - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: +/// An object used to group the information to be used by an `AvatarView`. +public struct Avatar { + // MARK: - Properties - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. + /// The image to be used for an `AvatarView`. + public let image: UIImage? - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ + /// The placeholder initials to be used in the case where no image is provided. + /// + /// The default value of this property is "?". + public var initials = "?" -import Foundation + // MARK: - Initializer -/// An object used to group the information to be used by an `AvatarView`. -public struct Avatar { - - // MARK: - Properties - - /// The image to be used for an `AvatarView`. - public let image: UIImage? - - /// The placeholder initials to be used in the case where no image is provided. - /// - /// The default value of this property is "?". - public var initials: String = "?" - - // MARK: - Initializer - - public init(image: UIImage? = nil, initials: String = "?") { - self.image = image - self.initials = initials - } - + public init(image: UIImage? = nil, initials: String = "?") { + self.image = image + self.initials = initials + } } diff --git a/Sources/Models/AvatarPosition.swift b/Sources/Models/AvatarPosition.swift index 3cf2bca25..9f6000a26 100644 --- a/Sources/Models/AvatarPosition.swift +++ b/Sources/Models/AvatarPosition.swift @@ -1,98 +1,92 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation +import UIKit + +// MARK: - AvatarPosition /// Used to determine the `Horizontal` and `Vertical` position of // an `AvatarView` in a `MessageCollectionViewCell`. -public struct AvatarPosition { - - /// An enum representing the horizontal alignment of an `AvatarView`. - public enum Horizontal { - - /// Positions the `AvatarView` on the side closest to the cell's leading edge. - case cellLeading - - /// Positions the `AvatarView` on the side closest to the cell's trailing edge. - case cellTrailing - - /// Positions the `AvatarView` based on whether the message is from the current Sender. - /// The cell is positioned `.cellTrailling` if `isFromCurrentSender` is true - /// and `.cellLeading` if false. - case natural - } - - /// An enum representing the verical alignment for an `AvatarView`. - public enum Vertical { - - /// Aligns the `AvatarView`'s top edge to the cell's top edge. - case cellTop - - /// Aligns the `AvatarView`'s top edge to the `messageTopLabel`'s top edge. - case messageLabelTop - - /// Aligns the `AvatarView`'s top edge to the `MessageContainerView`'s top edge. - case messageTop - - /// Aligns the `AvatarView` center to the `MessageContainerView` center. - case messageCenter - - /// Aligns the `AvatarView`'s bottom edge to the `MessageContainerView`s bottom edge. - case messageBottom - - /// Aligns the `AvatarView`'s bottom edge to the cell's bottom edge. - case cellBottom - - } - - // MARK: - Properties - - // The vertical position - public var vertical: Vertical - - // The horizontal position - public var horizontal: Horizontal - - // MARK: - Initializers - - public init(horizontal: Horizontal, vertical: Vertical) { - self.horizontal = horizontal - self.vertical = vertical - } - - public init(vertical: Vertical) { - self.init(horizontal: .natural, vertical: vertical) - } - -} +public struct AvatarPosition: Equatable { + // MARK: Lifecycle -// MARK: - Equatable Conformance + public init(horizontal: Horizontal, vertical: Vertical) { + self.horizontal = horizontal + self.vertical = vertical + } + + public init(vertical: Vertical) { + self.init(horizontal: .natural, vertical: vertical) + } + + // MARK: Public + + /// An enum representing the horizontal alignment of an `AvatarView`. + public enum Horizontal { + /// Positions the `AvatarView` on the side closest to the cell's leading edge. + case cellLeading + + /// Positions the `AvatarView` on the side closest to the cell's trailing edge. + case cellTrailing + + /// Positions the `AvatarView` based on whether the message is from the current Sender. + /// The cell is positioned `.cellTrailing` if `isFromCurrentSender` is true + /// and `.cellLeading` if false. + case natural + } + + /// An enum representing the vertical alignment for an `AvatarView`. + public enum Vertical { + /// Aligns the `AvatarView`'s top edge to the cell's top edge. + case cellTop -extension AvatarPosition: Equatable { // swiftlint:disable:this explicit_acl explicit_top_level_acl + /// Aligns the `AvatarView`'s top edge to the `messageTopLabel`'s top edge. + case messageLabelTop - public static func == (lhs: AvatarPosition, rhs: AvatarPosition) -> Bool { - return lhs.vertical == rhs.vertical && lhs.horizontal == rhs.horizontal - } + /// Aligns the `AvatarView`'s top edge to the `MessageContainerView`'s top edge. + case messageTop + + /// Aligns the `AvatarView` center to the `MessageContainerView` center. + case messageCenter + + /// Aligns the `AvatarView`'s bottom edge to the `MessageContainerView`s bottom edge. + case messageBottom + + /// Aligns the `AvatarView`'s bottom edge to the cell's bottom edge. + case cellBottom + } + + // The vertical position + public var vertical: Vertical + + // The horizontal position + public var horizontal: Horizontal +} + +// MARK: - Equatable Conformance +extension AvatarPosition { + public static func == (lhs: AvatarPosition, rhs: AvatarPosition) -> Bool { + lhs.vertical == rhs.vertical && lhs.horizontal == rhs.horizontal + } } diff --git a/Sources/Models/DetectorType.swift b/Sources/Models/DetectorType.swift index a49b7b6d9..7fd0cb228 100644 --- a/Sources/Models/DetectorType.swift +++ b/Sources/Models/DetectorType.swift @@ -1,51 +1,80 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation -public enum DetectorType { +public enum DetectorType: Hashable, Sendable { + case address + case date + case phoneNumber + case url + case transitInformation + case custom(NSRegularExpression) - case address - case date - case phoneNumber - case url - case transitInformation + // MARK: Public - // MARK: - Not supported yet + // swiftlint:disable force_try + public static let hashtag = DetectorType.custom(try! NSRegularExpression(pattern: "#[a-zA-Z0-9]{4,}", options: [])) + public static let mention = DetectorType.custom(try! NSRegularExpression(pattern: "@[a-zA-Z0-9]{4,}", options: [])) - //case mention - //case hashtag - //case custom + /// Simply check if the detector type is a .custom + public var isCustom: Bool { + switch self { + case .custom: return true + default: return false + } + } + + /// The hashValue of the `DetectorType` so we can conform to `Hashable` and be sorted. + public func hash(into hasher: inout Hasher) { + hasher.combine(toInt()) + } + + // MARK: Internal - internal var textCheckingType: NSTextCheckingResult.CheckingType { - switch self { - case .address: return .address - case .date: return .date - case .phoneNumber: return .phoneNumber - case .url: return .link - case .transitInformation: return .transitInformation - } + // swiftlint:enable force_try + + internal var textCheckingType: NSTextCheckingResult.CheckingType { + switch self { + case .address: return .address + case .date: return .date + case .phoneNumber: return .phoneNumber + case .url: return .link + case .transitInformation: return .transitInformation + case .custom: return .regularExpression } + } + + // MARK: Private + /// Return an 'Int' value for each `DetectorType` type so `DetectorType` can conform to `Hashable` + private func toInt() -> Int { + switch self { + case .address: return 0 + case .date: return 1 + case .phoneNumber: return 2 + case .url: return 3 + case .transitInformation: return 4 + case .custom(let regex): return regex.hashValue + } + } } diff --git a/Sources/Models/HorizontalEdgeInsets.swift b/Sources/Models/HorizontalEdgeInsets.swift index 6f42cffbc..86cfc3f2d 100644 --- a/Sources/Models/HorizontalEdgeInsets.swift +++ b/Sources/Models/HorizontalEdgeInsets.swift @@ -1,55 +1,55 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation +import UIKit -/// A varient of `UIEdgeInsets` that only has horizontal inset properties -public struct HorizontalEdgeInsets { +// MARK: - HorizontalEdgeInsets - public var left: CGFloat - public var right: CGFloat +/// A variant of `UIEdgeInsets` that only has horizontal inset properties +public struct HorizontalEdgeInsets: Equatable { + public var left: CGFloat + public var right: CGFloat - public init(left: CGFloat, right: CGFloat) { - self.left = left - self.right = right - } + public init(left: CGFloat, right: CGFloat) { + self.left = left + self.right = right + } - public static var zero: HorizontalEdgeInsets { - return HorizontalEdgeInsets(left: 0, right: 0) - } + public static var zero: HorizontalEdgeInsets { + HorizontalEdgeInsets(left: 0, right: 0) + } } -extension HorizontalEdgeInsets: Equatable { // swiftlint:disable:this explicit_acl explicit_top_level_acl +// MARK: Equatable Conformance - public static func == (lhs: HorizontalEdgeInsets, rhs: HorizontalEdgeInsets) -> Bool { - return lhs.left == rhs.left && lhs.right == rhs.right - } +extension HorizontalEdgeInsets { + public static func == (lhs: HorizontalEdgeInsets, rhs: HorizontalEdgeInsets) -> Bool { + lhs.left == rhs.left && lhs.right == rhs.right + } } -internal extension HorizontalEdgeInsets { - - var horizontal: CGFloat { // swiftlint:disable:this explicit_acl - return left + right - } +extension HorizontalEdgeInsets { + internal var horizontal: CGFloat { + left + right + } } diff --git a/Sources/Models/LabelAlignment.swift b/Sources/Models/LabelAlignment.swift index 29624455c..fc9616dcf 100644 --- a/Sources/Models/LabelAlignment.swift +++ b/Sources/Models/LabelAlignment.swift @@ -1,47 +1,43 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import UIKit -public struct LabelAlignment { +// MARK: - LabelAlignment - public var textAlignment: NSTextAlignment - public var textInsets: UIEdgeInsets - - public init(textAlignment: NSTextAlignment, textInsets: UIEdgeInsets) { - self.textAlignment = textAlignment - self.textInsets = textInsets - } +public struct LabelAlignment: Equatable { + public var textAlignment: NSTextAlignment + public var textInsets: UIEdgeInsets + public init(textAlignment: NSTextAlignment, textInsets: UIEdgeInsets) { + self.textAlignment = textAlignment + self.textInsets = textInsets + } } // MARK: - Equatable Conformance -extension LabelAlignment: Equatable { // swiftlint:disable:this explicit_acl explicit_top_level_acl - - public static func == (lhs: LabelAlignment, rhs: LabelAlignment) -> Bool { - return lhs.textAlignment == rhs.textAlignment && lhs.textInsets == rhs.textInsets - } - +extension LabelAlignment { + public static func == (lhs: LabelAlignment, rhs: LabelAlignment) -> Bool { + lhs.textAlignment == rhs.textAlignment && lhs.textInsets == rhs.textInsets + } } diff --git a/Sources/Models/LinkPreviewFonts.swift b/Sources/Models/LinkPreviewFonts.swift new file mode 100644 index 000000000..8d6c14733 --- /dev/null +++ b/Sources/Models/LinkPreviewFonts.swift @@ -0,0 +1,30 @@ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import UIKit + +public struct LinkPreviewFonts: Equatable { + let titleFont: UIFont + let teaserFont: UIFont + let domainFont: UIFont +} diff --git a/Sources/Models/LocationMessageSnapshotOptions.swift b/Sources/Models/LocationMessageSnapshotOptions.swift index 21628fcb0..792a07310 100644 --- a/Sources/Models/LocationMessageSnapshotOptions.swift +++ b/Sources/Models/LocationMessageSnapshotOptions.swift @@ -1,64 +1,70 @@ -/* - MIT License +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. - Copyright (c) 2017-2018 MessageKit +import MapKit - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: +/// An object grouping the settings used by the `MKMapSnapshotter` through the `LocationMessageDisplayDelegate`. +@MainActor +public struct LocationMessageSnapshotOptions { + // MARK: Lifecycle - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. + /// Initialize LocationMessageSnapshotOptions with given parameters + /// + /// - Parameters: + /// - showsBuildings: A Boolean value indicating whether the snapshot image should display buildings. + /// - showsPointsOfInterest: A Boolean value indicating whether the snapshot image should display points of interest. + /// - span: The span of the snapshot. + /// - scale: The scale of the snapshot. + public init( + showsBuildings: Bool = false, + showsPointsOfInterest: Bool = false, + span: MKCoordinateSpan = MKCoordinateSpan(latitudeDelta: 0, longitudeDelta: 0), + scale: CGFloat = UIScreen.main.scale) + { + self.showsBuildings = showsBuildings + self.showsPointsOfInterest = showsPointsOfInterest + self.span = span + self.scale = scale + } - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ + // MARK: Public -import MapKit + /// A Boolean value indicating whether the snapshot image should display buildings. + /// + /// The default value of this property is `false`. + public var showsBuildings: Bool -/// An object grouping the settings used by the `MKMapSnapshotter` through the `LocationMessageDisplayDelegate`. -public struct LocationMessageSnapshotOptions { + /// A Boolean value indicating whether the snapshot image should display points of interest. + /// + /// The default value of this property is `false`. + public var showsPointsOfInterest: Bool + + /// The span of the snapshot. + /// + /// The default value of this property uses a width of `0` and height of `0`. + public var span: MKCoordinateSpan - /// Initialize LocationMessageSnapshotOptions with given parameters - /// - /// - Parameters: - /// - showsBuildings: A Boolean value indicating whether the snapshot image should display buildings. - /// - showsPointsOfInterest: A Boolean value indicating whether the snapshot image should display points of interest. - /// - span: The span of the snapshot. - /// - scale: The scale of the snapshot. - public init(showsBuildings: Bool = false, showsPointsOfInterest: Bool = false, span: MKCoordinateSpan = MKCoordinateSpan(latitudeDelta: 0, longitudeDelta: 0), scale: CGFloat = UIScreen.main.scale) { - self.showsBuildings = showsBuildings - self.showsPointsOfInterest = showsPointsOfInterest - self.span = span - self.scale = scale - } - - /// A Boolean value indicating whether the snapshot image should display buildings. - /// - /// The default value of this property is `false`. - public var showsBuildings: Bool - - /// A Boolean value indicating whether the snapshot image should display points of interest. - /// - /// The default value of this property is `false`. - public var showsPointsOfInterest: Bool - - /// The span of the snapshot. - /// - /// The default value of this property uses a width of `0` and height of `0`. - public var span: MKCoordinateSpan - - /// The scale of the snapshot. - /// - /// The default value of this property uses the `UIScreen.main.scale`. - public var scale: CGFloat - + /// The scale of the snapshot. + /// + /// The default value of this property uses the `UIScreen.main.scale`. + public var scale: CGFloat } diff --git a/Sources/Models/MessageInputBarKind.swift b/Sources/Models/MessageInputBarKind.swift new file mode 100644 index 000000000..6a127b01c --- /dev/null +++ b/Sources/Models/MessageInputBarKind.swift @@ -0,0 +1,29 @@ +// MIT License +// +// Copyright (c) 2017-2022 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import UIKit + +public enum MessageInputBarKind: Equatable { + case messageInputBar + case custom(UIView) +} diff --git a/Sources/Models/MessageKind.swift b/Sources/Models/MessageKind.swift index a29d76d5c..cb11f2d12 100644 --- a/Sources/Models/MessageKind.swift +++ b/Sources/Models/MessageKind.swift @@ -1,68 +1,71 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation /// An enum representing the kind of message and its underlying kind. public enum MessageKind { + /// A standard text message. + /// + /// - Note: The font used for this message will be the value of the + /// `messageLabelFont` property in the `MessagesCollectionViewFlowLayout` object. + /// + /// Using `MessageKind.attributedText(NSAttributedString)` doesn't require you + /// to set this property and results in higher performance. + case text(String) - /// A standard text message. - /// - /// - Note: The font used for this message will be the value of the - /// `messageLabelFont` property in the `MessagesCollectionViewFlowLayout` object. - /// - /// Using `MessageKind.attributedText(NSAttributedString)` doesn't require you - /// to set this property and results in higher performance. - case text(String) - - /// A message with attributed text. - case attributedText(NSAttributedString) + /// A message with attributed text. + case attributedText(NSAttributedString) - /// A photo message. - case photo(MediaItem) + /// A photo message. + case photo(MediaItem) - /// A video message. - case video(MediaItem) + /// A video message. + case video(MediaItem) - /// A location message. - case location(LocationItem) + /// A location message. + case location(LocationItem) - /// An emoji message. - case emoji(String) + /// An emoji message. + case emoji(String) - /// A custom message. - /// - Note: Using this case requires that you implement the following methods and handle this case: - /// - MessagesDataSource: customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell - /// - MessagesLayoutDelegate: customCellSizeCalculator(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator - case custom(Any?) + /// An audio message. + case audio(AudioItem) - // MARK: - Not supported yet + /// A contact message. + case contact(ContactItem) + + /// A link preview message. + case linkPreview(LinkItem) + + /// A custom message. + /// - Note: Using this case requires that you implement the following methods and handle this case: + /// - MessagesDataSource: customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell + /// - MessagesLayoutDelegate: customCellSizeCalculator(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator + case custom(Any?) + + // MARK: - Not supported yet -// case audio(Data) -// // case system(String) -// +// // case placeholder - } diff --git a/Sources/Models/MessageKitDateFormatter.swift b/Sources/Models/MessageKitDateFormatter.swift index c0d02bc27..c3636034e 100644 --- a/Sources/Models/MessageKitDateFormatter.swift +++ b/Sources/Models/MessageKitDateFormatter.swift @@ -1,66 +1,70 @@ -/* - MIT License +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. - Copyright (c) 2017-2018 MessageKit +import Foundation - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: +open class MessageKitDateFormatter: @unchecked Sendable { + // MARK: Lifecycle - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. + // MARK: - Initializer - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ + private init() { } -import Foundation + // MARK: Open -open class MessageKitDateFormatter { + open func configureDateFormatter(for date: Date) { + switch true { + case Calendar.current.isDateInToday(date) || Calendar.current.isDateInYesterday(date): + formatter.doesRelativeDateFormatting = true + formatter.dateStyle = .short + formatter.timeStyle = .short + case Calendar.current.isDate(date, equalTo: Date(), toGranularity: .weekOfYear): + formatter.dateFormat = "EEEE h:mm a" + case Calendar.current.isDate(date, equalTo: Date(), toGranularity: .year): + formatter.dateFormat = "E, d MMM, h:mm a" + default: + formatter.dateFormat = "MMM d, yyyy, h:mm a" + } + } - // MARK: - Properties + // MARK: Public - public static let shared = MessageKitDateFormatter() + // MARK: - Properties - private let formatter = DateFormatter() + public static let shared = MessageKitDateFormatter() - // MARK: - Initializer + // MARK: - Methods - private init() {} + public func string(from date: Date) -> String { + configureDateFormatter(for: date) + return formatter.string(from: date) + } - // MARK: - Methods + public func attributedString(from date: Date, with attributes: [NSAttributedString.Key: Any]) -> NSAttributedString { + let dateString = string(from: date) + return NSAttributedString(string: dateString, attributes: attributes) + } - public func string(from date: Date) -> String { - configureDateFormatter(for: date) - return formatter.string(from: date) - } + // MARK: Private - public func attributedString(from date: Date, with attributes: [NSAttributedString.Key: Any]) -> NSAttributedString { - let dateString = string(from: date) - return NSAttributedString(string: dateString, attributes: attributes) - } - - open func configureDateFormatter(for date: Date) { - switch true { - case Calendar.current.isDateInToday(date) || Calendar.current.isDateInYesterday(date): - formatter.doesRelativeDateFormatting = true - formatter.dateStyle = .short - formatter.timeStyle = .short - case Calendar.current.isDate(date, equalTo: Date(), toGranularity: .weekOfYear): - formatter.dateFormat = "EEEE h:mm a" - case Calendar.current.isDate(date, equalTo: Date(), toGranularity: .year): - formatter.dateFormat = "E, d MMM, h:mm a" - default: - formatter.dateFormat = "MMM d, yyyy, h:mm a" - } - } - + private let formatter = DateFormatter() } diff --git a/Sources/Models/MessageKitError.swift b/Sources/Models/MessageKitError.swift index c30ac5709..2ac4ca46b 100644 --- a/Sources/Models/MessageKitError.swift +++ b/Sources/Models/MessageKitError.swift @@ -1,38 +1,36 @@ -/* - MIT License - - Copyright (c) 2017 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. internal enum MessageKitError { - internal static let avatarPositionUnresolved = "AvatarPosition Horizontal.natural needs to be resolved." - internal static let nilMessagesDataSource = "MessagesDataSource has not been set." - internal static let nilMessagesDisplayDelegate = "MessagesDisplayDelegate has not been set." - internal static let nilMessagesLayoutDelegate = "MessagesLayoutDelegate has not been set." - internal static let notMessagesCollectionView = "The collectionView is not a MessagesCollectionView." - internal static let layoutUsedOnForeignType = "MessagesCollectionViewFlowLayout is being used on a foreign type." - internal static let unrecognizedSectionKind = "Received unrecognized element kind:" - internal static let unrecognizedCheckingResult = "Received an unrecognized NSTextCheckingResult.CheckingType" - internal static let couldNotLoadAssetsBundle = "MessageKit: Could not load the assets bundle" - internal static let couldNotCreateAssetsPath = "MessageKit: Could not create path to the assets bundle." - internal static let customDataUnresolvedCell = "Did not return a cell for MessageKind.custom(Any)." - internal static let customDataUnresolvedSize = "Did not return a size for MessageKind.custom(Any)." + static let avatarPositionUnresolved = "AvatarPosition Horizontal.natural needs to be resolved." + static let nilMessagesDataSource = "MessagesDataSource has not been set." + static let nilMessagesDisplayDelegate = "MessagesDisplayDelegate has not been set." + static let nilMessagesLayoutDelegate = "MessagesLayoutDelegate has not been set." + static let notMessagesCollectionView = "The collectionView is not a MessagesCollectionView." + static let layoutUsedOnForeignType = "MessagesCollectionViewFlowLayout is being used on a foreign type." + static let unrecognizedSectionKind = "Received unrecognized element kind:" + static let unrecognizedCheckingResult = "Received an unrecognized NSTextCheckingResult.CheckingType" + static let couldNotLoadAssetsBundle = "MessageKit: Could not load the assets bundle" + static let customDataUnresolvedCell = "Did not return a cell for MessageKind.custom(Any)." + static let customDataUnresolvedSize = "Did not return a size for MessageKind.custom(Any)." + static let couldNotFindColorAsset = "MessageKit: Could not load the color asset." } diff --git a/Sources/Models/MessageStyle.swift b/Sources/Models/MessageStyle.swift index f8abd50e9..ed1dbeba4 100644 --- a/Sources/Models/MessageStyle.swift +++ b/Sources/Models/MessageStyle.swift @@ -1,151 +1,157 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import UIKit public enum MessageStyle { + // MARK: - MessageStyle - // MARK: - TailCorner + case none + case bubble + case bubbleOutline(UIColor) + case bubbleTail(TailCorner, TailStyle) + case bubbleTailOutline(UIColor, TailCorner, TailStyle) + case customImageTail(UIImage,TailCorner) + case custom((MessageContainerView) -> Void) - public enum TailCorner: String { + // MARK: Public - case topLeft - case bottomLeft - case topRight - case bottomRight + // MARK: - TailCorner - internal var imageOrientation: UIImage.Orientation { - switch self { + public enum TailCorner: String { + case topLeft + case bottomLeft + case topRight + case bottomRight + + internal var imageOrientation: UIImage.Orientation { + switch self { case .bottomRight: return .up case .bottomLeft: return .upMirrored case .topLeft: return .down case .topRight: return .downMirrored - } } } + } - // MARK: - TailStyle - - public enum TailStyle { + // MARK: - TailStyle - case curved - case pointedEdge + public enum TailStyle { + case curved + case pointedEdge - internal var imageNameSuffix: String { - switch self { - case .curved: - return "_tail_v2" - case .pointedEdge: - return "_tail_v1" - } - } + internal var imageNameSuffix: String { + switch self { + case .curved: + return "_tail_v2" + case .pointedEdge: + return "_tail_v1" + } + } + } + + public var image: UIImage? { + if + let imageCacheKey = imageCacheKey, + let cachedImage = MessageStyle.bubbleImageCache.object(forKey: imageCacheKey as NSString) + { + return cachedImage + } + + func strechAndCache(image: UIImage) -> UIImage { + let stretchedImage = stretch(image) + if let imageCacheKey = imageCacheKey { + MessageStyle.bubbleImageCache.setObject(stretchedImage, forKey: imageCacheKey as NSString) + } + return stretchedImage + } + + if case .customImageTail(let image, let corner) = self { + guard let cgImage = image.cgImage else { return nil } + let image = UIImage(cgImage: cgImage, scale: image.scale, orientation: corner.imageOrientation) + return strechAndCache(image: image) } - // MARK: - MessageStyle - - case none - case bubble - case bubbleOutline(UIColor) - case bubbleTail(TailCorner, TailStyle) - case bubbleTailOutline(UIColor, TailCorner, TailStyle) - case custom((MessageContainerView) -> Void) - - // MARK: - Public + guard + let imageName = imageName, + var image = UIImage(named: imageName, in: Bundle.messageKitAssetBundle, compatibleWith: nil) + else { + return nil + } - public var image: UIImage? { - - guard let imageCacheKey = imageCacheKey, let path = imagePath else { return nil } + switch self { + case .none, .custom: + return nil + case .bubble, .bubbleOutline, .customImageTail: + break + case .bubbleTail(let corner, _), .bubbleTailOutline(_, let corner, _): + guard let cgImage = image.cgImage else { return nil } + image = UIImage(cgImage: cgImage, scale: image.scale, orientation: corner.imageOrientation) + } - let cache = MessageStyle.bubbleImageCache + return strechAndCache(image: image) + } - if let cachedImage = cache.object(forKey: imageCacheKey as NSString) { - return cachedImage - } - guard var image = UIImage(contentsOfFile: path) else { return nil } - - switch self { - case .none, .custom: - return nil - case .bubble, .bubbleOutline: - break - case .bubbleTail(let corner, _), .bubbleTailOutline(_, let corner, _): - guard let cgImage = image.cgImage else { return nil } - image = UIImage(cgImage: cgImage, scale: image.scale, orientation: corner.imageOrientation) - } - - let stretchedImage = stretch(image) - cache.setObject(stretchedImage, forKey: imageCacheKey as NSString) - return stretchedImage - } + // MARK: Internal - // MARK: - Internal - - internal static let bubbleImageCache: NSCache = { + nonisolated(unsafe) internal static let bubbleImageCache: NSCache = { let cache = NSCache() cache.name = "com.messagekit.MessageKit.bubbleImageCache" return cache }() - - // MARK: - Private - - private var imageCacheKey: String? { - guard let imageName = imageName else { return nil } - - switch self { - case .bubble, .bubbleOutline: - return imageName - case .bubbleTail(let corner, _), .bubbleTailOutline(_, let corner, _): - return imageName + "_" + corner.rawValue - default: - return nil - } - } - private var imageName: String? { - switch self { - case .bubble: - return "bubble_full" - case .bubbleOutline: - return "bubble_outlined" - case .bubbleTail(_, let tailStyle): - return "bubble_full" + tailStyle.imageNameSuffix - case .bubbleTailOutline(_, _, let tailStyle): - return "bubble_outlined" + tailStyle.imageNameSuffix - case .none, .custom: - return nil - } - } + // MARK: Private - private var imagePath: String? { - guard let imageName = imageName else { return nil } - let assetBundle = Bundle.messageKitAssetBundle() - return assetBundle.path(forResource: imageName, ofType: "png", inDirectory: "Images") - } + private var imageCacheKey: String? { + guard let imageName = imageName else { return nil } - private func stretch(_ image: UIImage) -> UIImage { - let center = CGPoint(x: image.size.width / 2, y: image.size.height / 2) - let capInsets = UIEdgeInsets(top: center.y, left: center.x, bottom: center.y, right: center.x) - return image.resizableImage(withCapInsets: capInsets, resizingMode: .stretch) + switch self { + case .bubble, .bubbleOutline: + return imageName + case .bubbleTail(let corner, _), .bubbleTailOutline(_, let corner, _): + return imageName + "_" + corner.rawValue + default: + return nil + } + } + + private var imageName: String? { + switch self { + case .bubble: + return "bubble_full" + case .bubbleOutline: + return "bubble_outlined" + case .bubbleTail(_, let tailStyle): + return "bubble_full" + tailStyle.imageNameSuffix + case .bubbleTailOutline(_, _, let tailStyle): + return "bubble_outlined" + tailStyle.imageNameSuffix + case .none, .custom, .customImageTail: + return nil } + } + + private func stretch(_ image: UIImage) -> UIImage { + let center = CGPoint(x: image.size.width / 2, y: image.size.height / 2) + let capInsets = UIEdgeInsets(top: center.y, left: center.x, bottom: center.y, right: center.x) + return image.resizableImage(withCapInsets: capInsets, resizingMode: .stretch) + } } diff --git a/Sources/Models/NSConstraintLayoutSet.swift b/Sources/Models/NSConstraintLayoutSet.swift deleted file mode 100644 index cd063dfdc..000000000 --- a/Sources/Models/NSConstraintLayoutSet.swift +++ /dev/null @@ -1,81 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import UIKit - -internal class NSLayoutConstraintSet { - - internal var top: NSLayoutConstraint? - internal var bottom: NSLayoutConstraint? - internal var left: NSLayoutConstraint? - internal var right: NSLayoutConstraint? - internal var centerX: NSLayoutConstraint? - internal var centerY: NSLayoutConstraint? - internal var width: NSLayoutConstraint? - internal var height: NSLayoutConstraint? - - internal init(top: NSLayoutConstraint? = nil, bottom: NSLayoutConstraint? = nil, - left: NSLayoutConstraint? = nil, right: NSLayoutConstraint? = nil, - centerX: NSLayoutConstraint? = nil, centerY: NSLayoutConstraint? = nil, - width: NSLayoutConstraint? = nil, height: NSLayoutConstraint? = nil) { - self.top = top - self.bottom = bottom - self.left = left - self.right = right - self.centerX = centerX - self.centerY = centerY - self.width = width - self.height = height - } - - /// All of the currently configured constraints - private var availableConstraints: [NSLayoutConstraint] { - let constraints = [top, bottom, left, right, centerX, centerY, width, height] - var available: [NSLayoutConstraint] = [] - for constraint in constraints { - if let value = constraint { - available.append(value) - } - } - return available - } - - /// Activates all of the non-nil constraints - /// - /// - Returns: Self - @discardableResult - internal func activate() -> Self { - NSLayoutConstraint.activate(availableConstraints) - return self - } - - /// Deactivates all of the non-nil constraints - /// - /// - Returns: Self - @discardableResult - internal func deactivate() -> Self { - NSLayoutConstraint.deactivate(availableConstraints) - return self - } -} diff --git a/Sources/Models/Sender.swift b/Sources/Models/Sender.swift deleted file mode 100644 index 41995d371..000000000 --- a/Sources/Models/Sender.swift +++ /dev/null @@ -1,57 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import Foundation - -/// An object that groups the metadata of a messages sender. -public struct Sender { - - /// MARK: - Properties - - /// The unique String identifier for the sender. - /// - /// Note: This value must be unique across all senders. - public let id: String - - /// The display name of a sender. - public let displayName: String - - // MARK: - Intializers - - public init(id: String, displayName: String) { - self.id = id - self.displayName = displayName - } -} - -// MARK: - Equatable Conformance - -extension Sender: Equatable { // swiftlint:disable:this explicit_acl explicit_top_level_acl - - /// Two senders are considered equal if they have the same id. - public static func == (left: Sender, right: Sender) -> Bool { - return left.id == right.id - } - -} diff --git a/Sources/Protocols/AudioItem.swift b/Sources/Protocols/AudioItem.swift new file mode 100644 index 000000000..db24e7c58 --- /dev/null +++ b/Sources/Protocols/AudioItem.swift @@ -0,0 +1,37 @@ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import class AVFoundation.AVAudioPlayer +import Foundation +import UIKit + +/// A protocol used to represent the data for an audio message. +public protocol AudioItem { + /// The url where the audio file is located. + var url: URL { get } + + /// The audio file duration in seconds. + var duration: Float { get } + + /// The size of the audio item. + var size: CGSize { get } +} diff --git a/Sources/Protocols/ContactItem.swift b/Sources/Protocols/ContactItem.swift new file mode 100644 index 000000000..9a40fc78d --- /dev/null +++ b/Sources/Protocols/ContactItem.swift @@ -0,0 +1,38 @@ +// MIT License +// +// Copyright (c) 2017-2018 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +/// A protocol used to represent the data for a contact message. +public protocol ContactItem { + /// contact displayed name + var displayName: String { get } + + /// initials from contact first and last name + var initials: String { get } + + /// contact phone numbers + var phoneNumbers: [String] { get } + + /// contact emails + var emails: [String] { get } +} diff --git a/Sources/Protocols/LinkItem.swift b/Sources/Protocols/LinkItem.swift new file mode 100644 index 000000000..5d3342a6f --- /dev/null +++ b/Sources/Protocols/LinkItem.swift @@ -0,0 +1,66 @@ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import CoreGraphics +import Foundation +import UIKit + +// MARK: - LinkItem + +/// A protocol used to represent the data for a link preview message. +public protocol LinkItem { + /// A link item needs a message to present, it can be a simple String or + /// a NSAttributedString, but only one will be shown. + /// LinkItem.text has priority over LinkItem.attributedText. + + /// The message text. + var text: String? { get } + + /// The message attributed text. + var attributedText: NSAttributedString? { get } + + /// The URL. + var url: URL { get } + + /// The title. + var title: String? { get } + + /// The teaser text. + var teaser: String { get } + + /// The thumbnail image. + var thumbnailImage: UIImage { get } +} + +extension LinkItem { + public var textKind: MessageKind { + let kind: MessageKind + if let text = text { + kind = .text(text) + } else if let attributedText = attributedText { + kind = .attributedText(attributedText) + } else { + fatalError("LinkItem must have \"text\" or \"attributedText\"") + } + return kind + } +} diff --git a/Sources/Protocols/LocationItem.swift b/Sources/Protocols/LocationItem.swift index f08fccb37..8789bfa8a 100644 --- a/Sources/Protocols/LocationItem.swift +++ b/Sources/Protocols/LocationItem.swift @@ -1,36 +1,34 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import class CoreLocation.CLLocation +import Foundation +import UIKit /// A protocol used to represent the data for a location message. public protocol LocationItem { + /// The location. + var location: CLLocation { get } - /// The location. - var location: CLLocation { get } - - /// The size of the location item. - var size: CGSize { get } - + /// The size of the location item. + var size: CGSize { get } } diff --git a/Sources/Protocols/MediaItem.swift b/Sources/Protocols/MediaItem.swift index cddb94c7f..28160d4b8 100644 --- a/Sources/Protocols/MediaItem.swift +++ b/Sources/Protocols/MediaItem.swift @@ -1,42 +1,39 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation +import UIKit /// A protocol used to represent the data for a media message. public protocol MediaItem { + /// The url where the media is located. + var url: URL? { get } - /// The url where the media is located. - var url: URL? { get } - - /// The image. - var image: UIImage? { get } - - /// A placeholder image for when the image is obtained asychronously. - var placeholderImage: UIImage { get } + /// The image. + var image: UIImage? { get } - /// The size of the media item. - var size: CGSize { get } + /// A placeholder image for when the image is obtained asynchronously. + var placeholderImage: UIImage { get } + /// The size of the media item. + var size: CGSize { get } } diff --git a/Sources/Protocols/MessageCellDelegate.swift b/Sources/Protocols/MessageCellDelegate.swift index 4fbe9da92..a243a773d 100644 --- a/Sources/Protocols/MessageCellDelegate.swift +++ b/Sources/Protocols/MessageCellDelegate.swift @@ -1,106 +1,188 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation +// MARK: - MessageCellDelegate + /// A protocol used by `MessageContentCell` subclasses to detect taps in the cell's subviews. public protocol MessageCellDelegate: MessageLabelDelegate { + /// Triggered when a tap occurs in the background of the cell. + /// + /// - Parameters: + /// - cell: The cell where the tap occurred. + /// + /// - Note: + /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s + /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` + /// method `messageForItem(at:indexPath:messagesCollectionView)`. + func didTapBackground(in cell: MessageCollectionViewCell) + + /// Triggered when a tap occurs in the `MessageContainerView`. + /// + /// - Parameters: + /// - cell: The cell where the tap occurred. + /// + /// - Note: + /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s + /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` + /// method `messageForItem(at:indexPath:messagesCollectionView)`. + func didTapMessage(in cell: MessageCollectionViewCell) + + /// Triggered when a tap occurs in the `AvatarView`. + /// + /// - Parameters: + /// - cell: The cell where the tap occurred. + /// + /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s + /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` + /// method `messageForItem(at:indexPath:messagesCollectionView)`. + func didTapAvatar(in cell: MessageCollectionViewCell) + + /// Triggered when a tap occurs in the cellTopLabel. + /// + /// - Parameters: + /// - cell: The cell tap the touch occurred. + /// + /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s + /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` + /// method `messageForItem(at:indexPath:messagesCollectionView)`. + func didTapCellTopLabel(in cell: MessageCollectionViewCell) + + /// Triggered when a tap occurs in the cellBottomLabel. + /// + /// - Parameters: + /// - cell: The cell tap the touch occurred. + /// + /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s + /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` + /// method `messageForItem(at:indexPath:messagesCollectionView)`. + func didTapCellBottomLabel(in cell: MessageCollectionViewCell) + + /// Triggered when a tap occurs in the messageTopLabel. + /// + /// - Parameters: + /// - cell: The cell tap the touch occurred. + /// + /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s + /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` + /// method `messageForItem(at:indexPath:messagesCollectionView)`. + func didTapMessageTopLabel(in cell: MessageCollectionViewCell) + + /// Triggered when a tap occurs in the messageBottomLabel. + /// + /// - Parameters: + /// - cell: The cell where the tap occurred. + /// + /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s + /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` + /// method `messageForItem(at:indexPath:messagesCollectionView)`. + func didTapMessageBottomLabel(in cell: MessageCollectionViewCell) + + /// Triggered when a tap occurs in the accessoryView. + /// + /// - Parameters: + /// - cell: The cell where the tap occurred. + /// + /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s + /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` + /// method `messageForItem(at:indexPath:messagesCollectionView)`. + func didTapAccessoryView(in cell: MessageCollectionViewCell) + + /// Triggered when a tap occurs on the image. + /// + /// - Parameters: + /// - cell: The image where the touch occurred. + /// + /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s + /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` + /// method `messageForItem(at:indexPath:messagesCollectionView)`. + func didTapImage(in cell: MessageCollectionViewCell) - /// Triggered when a tap occurs in the `MessageContainerView`. - /// - /// - Parameters: - /// - cell: The cell where the tap occurred. - /// - /// - Note: - /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s - /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` - /// method `messageForItem(at:indexPath:messagesCollectionView)`. - func didTapMessage(in cell: MessageCollectionViewCell) - - /// Triggered when a tap occurs in the `AvatarView`. - /// - /// - Parameters: - /// - cell: The cell where the tap occurred. - /// - /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s - /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` - /// method `messageForItem(at:indexPath:messagesCollectionView)`. - func didTapAvatar(in cell: MessageCollectionViewCell) - - /// Triggered when a tap occurs in the cellTopLabel. - /// - /// - Parameters: - /// - cell: The cell tap the touch occurred. - /// - /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s - /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` - /// method `messageForItem(at:indexPath:messagesCollectionView)`. - func didTapCellTopLabel(in cell: MessageCollectionViewCell) - - /// Triggered when a tap occurs in the messageTopLabel. - /// - /// - Parameters: - /// - cell: The cell tap the touch occurred. - /// - /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s - /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` - /// method `messageForItem(at:indexPath:messagesCollectionView)`. - func didTapMessageTopLabel(in cell: MessageCollectionViewCell) - - /// Triggered when a tap occurs in the messageBottomLabel. - /// - /// - Parameters: - /// - cell: The cell where the tap occurred. - /// - /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s - /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` - /// method `messageForItem(at:indexPath:messagesCollectionView)`. - func didTapMessageBottomLabel(in cell: MessageCollectionViewCell) - - /// Triggered when a tap occurs in the accessoryView. - /// - /// - Parameters: - /// - cell: The cell where the tap occurred. - /// - /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s - /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` - /// method `messageForItem(at:indexPath:messagesCollectionView)`. - func didTapAccessoryView(in cell: MessageCollectionViewCell) + /// Triggered when a tap occurs on the play button from audio cell. + /// + /// - Parameters: + /// - cell: The audio cell where the touch occurred. + /// + /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s + /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` + /// method `messageForItem(at:indexPath:messagesCollectionView)`. + func didTapPlayButton(in cell: AudioMessageCell) + /// Triggered when audio player start playing audio. + /// + /// - Parameters: + /// - cell: The cell where the audio sound is playing. + /// + /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s + /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` + /// method `messageForItem(at:indexPath:messagesCollectionView)`. + func didStartAudio(in cell: AudioMessageCell) + + /// Triggered when audio player pause audio. + /// + /// - Parameters: + /// - cell: The cell where the audio sound is paused. + /// + /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s + /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` + /// method `messageForItem(at:indexPath:messagesCollectionView)`. + func didPauseAudio(in cell: AudioMessageCell) + + /// Triggered when audio player stoped audio. + /// + /// - Parameters: + /// - cell: The cell where the audio sound is stoped. + /// + /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s + /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` + /// method `messageForItem(at:indexPath:messagesCollectionView)`. + func didStopAudio(in cell: AudioMessageCell) } -public extension MessageCellDelegate { +extension MessageCellDelegate { + public func didTapBackground(in _: MessageCollectionViewCell) { } + + public func didTapMessage(in _: MessageCollectionViewCell) { } + + public func didTapAvatar(in _: MessageCollectionViewCell) { } + + public func didTapCellTopLabel(in _: MessageCollectionViewCell) { } + + public func didTapCellBottomLabel(in _: MessageCollectionViewCell) { } + + public func didTapMessageTopLabel(in _: MessageCollectionViewCell) { } + + public func didTapImage(in _: MessageCollectionViewCell) { } + + public func didTapPlayButton(in _: AudioMessageCell) { } - func didTapMessage(in cell: MessageCollectionViewCell) {} + public func didStartAudio(in _: AudioMessageCell) { } - func didTapAvatar(in cell: MessageCollectionViewCell) {} + public func didPauseAudio(in _: AudioMessageCell) { } - func didTapCellTopLabel(in cell: MessageCollectionViewCell) {} + public func didStopAudio(in _: AudioMessageCell) { } - func didTapMessageTopLabel(in cell: MessageCollectionViewCell) {} + public func didTapMessageBottomLabel(in _: MessageCollectionViewCell) { } - func didTapMessageBottomLabel(in cell: MessageCollectionViewCell) {} - - func didTapAccessoryView(in cell: MessageCollectionViewCell) {} + public func didTapAccessoryView(in _: MessageCollectionViewCell) { } } diff --git a/Sources/Protocols/MessageLabelDelegate.swift b/Sources/Protocols/MessageLabelDelegate.swift index e84ea622d..a1b7da6e5 100644 --- a/Sources/Protocols/MessageLabelDelegate.swift +++ b/Sources/Protocols/MessageLabelDelegate.swift @@ -1,74 +1,95 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation +// MARK: - MessageLabelDelegate + /// A protocol used to handle tap events on detected text. public protocol MessageLabelDelegate: AnyObject { + /// Triggered when a tap occurs on a detected address. + /// + /// - Parameters: + /// - addressComponents: The components of the selected address. + func didSelectAddress(_ addressComponents: [String: String]) + + /// Triggered when a tap occurs on a detected date. + /// + /// - Parameters: + /// - date: The selected date. + func didSelectDate(_ date: Date) + + /// Triggered when a tap occurs on a detected phone number. + /// + /// - Parameters: + /// - phoneNumber: The selected phone number. + func didSelectPhoneNumber(_ phoneNumber: String) + + /// Triggered when a tap occurs on a detected URL. + /// + /// - Parameters: + /// - url: The selected URL. + func didSelectURL(_ url: URL) + + /// Triggered when a tap occurs on detected transit information. + /// + /// - Parameters: + /// - transitInformation: The selected transit information. + func didSelectTransitInformation(_ transitInformation: [String: String]) + + /// Triggered when a tap occurs on a mention + /// + /// - Parameters: + /// - mention: The selected mention + func didSelectMention(_ mention: String) + + /// Triggered when a tap occurs on a hashtag + /// + /// - Parameters: + /// - mention: The selected hashtag + func didSelectHashtag(_ hashtag: String) + + /// Triggered when a tap occurs on a custom regular expression + /// + /// - Parameters: + /// - pattern: the pattern of the regular expression + /// - match: part that match with the regular expression + func didSelectCustom(_ pattern: String, match: String?) +} - /// Triggered when a tap occurs on a detected address. - /// - /// - Parameters: - /// - addressComponents: The components of the selected address. - func didSelectAddress(_ addressComponents: [String: String]) - - /// Triggered when a tap occurs on a detected date. - /// - /// - Parameters: - /// - date: The selected date. - func didSelectDate(_ date: Date) - - /// Triggered when a tap occurs on a detected phone number. - /// - /// - Parameters: - /// - phoneNumber: The selected phone number. - func didSelectPhoneNumber(_ phoneNumber: String) - - /// Triggered when a tap occurs on a detected URL. - /// - /// - Parameters: - /// - url: The selected URL. - func didSelectURL(_ url: URL) - - /// Triggered when a tap occurs on detected transit information. - /// - /// - Parameters: - /// - transitInformation: The selected transit information. - func didSelectTransitInformation(_ transitInformation: [String: String]) +extension MessageLabelDelegate { + public func didSelectAddress(_: [String: String]) { } -} + public func didSelectDate(_: Date) { } -public extension MessageLabelDelegate { + public func didSelectPhoneNumber(_: String) { } - func didSelectAddress(_ addressComponents: [String: String]) {} + public func didSelectURL(_: URL) { } - func didSelectDate(_ date: Date) {} + public func didSelectTransitInformation(_: [String: String]) { } - func didSelectPhoneNumber(_ phoneNumber: String) {} + public func didSelectMention(_: String) { } - func didSelectURL(_ url: URL) {} - - func didSelectTransitInformation(_ transitInformation: [String: String]) {} + public func didSelectHashtag(_: String) { } + public func didSelectCustom(_: String, match _: String?) { } } diff --git a/Sources/Protocols/MessageType.swift b/Sources/Protocols/MessageType.swift index 627b7f505..d7c91786a 100644 --- a/Sources/Protocols/MessageType.swift +++ b/Sources/Protocols/MessageType.swift @@ -1,43 +1,39 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation /// A standard protocol representing a message. /// Use this protocol to create your own message object to be used by MessageKit. public protocol MessageType { + /// The sender of the message. + var sender: SenderType { get } - /// The sender of the message. - var sender: Sender { get } + /// The unique identifier for the message. + var messageId: String { get } - /// The unique identifier for the message. - var messageId: String { get } - - /// The date the message was sent. - var sentDate: Date { get } - - /// The kind of message and its underlying kind. - var kind: MessageKind { get } + /// The date the message was sent. + var sentDate: Date { get } + /// The kind of message and its underlying kind. + var kind: MessageKind { get } } diff --git a/Sources/Protocols/MessagesDataSource.swift b/Sources/Protocols/MessagesDataSource.swift index a9d35d804..831d1cf2f 100644 --- a/Sources/Protocols/MessagesDataSource.swift +++ b/Sources/Protocols/MessagesDataSource.swift @@ -1,133 +1,265 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import UIKit +// MARK: - MessagesDataSource + /// An object that adopts the `MessagesDataSource` protocol is responsible for providing /// the data required by a `MessagesCollectionView`. +@MainActor public protocol MessagesDataSource: AnyObject { + /// The `SenderType` of new messages in the `MessagesCollectionView`. + var currentSender: SenderType { get } + + /// A helper method to determine if a given message is from the current `SenderType`. + /// + /// - Parameters: + /// - message: The message to check if it was sent by the current `SenderType`. + /// + /// - Note: + /// The default implementation of this method checks for equality between + /// the message's `SenderType` and the current `SenderType`. + func isFromCurrentSender(message: MessageType) -> Bool + + /// The message to be used for a `MessageCollectionViewCell` at the given `IndexPath`. + /// + /// - Parameters: + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which the message will be displayed. + func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType + + /// The number of sections to be displayed in the `MessagesCollectionView`. + /// + /// - Parameters: + /// - messagesCollectionView: The `MessagesCollectionView` in which the messages will be displayed. + func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int + + /// The number of cells to be displayed in the `MessagesCollectionView`. + /// + /// - Parameters: + /// - section: The number of the section in which the cells will be displayed. + /// - messagesCollectionView: The `MessagesCollectionView` in which the messages will be displayed. + /// - Note: + /// The default implementation of this method returns 1. Putting each message in its own section. + func numberOfItems(inSection section: Int, in messagesCollectionView: MessagesCollectionView) -> Int + + /// The attributed text to be used for cell's top label. + /// + /// - Parameters: + /// - message: The `MessageType` that will be displayed by this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// The default value returned by this method is `nil`. + func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? + + /// The attributed text to be used for cell's bottom label. + /// + /// - Parameters: + /// - message: The `MessageType` that will be displayed by this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// The default value returned by this method is `nil`. + func cellBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? + + /// The attributed text to be used for message bubble's top label. + /// + /// - Parameters: + /// - message: The `MessageType` that will be displayed by this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// The default value returned by this method is `nil`. + func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? + + /// The attributed text to be used for cell's bottom label. + /// + /// - Parameters: + /// - message: The `MessageType` that will be displayed by this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// The default value returned by this method is `nil`. + func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? + + /// The attributed text to be used for cell's timestamp label. + /// The timestamp label is shown when showMessageTimestampOnSwipeLeft is enabled by swiping left over the chat controller. + /// + /// - Parameters: + /// - message: The `MessageType` that will be displayed by this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// The default value returned by this method is `nil`. + func messageTimestampLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? + + /// Text collectionView cell for message with `text`, `attributedText`, `emoji` message types. + /// + /// - Parameters: + /// - message: The `text`, `attributedText`, `emoji` message type + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// This method will return nil by default. You must override this method only if you want your own cell. + func textCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) + -> UICollectionViewCell? + + /// Photo or Video collectionView cell for message with `photo`, `video` message types. + /// + /// - Parameters: + /// - message: The `photo`, `video` message type + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// This method will return nil by default. You must override this method only if you want your own cell. + func photoCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) + -> UICollectionViewCell? + + /// Location collectionView cell for message with `location` message type. + /// + /// - Parameters: + /// - message: The `location` message type + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// This method will return nil by default. You must override this method only if you want your own cell. + func locationCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) + -> UICollectionViewCell? + + /// Audio collectionView cell for message with `audio` message type. + /// + /// - Parameters: + /// - message: The `audio` message type + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// This method will return nil by default. You must override this method only if you want your own cell. + func audioCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) + -> UICollectionViewCell? + + /// Contact collectionView cell for message with `contact` message type. + /// + /// - Parameters: + /// - message: The `contact` message type + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// This method will return nil by default. You must override this method only if you want your own cell. + func contactCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) + -> UICollectionViewCell? - /// The `Sender` of new messages in the `MessagesCollectionView`. - func currentSender() -> Sender - - /// A helper method to determine if a given message is from the current `Sender`. - /// - /// - Parameters: - /// - message: The message to check if it was sent by the current `Sender`. - /// - /// - Note: - /// The default implementation of this method checks for equality between - /// the message's `Sender` and the current `Sender`. - func isFromCurrentSender(message: MessageType) -> Bool - - /// The message to be used for a `MessageCollectionViewCell` at the given `IndexPath`. - /// - /// - Parameters: - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which the message will be displayed. - func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType - - /// The number of sections to be displayed in the `MessagesCollectionView`. - /// - /// - Parameters: - /// - messagesCollectionView: The `MessagesCollectionView` in which the messages will be displayed. - func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int - - /// The number of cells to be displayed in the `MessagesCollectionView`. - /// - /// - Parameters: - /// - section: The number of the section in which the cells will be displayed. - /// - messagesCollectionView: The `MessagesCollectionView` in which the messages will be displayed. - /// - Note: - /// The default implementation of this method returns 1. Putting each message in its own section. - func numberOfItems(inSection section: Int, in messagesCollectionView: MessagesCollectionView) -> Int - - /// The attributed text to be used for cell's top label. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed by this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// The default value returned by this method is `nil`. - func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? - - /// The attributed text to be used for message bubble's top label. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed by this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// The default value returned by this method is `nil`. - func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? - - /// The attributed text to be used for cell's bottom label. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed by this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// The default value returned by this method is `nil`. - func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? - - /// Custom collectionView cell for message with `custom` message type. - /// - /// - Parameters: - /// - message: The `custom` message type - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// - Note: - /// This method will call fatalError() on default. You must override this method if you are using MessageKind.custom messages. - func customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell + /// Custom collectionView cell for message with `custom` message type. + /// + /// - Parameters: + /// - message: The `custom` message type + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// This method will call fatalError() on default. You must override this method if you are using MessageKind.custom messages. + func customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) + -> UICollectionViewCell + + /// Typing indicator cell used when the indicator is set to be shown + /// + /// - Parameters: + /// - indexPath: The index path to dequeue the cell at + /// - messagesCollectionView: The `MessagesCollectionView` the cell is to be rendered in + /// - Returns: A `UICollectionViewCell` that indicates a user is typing + func typingIndicator(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell } -public extension MessagesDataSource { - - func isFromCurrentSender(message: MessageType) -> Bool { - return message.sender == currentSender() - } - - func numberOfItems(inSection section: Int, in messagesCollectionView: MessagesCollectionView) -> Int { - return 1 - } - - func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { - return nil - } - - func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { - return nil - } - - func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { - return nil - } - - func customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell { - fatalError(MessageKitError.customDataUnresolvedCell) - } +extension MessagesDataSource { + public func isFromCurrentSender(message: MessageType) -> Bool { + message.sender.senderId == currentSender.senderId + } + + public func numberOfItems(inSection _: Int, in _: MessagesCollectionView) -> Int { + 1 + } + + public func cellTopLabelAttributedText(for _: MessageType, at _: IndexPath) -> NSAttributedString? { + nil + } + + public func cellBottomLabelAttributedText(for _: MessageType, at _: IndexPath) -> NSAttributedString? { + nil + } + + public func messageTopLabelAttributedText(for _: MessageType, at _: IndexPath) -> NSAttributedString? { + nil + } + + public func messageBottomLabelAttributedText(for _: MessageType, at _: IndexPath) -> NSAttributedString? { + nil + } + + public func messageTimestampLabelAttributedText(for message: MessageType, at _: IndexPath) -> NSAttributedString? { + let sentDate = message.sentDate + let sentDateString = MessageKitDateFormatter.shared.string(from: sentDate) + let timeLabelFont: UIFont = .boldSystemFont(ofSize: 10) + let timeLabelColor: UIColor + timeLabelColor = .systemGray + return NSAttributedString( + string: sentDateString, + attributes: [NSAttributedString.Key.font: timeLabelFont, NSAttributedString.Key.foregroundColor: timeLabelColor]) + } + + public func textCell(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UICollectionViewCell? { + nil + } + + public func photoCell(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UICollectionViewCell? { + nil + } + + public func locationCell(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UICollectionViewCell? { + nil + } + + public func audioCell(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UICollectionViewCell? { + nil + } + + public func contactCell(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UICollectionViewCell? { + nil + } + + public func customCell(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UICollectionViewCell { + fatalError(MessageKitError.customDataUnresolvedCell) + } + + public func typingIndicator( + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) + -> UICollectionViewCell + { + messagesCollectionView.dequeueReusableCell(TypingIndicatorCell.self, for: indexPath) + } } diff --git a/Sources/Protocols/MessagesDisplayDelegate.swift b/Sources/Protocols/MessagesDisplayDelegate.swift index 2af2eac5a..81a5d6722 100644 --- a/Sources/Protocols/MessagesDisplayDelegate.swift +++ b/Sources/Protocols/MessagesDisplayDelegate.swift @@ -1,246 +1,408 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation import MapKit +import UIKit + +// MARK: - MessagesDisplayDelegate /// A protocol used by the `MessagesViewController` to customize the appearance of a `MessageContentCell`. +@MainActor public protocol MessagesDisplayDelegate: AnyObject { - - // MARK: - All Messages - - /// Specifies the `MessageStyle` to be used for a `MessageContainerView`. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed by this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// - Note: - /// The default value returned by this method is `MessageStyle.bubble`. - func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle - - /// Specifies the background color of the `MessageContainerView`. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed by this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// - Note: - /// The default value is `UIColor.clear` for emoji messages. - /// For all other `MessageKind` cases, the color depends on the `Sender`. - /// - /// Current sender: Green - /// - /// All other senders: Gray - func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor - - /// The section header to use for a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed for this header. - /// - indexPath: The `IndexPath` of the header. - /// - messagesCollectionView: The `MessagesCollectionView` in which this header will be displayed. - func messageHeaderView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView - - /// The section footer to use for a given `IndexPath`. - /// - /// - Parameters: - /// - indexPath: The `IndexPath` of the footer. - /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed. - func messageFooterView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView - - /// Used to configure the `AvatarView`β€˜s image in a `MessageContentCell` class. - /// - /// - Parameters: - /// - avatarView: The `AvatarView` of the cell. - /// - message: The `MessageType` that will be displayed by this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// - Note: - /// The default image configured by this method is `?`. - func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) - - /// Used to configure the `AccessoryView` in a `MessageContentCell` class. - /// - /// - Parameters: - /// - accessoryView: The `AccessoryView` of the cell. - /// - message: The `MessageType` that will be displayed by this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// - Note: - /// The default image configured by this method is `?`. - func configureAccessoryView(_ accessoryView: UIView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) - - // MARK: - Text Messages - - /// Specifies the color of the text for a `TextMessageCell`. - /// - /// - Parameters: - /// - message: A `MessageType` with a `MessageKind` case of `.text` to which the color will apply. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// - Note: - /// The default value returned by this method is determined by the messages `Sender`. - /// - /// Current sender: UIColor.white - /// - /// All other senders: UIColor.darkText - func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor - - /// Specifies the `DetectorType`s to check for the `MessageType`'s text against. - /// - /// - Parameters: - /// - message: A `MessageType` with a `MessageKind` case of `.text` or `.attributedText` to which the detectors will apply. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// - Note: - /// This method returns an empty array by default. - func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] - - /// Specifies the attributes for a given `DetectorType` - /// - /// - Parameters: - /// - detector: The `DetectorType` for the applied attributes. - /// - message: A `MessageType` with a `MessageKind` case of `.text` or `.attributedText` to which the detectors will apply. - /// - indexPath: The `IndexPath` of the cell. - func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any] - - // MARK: - Location Messages - - /// Used to configure a `LocationMessageSnapshotOptions` instance to customize the map image on the given location message. - /// - /// - Parameters: - /// - message: A `MessageType` with a `MessageKind` case of `.location`. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` requesting the information. - /// - Returns: The LocationMessageSnapshotOptions instance with the options to customize map style. - func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions - - /// Used to configure the annoation view of the map image on the given location message. - /// - /// - Parameters: - /// - message: A `MessageType` with a `MessageKind` case of `.location`. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` requesting the information. - /// - Returns: The `MKAnnotationView` to use as the annotation view. - func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? - - /// Ask the delegate for a custom animation block to run when whe map screenshot is ready to be displaied in the given location message. - /// The animation block is called with the `UIImageView` to be animated. - /// - /// - Parameters: - /// - message: A `MessageType` with a `MessageKind` case of `.location`. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` requesting the information. - /// - Returns: The animation block to use to apply the location image. - func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)? - - // MARK: - Media Messages - - /// Used to configure the `UIImageView` of a `MediaMessageCell. - /// - /// - Parameters: - /// - imageView: The `UIImageView` of the cell. - /// - message: The `MessageType` that will be displayed by this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - func configureMediaMessageImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) - + // MARK: - All Messages + + /// Specifies the `MessageStyle` to be used for a `MessageContainerView`. + /// + /// - Parameters: + /// - message: The `MessageType` that will be displayed by this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default value returned by this method is `MessageStyle.bubble`. + func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) + -> MessageStyle + + /// Specifies the background color of the `MessageContainerView`. + /// + /// - Parameters: + /// - message: The `MessageType` that will be displayed by this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default value is `UIColor.clear` for emoji messages. + /// For all other `MessageKind` cases, the color depends on the `SenderType`. + /// + /// Current sender: Green + /// + /// All other senders: Gray + func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) + -> UIColor + + /// The section header to use for a given `IndexPath`. + /// + /// - Parameters: + /// - message: The `MessageType` that will be displayed for this header. + /// - indexPath: The `IndexPath` of the header. + /// - messagesCollectionView: The `MessagesCollectionView` in which this header will be displayed. + func messageHeaderView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView + + /// The section footer to use for a given `IndexPath`. + /// + /// - Parameters: + /// - indexPath: The `IndexPath` of the footer. + /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed. + func messageFooterView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView + + /// Used to configure the `AvatarView`β€˜s image in a `MessageContentCell` class. + /// + /// - Parameters: + /// - avatarView: The `AvatarView` of the cell. + /// - message: The `MessageType` that will be displayed by this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default image configured by this method is `?`. + func configureAvatarView( + _ avatarView: AvatarView, + for message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) + + /// Used to configure the `AccessoryView` in a `MessageContentCell` class. + /// + /// - Parameters: + /// - accessoryView: The `AccessoryView` of the cell. + /// - message: The `MessageType` that will be displayed by this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default image configured by this method is `?`. + func configureAccessoryView( + _ accessoryView: UIView, + for message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) + + // MARK: - Text Messages + + /// Specifies the color of the text for a `TextMessageCell`. + /// + /// - Parameters: + /// - message: A `MessageType` with a `MessageKind` case of `.text` to which the color will apply. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default value returned by this method is determined by the messages `SenderType`. + /// + /// Current sender: UIColor.white + /// + /// All other senders: UIColor.darkText + func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) + -> UIColor + + /// Specifies the `DetectorType`s to check for the `MessageType`'s text against. + /// + /// - Parameters: + /// - message: A `MessageType` with a `MessageKind` case of `.text` or `.attributedText` to which the detectors will apply. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// This method returns an empty array by default. + func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) + -> [DetectorType] + + /// Specifies the attributes for a given `DetectorType` + /// + /// - Parameters: + /// - detector: The `DetectorType` for the applied attributes. + /// - message: A `MessageType` with a `MessageKind` case of `.text` or `.attributedText` to which the detectors will apply. + /// - indexPath: The `IndexPath` of the cell. + func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) + -> [NSAttributedString.Key: Any] + + // MARK: - Location Messages + + /// Used to configure a `LocationMessageSnapshotOptions` instance to customize the map image on the given location message. + /// + /// - Parameters: + /// - message: A `MessageType` with a `MessageKind` case of `.location`. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` requesting the information. + /// - Returns: The LocationMessageSnapshotOptions instance with the options to customize map style. + func snapshotOptionsForLocation( + message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions + + /// Used to configure the annotation view of the map image on the given location message. + /// + /// - Parameters: + /// - message: A `MessageType` with a `MessageKind` case of `.location`. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` requesting the information. + /// - Returns: The `MKAnnotationView` to use as the annotation view. + func annotationViewForLocation( + message: MessageType, + at indexPath: IndexPath, + in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? + + /// Ask the delegate for a custom animation block to run when whe map screenshot is ready to be displaied in the given location message. + /// The animation block is called with the `UIImageView` to be animated. + /// + /// - Parameters: + /// - message: A `MessageType` with a `MessageKind` case of `.location`. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` requesting the information. + /// - Returns: The animation block to use to apply the location image. + func animationBlockForLocation( + message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)? + + // MARK: - Media Messages + + /// Used to configure the `UIImageView` of a `MediaMessageCell`. + /// + /// - Parameters: + /// - imageView: The `UIImageView` of the cell. + /// - message: The `MessageType` that will be displayed by this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + func configureMediaMessageImageView( + _ imageView: UIImageView, + for message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) + + // MARK: - Audio Message + + /// Used to configure the audio cell UI: + /// 1. play button selected state; + /// 2. progressView progress; + /// 3. durationLabel text; + /// + /// - Parameters: + /// - cell: The `AudioMessageCell` that needs to be configure. + /// - message: The `MessageType` that configures the cell. + /// + /// - Note: + /// This protocol method is called by MessageKit every time an audio cell needs to be configure + func configureAudioCell(_ cell: AudioMessageCell, message: MessageType) + + /// Specifies the tint color of play button and progress bar for an `AudioMessageCell`. + /// + /// - Parameters: + /// - message: A `MessageType` with a `MessageKind` case of `.audio` to which the color will apply. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default value returned by this method is UIColor.sendButtonBlue + func audioTintColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) + -> UIColor + + /// Used to format the audio sound duration in a readable string + /// + /// - Parameters: + /// - duration: The audio sound duration is seconds. + /// - audioCell: The `AudioMessageCell` that ask for formated duration. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default value is computed like fallow: + /// 1. return the time as 0:ss if duration is up to 59 seconds (e.g. 0:03 means 0 minutes and 3 seconds) + /// 2. return the time as m:ss if duration is greater than 59 and lower than 3600 (e.g. 12:23 means 12 minutes and 23 seconds) + /// 3. return the time as h:mm:ss for anything longer that 3600 seconds (e.g. 1:19:08 means 1 hour 19 minutes and 8 seconds) + func audioProgressTextFormat( + _ duration: Float, + for audioCell: AudioMessageCell, + in messageCollectionView: MessagesCollectionView) -> String + + /// Used to configure the `UIImageView` of a `LinkPreviewMessageCell`. + /// - Parameters: + /// - imageView: The `UIImageView` of the cell. + /// - message: The `MessageType` that will be displayed by this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + func configureLinkPreviewImageView( + _ imageView: UIImageView, + for message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) } -public extension MessagesDisplayDelegate { - - // MARK: - All Messages Defaults - - func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle { - return .bubble +extension MessagesDisplayDelegate { + // MARK: - All Messages Defaults + + public func messageStyle(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> MessageStyle { + .bubble + } + + public func backgroundColor( + for message: MessageType, + at _: IndexPath, + in messagesCollectionView: MessagesCollectionView) + -> UIColor + { + switch message.kind { + case .emoji: + return .clear + default: + guard let dataSource = messagesCollectionView.messagesDataSource else { + return .white + } + return dataSource.isFromCurrentSender(message: message) ? .outgoingMessageBackground : .incomingMessageBackground } - - func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { - - switch message.kind { - case .emoji: - return .clear - default: - guard let dataSource = messagesCollectionView.messagesDataSource else { return .white } - return dataSource.isFromCurrentSender(message: message) ? .outgoingGreen : .incomingGray - } + } + + public func messageHeaderView( + for indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) + -> MessageReusableView + { + messagesCollectionView.dequeueReusableHeaderView(MessageReusableView.self, for: indexPath) + } + + public func messageFooterView( + for indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) + -> MessageReusableView + { + messagesCollectionView.dequeueReusableFooterView(MessageReusableView.self, for: indexPath) + } + + public func configureAvatarView(_ avatarView: AvatarView, for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) { + avatarView.initials = "?" + } + + public func configureAccessoryView(_: UIView, for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) { } + + // MARK: - Text Messages Defaults + + public func textColor( + for message: MessageType, + at _: IndexPath, + in messagesCollectionView: MessagesCollectionView) + -> UIColor + { + guard let dataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) } - - func messageHeaderView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView { - return messagesCollectionView.dequeueReusableHeaderView(MessageReusableView.self, for: indexPath) + return dataSource.isFromCurrentSender(message: message) ? .outgoingMessageLabel : .incomingMessageLabel + } + + public func enabledDetectors(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> [DetectorType] { + [] + } + + public func detectorAttributes(for _: DetectorType, and _: MessageType, at _: IndexPath) -> [NSAttributedString.Key: Any] { + MessageLabel.defaultAttributes + } + + // MARK: - Location Messages Defaults + + public func snapshotOptionsForLocation( + message _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) + -> LocationMessageSnapshotOptions + { + LocationMessageSnapshotOptions() + } + + public func annotationViewForLocation( + message _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) + -> MKAnnotationView? + { + MKPinAnnotationView(annotation: nil, reuseIdentifier: nil) + } + + public func animationBlockForLocation( + message _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) -> ((UIImageView) -> Void)? + { + nil + } + + // MARK: - Media Message Defaults + + public func configureMediaMessageImageView( + _: UIImageView, + for _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) { } + + // MARK: - Audio Message Defaults + + public func configureAudioCell(_: AudioMessageCell, message _: MessageType) { } + + public func audioTintColor( + for message: MessageType, + at _: IndexPath, + in messagesCollectionView: MessagesCollectionView) + -> UIColor + { + guard let dataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) } - - func messageFooterView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView { - return messagesCollectionView.dequeueReusableFooterView(MessageReusableView.self, for: indexPath) - } - - func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { - avatarView.initials = "?" + return dataSource.isFromCurrentSender(message: message) ? .outgoingAudioMessageTint : .incomingAudioMessageTint + } + + public func audioProgressTextFormat(_ duration: Float, for _: AudioMessageCell, in _: MessagesCollectionView) -> String { + var returnValue = "0:00" + // print the time as 0:ss if duration is up to 59 seconds + // print the time as m:ss if duration is up to 59:59 seconds + // print the time as h:mm:ss for anything longer + if duration < 60 { + returnValue = String(format: "0:%.02d", Int(duration.rounded(.up))) + } else if duration < 3600 { + returnValue = String(format: "%.02d:%.02d", Int(duration / 60), Int(duration) % 60) + } else if duration.isFinite { + let hours = Int(duration / 3600) + let remainingMinutesInSeconds = Int(duration) - hours * 3600 + returnValue = String( + format: "%.02d:%.02d:%.02d", + hours, + Int(remainingMinutesInSeconds / 60), + Int(remainingMinutesInSeconds) % 60) } + return returnValue + } - func configureAccessoryView(_ accessoryView: UIView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {} - - // MARK: - Text Messages Defaults + // MARK: - LinkPreview Message Defaults - func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { - guard let dataSource = messagesCollectionView.messagesDataSource else { - fatalError(MessageKitError.nilMessagesDataSource) - } - return dataSource.isFromCurrentSender(message: message) ? .white : .darkText - } - - func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] { - return [] - } - - func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any] { - return MessageLabel.defaultAttributes - } - - // MARK: - Location Messages Defaults - - func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions { - return LocationMessageSnapshotOptions() - } - - func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? { - return MKPinAnnotationView(annotation: nil, reuseIdentifier: nil) - } - - func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)? { - return nil - } - - // MARK: - Media Message Defaults - - func configureMediaMessageImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { - } + public func configureLinkPreviewImageView( + _: UIImageView, + for _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) { } } diff --git a/Sources/Protocols/MessagesLayoutDelegate.swift b/Sources/Protocols/MessagesLayoutDelegate.swift index 4dd75743f..015c45c30 100644 --- a/Sources/Protocols/MessagesLayoutDelegate.swift +++ b/Sources/Protocols/MessagesLayoutDelegate.swift @@ -1,121 +1,413 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2022 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation +import UIKit + +// MARK: - MessagesLayoutDelegate /// A protocol used by the `MessagesCollectionViewFlowLayout` object to determine /// the size and layout of a `MessageCollectionViewCell` and its contents. +@MainActor public protocol MessagesLayoutDelegate: AnyObject { + /// Specifies the size to use for a header view. + /// + /// - Parameters: + /// - section: The section number of the header. + /// - messagesCollectionView: The `MessagesCollectionView` in which this header will be displayed. + /// + /// - Note: + /// The default value returned by this method is a size of `GGSize.zero`. + func headerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize + + /// Specifies the size to use for a footer view. + /// + /// - Parameters: + /// - section: The section number of the footer. + /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed. + /// + /// - Note: + /// The default value returned by this method is a size of `GGSize.zero`. + func footerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize + + /// Specifies the size to use for a typing indicator view. + /// + /// - Parameters: + /// - layout: The `MessagesCollectionViewFlowLayout` layout. + /// + /// - Note: + /// The default value returned by this method is the width of the `messagesCollectionView` minus insets and a height of 62. + func typingIndicatorViewSize(for layout: MessagesCollectionViewFlowLayout) -> CGSize + + /// Specifies the top inset to use for a typing indicator view. + /// + /// - Parameters: + /// - messagesCollectionView: The `MessagesCollectionView` in which this view will be displayed. + /// + /// - Note: + /// The default value returned by this method is a top inset of 15. + func typingIndicatorViewTopInset(in messagesCollectionView: MessagesCollectionView) -> CGFloat + + /// Specifies the height for the `MessageContentCell`'s top label. + /// + /// - Parameters: + /// - message: The `MessageType` that will be displayed for this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default value returned by this method is zero. + func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) + -> CGFloat + + /// Specifies the height for the `MessageContentCell`'s bottom label. + /// + /// - Parameters: + /// - message: The `MessageType` that will be displayed for this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default value returned by this method is zero. + func cellBottomLabelHeight( + for message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) -> CGFloat + + /// Specifies the height for the message bubble's top label. + /// + /// - Parameters: + /// - message: The `MessageType` that will be displayed for this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default value returned by this method is zero. + func messageTopLabelHeight( + for message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) -> CGFloat + + /// Specifies the label alignment for the message bubble's top label. + /// - Parameters: + /// - message: The `MessageType` that will be displayed for this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// - Returns: Optional LabelAlignment for the message bubble's top label. If nil is returned or the delegate method is not implemented, + /// alignment from MessageSizeCalculator will be used depending if the message is outgoing or incoming + func messageTopLabelAlignment( + for message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) -> LabelAlignment? + + /// Specifies the height for the `MessageContentCell`'s bottom label. + /// + /// - Parameters: + /// - message: The `MessageType` that will be displayed for this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default value returned by this method is zero. + func messageBottomLabelHeight( + for message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) -> CGFloat + + /// Specifies the label alignment for the message bubble's bottom label. + /// - Parameters: + /// - message: The `MessageType` that will be displayed for this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// - Returns: Optional LabelAlignment for the message bubble's bottom label. If nil is returned or the delegate method is not implemented, + /// alignment from MessageSizeCalculator will be used depending if the message is outgoing or incoming + func messageBottomLabelAlignment( + for message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) -> LabelAlignment? + + /// Specifies the size for the `MessageContentCell`'s avatar image view. + /// - Parameters: + /// - message: The `MessageType` that will be displayed for this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// - Returns: Optional CGSize for the avatar image view. If nil is returned or delegate method is not implemented, + /// size from `MessageSizeCalculator`'s `incomingAvatarSize` or `outgoingAvatarSize` will be used depending if the message is outgoing or incoming + func avatarSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) + -> CGSize? + + /// Text cell size calculator for messages with MessageType.text. + /// + /// - Parameters: + /// - message: The text message + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.text. + func textCellSizeCalculator( + for message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator? + + /// Attributed text cell size calculator for messages with MessageType.attributedText. + /// + /// - Parameters: + /// - message: The attributedText message + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.attributedText. + func attributedTextCellSizeCalculator( + for message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator? + + /// Emoji cell size calculator for messages with MessageType.emoji. + /// + /// - Parameters: + /// - message: The emoji message + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.emoji. + func emojiCellSizeCalculator( + for message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator? + + /// Photo cell size calculator for messages with MessageType.photo. + /// + /// - Parameters: + /// - message: The photo message + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.text. + func photoCellSizeCalculator( + for message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator? + + /// Video cell size calculator for messages with MessageType.video. + /// + /// - Parameters: + /// - message: The video message + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.video. + func videoCellSizeCalculator( + for message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator? + + /// Location cell size calculator for messages with MessageType.location. + /// + /// - Parameters: + /// - message: The location message + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.location. + func locationCellSizeCalculator( + for message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator? + + /// Audio cell size calculator for messages with MessageType.audio. + /// + /// - Parameters: + /// - message: The audio message + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.audio. + func audioCellSizeCalculator( + for message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator? + + /// Contact cell size calculator for messages with MessageType.contact. + /// + /// - Parameters: + /// - message: The contact message + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default implementation will return nil. You must override this method if you are using your own cell for messages with MessageType.contact. + func contactCellSizeCalculator( + for message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator? - /// Specifies the size to use for a header view. - /// - /// - Parameters: - /// - section: The section number of the header. - /// - messagesCollectionView: The `MessagesCollectionView` in which this header will be displayed. - /// - /// - Note: - /// The default value returned by this method is a size of `GGSize.zero`. - func headerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize - - /// Specifies the size to use for a footer view. - /// - /// - Parameters: - /// - section: The section number of the footer. - /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed. - /// - /// - Note: - /// The default value returned by this method is a size of `GGSize.zero`. - func footerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize - - /// Specifies the height for the `MessageContentCell`'s top label. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed for this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// - Note: - /// The default value returned by this method is zero. - func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat - - /// Specifies the height for the message bubble's top label. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed for this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// - Note: - /// The default value returned by this method is zero. - func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat - - /// Specifies the height for the `MessageContentCell`'s bottom label. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed for this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// - Note: - /// The default value returned by this method is zero. - func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat - - /// Custom cell size calculator for messages with MessageType.custom. - /// - /// - Parameters: - /// - message: The custom message - /// - indexPath: The `IndexPath` of the cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// - Note: - /// The default implementation will throw fatalError(). You must override this method if you are using messages with MessageType.custom. - func customCellSizeCalculator(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator + /// Custom cell size calculator for messages with MessageType.custom. + /// + /// - Parameters: + /// - message: The custom message + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default implementation will throw fatalError(). You must override this method if you are using messages with MessageType.custom. + func customCellSizeCalculator( + for message: MessageType, + at indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator } -public extension MessagesLayoutDelegate { - - func headerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize { - return .zero - } - - func footerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize { - return .zero - } - - func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return 0 - } - - func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return 0 - } - - func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return 0 - } - - func customCellSizeCalculator(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator { - fatalError("Must return a CellSizeCalculator for MessageKind.custom(Any?)") - } +extension MessagesLayoutDelegate { + public func headerViewSize(for _: Int, in _: MessagesCollectionView) -> CGSize { + .zero + } + + public func footerViewSize(for _: Int, in _: MessagesCollectionView) -> CGSize { + .zero + } + + public func typingIndicatorViewSize(for layout: MessagesCollectionViewFlowLayout) -> CGSize { + let collectionViewWidth = layout.messagesCollectionView.bounds.width + let contentInset = layout.messagesCollectionView.contentInset + let inset = layout.sectionInset.horizontal + contentInset.horizontal + return CGSize(width: collectionViewWidth - inset, height: 62) + } + + public func typingIndicatorViewTopInset(in _: MessagesCollectionView) -> CGFloat { + 15 + } + + public func cellTopLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat { + 0 + } + + public func cellBottomLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat { + 0 + } + + public func messageTopLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat { + 0 + } + + public func messageTopLabelAlignment(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> LabelAlignment? { + nil + } + + public func messageBottomLabelHeight(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGFloat { + 0 + } + + public func avatarSize(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CGSize? { + nil + } + + public func messageBottomLabelAlignment( + for _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) + -> LabelAlignment? + { + nil + } + + public func textCellSizeCalculator(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> CellSizeCalculator? { + nil + } + + public func attributedTextCellSizeCalculator( + for _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) + -> CellSizeCalculator? + { + nil + } + + public func emojiCellSizeCalculator( + for _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) + -> CellSizeCalculator? + { + nil + } + + public func photoCellSizeCalculator( + for _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) + -> CellSizeCalculator? + { + nil + } + + public func videoCellSizeCalculator( + for _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) + -> CellSizeCalculator? + { + nil + } + + public func locationCellSizeCalculator( + for _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) + -> CellSizeCalculator? + { + nil + } + + public func audioCellSizeCalculator( + for _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) + -> CellSizeCalculator? + { + nil + } + + public func contactCellSizeCalculator( + for _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) + -> CellSizeCalculator? + { + nil + } + + public func customCellSizeCalculator( + for _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) + -> CellSizeCalculator + { + fatalError("Must return a CellSizeCalculator for MessageKind.custom(Any?)") + } } diff --git a/Sources/Protocols/SenderType.swift b/Sources/Protocols/SenderType.swift new file mode 100644 index 000000000..e22605f0e --- /dev/null +++ b/Sources/Protocols/SenderType.swift @@ -0,0 +1,35 @@ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +/// A standard protocol representing a sender. +/// Use this protocol to adhere a object as the sender of a MessageType +public protocol SenderType { + /// The unique String identifier for the sender. + /// + /// Note: This value must be unique across all senders. + var senderId: String { get } + + /// The display name of a sender. + var displayName: String { get } +} diff --git a/Sources/Supporting/Info.plist b/Sources/Supporting/Info.plist index 60b9c008e..ca23c84f4 100644 --- a/Sources/Supporting/Info.plist +++ b/Sources/Supporting/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.0.0 + $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion diff --git a/Sources/Supporting/MessageKit+Availability.swift b/Sources/Supporting/MessageKit+Availability.swift deleted file mode 100644 index 735409e32..000000000 --- a/Sources/Supporting/MessageKit+Availability.swift +++ /dev/null @@ -1,72 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import Foundation - -public extension MessagesLayoutDelegate { - - func avatarSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { - fatalError("avatarSize(for:at:in) has been removed in MessageKit 1.0.") - } - - func avatarPosition(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarPosition { - fatalError("avatarPosition(for:at:in) has been removed in MessageKit 1.0.") - } - - func messageLabelInset(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets { - fatalError("messageLabelInset(for:at:in) has been removed in MessageKit 1.0") - } - - func messagePadding(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets { - fatalError("messagePadding(for:at:in) has been removed in MessageKit 1.0.") - } - - func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment { - fatalError("cellTopLabelAlignment(for:at:in) has been removed in MessageKit 1.0.") - } - - func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment { - fatalError("cellBottomLabelAlignment(for:at:in) has been removed in MessageKit 1.0.") - } - - func widthForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - fatalError("widthForMedia(message:at:with:in) has been removed in MessageKit 1.0.") - } - - func heightForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - fatalError("heightForMedia(message:at:with:in) has been removed in MessageKit 1.0.") - } - - func widthForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - fatalError("widthForLocation(message:at:with:in) has been removed in MessageKit 1.0.") - } - - func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - fatalError("heightForLocation(message:at:with:in) has been removed in MessageKit 1.0.") - } - - func shouldCacheLayoutAttributes(for message: MessageType) -> Bool { - fatalError("shouldCacheLayoutAttributes(for:) has been removed in MessageKit 1.0.") - } -} diff --git a/Sources/Views/AvatarView.swift b/Sources/Views/AvatarView.swift index cf3c14561..3fe8ed56e 100644 --- a/Sources/Views/AvatarView.swift +++ b/Sources/Views/AvatarView.swift @@ -1,197 +1,218 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation +import UIKit + +// MARK: - AvatarView open class AvatarView: UIImageView { + // MARK: Lifecycle - // MARK: - Properties - - open var initials: String? { - didSet { - setImageFrom(initials: initials) - } - } + // MARK: - Initializers + public override init(frame: CGRect) { + super.init(frame: frame) + prepareView() + } - open var placeholderFont: UIFont = UIFont.preferredFont(forTextStyle: .caption1) { - didSet { - setImageFrom(initials: initials) - } - } + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + prepareView() + } - open var placeholderTextColor: UIColor = .white { - didSet { - setImageFrom(initials: initials) - } - } + convenience public init() { + self.init(frame: .zero) + prepareView() + } - open var fontMinimumScaleFactor: CGFloat = 0.5 + // MARK: Open - open var adjustsFontSizeToFitWidth = true + open var fontMinimumScaleFactor: CGFloat = 0.5 - private var minimumFontSize: CGFloat { - return placeholderFont.pointSize * fontMinimumScaleFactor - } + open var adjustsFontSizeToFitWidth = true - private var radius: CGFloat? + // MARK: - Properties - // MARK: - Overridden Properties - open override var frame: CGRect { - didSet { - setCorner(radius: self.radius) - } + open var initials: String? { + didSet { + setImageFrom(initials: initials) } + } - open override var bounds: CGRect { - didSet { - setCorner(radius: self.radius) - } - } - - // MARK: - Initializers - public override init(frame: CGRect) { - super.init(frame: frame) - prepareView() - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - prepareView() - } - - convenience public init() { - self.init(frame: .zero) - prepareView() - } - - private func setImageFrom(initials: String?) { - guard let initials = initials else { return } - image = getImageFrom(initials: initials) + open var placeholderFont = UIFont.preferredFont(forTextStyle: .caption1) { + didSet { + setImageFrom(initials: initials) } + } - private func getImageFrom(initials: String) -> UIImage { - let width = frame.width - let height = frame.height - if width == 0 || height == 0 {return UIImage()} - var font = placeholderFont - - _ = UIGraphicsBeginImageContextWithOptions(CGSize(width: width, height: height), false, UIScreen.main.scale) - defer { UIGraphicsEndImageContext() } - let context = UIGraphicsGetCurrentContext()! - - //// Text Drawing - let textRect = calculateTextRect(outerViewWidth: width, outerViewHeight: height) - let initialsText = NSAttributedString(string: initials, attributes: [.font: font]) - if adjustsFontSizeToFitWidth, - initialsText.width(considering: textRect.height) > textRect.width { - let newFontSize = calculateFontSize(text: initials, font: font, width: textRect.width, height: textRect.height) - font = placeholderFont.withSize(newFontSize) - } - - let textStyle = NSMutableParagraphStyle() - textStyle.alignment = .center - let textFontAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: placeholderTextColor, NSAttributedString.Key.paragraphStyle: textStyle] - - let textTextHeight: CGFloat = initials.boundingRect(with: CGSize(width: textRect.width, height: CGFloat.infinity), options: .usesLineFragmentOrigin, attributes: textFontAttributes, context: nil).height - context.saveGState() - context.clip(to: textRect) - initials.draw(in: CGRect(textRect.minX, textRect.minY + (textRect.height - textTextHeight) / 2, textRect.width, textTextHeight), withAttributes: textFontAttributes) - context.restoreGState() - guard let renderedImage = UIGraphicsGetImageFromCurrentImageContext() else { assertionFailure("Could not create image from context"); return UIImage()} - return renderedImage + open var placeholderTextColor: UIColor = .white { + didSet { + setImageFrom(initials: initials) } - - /** - Recursively find the biggest size to fit the text with a given width and height - */ - private func calculateFontSize(text: String, font: UIFont, width: CGFloat, height: CGFloat) -> CGFloat { - let attributedText = NSAttributedString(string: text, attributes: [.font: font]) - if attributedText.width(considering: height) > width { - let newFont = font.withSize(font.pointSize - 1) - if newFont.pointSize > minimumFontSize { - return font.pointSize - } else { - return calculateFontSize(text: text, font: newFont, width: width, height: height) - } - } + } + + // MARK: - Overridden Properties + open override var frame: CGRect { + didSet { + setCorner(radius: self.radius) + } + } + + open override var bounds: CGRect { + didSet { + setCorner(radius: self.radius) + } + } + + // MARK: - Open setters + + open func set(avatar: Avatar) { + if let image = avatar.image { + self.image = image + } else { + initials = avatar.initials + } + } + + open func setCorner(radius: CGFloat?) { + guard let radius = radius else { + // if corner radius not set default to Circle + let cornerRadius = min(frame.width, frame.height) + layer.cornerRadius = cornerRadius / 2 + return + } + self.radius = radius + layer.cornerRadius = radius + } + + // MARK: Internal + + // MARK: - Internal methods + + internal func prepareView() { + backgroundColor = .avatarViewBackground + contentMode = .scaleAspectFill + layer.masksToBounds = true + clipsToBounds = true + setCorner(radius: nil) + } + + // MARK: Private + + private var radius: CGFloat? + + private var minimumFontSize: CGFloat { + placeholderFont.pointSize * fontMinimumScaleFactor + } + + private func setImageFrom(initials: String?) { + guard let initials = initials else { return } + image = getImageFrom(initials: initials) + } + + private func getImageFrom(initials: String) -> UIImage { + let width = frame.width + let height = frame.height + if width == 0 || height == 0 { return UIImage() } + var font = placeholderFont + + UIGraphicsBeginImageContextWithOptions(CGSize(width: width, height: height), false, UIScreen.main.scale) + defer { UIGraphicsEndImageContext() } + let context = UIGraphicsGetCurrentContext()! + + //// Text Drawing + let textRect = calculateTextRect(outerViewWidth: width, outerViewHeight: height) + let initialsText = NSAttributedString(string: initials, attributes: [.font: font]) + if + adjustsFontSizeToFitWidth, + initialsText.width(considering: textRect.height) > textRect.width + { + let newFontSize = calculateFontSize(text: initials, font: font, width: textRect.width, height: textRect.height) + font = placeholderFont.withSize(newFontSize) + } + + let textStyle = NSMutableParagraphStyle() + textStyle.alignment = .center + let textFontAttributes: [NSAttributedString.Key: Any] = [ + NSAttributedString.Key.font: font, + NSAttributedString.Key.foregroundColor: placeholderTextColor, + NSAttributedString.Key.paragraphStyle: textStyle, + ] + + let textTextHeight: CGFloat = initials.boundingRect( + with: CGSize(width: textRect.width, height: CGFloat.infinity), + options: .usesLineFragmentOrigin, + attributes: textFontAttributes, + context: nil).height + context.saveGState() + context.clip(to: textRect) + initials.draw( + in: CGRect(textRect.minX, textRect.minY + (textRect.height - textTextHeight) / 2, textRect.width, textTextHeight), + withAttributes: textFontAttributes) + context.restoreGState() + guard let renderedImage = UIGraphicsGetImageFromCurrentImageContext() + else { assertionFailure("Could not create image from context"); return UIImage() } + return renderedImage + } + + /// Recursively find the biggest size to fit the text with a given width and height + private func calculateFontSize(text: String, font: UIFont, width: CGFloat, height: CGFloat) -> CGFloat { + let attributedText = NSAttributedString(string: text, attributes: [.font: font]) + if attributedText.width(considering: height) > width { + let newFont = font.withSize(font.pointSize - 1) + if newFont.pointSize > minimumFontSize { return font.pointSize - } - - /** - Calculates the inner circle's width. - Note: Assumes corner radius cannot be more than width/2 (this creates circle). - */ - private func calculateTextRect(outerViewWidth: CGFloat, outerViewHeight: CGFloat) -> CGRect { - guard outerViewWidth > 0 else { - return CGRect.zero - } - let shortEdge = min(outerViewHeight, outerViewWidth) - // Converts degree to radian degree and calculate the - // Assumes, it is a perfect circle based on the shorter part of ellipsoid - // calculate a rectangle - let w = shortEdge * sin(CGFloat(45).degreesToRadians) * 2 - let h = shortEdge * cos(CGFloat(45).degreesToRadians) * 2 - let startX = (outerViewWidth - w)/2 - let startY = (outerViewHeight - h)/2 - // In case the font exactly fits to the region, put 2 pixel both left and right - return CGRect(startX+2, startY, w-4, h) - } - - // MARK: - Internal methods - - internal func prepareView() { - backgroundColor = .gray - contentMode = .scaleAspectFill - layer.masksToBounds = true - clipsToBounds = true - setCorner(radius: nil) - } + } else { + return calculateFontSize(text: text, font: newFont, width: width, height: height) + } + } + return font.pointSize + } + + /// Calculates the inner circle's width. + /// - Note: Assumes corner radius cannot be more than width/2 (this creates circle). + private func calculateTextRect(outerViewWidth: CGFloat, outerViewHeight: CGFloat) -> CGRect { + guard outerViewWidth > 0 else { + return CGRect.zero + } + let shortEdge = min(outerViewHeight, outerViewWidth) + // Converts degree to radian degree and calculate the + // Assumes, it is a perfect circle based on the shorter part of ellipsoid + // calculate a rectangle + let w = shortEdge * sin(CGFloat(45).degreesToRadians) * 2 + let h = shortEdge * cos(CGFloat(45).degreesToRadians) * 2 + let startX = (outerViewWidth - w) / 2 + let startY = (outerViewHeight - h) / 2 + // In case the font exactly fits to the region, put 2 pixel both left and right + return CGRect(startX + 2, startY, w - 4, h) + } +} - // MARK: - Open setters - - open func set(avatar: Avatar) { - if let image = avatar.image { - self.image = image - } else { - initials = avatar.initials - } - } +extension FloatingPoint { + // MARK: Fileprivate - open func setCorner(radius: CGFloat?) { - guard let radius = radius else { - //if corner radius not set default to Circle - let cornerRadius = min(frame.width, frame.height) - layer.cornerRadius = cornerRadius/2 - return - } - self.radius = radius - layer.cornerRadius = radius - } + fileprivate var degreesToRadians: Self { self * .pi / 180 } -} + // MARK: Private -fileprivate extension FloatingPoint { - var degreesToRadians: Self { return self * .pi / 180 } - var radiansToDegrees: Self { return self * 180 / .pi } + private var radiansToDegrees: Self { self * 180 / .pi } } diff --git a/Sources/Views/BubbleCircle.swift b/Sources/Views/BubbleCircle.swift new file mode 100644 index 000000000..93acf6f0b --- /dev/null +++ b/Sources/Views/BubbleCircle.swift @@ -0,0 +1,48 @@ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit + +/// A `UIView` subclass that maintains a mask to keep it fully circular +open class BubbleCircle: UIView { + /// Lays out subviews and applies a circular mask to the layer + open override func layoutSubviews() { + super.layoutSubviews() + layer.mask = roundedMask(corners: .allCorners, radius: bounds.height / 2) + } + + /// Returns a rounded mask of the view + /// + /// - Parameters: + /// - corners: The corners to round + /// - radius: The radius of curve + /// - Returns: A mask + open func roundedMask(corners: UIRectCorner, radius: CGFloat) -> CAShapeLayer { + let path = UIBezierPath( + roundedRect: bounds, + byRoundingCorners: corners, + cornerRadii: CGSize(width: radius, height: radius)) + let mask = CAShapeLayer() + mask.path = path.cgPath + return mask + } +} diff --git a/Sources/Views/Cells/AudioMessageCell.swift b/Sources/Views/Cells/AudioMessageCell.swift new file mode 100644 index 000000000..e32ecd8cd --- /dev/null +++ b/Sources/Views/Cells/AudioMessageCell.swift @@ -0,0 +1,162 @@ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import AVFoundation +import UIKit + +/// A subclass of `MessageContentCell` used to display video and audio messages. +open class AudioMessageCell: MessageContentCell { + // MARK: Open + + /// Responsible for setting up the constraints of the cell's subviews. + open func setupConstraints() { + playButton.constraint(equalTo: CGSize(width: 25, height: 25)) + playButton.addConstraints( + left: messageContainerView.leftAnchor, + centerY: messageContainerView.centerYAnchor, + leftConstant: 5) + activityIndicatorView.addConstraints(centerY: playButton.centerYAnchor, centerX: playButton.centerXAnchor) + durationLabel.addConstraints( + right: messageContainerView.rightAnchor, + centerY: messageContainerView.centerYAnchor, + rightConstant: 15) + progressView.addConstraints( + left: playButton.rightAnchor, + right: durationLabel.leftAnchor, + centerY: messageContainerView.centerYAnchor, + leftConstant: 5, + rightConstant: 5) + } + + open override func setupSubviews() { + super.setupSubviews() + messageContainerView.addSubview(playButton) + messageContainerView.addSubview(activityIndicatorView) + messageContainerView.addSubview(durationLabel) + messageContainerView.addSubview(progressView) + setupConstraints() + } + + open override func prepareForReuse() { + super.prepareForReuse() + progressView.progress = 0 + playButton.isSelected = false + activityIndicatorView.stopAnimating() + playButton.isHidden = false + durationLabel.text = "0:00" + } + + /// Handle tap gesture on contentView and its subviews. + open override func handleTapGesture(_ gesture: UIGestureRecognizer) { + let touchLocation = gesture.location(in: self) + // compute play button touch area, currently play button size is (25, 25) which is hardly touchable + // add 10 px around current button frame and test the touch against this new frame + let playButtonTouchArea = CGRect( + playButton.frame.origin.x - 10.0, + playButton.frame.origin.y - 10, + playButton.frame.size.width + 20, + playButton.frame.size.height + 20) + let translateTouchLocation = convert(touchLocation, to: messageContainerView) + if playButtonTouchArea.contains(translateTouchLocation) { + delegate?.didTapPlayButton(in: self) + } else { + super.handleTapGesture(gesture) + } + } + + // MARK: - Configure Cell + + open override func configure( + with message: MessageType, + at indexPath: IndexPath, + and messagesCollectionView: MessagesCollectionView) + { + super.configure(with: message, at: indexPath, and: messagesCollectionView) + + guard let dataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) + } + + let playButtonLeftConstraint = messageContainerView.constraints.filter { $0.identifier == "left" }.first + let durationLabelRightConstraint = messageContainerView.constraints.filter { $0.identifier == "right" }.first + + if !dataSource.isFromCurrentSender(message: message) { + playButtonLeftConstraint?.constant = 12 + durationLabelRightConstraint?.constant = -8 + } else { + playButtonLeftConstraint?.constant = 5 + durationLabelRightConstraint?.constant = -15 + } + + guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { + fatalError(MessageKitError.nilMessagesDisplayDelegate) + } + + let tintColor = displayDelegate.audioTintColor(for: message, at: indexPath, in: messagesCollectionView) + playButton.imageView?.tintColor = tintColor + durationLabel.textColor = tintColor + progressView.tintColor = tintColor + + if case .audio(let audioItem) = message.kind { + durationLabel.text = displayDelegate.audioProgressTextFormat( + audioItem.duration, + for: self, + in: messagesCollectionView) + } + + displayDelegate.configureAudioCell(self, message: message) + } + + // MARK: Public + + /// The play button view to display on audio messages. + public lazy var playButton: UIButton = { + let playButton = UIButton(type: .custom) + let playImage = UIImage.messageKitImageWith(type: .play) + let pauseImage = UIImage.messageKitImageWith(type: .pause) + playButton.setImage(playImage?.withRenderingMode(.alwaysTemplate), for: .normal) + playButton.setImage(pauseImage?.withRenderingMode(.alwaysTemplate), for: .selected) + return playButton + }() + + /// The time duration label to display on audio messages. + public lazy var durationLabel: UILabel = { + let durationLabel = UILabel(frame: CGRect.zero) + durationLabel.textAlignment = .right + durationLabel.font = UIFont.systemFont(ofSize: 14) + durationLabel.text = "0:00" + return durationLabel + }() + + public lazy var activityIndicatorView: UIActivityIndicatorView = { + let activityIndicatorView = UIActivityIndicatorView(style: .medium) + activityIndicatorView.hidesWhenStopped = true + activityIndicatorView.isHidden = true + return activityIndicatorView + }() + + public lazy var progressView: UIProgressView = { + let progressView = UIProgressView(progressViewStyle: .default) + progressView.progress = 0.0 + return progressView + }() +} diff --git a/Sources/Views/Cells/ContactMessageCell.swift b/Sources/Views/Cells/ContactMessageCell.swift new file mode 100644 index 000000000..cc3e64cc0 --- /dev/null +++ b/Sources/Views/Cells/ContactMessageCell.swift @@ -0,0 +1,152 @@ +// MIT License +// +// Copyright (c) 2017-2018 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import UIKit + +open class ContactMessageCell: MessageContentCell { + // MARK: Open + + // MARK: - Methods + open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { + super.apply(layoutAttributes) + guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { + return + } + nameLabel.font = attributes.messageLabelFont + } + + open override func setupSubviews() { + super.setupSubviews() + messageContainerView.addSubview(initialsContainerView) + messageContainerView.addSubview(nameLabel) + messageContainerView.addSubview(disclosureImageView) + initialsContainerView.addSubview(initialsLabel) + setupConstraints() + } + + open override func prepareForReuse() { + super.prepareForReuse() + nameLabel.text = "" + initialsLabel.text = "" + } + + open func setupConstraints() { + initialsContainerView.constraint(equalTo: CGSize(width: 26, height: 26)) + let initialsConstraints = initialsContainerView.addConstraints( + left: messageContainerView.leftAnchor, + centerY: messageContainerView.centerYAnchor, + leftConstant: 5) + initialsConstraints.first?.identifier = ConstraintsID.initialsContainerLeftConstraint.rawValue + initialsContainerView.layer.cornerRadius = 13 + initialsLabel.fillSuperview() + disclosureImageView.constraint(equalTo: CGSize(width: 20, height: 20)) + let disclosureConstraints = disclosureImageView.addConstraints( + right: messageContainerView.rightAnchor, + centerY: messageContainerView.centerYAnchor, + rightConstant: -10) + disclosureConstraints.first?.identifier = ConstraintsID.disclosureRightConstraint.rawValue + nameLabel.addConstraints( + messageContainerView.topAnchor, + left: initialsContainerView.rightAnchor, + bottom: messageContainerView.bottomAnchor, + right: disclosureImageView.leftAnchor, + topConstant: 0, + leftConstant: 10, + bottomConstant: 0, + rightConstant: 5) + } + + // MARK: - Configure Cell + open override func configure( + with message: MessageType, + at indexPath: IndexPath, + and messagesCollectionView: MessagesCollectionView) + { + super.configure(with: message, at: indexPath, and: messagesCollectionView) + // setup data + guard case .contact(let contactItem) = message.kind else { fatalError("Failed decorate audio cell") } + nameLabel.text = contactItem.displayName + initialsLabel.text = contactItem.initials + // setup constraints + guard let dataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) + } + let initialsContainerLeftConstraint = messageContainerView.constraints.filter { constraint -> Bool in + constraint.identifier == ConstraintsID.initialsContainerLeftConstraint.rawValue + }.first + let disclosureRightConstraint = messageContainerView.constraints.filter { constraint -> Bool in + constraint.identifier == ConstraintsID.disclosureRightConstraint.rawValue + }.first + if dataSource.isFromCurrentSender(message: message) { // outgoing message + initialsContainerLeftConstraint?.constant = 5 + disclosureRightConstraint?.constant = -10 + } else { // incoming message + initialsContainerLeftConstraint?.constant = 10 + disclosureRightConstraint?.constant = -5 + } + // setup colors + guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { + fatalError(MessageKitError.nilMessagesDisplayDelegate) + } + let textColor = displayDelegate.textColor(for: message, at: indexPath, in: messagesCollectionView) + nameLabel.textColor = textColor + disclosureImageView.tintColor = textColor + } + + // MARK: Public + + public enum ConstraintsID: String { + case initialsContainerLeftConstraint + case disclosureRightConstraint + } + + /// The view container that holds contact initials + public lazy var initialsContainerView: UIView = { + let initialsContainer = UIView(frame: CGRect.zero) + initialsContainer.backgroundColor = .collectionViewBackground + return initialsContainer + }() + + /// The label that display the contact initials + public lazy var initialsLabel: UILabel = { + let initialsLabel = UILabel(frame: CGRect.zero) + initialsLabel.textAlignment = .center + initialsLabel.textColor = .label + initialsLabel.font = UIFont.preferredFont(forTextStyle: .footnote) + return initialsLabel + }() + + /// The label that display contact name + public lazy var nameLabel: UILabel = { + let nameLabel = UILabel(frame: CGRect.zero) + nameLabel.numberOfLines = 0 + return nameLabel + }() + + /// The disclosure image view + public lazy var disclosureImageView: UIImageView = { + let disclosureImage = UIImage.messageKitImageWith(type: .disclosure)?.withRenderingMode(.alwaysTemplate) + let disclosure = UIImageView(image: disclosureImage) + return disclosure + }() +} diff --git a/Sources/Views/Cells/LinkPreviewMessageCell.swift b/Sources/Views/Cells/LinkPreviewMessageCell.swift new file mode 100644 index 000000000..6d2df58f7 --- /dev/null +++ b/Sources/Views/Cells/LinkPreviewMessageCell.swift @@ -0,0 +1,111 @@ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit + +open class LinkPreviewMessageCell: TextMessageCell { + // MARK: Open + + open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { + super.apply(layoutAttributes) + guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return } + linkPreviewView.titleLabel.font = attributes.linkPreviewFonts.titleFont + linkPreviewView.teaserLabel.font = attributes.linkPreviewFonts.teaserFont + linkPreviewView.domainLabel.font = attributes.linkPreviewFonts.domainFont + } + + open override func configure( + with message: MessageType, + at indexPath: IndexPath, + and messagesCollectionView: MessagesCollectionView) + { + let displayDelegate = messagesCollectionView.messagesDisplayDelegate + + if let textColor: UIColor = displayDelegate?.textColor(for: message, at: indexPath, in: messagesCollectionView) { + linkPreviewView.titleLabel.textColor = textColor + linkPreviewView.teaserLabel.textColor = textColor + linkPreviewView.domainLabel.textColor = textColor + } + + guard case MessageKind.linkPreview(let linkItem) = message.kind else { + fatalError("LinkPreviewMessageCell received unhandled MessageDataType: \(message.kind)") + } + + super.configure(with: message, at: indexPath, and: messagesCollectionView) + + linkPreviewView.titleLabel.text = linkItem.title + linkPreviewView.teaserLabel.text = linkItem.teaser + linkPreviewView.domainLabel.text = linkItem.url.host?.lowercased() + linkPreviewView.imageView.image = linkItem.thumbnailImage + linkURL = linkItem.url + + displayDelegate?.configureLinkPreviewImageView( + linkPreviewView.imageView, + for: message, + at: indexPath, + in: messagesCollectionView) + } + + open override func prepareForReuse() { + super.prepareForReuse() + linkPreviewView.titleLabel.text = nil + linkPreviewView.teaserLabel.text = nil + linkPreviewView.domainLabel.text = nil + linkPreviewView.imageView.image = nil + linkURL = nil + } + + open override func handleTapGesture(_ gesture: UIGestureRecognizer) { + let touchLocation = gesture.location(in: linkPreviewView) + + guard linkPreviewView.frame.contains(touchLocation), let url = linkURL else { + super.handleTapGesture(gesture) + return + } + delegate?.didSelectURL(url) + } + + // MARK: Public + + public lazy var linkPreviewView: LinkPreviewView = { + let view = LinkPreviewView() + view.translatesAutoresizingMaskIntoConstraints = false + messageContainerView.addSubview(view) + + NSLayoutConstraint.activate([ + view.leadingAnchor.constraint( + equalTo: messageContainerView.leadingAnchor, + constant: messageLabel.textInsets.left), + view.trailingAnchor.constraint( + equalTo: messageContainerView.trailingAnchor, + constant: messageLabel.textInsets.right * -1), + view.bottomAnchor.constraint( + equalTo: messageContainerView.bottomAnchor, + constant: messageLabel.textInsets.bottom * -1), + ]) + return view + }() + + // MARK: Private + + private var linkURL: URL? +} diff --git a/Sources/Views/Cells/LocationMessageCell.swift b/Sources/Views/Cells/LocationMessageCell.swift index ed84ed752..44601b1b8 100644 --- a/Sources/Views/Cells/LocationMessageCell.swift +++ b/Sources/Views/Cells/LocationMessageCell.swift @@ -1,111 +1,126 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. -import UIKit import MapKit +import UIKit /// A subclass of `MessageContentCell` used to display location messages. open class LocationMessageCell: MessageContentCell { - - /// The activity indicator to be displayed while the map image is loading. - open var activityIndicator = UIActivityIndicatorView(style: .gray) - - /// The image view holding the map image. - open var imageView = UIImageView() - - private weak var snapShotter: MKMapSnapshotter? - - open override func setupSubviews() { - super.setupSubviews() - imageView.contentMode = .scaleAspectFill - messageContainerView.addSubview(imageView) - messageContainerView.addSubview(activityIndicator) - setupConstraints() + // MARK: Open + + /// The activity indicator to be displayed while the map image is loading. + open var activityIndicator = UIActivityIndicatorView(style: .medium) + + /// The image view holding the map image. + open var imageView = UIImageView() + + open override func setupSubviews() { + super.setupSubviews() + imageView.contentMode = .scaleAspectFill + messageContainerView.addSubview(imageView) + messageContainerView.addSubview(activityIndicator) + setupConstraints() + } + + /// Responsible for setting up the constraints of the cell's subviews. + open func setupConstraints() { + imageView.fillSuperview() + activityIndicator.centerInSuperview() + } + + open override func prepareForReuse() { + super.prepareForReuse() + snapShotter?.cancel() + } + + open override func configure( + with message: MessageType, + at indexPath: IndexPath, + and messagesCollectionView: MessagesCollectionView) + { + super.configure(with: message, at: indexPath, and: messagesCollectionView) + + guard case .location(let locationItem) = message.kind else { fatalError("Configuring LocationMessageCell with wrong message kind") } + guard CLLocationCoordinate2DIsValid(locationItem.location.coordinate) else { + return } - /// Responsible for setting up the constraints of the cell's subviews. - open func setupConstraints() { - imageView.fillSuperview() - activityIndicator.centerInSuperview() + guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { + fatalError(MessageKitError.nilMessagesDisplayDelegate) } - - open override func prepareForReuse() { - super.prepareForReuse() - snapShotter?.cancel() + let options = displayDelegate.snapshotOptionsForLocation(message: message, at: indexPath, in: messagesCollectionView) + let annotationView = displayDelegate.annotationViewForLocation( + message: message, + at: indexPath, + in: messagesCollectionView) + let animationBlock = displayDelegate.animationBlockForLocation( + message: message, + at: indexPath, + in: messagesCollectionView) + + activityIndicator.startAnimating() + + let snapshotOptions = MKMapSnapshotter.Options() + snapshotOptions.region = MKCoordinateRegion(center: locationItem.location.coordinate, span: options.span) + snapshotOptions.showsBuildings = options.showsBuildings + snapshotOptions.pointOfInterestFilter = options.showsPointsOfInterest ? .includingAll : .excludingAll + + let snapShotter = MKMapSnapshotter(options: snapshotOptions) + self.snapShotter = snapShotter + snapShotter.start { snapshot, error in + defer { + self.activityIndicator.stopAnimating() + } + guard let snapshot = snapshot, error == nil else { + // show an error image? + return + } + + guard let annotationView = annotationView else { + self.imageView.image = snapshot.image + return + } + + UIGraphicsBeginImageContextWithOptions(snapshotOptions.size, true, 0) + + snapshot.image.draw(at: .zero) + + var point = snapshot.point(for: locationItem.location.coordinate) + // Move point to reflect annotation anchor + point.x -= annotationView.bounds.size.width / 2 + point.y -= annotationView.bounds.size.height / 2 + point.x += annotationView.centerOffset.x + point.y += annotationView.centerOffset.y + + annotationView.image?.draw(at: point) + let composedImage = UIGraphicsGetImageFromCurrentImageContext() + + UIGraphicsEndImageContext() + self.imageView.image = composedImage + animationBlock?(self.imageView) } + } - open override func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) { - super.configure(with: message, at: indexPath, and: messagesCollectionView) - guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { - fatalError(MessageKitError.nilMessagesDisplayDelegate) - } - let options = displayDelegate.snapshotOptionsForLocation(message: message, at: indexPath, in: messagesCollectionView) - let annotationView = displayDelegate.annotationViewForLocation(message: message, at: indexPath, in: messagesCollectionView) - let animationBlock = displayDelegate.animationBlockForLocation(message: message, at: indexPath, in: messagesCollectionView) - - guard case let .location(locationItem) = message.kind else { fatalError("") } - - activityIndicator.startAnimating() - - let snapshotOptions = MKMapSnapshotter.Options() - snapshotOptions.region = MKCoordinateRegion(center: locationItem.location.coordinate, span: options.span) - snapshotOptions.showsBuildings = options.showsBuildings - snapshotOptions.showsPointsOfInterest = options.showsPointsOfInterest - - let snapShotter = MKMapSnapshotter(options: snapshotOptions) - self.snapShotter = snapShotter - snapShotter.start { (snapshot, error) in - defer { - self.activityIndicator.stopAnimating() - } - guard let snapshot = snapshot, error == nil else { - //show an error image? - return - } - - guard let annotationView = annotationView else { - self.imageView.image = snapshot.image - return - } - - UIGraphicsBeginImageContextWithOptions(snapshotOptions.size, true, 0) - - snapshot.image.draw(at: .zero) - - var point = snapshot.point(for: locationItem.location.coordinate) - //Move point to reflect annotation anchor - point.x -= annotationView.bounds.size.width / 2 - point.y -= annotationView.bounds.size.height / 2 - point.x += annotationView.centerOffset.x - point.y += annotationView.centerOffset.y - - annotationView.image?.draw(at: point) - let composedImage = UIGraphicsGetImageFromCurrentImageContext() - - UIGraphicsEndImageContext() - self.imageView.image = composedImage - animationBlock?(self.imageView) - } - } + // MARK: Private + + private weak var snapShotter: MKMapSnapshotter? } diff --git a/Sources/Views/Cells/MediaMessageCell.swift b/Sources/Views/Cells/MediaMessageCell.swift index 94f885e80..f93779352 100644 --- a/Sources/Views/Cells/MediaMessageCell.swift +++ b/Sources/Views/Cells/MediaMessageCell.swift @@ -1,79 +1,96 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import UIKit /// A subclass of `MessageContentCell` used to display video and audio messages. open class MediaMessageCell: MessageContentCell { + /// The play button view to display on video messages. + open lazy var playButtonView: PlayButtonView = { + let playButtonView = PlayButtonView() + return playButtonView + }() - /// The play button view to display on video messages. - open lazy var playButtonView: PlayButtonView = { - let playButtonView = PlayButtonView() - return playButtonView - }() + /// The image view display the media content. + open var imageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + return imageView + }() - /// The image view display the media content. - open var imageView: UIImageView = { - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFill - return imageView - }() + // MARK: - Methods - // MARK: - Methods + /// Responsible for setting up the constraints of the cell's subviews. + open func setupConstraints() { + imageView.fillSuperview() + playButtonView.centerInSuperview() + playButtonView.constraint(equalTo: CGSize(width: 35, height: 35)) + } - /// Responsible for setting up the constraints of the cell's subviews. - open func setupConstraints() { - imageView.fillSuperview() - playButtonView.centerInSuperview() - playButtonView.constraint(equalTo: CGSize(width: 35, height: 35)) - } + open override func setupSubviews() { + super.setupSubviews() + messageContainerView.addSubview(imageView) + messageContainerView.addSubview(playButtonView) + setupConstraints() + } + + open override func prepareForReuse() { + super.prepareForReuse() + imageView.image = nil + } - open override func setupSubviews() { - super.setupSubviews() - messageContainerView.addSubview(imageView) - messageContainerView.addSubview(playButtonView) - setupConstraints() + open override func configure( + with message: MessageType, + at indexPath: IndexPath, + and messagesCollectionView: MessagesCollectionView) + { + super.configure(with: message, at: indexPath, and: messagesCollectionView) + + guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { + fatalError(MessageKitError.nilMessagesDisplayDelegate) } - open override func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) { - super.configure(with: message, at: indexPath, and: messagesCollectionView) + switch message.kind { + case .photo(let mediaItem): + imageView.image = mediaItem.image ?? mediaItem.placeholderImage + playButtonView.isHidden = true + case .video(let mediaItem): + imageView.image = mediaItem.image ?? mediaItem.placeholderImage + playButtonView.isHidden = false + default: + break + } - guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { - fatalError(MessageKitError.nilMessagesDisplayDelegate) - } + displayDelegate.configureMediaMessageImageView(imageView, for: message, at: indexPath, in: messagesCollectionView) + } - switch message.kind { - case .photo(let mediaItem): - imageView.image = mediaItem.image ?? mediaItem.placeholderImage - playButtonView.isHidden = true - case .video(let mediaItem): - imageView.image = mediaItem.image ?? mediaItem.placeholderImage - playButtonView.isHidden = false - default: - break - } + /// Handle tap gesture on contentView and its subviews. + open override func handleTapGesture(_ gesture: UIGestureRecognizer) { + let touchLocation = gesture.location(in: imageView) - displayDelegate.configureMediaMessageImageView(imageView, for: message, at: indexPath, in: messagesCollectionView) + guard imageView.frame.contains(touchLocation) else { + super.handleTapGesture(gesture) + return } + delegate?.didTapImage(in: self) + } } diff --git a/Sources/Views/Cells/MessageCollectionViewCell.swift b/Sources/Views/Cells/MessageCollectionViewCell.swift index 08114cb59..41cbd1f02 100644 --- a/Sources/Views/Cells/MessageCollectionViewCell.swift +++ b/Sources/Views/Cells/MessageCollectionViewCell.swift @@ -1,40 +1,45 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2022 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import UIKit /// A subclass of `UICollectionViewCell` to be used inside of a `MessagesCollectionView`. open class MessageCollectionViewCell: UICollectionViewCell { + // MARK: Lifecycle + + // MARK: - Initializers - // MARK: - Initializers + public override init(frame: CGRect) { + super.init(frame: frame) + } - public override init(frame: CGRect) { - super.init(frame: frame) - } + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } + // MARK: Open + /// Handle tap gesture on contentView and its subviews. + open func handleTapGesture(_: UIGestureRecognizer) { + // Should be overridden + } } diff --git a/Sources/Views/Cells/MessageContentCell.swift b/Sources/Views/Cells/MessageContentCell.swift index 3a194a68f..471f36c3b 100644 --- a/Sources/Views/Cells/MessageContentCell.swift +++ b/Sources/Views/Cells/MessageContentCell.swift @@ -1,301 +1,373 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - +// MIT License +// +// Copyright (c) 2017-2022 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation import UIKit /// A subclass of `MessageCollectionViewCell` used to display text, media, and location messages. open class MessageContentCell: MessageCollectionViewCell { - - /// The image view displaying the avatar. - open var avatarView = AvatarView() - - /// The container used for styling and holding the message's content view. - open var messageContainerView: MessageContainerView = { - let containerView = MessageContainerView() - containerView.clipsToBounds = true - containerView.layer.masksToBounds = true - return containerView - }() - - /// The top label of the cell. - open var cellTopLabel: InsetLabel = { - let label = InsetLabel() - label.numberOfLines = 0 - label.textAlignment = .center - return label - }() - - /// The top label of the messageBubble. - open var messageTopLabel: InsetLabel = { - let label = InsetLabel() - label.numberOfLines = 0 - return label - }() - - /// The bottom label of the messageBubble. - open var messageBottomLabel: InsetLabel = { - let label = InsetLabel() - label.numberOfLines = 0 - return label - }() - - // Should only add customized subviews - don't change accessoryView itself. - open var accessoryView: UIView = UIView() - - /// The `MessageCellDelegate` for the cell. - open weak var delegate: MessageCellDelegate? - - public override init(frame: CGRect) { - super.init(frame: frame) - contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - setupSubviews() + // MARK: Lifecycle + + public override init(frame: CGRect) { + super.init(frame: frame) + contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + setupSubviews() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + setupSubviews() + } + + // MARK: Open + + /// The image view displaying the avatar. + open var avatarView = AvatarView() + + /// The container used for styling and holding the message's content view. + open var messageContainerView: MessageContainerView = { + let containerView = MessageContainerView() + containerView.clipsToBounds = true + containerView.layer.masksToBounds = true + return containerView + }() + + /// The top label of the cell. + open var cellTopLabel: InsetLabel = { + let label = InsetLabel() + label.numberOfLines = 0 + label.textAlignment = .center + return label + }() + + /// The bottom label of the cell. + open var cellBottomLabel: InsetLabel = { + let label = InsetLabel() + label.numberOfLines = 0 + label.textAlignment = .center + return label + }() + + /// The top label of the messageBubble. + open var messageTopLabel: InsetLabel = { + let label = InsetLabel() + label.numberOfLines = 0 + return label + }() + + /// The bottom label of the messageBubble. + open var messageBottomLabel: InsetLabel = { + let label = InsetLabel() + label.numberOfLines = 0 + return label + }() + + /// The time label of the messageBubble. + open var messageTimestampLabel = InsetLabel() + + // Should only add customized subviews - don't change accessoryView itself. + open var accessoryView = UIView() + + /// The `MessageCellDelegate` for the cell. + open weak var delegate: MessageCellDelegate? + + open override func prepareForReuse() { + super.prepareForReuse() + cellTopLabel.text = nil + cellBottomLabel.text = nil + messageTopLabel.text = nil + messageBottomLabel.text = nil + messageTimestampLabel.attributedText = nil + } + + open func setupSubviews() { + contentView.addSubviews( + accessoryView, + cellTopLabel, + messageTopLabel, + messageBottomLabel, + cellBottomLabel, + messageContainerView, + avatarView, + messageTimestampLabel) + } + + // MARK: - Configuration + + open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { + super.apply(layoutAttributes) + guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return } + // Call this before other laying out other subviews + layoutMessageContainerView(with: attributes) + layoutMessageBottomLabel(with: attributes) + layoutCellBottomLabel(with: attributes) + layoutCellTopLabel(with: attributes) + layoutMessageTopLabel(with: attributes) + layoutAvatarView(with: attributes) + layoutAccessoryView(with: attributes) + layoutTimeLabelView(with: attributes) + } + + /// Used to configure the cell. + /// + /// - Parameters: + /// - message: The `MessageType` this cell displays. + /// - indexPath: The `IndexPath` for this cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell is contained. + open func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) { + guard let dataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - setupSubviews() + guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { + fatalError(MessageKitError.nilMessagesDisplayDelegate) } - open func setupSubviews() { - contentView.addSubview(accessoryView) - contentView.addSubview(cellTopLabel) - contentView.addSubview(messageTopLabel) - contentView.addSubview(messageBottomLabel) - contentView.addSubview(messageContainerView) - contentView.addSubview(avatarView) + delegate = messagesCollectionView.messageCellDelegate + + let messageColor = displayDelegate.backgroundColor(for: message, at: indexPath, in: messagesCollectionView) + let messageStyle = displayDelegate.messageStyle(for: message, at: indexPath, in: messagesCollectionView) + + displayDelegate.configureAvatarView(avatarView, for: message, at: indexPath, in: messagesCollectionView) + + displayDelegate.configureAccessoryView(accessoryView, for: message, at: indexPath, in: messagesCollectionView) + + messageContainerView.backgroundColor = messageColor + messageContainerView.style = messageStyle + + let topCellLabelText = dataSource.cellTopLabelAttributedText(for: message, at: indexPath) + let bottomCellLabelText = dataSource.cellBottomLabelAttributedText(for: message, at: indexPath) + let topMessageLabelText = dataSource.messageTopLabelAttributedText(for: message, at: indexPath) + let bottomMessageLabelText = dataSource.messageBottomLabelAttributedText(for: message, at: indexPath) + let messageTimestampLabelText = dataSource.messageTimestampLabelAttributedText(for: message, at: indexPath) + cellTopLabel.attributedText = topCellLabelText + cellBottomLabel.attributedText = bottomCellLabelText + messageTopLabel.attributedText = topMessageLabelText + messageBottomLabel.attributedText = bottomMessageLabelText + messageTimestampLabel.attributedText = messageTimestampLabelText + messageTimestampLabel.isHidden = !messagesCollectionView.showMessageTimestampOnSwipeLeft + } + + /// Handle tap gesture on contentView and its subviews. + open override func handleTapGesture(_ gesture: UIGestureRecognizer) { + let touchLocation = gesture.location(in: self) + + switch true { + case messageContainerView.frame + .contains(touchLocation) && !cellContentView(canHandle: convert(touchLocation, to: messageContainerView)): + delegate?.didTapMessage(in: self) + case avatarView.frame.contains(touchLocation): + delegate?.didTapAvatar(in: self) + case cellTopLabel.frame.contains(touchLocation): + delegate?.didTapCellTopLabel(in: self) + case cellBottomLabel.frame.contains(touchLocation): + delegate?.didTapCellBottomLabel(in: self) + case messageTopLabel.frame.contains(touchLocation): + delegate?.didTapMessageTopLabel(in: self) + case messageBottomLabel.frame.contains(touchLocation): + delegate?.didTapMessageBottomLabel(in: self) + case accessoryView.frame.contains(touchLocation): + delegate?.didTapAccessoryView(in: self) + default: + delegate?.didTapBackground(in: self) } - - open override func prepareForReuse() { - super.prepareForReuse() - cellTopLabel.text = nil - messageTopLabel.text = nil - messageBottomLabel.text = nil + } + + /// Handle long press gesture, return true when gestureRecognizer's touch point in `messageContainerView`'s frame + open override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + let touchPoint = gestureRecognizer.location(in: self) + guard gestureRecognizer.isKind(of: UILongPressGestureRecognizer.self) else { return false } + return messageContainerView.frame.contains(touchPoint) + } + + /// Handle `ContentView`'s tap gesture, return false when `ContentView` doesn't needs to handle gesture + open func cellContentView(canHandle _: CGPoint) -> Bool { + false + } + + // MARK: - Origin Calculations + + /// Positions the cell's `AvatarView`. + /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. + open func layoutAvatarView(with attributes: MessagesCollectionViewLayoutAttributes) { + var origin: CGPoint = .zero + let padding = attributes.avatarLeadingTrailingPadding + + switch attributes.avatarPosition.horizontal { + case .cellLeading: + origin.x = padding + case .cellTrailing: + origin.x = attributes.frame.width - attributes.avatarSize.width - padding + case .natural: + fatalError(MessageKitError.avatarPositionUnresolved) } - // MARK: - Configuration - - open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { - super.apply(layoutAttributes) - guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return } - // Call this before other laying out other subviews - layoutMessageContainerView(with: attributes) - layoutBottomLabel(with: attributes) - layoutCellTopLabel(with: attributes) - layoutMessageTopLabel(with: attributes) - layoutAvatarView(with: attributes) - layoutAccessoryView(with: attributes) + switch attributes.avatarPosition.vertical { + case .messageLabelTop: + origin.y = messageTopLabel.frame.minY + case .messageTop: // Needs messageContainerView frame to be set + origin.y = messageContainerView.frame.minY + case .messageBottom: // Needs messageContainerView frame to be set + origin.y = messageContainerView.frame.maxY - attributes.avatarSize.height + case .messageCenter: // Needs messageContainerView frame to be set + origin.y = messageContainerView.frame.midY - (attributes.avatarSize.height / 2) + case .cellBottom: + origin.y = attributes.frame.height - attributes.avatarSize.height + default: + break } - /// Used to configure the cell. - /// - /// - Parameters: - /// - message: The `MessageType` this cell displays. - /// - indexPath: The `IndexPath` for this cell. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell is contained. - open func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) { - guard let dataSource = messagesCollectionView.messagesDataSource else { - fatalError(MessageKitError.nilMessagesDataSource) - } - guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { - fatalError(MessageKitError.nilMessagesDisplayDelegate) - } - - delegate = messagesCollectionView.messageCellDelegate - - let messageColor = displayDelegate.backgroundColor(for: message, at: indexPath, in: messagesCollectionView) - let messageStyle = displayDelegate.messageStyle(for: message, at: indexPath, in: messagesCollectionView) - - displayDelegate.configureAvatarView(avatarView, for: message, at: indexPath, in: messagesCollectionView) - - displayDelegate.configureAccessoryView(accessoryView, for: message, at: indexPath, in: messagesCollectionView) - - messageContainerView.backgroundColor = messageColor - messageContainerView.style = messageStyle - - let topCellLabelText = dataSource.cellTopLabelAttributedText(for: message, at: indexPath) - let topMessageLabelText = dataSource.messageTopLabelAttributedText(for: message, at: indexPath) - let bottomText = dataSource.messageBottomLabelAttributedText(for: message, at: indexPath) - - cellTopLabel.attributedText = topCellLabelText - messageTopLabel.attributedText = topMessageLabelText - messageBottomLabel.attributedText = bottomText + avatarView.frame = CGRect(origin: origin, size: attributes.avatarSize) + } + + /// Positions the cell's `MessageContainerView`. + /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. + open func layoutMessageContainerView(with attributes: MessagesCollectionViewLayoutAttributes) { + var origin: CGPoint = .zero + + switch attributes.avatarPosition.vertical { + case .messageBottom: + origin.y = attributes.size.height - attributes.messageContainerPadding.bottom - attributes.cellBottomLabelSize + .height - attributes.messageBottomLabelSize.height - attributes.messageContainerSize.height - attributes + .messageContainerPadding.top + case .messageCenter: + if attributes.avatarSize.height > attributes.messageContainerSize.height { + let messageHeight = attributes.messageContainerSize.height + attributes.messageContainerPadding.vertical + origin.y = (attributes.size.height / 2) - (messageHeight / 2) + } else { + fallthrough + } + default: + if attributes.accessoryViewSize.height > attributes.messageContainerSize.height { + let messageHeight = attributes.messageContainerSize.height + attributes.messageContainerPadding.vertical + origin.y = (attributes.size.height / 2) - (messageHeight / 2) + } else { + origin.y = attributes.cellTopLabelSize.height + attributes.messageTopLabelSize.height + attributes + .messageContainerPadding.top + } } - /// Handle tap gesture on contentView and its subviews. - open func handleTapGesture(_ gesture: UIGestureRecognizer) { - let touchLocation = gesture.location(in: self) - - switch true { - case messageContainerView.frame.contains(touchLocation) && !cellContentView(canHandle: convert(touchLocation, to: messageContainerView)): - delegate?.didTapMessage(in: self) - case avatarView.frame.contains(touchLocation): - delegate?.didTapAvatar(in: self) - case cellTopLabel.frame.contains(touchLocation): - delegate?.didTapCellTopLabel(in: self) - case messageTopLabel.frame.contains(touchLocation): - delegate?.didTapMessageTopLabel(in: self) - case messageBottomLabel.frame.contains(touchLocation): - delegate?.didTapMessageBottomLabel(in: self) - case accessoryView.frame.contains(touchLocation): - delegate?.didTapAccessoryView(in: self) - default: - break - } + let avatarPadding = attributes.avatarLeadingTrailingPadding + switch attributes.avatarPosition.horizontal { + case .cellLeading: + origin.x = attributes.avatarSize.width + attributes.messageContainerPadding.left + avatarPadding + case .cellTrailing: + origin.x = attributes.frame.width - attributes.avatarSize.width - attributes.messageContainerSize.width - attributes + .messageContainerPadding.right - avatarPadding + case .natural: + fatalError(MessageKitError.avatarPositionUnresolved) } - /// Handle long press gesture, return true when gestureRecognizer's touch point in `messageContainerView`'s frame - open override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - let touchPoint = gestureRecognizer.location(in: self) - guard gestureRecognizer.isKind(of: UILongPressGestureRecognizer.self) else { return false } - return messageContainerView.frame.contains(touchPoint) + messageContainerView.frame = CGRect(origin: origin, size: attributes.messageContainerSize) + } + + /// Positions the cell's top label. + /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. + open func layoutCellTopLabel(with attributes: MessagesCollectionViewLayoutAttributes) { + cellTopLabel.textAlignment = attributes.cellTopLabelAlignment.textAlignment + cellTopLabel.textInsets = attributes.cellTopLabelAlignment.textInsets + + cellTopLabel.frame = CGRect(origin: .zero, size: attributes.cellTopLabelSize) + } + + /// Positions the cell's bottom label. + /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. + open func layoutCellBottomLabel(with attributes: MessagesCollectionViewLayoutAttributes) { + cellBottomLabel.textAlignment = attributes.cellBottomLabelAlignment.textAlignment + cellBottomLabel.textInsets = attributes.cellBottomLabelAlignment.textInsets + + let y = messageBottomLabel.frame.maxY + let origin = CGPoint(x: 0, y: y) + + cellBottomLabel.frame = CGRect(origin: origin, size: attributes.cellBottomLabelSize) + } + + /// Positions the message bubble's top label. + /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. + open func layoutMessageTopLabel(with attributes: MessagesCollectionViewLayoutAttributes) { + messageTopLabel.textAlignment = attributes.messageTopLabelAlignment.textAlignment + messageTopLabel.textInsets = attributes.messageTopLabelAlignment.textInsets + + let y = messageContainerView.frame.minY - attributes.messageContainerPadding.top - attributes.messageTopLabelSize.height + let origin = CGPoint(x: 0, y: y) + + messageTopLabel.frame = CGRect(origin: origin, size: attributes.messageTopLabelSize) + } + + /// Positions the message bubble's bottom label. + /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. + open func layoutMessageBottomLabel(with attributes: MessagesCollectionViewLayoutAttributes) { + messageBottomLabel.textAlignment = attributes.messageBottomLabelAlignment.textAlignment + messageBottomLabel.textInsets = attributes.messageBottomLabelAlignment.textInsets + + let y = messageContainerView.frame.maxY + attributes.messageContainerPadding.bottom + let origin = CGPoint(x: 0, y: y) + + messageBottomLabel.frame = CGRect(origin: origin, size: attributes.messageBottomLabelSize) + } + + /// Positions the cell's accessory view. + /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. + open func layoutAccessoryView(with attributes: MessagesCollectionViewLayoutAttributes) { + var origin: CGPoint = .zero + + // Accessory view is set at the side space of the messageContainerView + switch attributes.accessoryViewPosition { + case .messageLabelTop: + origin.y = messageTopLabel.frame.minY + case .messageTop: + origin.y = messageContainerView.frame.minY + case .messageBottom: + origin.y = messageContainerView.frame.maxY - attributes.accessoryViewSize.height + case .messageCenter: + origin.y = messageContainerView.frame.midY - (attributes.accessoryViewSize.height / 2) + case .cellBottom: + origin.y = attributes.frame.height - attributes.accessoryViewSize.height + default: + break } - /// Handle `ContentView`'s tap gesture, return false when `ContentView` doesn't needs to handle gesture - open func cellContentView(canHandle touchPoint: CGPoint) -> Bool { - return false + // Accessory view is always on the opposite side of avatar + switch attributes.avatarPosition.horizontal { + case .cellLeading: + origin.x = messageContainerView.frame.maxX + attributes.accessoryViewPadding.left + case .cellTrailing: + origin.x = messageContainerView.frame.minX - attributes.accessoryViewPadding.right - attributes.accessoryViewSize + .width + case .natural: + fatalError(MessageKitError.avatarPositionUnresolved) } - // MARK: - Origin Calculations - - /// Positions the cell's `AvatarView`. - /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. - open func layoutAvatarView(with attributes: MessagesCollectionViewLayoutAttributes) { - var origin: CGPoint = .zero - - switch attributes.avatarPosition.horizontal { - case .cellLeading: - break - case .cellTrailing: - origin.x = attributes.frame.width - attributes.avatarSize.width - case .natural: - fatalError(MessageKitError.avatarPositionUnresolved) - } - - switch attributes.avatarPosition.vertical { - case .messageLabelTop: - origin.y = messageTopLabel.frame.minY - case .messageTop: // Needs messageContainerView frame to be set - origin.y = messageContainerView.frame.minY - case .messageBottom: // Needs messageContainerView frame to be set - origin.y = messageContainerView.frame.maxY - attributes.avatarSize.height - case .messageCenter: // Needs messageContainerView frame to be set - origin.y = messageContainerView.frame.midY - (attributes.avatarSize.height/2) - case .cellBottom: - origin.y = attributes.frame.height - attributes.avatarSize.height - default: - break - } - - avatarView.frame = CGRect(origin: origin, size: attributes.avatarSize) - } - - /// Positions the cell's `MessageContainerView`. - /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. - open func layoutMessageContainerView(with attributes: MessagesCollectionViewLayoutAttributes) { - var origin: CGPoint = .zero - - switch attributes.avatarPosition.vertical { - case .messageBottom: - origin.y = attributes.size.height - attributes.messageContainerPadding.bottom - attributes.messageBottomLabelSize.height - attributes.messageContainerSize.height - attributes.messageContainerPadding.top - case .messageCenter: - if attributes.avatarSize.height > attributes.messageContainerSize.height { - let messageHeight = attributes.messageContainerSize.height + attributes.messageContainerPadding.vertical - origin.y = (attributes.size.height / 2) - (messageHeight / 2) - } else { - fallthrough - } - default: - if attributes.accessoryViewSize.height > attributes.messageContainerSize.height { - let messageHeight = attributes.messageContainerSize.height + attributes.messageContainerPadding.vertical - origin.y = (attributes.size.height / 2) - (messageHeight / 2) - } else { - origin.y = attributes.cellTopLabelSize.height + attributes.messageTopLabelSize.height + attributes.messageContainerPadding.top - } - } - - switch attributes.avatarPosition.horizontal { - case .cellLeading: - origin.x = attributes.avatarSize.width + attributes.messageContainerPadding.left - case .cellTrailing: - origin.x = attributes.frame.width - attributes.avatarSize.width - attributes.messageContainerSize.width - attributes.messageContainerPadding.right - case .natural: - fatalError(MessageKitError.avatarPositionUnresolved) - } - - messageContainerView.frame = CGRect(origin: origin, size: attributes.messageContainerSize) - } - - /// Positions the cell's top label. - /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. - open func layoutCellTopLabel(with attributes: MessagesCollectionViewLayoutAttributes) { - cellTopLabel.frame = CGRect(origin: .zero, size: attributes.cellTopLabelSize) - } - - /// Positions the message bubble's top label. - /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. - open func layoutMessageTopLabel(with attributes: MessagesCollectionViewLayoutAttributes) { - messageTopLabel.textAlignment = attributes.messageTopLabelAlignment.textAlignment - messageTopLabel.textInsets = attributes.messageTopLabelAlignment.textInsets - - let y = messageContainerView.frame.minY - attributes.messageContainerPadding.top - attributes.messageTopLabelSize.height - let origin = CGPoint(x: 0, y: y) - - messageTopLabel.frame = CGRect(origin: origin, size: attributes.messageTopLabelSize) - } - - /// Positions the cell's bottom label. - /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. - open func layoutBottomLabel(with attributes: MessagesCollectionViewLayoutAttributes) { - messageBottomLabel.textAlignment = attributes.messageBottomLabelAlignment.textAlignment - messageBottomLabel.textInsets = attributes.messageBottomLabelAlignment.textInsets - - let y = messageContainerView.frame.maxY + attributes.messageContainerPadding.bottom - let origin = CGPoint(x: 0, y: y) - - messageBottomLabel.frame = CGRect(origin: origin, size: attributes.messageBottomLabelSize) - } - - /// Positions the cell's accessory view. - /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. - open func layoutAccessoryView(with attributes: MessagesCollectionViewLayoutAttributes) { - - // Accessory view aligned to the middle of the messageContainerView - let y = messageContainerView.frame.midY - (attributes.accessoryViewSize.height / 2) - - var origin = CGPoint(x: 0, y: y) - - // Accessory view is always on the opposite side of avatar - switch attributes.avatarPosition.horizontal { - case .cellLeading: - origin.x = messageContainerView.frame.maxX + attributes.accessoryViewPadding.left - case .cellTrailing: - origin.x = messageContainerView.frame.minX - attributes.accessoryViewPadding.right - attributes.accessoryViewSize.width - case .natural: - fatalError(MessageKitError.avatarPositionUnresolved) - } - - accessoryView.frame = CGRect(origin: origin, size: attributes.accessoryViewSize) - } + accessoryView.frame = CGRect(origin: origin, size: attributes.accessoryViewSize) + } + + /// Positions the message bubble's time label. + /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. + open func layoutTimeLabelView(with attributes: MessagesCollectionViewLayoutAttributes) { + let paddingLeft: CGFloat = 10 + let origin = CGPoint( + x: self.frame.maxX + paddingLeft, + y: messageContainerView.frame.minY + messageContainerView.frame.height * 0.5 - messageTimestampLabel.font.ascender * 0.5) + let size = CGSize(width: attributes.messageTimeLabelSize.width, height: attributes.messageTimeLabelSize.height) + messageTimestampLabel.frame = CGRect(origin: origin, size: size) + } } diff --git a/Sources/Views/Cells/TextMessageCell.swift b/Sources/Views/Cells/TextMessageCell.swift index 481411ba3..08fcf381b 100644 --- a/Sources/Views/Cells/TextMessageCell.swift +++ b/Sources/Views/Cells/TextMessageCell.swift @@ -1,101 +1,102 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import UIKit /// A subclass of `MessageContentCell` used to display text messages. open class TextMessageCell: MessageContentCell { + /// The label used to display the message's text. + open var messageLabel = MessageLabel() - // MARK: - Properties + // MARK: - Properties - /// The `MessageCellDelegate` for the cell. - open override weak var delegate: MessageCellDelegate? { - didSet { - messageLabel.delegate = delegate - } + /// The `MessageCellDelegate` for the cell. + open override weak var delegate: MessageCellDelegate? { + didSet { + messageLabel.delegate = delegate } + } - /// The label used to display the message's text. - open var messageLabel = MessageLabel() - - // MARK: - Methods + // MARK: - Methods - open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { - super.apply(layoutAttributes) - if let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes { - messageLabel.textInsets = attributes.messageLabelInsets - messageLabel.messageLabelFont = attributes.messageLabelFont - messageLabel.frame = messageContainerView.bounds - } + open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { + super.apply(layoutAttributes) + if let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes { + messageLabel.textInsets = attributes.messageLabelInsets + messageLabel.messageLabelFont = attributes.messageLabelFont + messageLabel.frame = messageContainerView.bounds } - - open override func prepareForReuse() { - super.prepareForReuse() - messageLabel.attributedText = nil - messageLabel.text = nil + } + + open override func prepareForReuse() { + super.prepareForReuse() + messageLabel.attributedText = nil + messageLabel.text = nil + } + + open override func setupSubviews() { + super.setupSubviews() + messageContainerView.addSubview(messageLabel) + } + + open override func configure( + with message: MessageType, + at indexPath: IndexPath, + and messagesCollectionView: MessagesCollectionView) + { + super.configure(with: message, at: indexPath, and: messagesCollectionView) + + guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { + fatalError(MessageKitError.nilMessagesDisplayDelegate) } - open override func setupSubviews() { - super.setupSubviews() - messageContainerView.addSubview(messageLabel) - } - - open override func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) { - super.configure(with: message, at: indexPath, and: messagesCollectionView) - - guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { - fatalError(MessageKitError.nilMessagesDisplayDelegate) + let enabledDetectors = displayDelegate.enabledDetectors(for: message, at: indexPath, in: messagesCollectionView) + + messageLabel.configure { + messageLabel.enabledDetectors = enabledDetectors + for detector in enabledDetectors { + let attributes = displayDelegate.detectorAttributes(for: detector, and: message, at: indexPath) + messageLabel.setAttributes(attributes, detector: detector) + } + let textMessageKind = message.kind.textMessageKind + switch textMessageKind { + case .text(let text), .emoji(let text): + let textColor = displayDelegate.textColor(for: message, at: indexPath, in: messagesCollectionView) + messageLabel.text = text + messageLabel.textColor = textColor + if let font = messageLabel.messageLabelFont { + messageLabel.font = font } - - let enabledDetectors = displayDelegate.enabledDetectors(for: message, at: indexPath, in: messagesCollectionView) - - messageLabel.configure { - messageLabel.enabledDetectors = enabledDetectors - for detector in enabledDetectors { - let attributes = displayDelegate.detectorAttributes(for: detector, and: message, at: indexPath) - messageLabel.setAttributes(attributes, detector: detector) - } - switch message.kind { - case .text(let text), .emoji(let text): - let textColor = displayDelegate.textColor(for: message, at: indexPath, in: messagesCollectionView) - messageLabel.text = text - messageLabel.textColor = textColor - if let font = messageLabel.messageLabelFont { - messageLabel.font = font - } - case .attributedText(let text): - messageLabel.attributedText = text - default: - break - } - } - } - - /// Used to handle the cell's contentView's tap gesture. - /// Return false when the contentView does not need to handle the gesture. - open override func cellContentView(canHandle touchPoint: CGPoint) -> Bool { - return messageLabel.handleGesture(touchPoint) + case .attributedText(let text): + messageLabel.attributedText = text + default: + break + } } + } + /// Used to handle the cell's contentView's tap gesture. + /// Return false when the contentView does not need to handle the gesture. + open override func cellContentView(canHandle touchPoint: CGPoint) -> Bool { + messageLabel.handleGesture(touchPoint) + } } diff --git a/Sources/Views/Cells/TypingIndicatorCell.swift b/Sources/Views/Cells/TypingIndicatorCell.swift new file mode 100644 index 000000000..368d25b50 --- /dev/null +++ b/Sources/Views/Cells/TypingIndicatorCell.swift @@ -0,0 +1,62 @@ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit + +/// A subclass of `MessageCollectionViewCell` used to display the typing indicator. +open class TypingIndicatorCell: MessageCollectionViewCell { + // MARK: Lifecycle + + public override init(frame: CGRect) { + super.init(frame: frame) + setupSubviews() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupSubviews() + } + + // MARK: Open + + open func setupSubviews() { + addSubview(typingBubble) + } + + open override func prepareForReuse() { + super.prepareForReuse() + if typingBubble.isAnimating { + typingBubble.stopAnimating() + } + } + + open override func layoutSubviews() { + super.layoutSubviews() + typingBubble.frame = bounds.inset(by: insets) + } + + // MARK: Public + + public var insets = UIEdgeInsets(top: 15, left: 0, bottom: 0, right: 0) + + public let typingBubble = TypingBubble() +} diff --git a/Sources/Views/Headers & Footers/MessageReusableView.swift b/Sources/Views/Headers & Footers/MessageReusableView.swift deleted file mode 100644 index 56270dd3c..000000000 --- a/Sources/Views/Headers & Footers/MessageReusableView.swift +++ /dev/null @@ -1,39 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import Foundation - -open class MessageReusableView: UICollectionReusableView { - - // MARK: - Initializers - - public override init(frame: CGRect) { - super.init(frame: frame) - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - -} diff --git a/Sources/Views/HeadersFooters/MessageReusableView.swift b/Sources/Views/HeadersFooters/MessageReusableView.swift new file mode 100644 index 000000000..77654ed72 --- /dev/null +++ b/Sources/Views/HeadersFooters/MessageReusableView.swift @@ -0,0 +1,36 @@ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import UIKit + +open class MessageReusableView: UICollectionReusableView { + // MARK: - Initializers + + public override init(frame: CGRect) { + super.init(frame: frame) + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } +} diff --git a/Sources/Views/InsetLabel.swift b/Sources/Views/InsetLabel.swift index 72bb9f8dc..9e6c7b62f 100644 --- a/Sources/Views/InsetLabel.swift +++ b/Sources/Views/InsetLabel.swift @@ -1,38 +1,34 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import UIKit open class InsetLabel: UILabel { - - open var textInsets: UIEdgeInsets = .zero { - didSet { setNeedsDisplay() } - } - - open override func drawText(in rect: CGRect) { - let insetRect = rect.inset(by: textInsets) - super.drawText(in: insetRect) - } - + open var textInsets: UIEdgeInsets = .zero { + didSet { setNeedsDisplay() } + } + + open override func drawText(in rect: CGRect) { + let insetRect = rect.inset(by: textInsets) + super.drawText(in: insetRect) + } } diff --git a/Sources/Views/LinkPreviewView.swift b/Sources/Views/LinkPreviewView.swift new file mode 100644 index 000000000..28fc1b1c1 --- /dev/null +++ b/Sources/Views/LinkPreviewView.swift @@ -0,0 +1,120 @@ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit + +open class LinkPreviewView: UIView { + // MARK: Lifecycle + + init() { + super.init(frame: .zero) + + contentView.addSubview(titleLabel) + NSLayoutConstraint.activate([ + titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor), + titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + ]) + + contentView.addSubview(teaserLabel) + NSLayoutConstraint.activate([ + teaserLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + teaserLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 3), + teaserLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + ]) + teaserLabel.setContentHuggingPriority(.init(249), for: .vertical) + + contentView.addSubview(domainLabel) + NSLayoutConstraint.activate([ + domainLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + domainLabel.topAnchor.constraint(equalTo: teaserLabel.bottomAnchor, constant: 3), + domainLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + domainLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + ]) + } + + required public init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Internal + + lazy var imageView: UIImageView = { + let imageView: UIImageView = .init() + imageView.clipsToBounds = true + imageView.contentMode = .scaleAspectFill + imageView.translatesAutoresizingMaskIntoConstraints = false + + addSubview(imageView) + + NSLayoutConstraint.activate([ + imageView.leadingAnchor.constraint(equalTo: leadingAnchor), + imageView.topAnchor.constraint(equalTo: topAnchor), + imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1), + imageView.widthAnchor.constraint(equalToConstant: LinkPreviewMessageSizeCalculator.imageViewSize), + imageView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor), + ]) + + return imageView + }() + + lazy var titleLabel: UILabel = { + let label: UILabel = .init() + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + lazy var teaserLabel: UILabel = { + let label: UILabel = .init() + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + lazy var domainLabel: UILabel = { + let label: UILabel = .init() + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + // MARK: Private + + private lazy var contentView: UIView = { + let view: UIView = .init(frame: .zero) + view.translatesAutoresizingMaskIntoConstraints = false + + addSubview(view) + + NSLayoutConstraint.activate([ + view.topAnchor.constraint(equalTo: topAnchor), + view.leadingAnchor.constraint( + equalTo: imageView.trailingAnchor, + constant: LinkPreviewMessageSizeCalculator.imageViewMargin), + view.trailingAnchor.constraint(equalTo: trailingAnchor), + view.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + + return view + }() +} diff --git a/Sources/Views/MessageContainerView.swift b/Sources/Views/MessageContainerView.swift index 714232936..e816f0d19 100644 --- a/Sources/Views/MessageContainerView.swift +++ b/Sources/Views/MessageContainerView.swift @@ -1,88 +1,89 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import UIKit open class MessageContainerView: UIImageView { + // MARK: Open - // MARK: - Properties - - private let imageMask = UIImageView() - - open var style: MessageStyle = .none { - didSet { - applyMessageStyle() - } + open var style: MessageStyle = .none { + didSet { + applyMessageStyle() } + } - open override var frame: CGRect { - didSet { - sizeMaskToView() - } + open override var frame: CGRect { + didSet { + sizeMaskToView() } + } + + // MARK: Private + + // MARK: - Properties + + private let imageMask = UIImageView() - // MARK: - Methods + // MARK: - Methods - private func sizeMaskToView() { - switch style { - case .none, .custom: - break - case .bubble, .bubbleTail, .bubbleOutline, .bubbleTailOutline: - imageMask.frame = bounds - } + private func sizeMaskToView() { + switch style { + case .none, .custom: + break + case .bubble, .bubbleTail, .bubbleOutline, .bubbleTailOutline, .customImageTail: + imageMask.frame = bounds } + } - private func applyMessageStyle() { - switch style { - case .bubble, .bubbleTail: - imageMask.image = style.image - sizeMaskToView() - mask = imageMask - image = nil - case .bubbleOutline(let color): - let bubbleStyle: MessageStyle = .bubble - imageMask.image = bubbleStyle.image - sizeMaskToView() - mask = imageMask - image = style.image?.withRenderingMode(.alwaysTemplate) - tintColor = color - case .bubbleTailOutline(let color, let tail, let corner): - let bubbleStyle: MessageStyle = .bubbleTail(tail, corner) - imageMask.image = bubbleStyle.image - sizeMaskToView() - mask = imageMask - image = style.image?.withRenderingMode(.alwaysTemplate) - tintColor = color - case .none: - mask = nil - image = nil - tintColor = nil - case .custom(let configurationClosure): - mask = nil - image = nil - tintColor = nil - configurationClosure(self) - } + private func applyMessageStyle() { + switch style { + case .bubble, .bubbleTail, .customImageTail: + imageMask.image = style.image + sizeMaskToView() + mask = imageMask + image = nil + case .bubbleOutline(let color): + let bubbleStyle: MessageStyle = .bubble + imageMask.image = bubbleStyle.image + sizeMaskToView() + mask = imageMask + image = style.image?.withRenderingMode(.alwaysTemplate) + tintColor = color + case .bubbleTailOutline(let color, let tail, let corner): + let bubbleStyle: MessageStyle = .bubbleTail(tail, corner) + imageMask.image = bubbleStyle.image + sizeMaskToView() + mask = imageMask + image = style.image?.withRenderingMode(.alwaysTemplate) + tintColor = color + case .none: + mask = nil + image = nil + tintColor = nil + case .custom(let configurationClosure): + mask = nil + image = nil + tintColor = nil + configurationClosure(self) } + } } diff --git a/Sources/Views/MessageLabel.swift b/Sources/Views/MessageLabel.swift index d451d4864..036bc3565 100644 --- a/Sources/Views/MessageLabel.swift +++ b/Sources/Views/MessageLabel.swift @@ -1,474 +1,581 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import UIKit +// MARK: - MessageLabel + open class MessageLabel: UILabel { + // MARK: Lifecycle - // MARK: - Private Properties - - private lazy var layoutManager: NSLayoutManager = { - let layoutManager = NSLayoutManager() - layoutManager.addTextContainer(self.textContainer) - return layoutManager - }() - - private lazy var textContainer: NSTextContainer = { - let textContainer = NSTextContainer() - textContainer.lineFragmentPadding = 0 - textContainer.maximumNumberOfLines = self.numberOfLines - textContainer.lineBreakMode = self.lineBreakMode - textContainer.size = self.bounds.size - return textContainer - }() - - private lazy var textStorage: NSTextStorage = { - let textStorage = NSTextStorage() - textStorage.addLayoutManager(self.layoutManager) - return textStorage - }() - - private lazy var rangesForDetectors: [DetectorType: [(NSRange, MessageTextCheckingType)]] = [:] - - private var isConfiguring: Bool = false + // MARK: - Initializers - // MARK: - Public Properties + public override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } - open weak var delegate: MessageLabelDelegate? + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupView() + } - open var enabledDetectors: [DetectorType] = [] { - didSet { - setTextStorage(attributedText, shouldParse: true) - } - } + // MARK: Open - open override var attributedText: NSAttributedString? { - didSet { - setTextStorage(attributedText, shouldParse: true) - } + // MARK: - Public Properties + + open weak var delegate: MessageLabelDelegate? + + open internal(set) var addressAttributes: [NSAttributedString.Key: Any] = defaultAttributes + + open internal(set) var dateAttributes: [NSAttributedString.Key: Any] = defaultAttributes + + open internal(set) var phoneNumberAttributes: [NSAttributedString.Key: Any] = defaultAttributes + + open internal(set) var urlAttributes: [NSAttributedString.Key: Any] = defaultAttributes + + open internal(set) var transitInformationAttributes: [NSAttributedString.Key: Any] = defaultAttributes + + open internal(set) var hashtagAttributes: [NSAttributedString.Key: Any] = defaultAttributes + + open internal(set) var mentionAttributes: [NSAttributedString.Key: Any] = defaultAttributes + + open internal(set) var customAttributes: [NSRegularExpression: [NSAttributedString.Key: Any]] = [:] + + open var enabledDetectors: [DetectorType] = [] { + didSet { + setTextStorage(attributedText, shouldParse: true) } + } - open override var text: String? { - didSet { - setTextStorage(attributedText, shouldParse: true) - } + open override var attributedText: NSAttributedString? { + didSet { + setTextStorage(attributedText, shouldParse: true) } + } - open override var font: UIFont! { - didSet { - setTextStorage(attributedText, shouldParse: false) - } + open override var text: String? { + didSet { + setTextStorage(attributedText, shouldParse: true) } + } - open override var textColor: UIColor! { - didSet { - setTextStorage(attributedText, shouldParse: false) - } + // swiftlint:disable:next implicitly_unwrapped_optional + open override var font: UIFont! { + didSet { + setTextStorage(attributedText, shouldParse: false) } + } - open override var lineBreakMode: NSLineBreakMode { - didSet { - textContainer.lineBreakMode = lineBreakMode - if !isConfiguring { setNeedsDisplay() } - } + // swiftlint:disable:next implicitly_unwrapped_optional + open override var textColor: UIColor! { + didSet { + setTextStorage(attributedText, shouldParse: false) } + } - open override var numberOfLines: Int { - didSet { - textContainer.maximumNumberOfLines = numberOfLines - if !isConfiguring { setNeedsDisplay() } - } + open override var lineBreakMode: NSLineBreakMode { + didSet { + textContainer.lineBreakMode = lineBreakMode + if !isConfiguring { setNeedsDisplay() } } + } - open override var textAlignment: NSTextAlignment { - didSet { - setTextStorage(attributedText, shouldParse: false) - } + open override var numberOfLines: Int { + didSet { + textContainer.maximumNumberOfLines = numberOfLines + if !isConfiguring { setNeedsDisplay() } } + } - open var textInsets: UIEdgeInsets = .zero { - didSet { - if !isConfiguring { setNeedsDisplay() } - } + open override var textAlignment: NSTextAlignment { + didSet { + setTextStorage(attributedText, shouldParse: false) } + } - open override var intrinsicContentSize: CGSize { - var size = super.intrinsicContentSize - size.width += textInsets.horizontal - size.height += textInsets.vertical - return size + open var textInsets: UIEdgeInsets = .zero { + didSet { + if !isConfiguring { setNeedsDisplay() } } - - internal var messageLabelFont: UIFont? + } - private var attributesNeedUpdate = false + open override var intrinsicContentSize: CGSize { + var size = super.intrinsicContentSize + size.width += textInsets.horizontal + size.height += textInsets.vertical + return size + } - public static var defaultAttributes: [NSAttributedString.Key: Any] = { - return [ - NSAttributedString.Key.foregroundColor: UIColor.darkText, - NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue, - NSAttributedString.Key.underlineColor: UIColor.darkText - ] - }() + // MARK: - Open Methods - open internal(set) var addressAttributes: [NSAttributedString.Key: Any] = defaultAttributes + open override func drawText(in rect: CGRect) { + let insetRect = rect.inset(by: textInsets) + textContainer.size = CGSize(width: insetRect.width, height: insetRect.height) - open internal(set) var dateAttributes: [NSAttributedString.Key: Any] = defaultAttributes + let origin = insetRect.origin + let range = layoutManager.glyphRange(for: textContainer) - open internal(set) var phoneNumberAttributes: [NSAttributedString.Key: Any] = defaultAttributes + layoutManager.drawBackground(forGlyphRange: range, at: origin) + layoutManager.drawGlyphs(forGlyphRange: range, at: origin) + } - open internal(set) var urlAttributes: [NSAttributedString.Key: Any] = defaultAttributes - - open internal(set) var transitInformationAttributes: [NSAttributedString.Key: Any] = defaultAttributes - - public func setAttributes(_ attributes: [NSAttributedString.Key: Any], detector: DetectorType) { - switch detector { - case .phoneNumber: - phoneNumberAttributes = attributes - case .address: - addressAttributes = attributes - case .date: - dateAttributes = attributes - case .url: - urlAttributes = attributes - case .transitInformation: - transitInformationAttributes = attributes - } - if isConfiguring { - attributesNeedUpdate = true - } else { - updateAttributes(for: [detector]) + open func handleGesture(_ touchLocation: CGPoint) -> Bool { + guard let index = stringIndex(at: touchLocation) else { return false } + + for (detectorType, ranges) in rangesForDetectors { + for (range, value) in ranges { + if range.contains(index) { + handleGesture(for: detectorType, value: value) + return true } + } } + return false + } + + // MARK: Public + + public static var defaultAttributes: [NSAttributedString.Key: Any] = { + [ + NSAttributedString.Key.foregroundColor: UIColor.darkText, + NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue, + NSAttributedString.Key.underlineColor: UIColor.darkText, + ] + }() + + public func setAttributes(_ attributes: [NSAttributedString.Key: Any], detector: DetectorType) { + switch detector { + case .phoneNumber: + phoneNumberAttributes = attributes + case .address: + addressAttributes = attributes + case .date: + dateAttributes = attributes + case .url: + urlAttributes = attributes + case .transitInformation: + transitInformationAttributes = attributes + case .mention: + mentionAttributes = attributes + case .hashtag: + hashtagAttributes = attributes + case .custom(let regex): + customAttributes[regex] = attributes + } + if isConfiguring { + attributesNeedUpdate = true + } else { + updateAttributes(for: [detector]) + } + } - // MARK: - Initializers + // MARK: - Public Methods - public override init(frame: CGRect) { - super.init(frame: frame) - setupView() + public func configure(block: () -> Void) { + isConfiguring = true + block() + if attributesNeedUpdate { + updateAttributes(for: enabledDetectors) } + attributesNeedUpdate = false + isConfiguring = false + setNeedsDisplay() + } - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setupView() - } + // MARK: Internal - // MARK: - Open Methods + internal lazy var rangesForDetectors: [DetectorType: [(NSRange, MessageTextCheckingType)]] = [:] - open override func drawText(in rect: CGRect) { + internal var messageLabelFont: UIFont? - let insetRect = rect.inset(by: textInsets) - textContainer.size = CGSize(width: insetRect.width, height: rect.height) + // MARK: Private - let origin = insetRect.origin - let range = layoutManager.glyphRange(for: textContainer) + // MARK: - Private Properties - layoutManager.drawBackground(forGlyphRange: range, at: origin) - layoutManager.drawGlyphs(forGlyphRange: range, at: origin) - } + private lazy var layoutManager: NSLayoutManager = { + let layoutManager = NSLayoutManager() + layoutManager.addTextContainer(self.textContainer) + return layoutManager + }() - // MARK: - Public Methods - - public func configure(block: () -> Void) { - isConfiguring = true - block() - if attributesNeedUpdate { - updateAttributes(for: enabledDetectors) - } - attributesNeedUpdate = false - isConfiguring = false - setNeedsDisplay() - } + private lazy var textContainer: NSTextContainer = { + let textContainer = NSTextContainer() + textContainer.lineFragmentPadding = 0 + textContainer.maximumNumberOfLines = self.numberOfLines + textContainer.lineBreakMode = self.lineBreakMode + textContainer.size = self.bounds.size + return textContainer + }() - // MARK: - Private Methods + private lazy var textStorage: NSTextStorage = { + let textStorage = NSTextStorage() + textStorage.addLayoutManager(self.layoutManager) + return textStorage + }() - private func setTextStorage(_ newText: NSAttributedString?, shouldParse: Bool) { + private var isConfiguring = false - guard let newText = newText, newText.length > 0 else { - textStorage.setAttributedString(NSAttributedString()) - setNeedsDisplay() - return - } - - let style = paragraphStyle(for: newText) - let range = NSRange(location: 0, length: newText.length) - - let mutableText = NSMutableAttributedString(attributedString: newText) - mutableText.addAttribute(.paragraphStyle, value: style, range: range) - - if shouldParse { - rangesForDetectors.removeAll() - let results = parse(text: mutableText) - setRangesForDetectors(in: results) - } - - for (detector, rangeTuples) in rangesForDetectors { - if enabledDetectors.contains(detector) { - let attributes = detectorAttributes(for: detector) - rangeTuples.forEach { (range, _) in - mutableText.addAttributes(attributes, range: range) - } - } - } + private var attributesNeedUpdate = false - let modifiedText = NSAttributedString(attributedString: mutableText) - textStorage.setAttributedString(modifiedText) + // MARK: - Private Methods - if !isConfiguring { setNeedsDisplay() } + private func setTextStorage(_ newText: NSAttributedString?, shouldParse: Bool) { + guard let newText = newText, newText.length > 0 else { + textStorage.setAttributedString(NSAttributedString()) + setNeedsDisplay() + return + } + + let style = paragraphStyle(for: newText) + let range = NSRange(location: 0, length: newText.length) + + let mutableText = NSMutableAttributedString(attributedString: newText) + mutableText.addAttribute(.paragraphStyle, value: style, range: range) + if shouldParse { + rangesForDetectors.removeAll() + let results = parse(text: mutableText) + setRangesForDetectors(in: results) } - - private func paragraphStyle(for text: NSAttributedString) -> NSParagraphStyle { - guard text.length > 0 else { return NSParagraphStyle() } - - var range = NSRange(location: 0, length: text.length) - let existingStyle = text.attribute(.paragraphStyle, at: 0, effectiveRange: &range) as? NSMutableParagraphStyle - let style = existingStyle ?? NSMutableParagraphStyle() - - style.lineBreakMode = lineBreakMode - style.alignment = textAlignment - - return style + + for (detector, rangeTuples) in rangesForDetectors { + if enabledDetectors.contains(detector) { + let attributes = detectorAttributes(for: detector) + rangeTuples.forEach { range, _ in + mutableText.addAttributes(attributes, range: range) + } + } } - private func updateAttributes(for detectors: [DetectorType]) { + let modifiedText = NSAttributedString(attributedString: mutableText) + textStorage.setAttributedString(modifiedText) + + if !isConfiguring { setNeedsDisplay() } + } + + private func paragraphStyle(for text: NSAttributedString) -> NSParagraphStyle { + guard text.length > 0 else { return NSParagraphStyle() } - guard let attributedText = attributedText, attributedText.length > 0 else { return } - let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText) + var range = NSRange(location: 0, length: text.length) + let existingStyle = text.attribute(.paragraphStyle, at: 0, effectiveRange: &range) as? NSMutableParagraphStyle + let style = existingStyle ?? NSMutableParagraphStyle() - for detector in detectors { - guard let rangeTuples = rangesForDetectors[detector] else { continue } + style.lineBreakMode = lineBreakMode + style.alignment = textAlignment - for (range, _) in rangeTuples { - let attributes = detectorAttributes(for: detector) - mutableAttributedString.addAttributes(attributes, range: range) - } + return style + } - let updatedString = NSAttributedString(attributedString: mutableAttributedString) - textStorage.setAttributedString(updatedString) + private func updateAttributes(for detectors: [DetectorType]) { + guard let attributedText = attributedText, attributedText.length > 0 else { return } + let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText) + + for detector in detectors { + guard let rangeTuples = rangesForDetectors[detector] else { continue } + + for (range, _) in rangeTuples { + // This will enable us to attribute it with our own styles, since `UILabel` does not provide link attribute overrides like `UITextView` does + if detector.textCheckingType == .link { + mutableAttributedString.removeAttribute(NSAttributedString.Key.link, range: range) } + + let attributes = detectorAttributes(for: detector) + mutableAttributedString.addAttributes(attributes, range: range) + } + + let updatedString = NSAttributedString(attributedString: mutableAttributedString) + textStorage.setAttributedString(updatedString) + } + } + + private func detectorAttributes(for detectorType: DetectorType) -> [NSAttributedString.Key: Any] { + switch detectorType { + case .address: + return addressAttributes + case .date: + return dateAttributes + case .phoneNumber: + return phoneNumberAttributes + case .url: + return urlAttributes + case .transitInformation: + return transitInformationAttributes + case .mention: + return mentionAttributes + case .hashtag: + return hashtagAttributes + case .custom(let regex): + return customAttributes[regex] ?? MessageLabel.defaultAttributes + } + } + + private func detectorAttributes(for checkingResultType: NSTextCheckingResult.CheckingType) -> [NSAttributedString.Key: Any] { + switch checkingResultType { + case .address: + return addressAttributes + case .date: + return dateAttributes + case .phoneNumber: + return phoneNumberAttributes + case .link: + return urlAttributes + case .transitInformation: + return transitInformationAttributes + default: + fatalError(MessageKitError.unrecognizedCheckingResult) + } + } + + private func setupView() { + numberOfLines = 0 + lineBreakMode = .byWordWrapping + } + + // MARK: - Parsing Text + + private func parse(text: NSAttributedString) -> [NSTextCheckingResult] { + guard enabledDetectors.isEmpty == false else { return [] } + let range = NSRange(location: 0, length: text.length) + var matches = [NSTextCheckingResult]() + + // Get matches of all .custom DetectorType and add it to matches array + let regexs = enabledDetectors + .filter { $0.isCustom } + .map { parseForMatches(with: $0, in: text, for: range) } + .joined() + matches.append(contentsOf: removeOverlappingResults(Array(regexs))) + + // Get all Checking Types of detectors, except for .custom because they contain their own regex + let detectorCheckingTypes = enabledDetectors + .filter { !$0.isCustom } + .reduce(0) { $0 | $1.textCheckingType.rawValue } + if detectorCheckingTypes > 0, let detector = try? NSDataDetector(types: detectorCheckingTypes) { + let detectorMatches = detector.matches(in: text.string, options: [], range: range) + matches.append(contentsOf: detectorMatches) } - private func detectorAttributes(for detectorType: DetectorType) -> [NSAttributedString.Key: Any] { - - switch detectorType { - case .address: - return addressAttributes - case .date: - return dateAttributes - case .phoneNumber: - return phoneNumberAttributes - case .url: - return urlAttributes - case .transitInformation: - return transitInformationAttributes - } + guard enabledDetectors.contains(.url) else { + return matches + } + + // Enumerate NSAttributedString NSLinks and append ranges + var results: [NSTextCheckingResult] = matches + text.enumerateAttribute(NSAttributedString.Key.link, in: range, options: []) { value, range, _ in + guard let url = value as? URL else { return } + let result = NSTextCheckingResult.linkCheckingResult(range: range, url: url) + results.append(result) } - private func detectorAttributes(for checkingResultType: NSTextCheckingResult.CheckingType) -> [NSAttributedString.Key: Any] { - switch checkingResultType { - case .address: - return addressAttributes - case .date: - return dateAttributes - case .phoneNumber: - return phoneNumberAttributes - case .link: - return urlAttributes - case .transitInformation: - return transitInformationAttributes - default: - fatalError(MessageKitError.unrecognizedCheckingResult) - } + return results + } + + private func parseForMatches( + with detector: DetectorType, + in text: NSAttributedString, + for range: NSRange) -> [NSTextCheckingResult] + { + switch detector { + case .custom(let regex): + return regex.matches(in: text.string, options: [], range: range) + default: + fatalError("You must pass a .custom DetectorType") } + } + + private func removeOverlappingResults(_ results: [NSTextCheckingResult]) -> [NSTextCheckingResult] + { + var filteredResults: [NSTextCheckingResult] = [] - private func setupView() { - numberOfLines = 0 - lineBreakMode = .byWordWrapping + for result in results { + let overlappingResults = results.filter { $0.range.intersection(result.range)?.length ?? 0 > 0 } + + if overlappingResults.count <= 1 { + filteredResults.append(result) + continue + } + + guard !filteredResults.contains(where: { $0.range == result.range }) else { continue } + let maxRangeResult = overlappingResults.max { $0.range.upperBound - $0.range.lowerBound < $1.range.upperBound - $1.range.lowerBound } + if let maxRangeResult { + filteredResults.append(maxRangeResult) + } } + + return filteredResults + } + + private func setRangesForDetectors(in checkingResults: [NSTextCheckingResult]) { + guard checkingResults.isEmpty == false else { return } + + for result in checkingResults { + switch result.resultType { + case .address: + var ranges = rangesForDetectors[.address] ?? [] + let tuple: (NSRange, MessageTextCheckingType) = (result.range, .addressComponents(result.addressComponents)) + ranges.append(tuple) + rangesForDetectors.updateValue(ranges, forKey: .address) + case .date: + var ranges = rangesForDetectors[.date] ?? [] + let tuple: (NSRange, MessageTextCheckingType) = (result.range, .date(result.date)) + ranges.append(tuple) + rangesForDetectors.updateValue(ranges, forKey: .date) + case .phoneNumber: + var ranges = rangesForDetectors[.phoneNumber] ?? [] + let tuple: (NSRange, MessageTextCheckingType) = (result.range, .phoneNumber(result.phoneNumber)) + ranges.append(tuple) + rangesForDetectors.updateValue(ranges, forKey: .phoneNumber) + case .link: + var ranges = rangesForDetectors[.url] ?? [] + let tuple: (NSRange, MessageTextCheckingType) = (result.range, .link(result.url)) + ranges.append(tuple) + rangesForDetectors.updateValue(ranges, forKey: .url) + case .transitInformation: + var ranges = rangesForDetectors[.transitInformation] ?? [] + let tuple: (NSRange, MessageTextCheckingType) = (result.range, .transitInfoComponents(result.components)) + ranges.append(tuple) + rangesForDetectors.updateValue(ranges, forKey: .transitInformation) + case .regularExpression: + guard + let text = text, let regex = result.regularExpression, + let range = Range(result.range, in: text) else { return } + let detector = DetectorType.custom(regex) + var ranges = rangesForDetectors[detector] ?? [] + let tuple: (NSRange, MessageTextCheckingType) = ( + result.range, + .custom(pattern: regex.pattern, match: String(text[range]))) + ranges.append(tuple) + rangesForDetectors.updateValue(ranges, forKey: detector) + default: + fatalError("Received an unrecognized NSTextCheckingResult.CheckingType") + } + } + } - // MARK: - Parsing Text - - private func parse(text: NSAttributedString) -> [NSTextCheckingResult] { - guard enabledDetectors.isEmpty == false else { return [] } - let checkingTypes = enabledDetectors.reduce(0) { $0 | $1.textCheckingType.rawValue } - let detector = try? NSDataDetector(types: checkingTypes) - let range = NSRange(location: 0, length: text.length) - let matches = detector?.matches(in: text.string, options: [], range: range) ?? [] + // MARK: - Gesture Handling - guard enabledDetectors.contains(.url) else { - return matches - } + private func stringIndex(at location: CGPoint) -> Int? { + guard textStorage.length > 0 else { return nil } - // Enumerate NSAttributedString NSLinks and append ranges - var results: [NSTextCheckingResult] = matches + var location = location - text.enumerateAttribute(NSAttributedString.Key.link, in: range, options: []) { value, range, _ in - guard let url = value as? URL else { return } - let result = NSTextCheckingResult.linkCheckingResult(range: range, url: url) - results.append(result) - } + location.x -= textInsets.left + location.y -= textInsets.top - return results - } + let index = layoutManager.glyphIndex(for: location, in: textContainer) - private func setRangesForDetectors(in checkingResults: [NSTextCheckingResult]) { - - guard checkingResults.isEmpty == false else { return } - - for result in checkingResults { - - switch result.resultType { - case .address: - var ranges = rangesForDetectors[.address] ?? [] - let tuple: (NSRange, MessageTextCheckingType) = (result.range, .addressComponents(result.addressComponents)) - ranges.append(tuple) - rangesForDetectors.updateValue(ranges, forKey: .address) - case .date: - var ranges = rangesForDetectors[.date] ?? [] - let tuple: (NSRange, MessageTextCheckingType) = (result.range, .date(result.date)) - ranges.append(tuple) - rangesForDetectors.updateValue(ranges, forKey: .date) - case .phoneNumber: - var ranges = rangesForDetectors[.phoneNumber] ?? [] - let tuple: (NSRange, MessageTextCheckingType) = (result.range, .phoneNumber(result.phoneNumber)) - ranges.append(tuple) - rangesForDetectors.updateValue(ranges, forKey: .phoneNumber) - case .link: - var ranges = rangesForDetectors[.url] ?? [] - let tuple: (NSRange, MessageTextCheckingType) = (result.range, .link(result.url)) - ranges.append(tuple) - rangesForDetectors.updateValue(ranges, forKey: .url) - case .transitInformation: - var ranges = rangesForDetectors[.transitInformation] ?? [] - let tuple: (NSRange, MessageTextCheckingType) = (result.range, .transitInfoComponents(result.components)) - ranges.append(tuple) - rangesForDetectors.updateValue(ranges, forKey: .transitInformation) - - default: - fatalError("Received an unrecognized NSTextCheckingResult.CheckingType") - } + let lineRect = layoutManager.lineFragmentUsedRect(forGlyphAt: index, effectiveRange: nil) - } + var characterIndex: Int? + if lineRect.contains(location) { + characterIndex = layoutManager.characterIndexForGlyph(at: index) } - // MARK: - Gesture Handling + return characterIndex + } + + // swiftlint:disable cyclomatic_complexity + private func handleGesture(for detectorType: DetectorType, value: MessageTextCheckingType) { + switch value { + case .addressComponents(let addressComponents): + var transformedAddressComponents = [String: String]() + guard let addressComponents = addressComponents else { return } + addressComponents.forEach { key, value in + transformedAddressComponents[key.rawValue] = value + } + handleAddress(transformedAddressComponents) + case .phoneNumber(let phoneNumber): + guard let phoneNumber = phoneNumber else { return } + handlePhoneNumber(phoneNumber) + case .date(let date): + guard let date = date else { return } + handleDate(date) + case .link(let url): + guard let url = url else { return } + handleURL(url) + case .transitInfoComponents(let transitInformation): + var transformedTransitInformation = [String: String]() + guard let transitInformation = transitInformation else { return } + transitInformation.forEach { key, value in + transformedTransitInformation[key.rawValue] = value + } + handleTransitInformation(transformedTransitInformation) + case .custom(let pattern, let match): + guard let match = match else { return } + switch detectorType { + case .hashtag: + handleHashtag(match) + case .mention: + handleMention(match) + default: + handleCustom(pattern, match: match) + } + } + } - private func stringIndex(at location: CGPoint) -> Int? { - guard textStorage.length > 0 else { return nil } + // swiftlint:enable cyclomatic_complexity - var location = location + private func handleAddress(_ addressComponents: [String: String]) { + delegate?.didSelectAddress(addressComponents) + } - location.x -= textInsets.left - location.y -= textInsets.top + private func handleDate(_ date: Date) { + delegate?.didSelectDate(date) + } - let index = layoutManager.glyphIndex(for: location, in: textContainer) + private func handleURL(_ url: URL) { + delegate?.didSelectURL(url) + } - let lineRect = layoutManager.lineFragmentUsedRect(forGlyphAt: index, effectiveRange: nil) - - var characterIndex: Int? - - if lineRect.contains(location) { - characterIndex = layoutManager.characterIndexForGlyph(at: index) - } - - return characterIndex + private func handlePhoneNumber(_ phoneNumber: String) { + delegate?.didSelectPhoneNumber(phoneNumber) + } - } + private func handleTransitInformation(_ components: [String: String]) { + delegate?.didSelectTransitInformation(components) + } - open func handleGesture(_ touchLocation: CGPoint) -> Bool { + private func handleHashtag(_ hashtag: String) { + delegate?.didSelectHashtag(hashtag) + } - guard let index = stringIndex(at: touchLocation) else { return false } + private func handleMention(_ mention: String) { + delegate?.didSelectMention(mention) + } - for (detectorType, ranges) in rangesForDetectors { - for (range, value) in ranges { - if range.contains(index) { - handleGesture(for: detectorType, value: value) - return true - } - } - } - return false - } - - private func handleGesture(for detectorType: DetectorType, value: MessageTextCheckingType) { - - switch value { - case let .addressComponents(addressComponents): - var transformedAddressComponents = [String: String]() - guard let addressComponents = addressComponents else { return } - addressComponents.forEach { (key, value) in - transformedAddressComponents[key.rawValue] = value - } - handleAddress(transformedAddressComponents) - case let .phoneNumber(phoneNumber): - guard let phoneNumber = phoneNumber else { return } - handlePhoneNumber(phoneNumber) - case let .date(date): - guard let date = date else { return } - handleDate(date) - case let .link(url): - guard let url = url else { return } - handleURL(url) - case let .transitInfoComponents(transitInformation): - var transformedTransitInformation = [String: String]() - guard let transitInformation = transitInformation else { return } - transitInformation.forEach { (key, value) in - transformedTransitInformation[key.rawValue] = value - } - handleTransitInformation(transformedTransitInformation) - } - } - - private func handleAddress(_ addressComponents: [String: String]) { - delegate?.didSelectAddress(addressComponents) - } - - private func handleDate(_ date: Date) { - delegate?.didSelectDate(date) - } - - private func handleURL(_ url: URL) { - delegate?.didSelectURL(url) - } - - private func handlePhoneNumber(_ phoneNumber: String) { - delegate?.didSelectPhoneNumber(phoneNumber) - } - - private func handleTransitInformation(_ components: [String: String]) { - delegate?.didSelectTransitInformation(components) - } - + private func handleCustom(_ pattern: String, match: String) { + delegate?.didSelectCustom(pattern, match: match) + } } -private enum MessageTextCheckingType { - case addressComponents([NSTextCheckingKey: String]?) - case date(Date?) - case phoneNumber(String?) - case link(URL?) - case transitInfoComponents([NSTextCheckingKey: String]?) +// MARK: - MessageTextCheckingType + +internal enum MessageTextCheckingType { + case addressComponents([NSTextCheckingKey: String]?) + case date(Date?) + case phoneNumber(String?) + case link(URL?) + case transitInfoComponents([NSTextCheckingKey: String]?) + case custom(pattern: String, match: String?) } diff --git a/Sources/Views/MessagesCollectionView.swift b/Sources/Views/MessagesCollectionView.swift index ee1e4cd3c..a551ef2f9 100644 --- a/Sources/Views/MessagesCollectionView.swift +++ b/Sources/Views/MessagesCollectionView.swift @@ -1,159 +1,225 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation import UIKit open class MessagesCollectionView: UICollectionView { + // MARK: Lifecycle - // MARK: - Properties + // MARK: - Initializers - open weak var messagesDataSource: MessagesDataSource? + public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { + super.init(frame: frame, collectionViewLayout: layout) + backgroundColor = .collectionViewBackground + registerReusableViews() + setupGestureRecognizers() + } - open weak var messagesDisplayDelegate: MessagesDisplayDelegate? + required public init?(coder _: NSCoder) { + super.init(frame: .zero, collectionViewLayout: MessagesCollectionViewFlowLayout()) + } - open weak var messagesLayoutDelegate: MessagesLayoutDelegate? + public convenience init() { + self.init(frame: .zero, collectionViewLayout: MessagesCollectionViewFlowLayout()) + } - open weak var messageCellDelegate: MessageCellDelegate? + // MARK: Open - private var indexPathForLastItem: IndexPath? { - let lastSection = numberOfSections - 1 - guard lastSection >= 0, numberOfItems(inSection: lastSection) > 0 else { return nil } - return IndexPath(item: numberOfItems(inSection: lastSection) - 1, section: lastSection) - } + // MARK: - Properties - // MARK: - Initializers + open weak var messagesDataSource: MessagesDataSource? - public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { - super.init(frame: frame, collectionViewLayout: layout) - backgroundColor = .white - registerReusableViews() - setupGestureRecognizers() - } - - required public init?(coder aDecoder: NSCoder) { - super.init(frame: .zero, collectionViewLayout: MessagesCollectionViewFlowLayout()) - } + open weak var messagesDisplayDelegate: MessagesDisplayDelegate? - public convenience init() { - self.init(frame: .zero, collectionViewLayout: MessagesCollectionViewFlowLayout()) - } + open weak var messagesLayoutDelegate: MessagesLayoutDelegate? + + open weak var messageCellDelegate: MessageCellDelegate? - // MARK: - Methods - - private func registerReusableViews() { - register(TextMessageCell.self) - register(MediaMessageCell.self) - register(LocationMessageCell.self) - register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader) - register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter) + open var isTypingIndicatorHidden: Bool { + messagesCollectionViewFlowLayout.isTypingIndicatorViewHidden + } + + open var messagesCollectionViewFlowLayout: MessagesCollectionViewFlowLayout { + guard let layout = collectionViewLayout as? MessagesCollectionViewFlowLayout else { + fatalError(MessageKitError.layoutUsedOnForeignType) + } + return layout + } + + @objc + open func handleTapGesture(_ gesture: UIGestureRecognizer) { + guard gesture.state == .ended else { return } + + let touchLocation = gesture.location(in: self) + guard let indexPath = indexPathForItem(at: touchLocation) else { return } + + let cell = cellForItem(at: indexPath) as? MessageCollectionViewCell + cell?.handleTapGesture(gesture) + } + + // MARK: Public + + // NOTE: It's possible for small content size this wouldn't work - https://github.com/MessageKit/MessageKit/issues/725 + public func scrollToLastItem(at pos: UICollectionView.ScrollPosition = .bottom, animated: Bool = true) { + guard let indexPath = indexPathForLastItem else { return } + + scrollToItem(at: indexPath, at: pos, animated: animated) + } + + public func reloadDataAndKeepOffset() { + // stop scrolling + setContentOffset(contentOffset, animated: false) + + // calculate the offset and reloadData + let beforeContentSize = contentSize + reloadData() + layoutIfNeeded() + let afterContentSize = contentSize + + // reset the contentOffset after data is updated + let newOffset = CGPoint( + x: contentOffset.x + (afterContentSize.width - beforeContentSize.width), + y: contentOffset.y + (afterContentSize.height - beforeContentSize.height)) + setContentOffset(newOffset, animated: false) + } + + /// A method that by default checks if the section is the last in the + /// `messagesCollectionView` and that `isTypingIndicatorViewHidden` + /// is FALSE + /// + /// - Parameter section + /// - Returns: A Boolean indicating if the TypingIndicator should be presented at the given section + public func isSectionReservedForTypingIndicator(_ section: Int) -> Bool { + messagesCollectionViewFlowLayout.isSectionReservedForTypingIndicator(section) + } + + // MARK: - View Register/Dequeue + + /// Registers a particular cell using its reuse-identifier + public func register(_ cellClass: T.Type) { + register(cellClass, forCellWithReuseIdentifier: String(describing: T.self)) + } + + /// Registers a reusable view for a specific SectionKind + public func register(_ reusableViewClass: T.Type, forSupplementaryViewOfKind kind: String) { + register( + reusableViewClass, + forSupplementaryViewOfKind: kind, + withReuseIdentifier: String(describing: T.self)) + } + + /// Registers a nib with reusable view for a specific SectionKind + public func register( + _ nib: UINib? = UINib(nibName: String(describing: T.self), bundle: nil), + headerFooterClassOfNib _: T.Type, + forSupplementaryViewOfKind kind: String) + { + register( + nib, + forSupplementaryViewOfKind: kind, + withReuseIdentifier: String(describing: T.self)) + } + + /// Generically dequeues a cell of the correct type allowing you to avoid scattering your code with guard-let-else-fatal + public func dequeueReusableCell(_ cellClass: T.Type, for indexPath: IndexPath) -> T { + guard let cell = dequeueReusableCell(withReuseIdentifier: String(describing: T.self), for: indexPath) as? T else { + fatalError("Unable to dequeue \(String(describing: cellClass)) with reuseId of \(String(describing: T.self))") } - - private func setupGestureRecognizers() { - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:))) - tapGesture.delaysTouchesBegan = true - addGestureRecognizer(tapGesture) + return cell + } + + /// Generically dequeues a header of the correct type allowing you to avoid scattering your code with guard-let-else-fatal + public func dequeueReusableHeaderView(_ viewClass: T.Type, for indexPath: IndexPath) -> T { + let view = dequeueReusableSupplementaryView( + ofKind: UICollectionView.elementKindSectionHeader, + withReuseIdentifier: String(describing: T.self), + for: indexPath) + guard let viewType = view as? T else { + fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(String(describing: T.self))") } - - @objc - open func handleTapGesture(_ gesture: UIGestureRecognizer) { - guard gesture.state == .ended else { return } - - let touchLocation = gesture.location(in: self) - guard let indexPath = indexPathForItem(at: touchLocation) else { return } - - let cell = cellForItem(at: indexPath) as? MessageContentCell - cell?.handleTapGesture(gesture) + return viewType + } + + /// Generically dequeues a footer of the correct type allowing you to avoid scattering your code with guard-let-else-fatal + public func dequeueReusableFooterView(_ viewClass: T.Type, for indexPath: IndexPath) -> T { + let view = dequeueReusableSupplementaryView( + ofKind: UICollectionView.elementKindSectionFooter, + withReuseIdentifier: String(describing: T.self), + for: indexPath) + guard let viewType = view as? T else { + fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(String(describing: T.self))") } + return viewType + } - public func scrollToBottom(animated: Bool = false) { - let collectionViewContentHeight = collectionViewLayout.collectionViewContentSize.height + // MARK: Internal - performBatchUpdates(nil) { _ in - self.scrollRectToVisible(CGRect(0.0, collectionViewContentHeight - 1.0, 1.0, 1.0), animated: animated) - } - } - - public func reloadDataAndKeepOffset() { - // stop scrolling - setContentOffset(contentOffset, animated: false) - - // calculate the offset and reloadData - let beforeContentSize = contentSize - reloadData() - layoutIfNeeded() - let afterContentSize = contentSize - - // reset the contentOffset after data is updated - let newOffset = CGPoint( - x: contentOffset.x + (afterContentSize.width - beforeContentSize.width), - y: contentOffset.y + (afterContentSize.height - beforeContentSize.height)) - setContentOffset(newOffset, animated: false) - } + /// Display the date of message by swiping left. + /// The default value of this property is `false`. + internal var showMessageTimestampOnSwipeLeft = false - /// Registers a particular cell using its reuse-identifier - public func register(_ cellClass: T.Type) { - register(cellClass, forCellWithReuseIdentifier: String(describing: T.self)) - } + // MARK: - Typing Indicator API - /// Registers a reusable view for a specific SectionKind - public func register(_ headerFooterClass: T.Type, forSupplementaryViewOfKind kind: String) { - register(headerFooterClass, - forSupplementaryViewOfKind: kind, - withReuseIdentifier: String(describing: T.self)) - } - - /// Registers a nib with reusable view for a specific SectionKind - public func register(_ nib: UINib? = UINib(nibName: String(describing: T.self), bundle: nil), headerFooterClassOfNib headerFooterClass: T.Type, forSupplementaryViewOfKind kind: String) { - register(nib, - forSupplementaryViewOfKind: kind, - withReuseIdentifier: String(describing: T.self)) - } + /// Notifies the layout that the typing indicator will change state + /// + /// - Parameters: + /// - isHidden: A Boolean value that is to be the new state of the typing indicator + internal func setTypingIndicatorViewHidden(_ isHidden: Bool) { + messagesCollectionViewFlowLayout.setTypingIndicatorViewHidden(isHidden) + } - /// Generically dequeues a cell of the correct type allowing you to avoid scattering your code with guard-let-else-fatal - public func dequeueReusableCell(_ cellClass: T.Type, for indexPath: IndexPath) -> T { - guard let cell = dequeueReusableCell(withReuseIdentifier: String(describing: T.self), for: indexPath) as? T else { - fatalError("Unable to dequeue \(String(describing: cellClass)) with reuseId of \(String(describing: T.self))") - } - return cell - } + // MARK: Private - /// Generically dequeues a header of the correct type allowing you to avoid scattering your code with guard-let-else-fatal - public func dequeueReusableHeaderView(_ viewClass: T.Type, for indexPath: IndexPath) -> T { - let view = dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: String(describing: T.self), for: indexPath) - guard let viewType = view as? T else { - fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(String(describing: T.self))") - } - return viewType - } + private var indexPathForLastItem: IndexPath? { + guard numberOfSections > 0 else { return nil } - /// Generically dequeues a footer of the correct type allowing you to avoid scattering your code with guard-let-else-fatal - public func dequeueReusableFooterView(_ viewClass: T.Type, for indexPath: IndexPath) -> T { - let view = dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: String(describing: T.self), for: indexPath) - guard let viewType = view as? T else { - fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(String(describing: T.self))") - } - return viewType + for offset in 1 ... numberOfSections { + let section = numberOfSections - offset + let lastItem = numberOfItems(inSection: section) - 1 + if lastItem >= 0 { + return IndexPath(item: lastItem, section: section) + } } - + return nil + } + + // MARK: - Methods + + private func registerReusableViews() { + register(TextMessageCell.self) + register(MediaMessageCell.self) + register(LocationMessageCell.self) + register(AudioMessageCell.self) + register(ContactMessageCell.self) + register(TypingIndicatorCell.self) + register(LinkPreviewMessageCell.self) + register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader) + register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter) + } + + private func setupGestureRecognizers() { + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:))) + tapGesture.delaysTouchesBegan = true + addGestureRecognizer(tapGesture) + } } diff --git a/Sources/Views/MessagesInputContainerView.swift b/Sources/Views/MessagesInputContainerView.swift new file mode 100644 index 000000000..1ff0ba533 --- /dev/null +++ b/Sources/Views/MessagesInputContainerView.swift @@ -0,0 +1,26 @@ +// MIT License +// +// Copyright (c) 2017-2022 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import UIKit + +public final class MessagesInputContainerView: UIView { } diff --git a/Sources/Views/PlayButtonView.swift b/Sources/Views/PlayButtonView.swift index b446ccfda..c78d96671 100644 --- a/Sources/Views/PlayButtonView.swift +++ b/Sources/Views/PlayButtonView.swift @@ -1,122 +1,136 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import UIKit open class PlayButtonView: UIView { + // MARK: Lifecycle - // MARK: - Properties - - public let triangleView = UIView() - - private var triangleCenterXConstraint: NSLayoutConstraint? - private var cacheFrame: CGRect = .zero - - // MARK: - Initializers - - public override init(frame: CGRect) { - super.init(frame: frame) - - setupSubviews() - setupConstraints() - setupView() - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - - setupSubviews() - setupConstraints() - setupView() - } - - // MARK: - Methods - - open override func layoutSubviews() { - super.layoutSubviews() - - guard !cacheFrame.equalTo(frame) else { return } - cacheFrame = frame - - updateTriangleConstraints() - applyCornerRadius() - applyTriangleMask() - } - - private func setupSubviews() { - addSubview(triangleView) - } - - private func setupView() { - triangleView.clipsToBounds = true - triangleView.backgroundColor = .black - - backgroundColor = .playButtonLightGray - } - - private func setupConstraints() { - triangleView.translatesAutoresizingMaskIntoConstraints = false - - let centerX = triangleView.centerXAnchor.constraint(equalTo: centerXAnchor) - let centerY = triangleView.centerYAnchor.constraint(equalTo: centerYAnchor) - let width = triangleView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.5) - let height = triangleView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.5) - - triangleCenterXConstraint = centerX - - NSLayoutConstraint.activate([centerX, centerY, width, height]) - } - - private func triangleMask(for frame: CGRect) -> CAShapeLayer { - let shapeLayer = CAShapeLayer() - let trianglePath = UIBezierPath() - - let point1 = CGPoint(x: frame.minX, y: frame.minY) - let point2 = CGPoint(x: frame.maxX, y: frame.maxY/2) - let point3 = CGPoint(x: frame.minX, y: frame.maxY) - - trianglePath .move(to: point1) - trianglePath .addLine(to: point2) - trianglePath .addLine(to: point3) - trianglePath .close() - - shapeLayer.path = trianglePath.cgPath - - return shapeLayer - } - - private func updateTriangleConstraints() { - triangleCenterXConstraint?.constant = frame.width/8 - } - - private func applyTriangleMask() { - let rect = CGRect(origin: .zero, size: triangleView.bounds.size) - triangleView.layer.mask = triangleMask(for: rect) - } - - private func applyCornerRadius() { - layer.cornerRadius = frame.width / 2 - } - + // MARK: - Initializers + + public override init(frame: CGRect) { + super.init(frame: frame) + + setupSubviews() + setupConstraints() + setupView() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + setupSubviews() + setupConstraints() + setupView() + } + + // MARK: Open + + // MARK: - Methods + + open override func layoutSubviews() { + super.layoutSubviews() + + guard !cacheFrame.equalTo(frame) else { return } + cacheFrame = frame + + updateTriangleConstraints() + applyCornerRadius() + applyTriangleMask() + } + + // MARK: Public + + // MARK: - Properties + + public let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) + public let triangleView = UIView() + + // MARK: Private + + private var triangleCenterXConstraint: NSLayoutConstraint? + private var cacheFrame: CGRect = .zero + + private func setupSubviews() { + addSubview(blurView) + addSubview(triangleView) + } + + private func setupView() { + triangleView.clipsToBounds = true + triangleView.backgroundColor = .black + blurView.clipsToBounds = true + backgroundColor = .clear + } + + private func setupConstraints() { + triangleView.translatesAutoresizingMaskIntoConstraints = false + + let centerX = triangleView.centerXAnchor.constraint(equalTo: centerXAnchor) + let centerY = triangleView.centerYAnchor.constraint(equalTo: centerYAnchor) + let width = triangleView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.5) + let height = triangleView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.5) + + triangleCenterXConstraint = centerX + + NSLayoutConstraint.activate([centerX, centerY, width, height]) + + blurView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + blurView.centerXAnchor.constraint(equalTo: centerXAnchor), + blurView.centerYAnchor.constraint(equalTo: centerYAnchor), + blurView.heightAnchor.constraint(equalTo: heightAnchor), + blurView.widthAnchor.constraint(equalTo: widthAnchor), + ]) + } + + private func triangleMask(for frame: CGRect) -> CAShapeLayer { + let shapeLayer = CAShapeLayer() + let trianglePath = UIBezierPath() + + let point1 = CGPoint(x: frame.minX, y: frame.minY) + let point2 = CGPoint(x: frame.maxX, y: frame.maxY / 2) + let point3 = CGPoint(x: frame.minX, y: frame.maxY) + + trianglePath.move(to: point1) + trianglePath.addLine(to: point2) + trianglePath.addLine(to: point3) + trianglePath.close() + + shapeLayer.path = trianglePath.cgPath + + return shapeLayer + } + + private func updateTriangleConstraints() { + triangleCenterXConstraint?.constant = triangleView.frame.width / 8 + } + + private func applyTriangleMask() { + let rect = CGRect(origin: .zero, size: triangleView.bounds.size) + triangleView.layer.mask = triangleMask(for: rect) + } + + private func applyCornerRadius() { + blurView.layer.cornerRadius = frame.width / 2 + } } diff --git a/Sources/Views/TypingBubble.swift b/Sources/Views/TypingBubble.swift new file mode 100644 index 000000000..aef03025b --- /dev/null +++ b/Sources/Views/TypingBubble.swift @@ -0,0 +1,173 @@ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit + +/// A subclass of `UIView` that mimics the iMessage typing bubble +open class TypingBubble: UIView { + // MARK: Lifecycle + + public override init(frame: CGRect) { + super.init(frame: frame) + setupSubviews() + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupSubviews() + } + + // MARK: Open + + // MARK: - Properties + + open var isPulseEnabled = true + + open override var backgroundColor: UIColor? { + set { + [contentBubble, cornerBubble, tinyBubble].forEach { $0.backgroundColor = newValue } + } + get { + contentBubble.backgroundColor + } + } + + // MARK: - Animation Layers + + open var contentPulseAnimationLayer: CABasicAnimation { + let animation = CABasicAnimation(keyPath: "transform.scale") + animation.fromValue = 1 + animation.toValue = 1.04 + animation.duration = 1 + animation.repeatCount = .infinity + animation.autoreverses = true + return animation + } + + open var circlePulseAnimationLayer: CABasicAnimation { + let animation = CABasicAnimation(keyPath: "transform.scale") + animation.fromValue = 1 + animation.toValue = 1.1 + animation.duration = 0.5 + animation.repeatCount = .infinity + animation.autoreverses = true + return animation + } + + open func setupSubviews() { + addSubview(tinyBubble) + addSubview(cornerBubble) + addSubview(contentBubble) + contentBubble.addSubview(typingIndicator) + backgroundColor = .incomingMessageBackground + } + + // MARK: - Layout + + open override func layoutSubviews() { + super.layoutSubviews() + + // To maintain the iMessage like bubble the width:height ratio of the frame + // must be close to 1.65 + + // In order to prevent NaN crash when assigning the frame of the contentBubble + guard + bounds.width > 0, + bounds.height > 0 + else { return } + + let ratio = bounds.width / bounds.height + let extraRightInset = bounds.width - 1.65 / ratio * bounds.width + + let tinyBubbleRadius: CGFloat = bounds.height / 6 + tinyBubble.frame = CGRect( + x: 0, + y: bounds.height - tinyBubbleRadius, + width: tinyBubbleRadius, + height: tinyBubbleRadius) + + let cornerBubbleRadius = tinyBubbleRadius * 2 + let offset: CGFloat = tinyBubbleRadius / 6 + cornerBubble.frame = CGRect( + x: tinyBubbleRadius - offset, + y: bounds.height - (1.5 * cornerBubbleRadius) + offset, + width: cornerBubbleRadius, + height: cornerBubbleRadius) + + let contentBubbleFrame = CGRect( + x: tinyBubbleRadius + offset, + y: 0, + width: bounds.width - (tinyBubbleRadius + offset) - extraRightInset, + height: bounds.height - (tinyBubbleRadius + offset)) + let contentBubbleFrameCornerRadius = contentBubbleFrame.height / 2 + + contentBubble.frame = contentBubbleFrame + contentBubble.layer.cornerRadius = contentBubbleFrameCornerRadius + + let insets = UIEdgeInsets( + top: offset, + left: contentBubbleFrameCornerRadius / 1.25, + bottom: offset, + right: contentBubbleFrameCornerRadius / 1.25) + typingIndicator.frame = contentBubble.bounds.inset(by: insets) + } + + // MARK: - Animation API + + open func startAnimating() { + defer { isAnimating = true } + guard !isAnimating else { return } + typingIndicator.startAnimating() + if isPulseEnabled { + contentBubble.layer.add(contentPulseAnimationLayer, forKey: AnimationKeys.pulse) + [cornerBubble, tinyBubble].forEach { $0.layer.add(circlePulseAnimationLayer, forKey: AnimationKeys.pulse) } + } + } + + open func stopAnimating() { + defer { isAnimating = false } + guard isAnimating else { return } + typingIndicator.stopAnimating() + [contentBubble, cornerBubble, tinyBubble].forEach { $0.layer.removeAnimation(forKey: AnimationKeys.pulse) } + } + + // MARK: Public + + public private(set) var isAnimating = false + + // MARK: - Subviews + + /// The indicator used to display the typing animation. + public let typingIndicator = TypingIndicator() + + public let contentBubble = UIView() + + public let cornerBubble = BubbleCircle() + + public let tinyBubble = BubbleCircle() + + // MARK: Private + + private enum AnimationKeys { + static let pulse = "typingBubble.pulse" + } +} diff --git a/Sources/Views/TypingIndicator.swift b/Sources/Views/TypingIndicator.swift new file mode 100644 index 000000000..355600987 --- /dev/null +++ b/Sources/Views/TypingIndicator.swift @@ -0,0 +1,164 @@ +// MIT License +// +// Copyright (c) 2017-2019 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit + +/// A `UIView` subclass that holds 3 dots which can be animated +open class TypingIndicator: UIView { + // MARK: Lifecycle + + public override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupView() + } + + // MARK: Open + + /// A convenience accessor for the `backgroundColor` of each dot + open var dotColor = UIColor.typingIndicatorDot { + didSet { + dots.forEach { $0.backgroundColor = dotColor } + } + } + + /// The `CABasicAnimation` applied when `isBounceEnabled` is TRUE to move the dot to the correct + /// initial offset + open var initialOffsetAnimationLayer: CABasicAnimation { + let animation = CABasicAnimation(keyPath: "transform.translation.y") + animation.byValue = -bounceOffset + animation.duration = 0.5 + animation.isRemovedOnCompletion = true + return animation + } + + /// The `CABasicAnimation` applied when `isBounceEnabled` is TRUE + open var bounceAnimationLayer: CABasicAnimation { + let animation = CABasicAnimation(keyPath: "transform.translation.y") + animation.toValue = -bounceOffset + animation.fromValue = bounceOffset + animation.duration = 0.5 + animation.repeatCount = .infinity + animation.autoreverses = true + return animation + } + + /// The `CABasicAnimation` applied when `isFadeEnabled` is TRUE + open var opacityAnimationLayer: CABasicAnimation { + let animation = CABasicAnimation(keyPath: "opacity") + animation.fromValue = 1 + animation.toValue = 0.5 + animation.duration = 0.5 + animation.repeatCount = .infinity + animation.autoreverses = true + return animation + } + + open override func layoutSubviews() { + super.layoutSubviews() + stackView.frame = bounds + stackView.spacing = bounds.width > 0 ? 5 : 0 + } + + // MARK: - Animation API + + /// Sets the state of the `TypingIndicator` to animating and applies animation layers + open func startAnimating() { + defer { isAnimating = true } + guard !isAnimating else { return } + var delay: TimeInterval = 0 + for dot in dots { + let currentDelay = delay + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in + guard let self = self else { return } + if self.isBounceEnabled { + dot.layer.add(self.initialOffsetAnimationLayer, forKey: AnimationKeys.offset) + let bounceLayer = self.bounceAnimationLayer + bounceLayer.timeOffset = currentDelay + 0.33 + dot.layer.add(bounceLayer, forKey: AnimationKeys.bounce) + } + if self.isFadeEnabled { + dot.layer.add(self.opacityAnimationLayer, forKey: AnimationKeys.opacity) + } + } + delay += 0.33 + } + } + + /// Sets the state of the `TypingIndicator` to not animating and removes animation layers + open func stopAnimating() { + defer { isAnimating = false } + guard isAnimating else { return } + dots.forEach { + $0.layer.removeAnimation(forKey: AnimationKeys.bounce) + $0.layer.removeAnimation(forKey: AnimationKeys.opacity) + } + } + + // MARK: Public + + /// The offset that each dot will transform by during the bounce animation + public var bounceOffset: CGFloat = 2.5 + + /// A flag that determines if the bounce animation is added in `startAnimating()` + public var isBounceEnabled = false + + /// A flag that determines if the opacity animation is added in `startAnimating()` + public var isFadeEnabled = true + + /// A flag indicating the animation state + public private(set) var isAnimating = false + + // MARK: - Subviews + + public let stackView = UIStackView() + + public let dots: [BubbleCircle] = { + [BubbleCircle(), BubbleCircle(), BubbleCircle()] + }() + + // MARK: Private + + /// Keys for each animation layer + private enum AnimationKeys { + static let offset = "typingIndicator.offset" + static let bounce = "typingIndicator.bounce" + static let opacity = "typingIndicator.opacity" + } + + /// Sets up the view + private func setupView() { + dots.forEach { + $0.backgroundColor = dotColor + $0.heightAnchor.constraint(equalTo: $0.widthAnchor).isActive = true + stackView.addArrangedSubview($0) + } + stackView.axis = .horizontal + stackView.alignment = .center + stackView.distribution = .fillEqually + addSubview(stackView) + } +} diff --git a/Tests/AvatarSpec.swift b/Tests/AvatarSpec.swift deleted file mode 100644 index ebd15e8b7..000000000 --- a/Tests/AvatarSpec.swift +++ /dev/null @@ -1,44 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import Quick -import Nimble -@testable import MessageKit - -final class AvatarSpec: QuickSpec { - - override func spec() { - describe("default property values") { - context("after intialization") { - let avatar = Avatar() - it("should set image to nil") { - expect(avatar.image).to(beNil()) - } - it("should set initials to ?") { - expect(avatar.initials).to(equal("?")) - } - } - } - } -} diff --git a/Tests/ControllersTest/MessageLabelSpec.swift b/Tests/ControllersTest/MessageLabelSpec.swift deleted file mode 100644 index abf28a00d..000000000 --- a/Tests/ControllersTest/MessageLabelSpec.swift +++ /dev/null @@ -1,251 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import Quick -import Nimble -@testable import MessageKit - -//swiftlint:disable todo -final class MessageLabelSpec: QuickSpec { - - //swiftlint:disable function_body_length - override func spec() { - - var messageLabel: MessageLabel! - - beforeEach { - messageLabel = MessageLabel() - } - -// describe("text recognized by a DetectorType") { -// context("address detection is enabled") { -// it("applies addressAttributes to text") { -// let expectedColor = UIColor.blue -// messageLabel.addressAttributes = [.foregroundColor: expectedColor] -// messageLabel.text = "One Infinite Loop Cupertino, CA 95014" -// messageLabel.enabledDetectors = [.address] -// let attributes = messageLabel.textAttributes -// let textColor = attributes[.foregroundColor] as? UIColor -// expect(textColor).to(equal(expectedColor)) -// } -// } -// context("phone number detection is enabled") { -// it("applies phoneNumberAttributes to text") { -// let expectedFont = UIFont.systemFont(ofSize: 8) -// messageLabel.phoneNumberAttributes = [.font: expectedFont] -// messageLabel.text = "1-800-555-1234" -// messageLabel.enabledDetectors = [.phoneNumber] -// let attributes = messageLabel.textAttributes -// let textFont = attributes[.font] as? UIFont -// expect(textFont).to(equal(expectedFont)) -// } -// } -// context("url detection is enabled") { -// it("applies urlAttributes to text") { -// let expectedColor = UIColor.green -// messageLabel.urlAttributes = [.foregroundColor: expectedColor] -// messageLabel.text = "https://github.com/MessageKit" -// messageLabel.enabledDetectors = [.url] -// let attributes = messageLabel.textAttributes -// let textColor = attributes[.foregroundColor] as? UIColor -// expect(textColor).to(equal(expectedColor)) -// } -// } -// context("date detection is enabled") { -// it("applies dateAttributes to text") { -// let expectedFont = UIFont.italicSystemFont(ofSize: 22) -// messageLabel.dateAttributes = [.font: expectedFont] -// messageLabel.text = "Today" -// messageLabel.enabledDetectors = [.date] -// let attributes = messageLabel.textAttributes -// let textFont = attributes[.font] as? UIFont -// expect(textFont).to(equal(expectedFont)) -// } -// } -// } - - describe("the synchronization between text and attributedText") { - context("when attributedText is set to a non-nil value") { - it("updates text with that value") { - let expectedText = "Some text" - messageLabel.attributedText = NSAttributedString(string: expectedText) - expect(messageLabel.text).to(equal(expectedText)) - } - } - context("when attributedText is set to nil") { - it("updates text with the nil value") { - messageLabel.text = "Not nil" - messageLabel.attributedText = nil - expect(messageLabel.text).to(beNil()) - } - } - context("when text is set to a non-nil value") { - it("updates attributedText with that value") { - let expectedText = "Some text" - messageLabel.text = expectedText - expect(messageLabel.attributedText?.string).to(equal(expectedText)) - } - } - context("when text is set to a nil value") { - it("updates attributedText with that value") { - messageLabel.attributedText = NSAttributedString(string: "Not nil") - messageLabel.text = nil - expect(messageLabel.attributedText).to(beNil()) - } - } - } - - describe("the labels text drawing rect") { - context("when textInset is .zero") { - it("does not inset the text rect") { - - } - } - context("when the textInset is not .zero") { - it("insets the text rect") { - - } - } - context("when the textInset is updated") { - it("updates the text rect") { - - } - } - } - - // TODO: - Not working - // This does not work for MessageLabel, the properties don't sync - describe("the synchronization between attributedText and format API") { - context("when font property is set") { - it("updates the font of attributedText") { - - } - } - context("when textColor property is set") { - it("updates the textColor of attributedText") { - - } - } - context("when lineBreakMode property is set") { - it("updates the lineBreakMode of attributedText") { - - } - } - context("when the textAlignment property is set") { - it("updates the textAlignment of attributedText") { - - } - } - } - - describe("updating the attributes of detected text") { - context("addressAttributes is set to a new value") { - it("updates ranges for detected addresses") { - - } - } - context("phoneNumberAttributes is set to a new value") { - it("updates ranges for detected phone numbers") { - - } - } - context("dateAttributes is set to a new value") { - it("updates ranges for detected dates") { - - } - } - context("urlAttributes is set to a new value") { - it("updates ranges for detected urls") { - - } - } - } - - describe("text dectection of multiple DetectorTypes") { - context("contains address, date, url, and phone number") { - it("applies addressAttributes to detected addresses") { - - } - it("applies phoneNumberAttributes to detected phone numbers") { - - } - it("applies urlAttributes to detected urls") { - - } - it("applies dateAttributes to detected dates") { - - } - it("does not apply attributes to non detected ranges") { - - } - } - } - - describe("touch handling for detected text") { - context("message text contains detected address") { - context("touch occurs on address") { - - } - context("touch does not occur on address") { - - } - } - context("message text contains detected phone number") { - context("touch occurs on phone number") { - - } - context("touch does not occur on phone number") { - - } - } - context("message text contains detected url") { - context("touch occurs on url") { - - } - context("touch does not occur on url") { - - } - } - context("message text contains detected date") { - context("touch occurs on date") { - - } - context("touch does not occur on date") { - - } - } - } - } -} - -// MARK: - Helpers - -fileprivate extension MessageLabel { - - var textAttributes: [NSAttributedString.Key: Any] { - let length = attributedText!.length - var range = NSRange(location: 0, length: length) - return attributedText!.attributes(at: 0, effectiveRange: &range) - } -} diff --git a/Tests/ControllersTest/MessagesViewControllerSpec.swift b/Tests/ControllersTest/MessagesViewControllerSpec.swift deleted file mode 100644 index 1cf4d5655..000000000 --- a/Tests/ControllersTest/MessagesViewControllerSpec.swift +++ /dev/null @@ -1,156 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import Quick -import Nimble -import MessageInputBar -@testable import MessageKit - -//swiftlint:disable function_body_length -final class MessagesViewControllerSpec: QuickSpec { - - override func spec() { - - var controller: MessagesViewController! - - beforeEach { - controller = MessagesViewController() - } - - describe("default property values") { - context("after initialization") { - it("sets scrollsToBottomOnKeyboardBeginsEditing to false") { - expect(controller.scrollsToBottomOnKeyboardBeginsEditing).to(beFalse()) - } - it("sets canBecomeFirstResponder to true") { - expect(controller.canBecomeFirstResponder).to(beTrue()) - } - it("sets shouldAutorotate to false") { - expect(controller.shouldAutorotate).to(beFalse()) - } - it("sets inputAccessoryView to the messageInputBar") { - expect(controller.inputAccessoryView).toNot(beNil()) - expect(controller.inputAccessoryView is MessageInputBar).to(beTrue()) - } - it("has a MessagesCollectionView") { - expect(controller.messagesCollectionView).toNot(beNil()) - } - it("sets the CollectionView's layout to be an instance of MessagesCollectionViewFlowLayout") { - expect(controller.messagesCollectionView.collectionViewLayout).to(beAnInstanceOf(MessagesCollectionViewFlowLayout.self)) - } - } - context("after viewDidLoad") { - beforeEach { - controller.view.layoutIfNeeded() - } - it("sets automaticallyAdjustsScrollViewInsets to false") { - expect(controller.automaticallyAdjustsScrollViewInsets).to(beFalse()) - } - it("sets extendedLayoutIncludesOpaqueBars to true") { - expect(controller.extendedLayoutIncludesOpaqueBars).to(beTrue()) - } - it("sets the background color to be white") { - expect(controller.view.backgroundColor).to(be(UIColor.white)) - } - it("sets keyboardDismissMode to .interactive") { - let dismissMode = controller.messagesCollectionView.keyboardDismissMode - expect(dismissMode).to(equal(UIScrollView.KeyboardDismissMode.interactive)) - } - it("sets alwaysBounceVertical to true") { - expect(controller.messagesCollectionView.alwaysBounceVertical).to(beTrue()) - } - } - } - - describe("subviews setup") { - it("should add messagesCollectionView as a subview of root view") { - let subviews = controller.view.subviews - let hasView = subviews.contains(controller.messagesCollectionView) - expect(hasView).to(beTrue()) - } - } - - describe("delegate and datasource setup") { - beforeEach { - controller.view.layoutIfNeeded() - } - it("should set messagesCollectionView.dataSource") { - let delegate = controller.messagesCollectionView.delegate - expect(delegate).to(be(controller)) - } - it("should set messagesCollectionView.delegate") { - let dataSource = controller.messagesCollectionView.dataSource - expect(dataSource).to(be(controller)) - } - } - - describe("the top contentInset") { - context("when controller is root view controller") { - - } - context("when controller is nested in a UINavigationController") { - - } - } - - describe("the bottom contentInset") { - context("when keyboard is showing") { - it("should be the height of the MessageInputBar + keyboard") { - - } - } - context("when keyboard is not showing") { - it("should be the height of the MessageInputBar") { - - } - } - } - - describe("scrolling behavior when keyboard begins editing") { - context("scrollsToBottomOnKeyboardBeginsEditing is true") { - it("should scroll to bottom") { - - } - } - context("scrollsToBottomOnKeyboardBeginsEditing is false") { - it("should not scroll to bottom") { - - } - } - } - - describe("calling messagesCollectionView.scrollToBottom()") { - context("all messages are visible") { - it("should scroll to bottom") { - - } - } - context("not all messages are visible") { - it("should scroll to bottom") { - - } - } - } - } -} diff --git a/Tests/ControllersTest/MessagesViewControllerTests.swift b/Tests/ControllersTest/MessagesViewControllerTests.swift deleted file mode 100644 index fb30c51fe..000000000 --- a/Tests/ControllersTest/MessagesViewControllerTests.swift +++ /dev/null @@ -1,200 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import XCTest -import CoreLocation -@testable import MessageKit - -class MessagesViewControllerTests: XCTestCase { - - var sut: MessagesViewController! - // swiftlint:disable weak_delegate - private var layoutDelegate = MockLayoutDelegate() - // swiftlint:enable weak_delegate - - // MARK: - Overridden Methods - - override func setUp() { - super.setUp() - - sut = MessagesViewController() - sut.messagesCollectionView.messagesLayoutDelegate = layoutDelegate - sut.messagesCollectionView.messagesDisplayDelegate = layoutDelegate - _ = sut.view - sut.beginAppearanceTransition(true, animated: true) - sut.endAppearanceTransition() - sut.view.layoutIfNeeded() - } - - override func tearDown() { - sut = nil - - super.tearDown() - } - - // MARK: - Test - - func testViewDidLoad_shouldSetDelegateAndDataSourceToTheSameObject() { - XCTAssertEqual(sut.messagesCollectionView.delegate as? MessagesViewController, - sut.messagesCollectionView.dataSource as? MessagesViewController) - } - - func testNumberOfSectionWithoutData_isZero() { - let messagesDataSource = MockMessagesDataSource() - sut.messagesCollectionView.messagesDataSource = messagesDataSource - - XCTAssertEqual(sut.messagesCollectionView.numberOfSections, 0) - } - - func testNumberOfSection_isNumberOfMessages() { - let messagesDataSource = MockMessagesDataSource() - sut.messagesCollectionView.messagesDataSource = messagesDataSource - messagesDataSource.messages = makeMessages(for: messagesDataSource.senders) - - sut.messagesCollectionView.reloadData() - - let count = sut.messagesCollectionView.numberOfSections - let expectedCount = messagesDataSource.numberOfSections(in: sut.messagesCollectionView) - - XCTAssertEqual(count, expectedCount) - } - - func testNumberOfItemInSection_isOne() { - let messagesDataSource = MockMessagesDataSource() - sut.messagesCollectionView.messagesDataSource = messagesDataSource - messagesDataSource.messages = makeMessages(for: messagesDataSource.senders) - - sut.messagesCollectionView.reloadData() - - XCTAssertEqual(sut.messagesCollectionView.numberOfItems(inSection: 0), 1) - XCTAssertEqual(sut.messagesCollectionView.numberOfItems(inSection: 1), 1) - } - - func testCellForItemWithTextData_returnsTextMessageCell() { - let messagesDataSource = MockMessagesDataSource() - sut.messagesCollectionView.messagesDataSource = messagesDataSource - messagesDataSource.messages.append(MockMessage(text: "Test", - sender: messagesDataSource.senders[0], - messageId: "test_id")) - - sut.messagesCollectionView.reloadData() - - let cell = sut.messagesCollectionView.dataSource?.collectionView(sut.messagesCollectionView, - cellForItemAt: IndexPath(item: 0, section: 0)) - - XCTAssertNotNil(cell) - XCTAssertTrue(cell is TextMessageCell) - } - - func testCellForItemWithAttributedTextData_returnsTextMessageCell() { - let messagesDataSource = MockMessagesDataSource() - sut.messagesCollectionView.messagesDataSource = messagesDataSource - let attributes = [NSAttributedString.Key.foregroundColor: UIColor.black] - let attriutedString = NSAttributedString(string: "Test", attributes: attributes) - messagesDataSource.messages.append(MockMessage(attributedText: attriutedString, - sender: messagesDataSource.senders[0], - messageId: "test_id")) - - sut.messagesCollectionView.reloadData() - - let cell = sut.messagesCollectionView.dataSource?.collectionView(sut.messagesCollectionView, - cellForItemAt: IndexPath(item: 0, section: 0)) - - XCTAssertNotNil(cell) - XCTAssertTrue(cell is TextMessageCell) - } - - func testCellForItemWithPhotoData_returnsMediaMessageCell() { - let messagesDataSource = MockMessagesDataSource() - sut.messagesCollectionView.messagesDataSource = messagesDataSource - messagesDataSource.messages.append(MockMessage(image: UIImage(), - sender: messagesDataSource.senders[0], - messageId: "test_id")) - - sut.messagesCollectionView.reloadData() - - let cell = sut.messagesCollectionView.dataSource?.collectionView(sut.messagesCollectionView, - cellForItemAt: IndexPath(item: 0, section: 0)) - - XCTAssertNotNil(cell) - XCTAssertTrue(cell is MediaMessageCell) - } - - func testCellForItemWithVideoData_returnsMediaMessageCell() { - let messagesDataSource = MockMessagesDataSource() - sut.messagesCollectionView.messagesDataSource = messagesDataSource - messagesDataSource.messages.append(MockMessage(thumbnail: UIImage(), - sender: messagesDataSource.senders[0], - messageId: "test_id")) - - sut.messagesCollectionView.reloadData() - - let cell = sut.messagesCollectionView.dataSource?.collectionView(sut.messagesCollectionView, - cellForItemAt: IndexPath(item: 0, section: 0)) - - XCTAssertNotNil(cell) - XCTAssertTrue(cell is MediaMessageCell) - } - - func testCellForItemWithLocationData_returnsLocationMessageCell() { - let messagesDataSource = MockMessagesDataSource() - sut.messagesCollectionView.messagesDataSource = messagesDataSource - messagesDataSource.messages.append(MockMessage(location: CLLocation(latitude: 60.0, longitude: 70.0), - sender: messagesDataSource.senders[0], - messageId: "test_id")) - - sut.messagesCollectionView.reloadData() - - let cell = sut.messagesCollectionView.dataSource?.collectionView(sut.messagesCollectionView, - cellForItemAt: IndexPath(item: 0, section: 0)) - - XCTAssertNotNil(cell) - XCTAssertTrue(cell is LocationMessageCell) - } - - // MARK: - Assistants - - private func makeMessages(for senders: [Sender]) -> [MessageType] { - return [MockMessage(text: "Text 1", sender: senders[0], messageId: "test_id_1"), - MockMessage(text: "Text 2", sender: senders[1], messageId: "test_id_2")] - } - -} - -private class MockLayoutDelegate: MessagesLayoutDelegate, MessagesDisplayDelegate { - - // MARK: - LocationMessageLayoutDelegate - - func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return 0.0 - } - - func heightForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return 10.0 - } - - func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions { - return LocationMessageSnapshotOptions() - } -} diff --git a/Tests/DetectorTypeSpec.swift b/Tests/DetectorTypeSpec.swift deleted file mode 100644 index 604261316..000000000 --- a/Tests/DetectorTypeSpec.swift +++ /dev/null @@ -1,65 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import Quick -import Nimble -@testable import MessageKit - -final class DetectorTypeSpec: QuickSpec { - - override func spec() { - describe("mapping to NSTextCheckingResult.CheckingType") { - context("case .address") { - it("should equal .address") { - let address = DetectorType.address.textCheckingType - expect(address).to(equal(NSTextCheckingResult.CheckingType.address)) - } - } - context("case .url") { - it("should equal .link") { - let url = DetectorType.url.textCheckingType - expect(url).to(equal(NSTextCheckingResult.CheckingType.link)) - } - } - context("case .date") { - it("should equal .date") { - let date = DetectorType.date.textCheckingType - expect(date).to(equal(NSTextCheckingResult.CheckingType.date)) - } - } - context("case .phoneNumber") { - it("should equal .phoneNumber") { - let phoneNumber = DetectorType.phoneNumber.textCheckingType - expect(phoneNumber).to(equal(NSTextCheckingResult.CheckingType.phoneNumber)) - } - } - context("case .transitInformation") { - it("should equal .transitInformation") { - let transitInformation = DetectorType.transitInformation.textCheckingType - expect(transitInformation).to(equal(NSTextCheckingResult.CheckingType.transitInformation)) - } - } - } - } -} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 000000000..616d3b476 --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,29 @@ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import MessageKitTests +import XCTest + +var tests = [XCTestCaseEntry]() +tests += MessageKitTests.allTests() +XCTMain(tests) diff --git a/Tests/MessageKitTests/Controllers Test/MessageLabelTests.swift b/Tests/MessageKitTests/Controllers Test/MessageLabelTests.swift new file mode 100644 index 000000000..78cae636b --- /dev/null +++ b/Tests/MessageKitTests/Controllers Test/MessageLabelTests.swift @@ -0,0 +1,169 @@ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import XCTest +@testable import MessageKit + +// MARK: - MessageLabelTests + +@MainActor +final class MessageLabelTests: XCTestCase { + // MARK: Internal + + let mentionsList = ["@julienkode", "@facebook", "@google", "@1234"] + let hashtagsList = ["#julienkode", "#facebook", "#google", "#1234"] + + func testMentionDetection() { + let messageLabel = MessageLabel() + let detector = DetectorType.mention + let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key(rawValue: "Mention"): "MentionDetected"] + + let text = mentionsList.joined(separator: " #test ") + set(text: text, and: [detector], with: attributes, to: messageLabel) + let matches = extractCustomDetectors(for: detector, with: messageLabel) + XCTAssertEqual(matches, mentionsList) + + let invalids = hashtagsList.joined(separator: " ") + set(text: invalids, and: [detector], with: attributes, to: messageLabel) + let invalidMatches = extractCustomDetectors(for: detector, with: messageLabel) + XCTAssertEqual(invalidMatches.count, 0) + } + + func testHashtagDetection() { + let messageLabel = MessageLabel() + let detector = DetectorType.hashtag + let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key(rawValue: "Hashtag"): "HashtagDetected"] + + let text = hashtagsList.joined(separator: " @test ") + set(text: text, and: [detector], with: attributes, to: messageLabel) + let matches = extractCustomDetectors(for: detector, with: messageLabel) + XCTAssertEqual(matches, hashtagsList) + + let invalids = mentionsList.joined(separator: " ") + set(text: invalids, and: [detector], with: attributes, to: messageLabel) + let invalidMatches = extractCustomDetectors(for: detector, with: messageLabel) + XCTAssertEqual(invalidMatches.count, 0) + } + + func testCustomDetection() { + let shouldPass = ["1234", "1", "09876"] + let shouldFailed = ["abcd", "a", "!!!", ";"] + + let messageLabel = MessageLabel() + let detector = DetectorType.custom(try! NSRegularExpression(pattern: "[0-9]+", options: .caseInsensitive)) + let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key(rawValue: "Custom"): "CustomDetected"] + + let text = shouldPass.joined(separator: " ") + set(text: text, and: [detector], with: attributes, to: messageLabel) + let matches = extractCustomDetectors(for: detector, with: messageLabel) + XCTAssertEqual(matches, shouldPass) + + let invalids = shouldFailed.joined(separator: " ") + set(text: invalids, and: [detector], with: attributes, to: messageLabel) + let invalidMatches = extractCustomDetectors(for: detector, with: messageLabel) + XCTAssertEqual(invalidMatches.count, 0) + } + + func testCustomDetectionOverlapping() { + let testText = "address MNtz8Zz1cPD1CZadoc38jT5qeqeFBS6Aif can match multiple regex's" + + let messageLabel = MessageLabel() + let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key(rawValue: "Custom"): "CustomDetected"] + let detectors = [ + DetectorType.custom(try! NSRegularExpression(pattern: "(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}", options: .caseInsensitive)), + DetectorType.custom(try! NSRegularExpression(pattern: #"([3ML][\w]{26,33})|ltc1[\w]+"#, options: .caseInsensitive)), + DetectorType.custom(try! NSRegularExpression(pattern: "[qmN][a-km-zA-HJ-NP-Z1-9]{26,33}", options: .caseInsensitive))] + + set(text: testText, and: detectors, with: attributes, to: messageLabel) + let matches = detectors.map { extractCustomDetectors(for: $0, with: messageLabel) }.joined() + XCTAssertEqual(matches.count, 1) + } + + func testSyncBetweenAttributedAndText() { + let messageLabel = MessageLabel() + let expectedText = "Some text" + messageLabel.attributedText = NSAttributedString(string: expectedText) + XCTAssertEqual(messageLabel.text, expectedText) + + messageLabel.attributedText = nil + XCTAssertNil(messageLabel.text) + + messageLabel.text = "some" + messageLabel.text = expectedText + XCTAssertEqual(messageLabel.attributedText?.string, expectedText) + + messageLabel.attributedText = NSAttributedString(string: "Not nil") + messageLabel.text = nil + XCTAssertNil(messageLabel.attributedText) + } + + // MARK: Private + + // MARK: - Private helpers API for Detectors + + /// Takes a given `DetectorType` and extract matches from a `MessageLabel` + /// + /// - Parameters: detector: `DetectorType` that you want to extract + /// - Parameters: label: `MessageLabel` where you want to get matches + /// + /// - Returns: an array of `String` that contains all matches for the given detector + private func extractCustomDetectors(for detector: DetectorType, with label: MessageLabel) -> [String] { + guard let detection = label.rangesForDetectors[detector] else { return [] } + return detection.compactMap({ _, messageChecking -> String? in + switch messageChecking { + case .custom(_, let match): + return match + default: + return nil + } + }) + } + + /// Simply set text, detectors and attriutes to a given label + /// + /// - Parameters: text: `String` that will be applied to the label + /// - Parameters: detector: `DetectorType` that you want to apply to the label + /// - Parameters: attributes: `[NSAttributedString.Key: Any]` that you want to apply to the label + /// - Parameters: label: `MessageLabel` that takes the previous parameters + /// + private func set( + text: String, + and detectors: [DetectorType], + with attributes: [NSAttributedString.Key: Any], + to label: MessageLabel) + { + label.mentionAttributes = attributes + label.enabledDetectors = detectors + label.text = text + } +} + +// MARK: - Helpers + +extension MessageLabel { + private var textAttributes: [NSAttributedString.Key: Any] { + let length = attributedText!.length + var range = NSRange(location: 0, length: length) + return attributedText!.attributes(at: 0, effectiveRange: &range) + } +} diff --git a/Tests/MessageKitTests/Controllers Test/MessagesViewControllerTests.swift b/Tests/MessageKitTests/Controllers Test/MessagesViewControllerTests.swift new file mode 100644 index 000000000..417bcea11 --- /dev/null +++ b/Tests/MessageKitTests/Controllers Test/MessagesViewControllerTests.swift @@ -0,0 +1,289 @@ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import CoreLocation +import XCTest +@testable import MessageKit + +// MARK: - MessagesViewControllerTests + +@MainActor +final class MessagesViewControllerTests: XCTestCase { + + // MARK: - Private helper API + + private func makeSUT() -> MessagesViewController { + let sut = MessagesViewController() + sut.messagesCollectionView.messagesLayoutDelegate = layoutDelegate + sut.messagesCollectionView.messagesDisplayDelegate = layoutDelegate + _ = sut.view + sut.beginAppearanceTransition(true, animated: true) + sut.endAppearanceTransition() + sut.view.layoutIfNeeded() + + return sut + } + + // MARK: - Test + + func testNumberOfSectionWithoutData_isZero() { + let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() + sut.messagesCollectionView.messagesDataSource = messagesDataSource + + XCTAssertEqual(sut.messagesCollectionView.numberOfSections, 0) + } + + func testNumberOfSection_isNumberOfMessages() { + let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() + sut.messagesCollectionView.messagesDataSource = messagesDataSource + messagesDataSource.messages = makeMessages(for: messagesDataSource.senders) + + sut.messagesCollectionView.reloadData() + + let count = sut.messagesCollectionView.numberOfSections + let expectedCount = messagesDataSource.numberOfSections(in: sut.messagesCollectionView) + + XCTAssertEqual(count, expectedCount) + } + + func testNumberOfItemInSection_isOne() { + let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() + sut.messagesCollectionView.messagesDataSource = messagesDataSource + messagesDataSource.messages = makeMessages(for: messagesDataSource.senders) + + sut.messagesCollectionView.reloadData() + + XCTAssertEqual(sut.messagesCollectionView.numberOfItems(inSection: 0), 1) + XCTAssertEqual(sut.messagesCollectionView.numberOfItems(inSection: 1), 1) + } + + func testCellForItemWithTextData_returnsTextMessageCell() { + let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() + sut.messagesCollectionView.messagesDataSource = messagesDataSource + messagesDataSource.messages.append(MockMessage( + text: "Test", + user: messagesDataSource.senders[0], + messageId: "test_id")) + + sut.messagesCollectionView.reloadData() + + let cell = sut.messagesCollectionView.dataSource?.collectionView( + sut.messagesCollectionView, + cellForItemAt: IndexPath(item: 0, section: 0)) + + XCTAssertNotNil(cell) + XCTAssertTrue(cell is TextMessageCell) + } + + func testCellForItemWithAttributedTextData_returnsTextMessageCell() { + let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() + sut.messagesCollectionView.messagesDataSource = messagesDataSource + let attributes = [NSAttributedString.Key.foregroundColor: UIColor.outgoingMessageLabel] + let attriutedString = NSAttributedString(string: "Test", attributes: attributes) + messagesDataSource.messages.append(MockMessage( + attributedText: attriutedString, + user: messagesDataSource.senders[0], + messageId: "test_id")) + + sut.messagesCollectionView.reloadData() + + let cell = sut.messagesCollectionView.dataSource?.collectionView( + sut.messagesCollectionView, + cellForItemAt: IndexPath(item: 0, section: 0)) + + XCTAssertNotNil(cell) + XCTAssertTrue(cell is TextMessageCell) + } + + func testCellForItemWithPhotoData_returnsMediaMessageCell() { + let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() + sut.messagesCollectionView.messagesDataSource = messagesDataSource + messagesDataSource.messages.append(MockMessage( + image: UIImage(), + user: messagesDataSource.senders[0], + messageId: "test_id")) + + sut.messagesCollectionView.reloadData() + + let cell = sut.messagesCollectionView.dataSource?.collectionView( + sut.messagesCollectionView, + cellForItemAt: IndexPath(item: 0, section: 0)) + + XCTAssertNotNil(cell) + XCTAssertTrue(cell is MediaMessageCell) + } + + func testCellForItemWithVideoData_returnsMediaMessageCell() { + let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() + sut.messagesCollectionView.messagesDataSource = messagesDataSource + messagesDataSource.messages.append(MockMessage( + thumbnail: UIImage(), + user: messagesDataSource.senders[0], + messageId: "test_id")) + + sut.messagesCollectionView.reloadData() + + let cell = sut.messagesCollectionView.dataSource?.collectionView( + sut.messagesCollectionView, + cellForItemAt: IndexPath(item: 0, section: 0)) + + XCTAssertNotNil(cell) + XCTAssertTrue(cell is MediaMessageCell) + } + + func testCellForItemWithLocationData_returnsLocationMessageCell() { + let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() + sut.messagesCollectionView.messagesDataSource = messagesDataSource + messagesDataSource.messages.append(MockMessage( + location: CLLocation(latitude: 60.0, longitude: 70.0), + user: messagesDataSource.senders[0], + messageId: "test_id")) + + sut.messagesCollectionView.reloadData() + + let cell = sut.messagesCollectionView.dataSource?.collectionView( + sut.messagesCollectionView, + cellForItemAt: IndexPath(item: 0, section: 0)) + + XCTAssertNotNil(cell) + XCTAssertTrue(cell is LocationMessageCell) + } + + func testCellForItemWithAudioData_returnsAudioMessageCell() { + let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() + sut.messagesCollectionView.messagesDataSource = messagesDataSource + messagesDataSource.messages.append(MockMessage( + audioURL: URL(fileURLWithPath: ""), + duration: 4.0, + user: messagesDataSource.senders[0], + messageId: "test_id")) + + sut.messagesCollectionView.reloadData() + + let cell = sut.messagesCollectionView.dataSource?.collectionView( + sut.messagesCollectionView, + cellForItemAt: IndexPath(item: 0, section: 0)) + + XCTAssertNotNil(cell) + XCTAssertTrue(cell is AudioMessageCell) + } + + func testCellForItemWithLinkPreviewData_returnsLinkPreviewMessageCell() { + let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() + sut.messagesCollectionView.messagesDataSource = messagesDataSource + + let linkItem = MockLinkItem( + text: "https://link.test", + attributedText: nil, + url: URL(string: "https://github.com/MessageKit")!, + title: "Link Title", + teaser: "Link Teaser", + thumbnailImage: UIImage()) + + messagesDataSource.messages.append(MockMessage( + linkItem: linkItem, + user: messagesDataSource.senders[0], + messageId: "test_id")) + + sut.messagesCollectionView.reloadData() + + let cell = sut.messagesCollectionView.dataSource?.collectionView( + sut.messagesCollectionView, + cellForItemAt: IndexPath(item: 0, section: 0)) + + XCTAssertNotNil(cell) + XCTAssertTrue(cell is LinkPreviewMessageCell) + } + + // MARK: - Setups + + func testSubviewsSetup() { + let controller = MessagesViewController() + XCTAssertTrue(controller.view.subviews.contains(controller.messagesCollectionView)) + } + + func testDelegateAndDataSourceSetup() { + let controller = MessagesViewController() + controller.view.layoutIfNeeded() + XCTAssertTrue(controller.messagesCollectionView.delegate is MessagesViewController) + XCTAssertTrue(controller.messagesCollectionView.dataSource is MessagesViewController) + } + + func testDefaultPropertyValues() { + let controller = MessagesViewController() + XCTAssertNotNil(controller.messagesCollectionView) + XCTAssertTrue(controller.messagesCollectionView.collectionViewLayout is MessagesCollectionViewFlowLayout) + + controller.view.layoutIfNeeded() + XCTAssertTrue(controller.extendedLayoutIncludesOpaqueBars) + XCTAssertEqual(controller.view.backgroundColor, UIColor.collectionViewBackground) + XCTAssertEqual(controller.messagesCollectionView.keyboardDismissMode, UIScrollView.KeyboardDismissMode.interactive) + XCTAssertTrue(controller.messagesCollectionView.alwaysBounceVertical) + } + + // MARK: Private + + // swiftlint:disable:next weak_delegate + private var layoutDelegate = MockLayoutDelegate() + + // MARK: - Assistants + + private func makeMessages(for senders: [MockUser]) -> [MessageType] { + [ + MockMessage(text: "Text 1", user: senders[0], messageId: "test_id_1"), + MockMessage(text: "Text 2", user: senders[1], messageId: "test_id_2"), + ] + } +} + +// MARK: - MockLayoutDelegate + +private class MockLayoutDelegate: MessagesLayoutDelegate, MessagesDisplayDelegate { + // MARK: - LocationMessageLayoutDelegate + + func heightForLocation(message _: MessageType, at _: IndexPath, with _: CGFloat, in _: MessagesCollectionView) -> CGFloat { + 0.0 + } + + func heightForMedia(message _: MessageType, at _: IndexPath, with _: CGFloat, in _: MessagesCollectionView) -> CGFloat { + 10.0 + } + + func snapshotOptionsForLocation( + message _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) + -> LocationMessageSnapshotOptions + { + LocationMessageSnapshotOptions() + } +} diff --git a/Tests/MessageKitTests/MessageKitTests.swift b/Tests/MessageKitTests/MessageKitTests.swift new file mode 100644 index 000000000..1b5169b52 --- /dev/null +++ b/Tests/MessageKitTests/MessageKitTests.swift @@ -0,0 +1,31 @@ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import XCTest +@testable import MessageKit + +final class MessageKitTests: XCTestCase { + static let allTests = [ + "", + ] +} diff --git a/Tests/MessageKitTests/Mocks/MockMessage.swift b/Tests/MessageKitTests/Mocks/MockMessage.swift new file mode 100644 index 000000000..efca37dfa --- /dev/null +++ b/Tests/MessageKitTests/Mocks/MockMessage.swift @@ -0,0 +1,139 @@ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import AVFoundation +import CoreLocation +import Foundation +import UIKit +@testable import MessageKit + +// MARK: - MockLocationItem + +struct MockLocationItem: LocationItem { + var location: CLLocation + var size: CGSize + + init(location: CLLocation) { + self.location = location + size = CGSize(width: 240, height: 240) + } +} + +// MARK: - MockMediaItem + +struct MockMediaItem: MediaItem { + var url: URL? + var image: UIImage? + var placeholderImage: UIImage + var size: CGSize + + init(image: UIImage) { + self.image = image + size = CGSize(width: 240, height: 240) + placeholderImage = UIImage() + } +} + +// MARK: - MockAudiotem + +private struct MockAudiotem: AudioItem { + var url: URL + var size: CGSize + var duration: Float + + init(url: URL, duration: Float) { + self.url = url + size = CGSize(width: 160, height: 35) + self.duration = duration + } +} + +// MARK: - MockLinkItem + +struct MockLinkItem: LinkItem { + let text: String? + let attributedText: NSAttributedString? + let url: URL + let title: String? + let teaser: String + let thumbnailImage: UIImage +} + +// MARK: - MockMessage + +struct MockMessage: MessageType { + // MARK: Lifecycle + + private init(kind: MessageKind, user: MockUser, messageId: String) { + self.kind = kind + self.user = user + self.messageId = messageId + sentDate = Date() + } + + init(text: String, user: MockUser, messageId: String) { + self.init(kind: .text(text), user: user, messageId: messageId) + } + + init(attributedText: NSAttributedString, user: MockUser, messageId: String) { + self.init(kind: .attributedText(attributedText), user: user, messageId: messageId) + } + + init(image: UIImage, user: MockUser, messageId: String) { + let mediaItem = MockMediaItem(image: image) + self.init(kind: .photo(mediaItem), user: user, messageId: messageId) + } + + init(thumbnail: UIImage, user: MockUser, messageId: String) { + let mediaItem = MockMediaItem(image: thumbnail) + self.init(kind: .video(mediaItem), user: user, messageId: messageId) + } + + init(location: CLLocation, user: MockUser, messageId: String) { + let locationItem = MockLocationItem(location: location) + self.init(kind: .location(locationItem), user: user, messageId: messageId) + } + + init(emoji: String, user: MockUser, messageId: String) { + self.init(kind: .emoji(emoji), user: user, messageId: messageId) + } + + init(audioURL: URL, duration: Float, user: MockUser, messageId: String) { + let audioItem = MockAudiotem(url: audioURL, duration: duration) + self.init(kind: .audio(audioItem), user: user, messageId: messageId) + } + + init(linkItem: LinkItem, user: MockUser, messageId: String) { + self.init(kind: .linkPreview(linkItem), user: user, messageId: messageId) + } + + // MARK: Internal + + var messageId: String + var sentDate: Date + var kind: MessageKind + var user: MockUser + + var sender: SenderType { + user + } +} diff --git a/Tests/MessageKitTests/Mocks/MockMessagesDataSource.swift b/Tests/MessageKitTests/Mocks/MockMessagesDataSource.swift new file mode 100644 index 000000000..dc9da0138 --- /dev/null +++ b/Tests/MessageKitTests/Mocks/MockMessagesDataSource.swift @@ -0,0 +1,49 @@ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +@testable import MessageKit + +@MainActor +class MockMessagesDataSource: MessagesDataSource { + var messages: [MessageType] = [] + let senders: [MockUser] = [ + MockUser(senderId: "sender_1", displayName: "Sender 1"), + MockUser(senderId: "sender_2", displayName: "Sender 2"), + ] + + var currentUser: MockUser { + senders[0] + } + + var currentSender: SenderType { + currentUser + } + + func numberOfSections(in _: MessagesCollectionView) -> Int { + messages.count + } + + func messageForItem(at indexPath: IndexPath, in _: MessagesCollectionView) -> MessageType { + messages[indexPath.section] + } +} diff --git a/Tests/MessageKitTests/Mocks/MockUser.swift b/Tests/MessageKitTests/Mocks/MockUser.swift new file mode 100644 index 000000000..a91b3f8c8 --- /dev/null +++ b/Tests/MessageKitTests/Mocks/MockUser.swift @@ -0,0 +1,29 @@ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +@testable import MessageKit + +struct MockUser: SenderType { + var senderId: String + var displayName: String +} diff --git a/Tests/MessageKitTests/Model Tests/AvatarTests.swift b/Tests/MessageKitTests/Model Tests/AvatarTests.swift new file mode 100644 index 000000000..f87804af6 --- /dev/null +++ b/Tests/MessageKitTests/Model Tests/AvatarTests.swift @@ -0,0 +1,33 @@ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import XCTest +@testable import MessageKit + +final class AvatarTests: XCTestCase { + func testAvatarDefaultPropertyValues() { + let avatar = Avatar() + XCTAssertNil(avatar.image) + XCTAssertEqual(avatar.initials, "?") + } +} diff --git a/Tests/MessageKitTests/Model Tests/DetectorTypeTests.swift b/Tests/MessageKitTests/Model Tests/DetectorTypeTests.swift new file mode 100644 index 000000000..9fa84b1c6 --- /dev/null +++ b/Tests/MessageKitTests/Model Tests/DetectorTypeTests.swift @@ -0,0 +1,45 @@ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import XCTest +@testable import MessageKit + +final class DetectorTypeTests: XCTestCase { + func testMappingToNSTextCheckingResultCheckingType() { + /// case .address should equal .address + let address = DetectorType.address.textCheckingType + XCTAssertEqual(address, NSTextCheckingResult.CheckingType.address) + /// case .url should equal to .link + let url = DetectorType.url.textCheckingType + XCTAssertEqual(url, NSTextCheckingResult.CheckingType.link) + /// case .date should equal to .date + let date = DetectorType.date.textCheckingType + XCTAssertEqual(date, NSTextCheckingResult.CheckingType.date) + /// case .phoneNumber should equal to .phoneNumber + let phoneNumber = DetectorType.phoneNumber.textCheckingType + XCTAssertEqual(phoneNumber, NSTextCheckingResult.CheckingType.phoneNumber) + /// case .transitInformation should equal to .transitInformation + let transitInformation = DetectorType.transitInformation.textCheckingType + XCTAssertEqual(transitInformation, NSTextCheckingResult.CheckingType.transitInformation) + } +} diff --git a/Tests/MessageKitTests/Model Tests/MessageKitDateFormatterTests.swift b/Tests/MessageKitTests/Model Tests/MessageKitDateFormatterTests.swift new file mode 100644 index 000000000..154a8c144 --- /dev/null +++ b/Tests/MessageKitTests/Model Tests/MessageKitDateFormatterTests.swift @@ -0,0 +1,116 @@ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import XCTest +@testable import MessageKit + +final class MessageKitDateFormatterTests: XCTestCase { + var formatter: DateFormatter! + let attributes = [NSAttributedString.Key.backgroundColor: "red"] + + override func setUp() { + super.setUp() + + formatter = DateFormatter() + } + + override func tearDown() { + formatter = nil + super.tearDown() + } + + func testConfigureDateFormatterToday() { + formatter.doesRelativeDateFormatting = true + formatter.dateStyle = .short + formatter.timeStyle = .short + + XCTAssertEqual(MessageKitDateFormatter.shared.string(from: Date()), formatter.string(from: Date())) + } + + func testConfigureDateFormatterTodayAttributed() { + formatter.doesRelativeDateFormatting = true + formatter.dateStyle = .short + formatter.timeStyle = .short + + XCTAssertEqual( + MessageKitDateFormatter.shared.attributedString(from: Date(), with: attributes), + NSAttributedString(string: formatter.string(from: Date()), attributes: attributes)) + } + + func testConfigureDateFormatterYesterday() { + formatter.doesRelativeDateFormatting = true + formatter.dateStyle = .short + formatter.timeStyle = .short + + let yesterday = (Calendar.current as NSCalendar).date(byAdding: .day, value: -1, to: Date(), options: [])! + XCTAssertEqual(MessageKitDateFormatter.shared.string(from: yesterday), formatter.string(from: yesterday)) + } + + func testConfigureDateFormatterYesterdayAttributed() { + formatter.doesRelativeDateFormatting = true + formatter.dateStyle = .short + formatter.timeStyle = .short + + let yesterday = (Calendar.current as NSCalendar).date(byAdding: .day, value: -1, to: Date(), options: [])! + XCTAssertEqual( + MessageKitDateFormatter.shared.attributedString( + from: yesterday, + with: attributes), + NSAttributedString(string: formatter.string(from: yesterday), attributes: attributes)) + } + + func testConfigureDateFormatterWeekAndYear() { + // First day of current week + var startOfWeek = Calendar.current + .date(from: Calendar.current.dateComponents([.yearForWeekOfYear, .weekOfYear], from: Date()))! + // Check if today or yesterday was the first day of the week, because it will be different format then + if Calendar.current.isDateInToday(startOfWeek) || Calendar.current.isDateInYesterday(startOfWeek) { + formatter.doesRelativeDateFormatting = true + formatter.dateStyle = .short + formatter.timeStyle = .short + + XCTAssertEqual(MessageKitDateFormatter.shared.string(from: startOfWeek), formatter.string(from: startOfWeek)) + } else { + formatter.dateFormat = "EEEE h:mm a" + XCTAssertEqual(MessageKitDateFormatter.shared.string(from: startOfWeek), formatter.string(from: startOfWeek)) + } + + /// Day of last week + startOfWeek = (Calendar.current as NSCalendar).date(byAdding: .day, value: -2, to: startOfWeek, options: [])! + + if Calendar.current.isDate(startOfWeek, equalTo: Date(), toGranularity: .year) { + formatter.dateFormat = "E, d MMM, h:mm a" + } else { + formatter.dateFormat = "MMM d, yyyy, h:mm a" + } + + XCTAssertEqual(MessageKitDateFormatter.shared.string(from: startOfWeek), formatter.string(from: startOfWeek)) + } + + func testConfigureDateFormatterForMoreThanYear() { + formatter.dateFormat = "MMM d, yyyy, h:mm a" + let lastYear = (Calendar.current as NSCalendar).date(byAdding: .year, value: -1, to: Date(), options: [])! + + XCTAssertEqual(formatter.string(from: lastYear), MessageKitDateFormatter.shared.string(from: lastYear)) + } +} diff --git a/Tests/MessageKitTests/Model Tests/MessageStyleTests.swift b/Tests/MessageKitTests/Model Tests/MessageStyleTests.swift new file mode 100644 index 000000000..dc12297a2 --- /dev/null +++ b/Tests/MessageKitTests/Model Tests/MessageStyleTests.swift @@ -0,0 +1,159 @@ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import XCTest +@testable import MessageKit + +final class MessageStyleTests: XCTestCase { + // MARK: Internal + + func testTailCornerImageOrientation() { + XCTAssertEqual(MessageStyle.TailCorner.bottomRight.imageOrientation, UIImage.Orientation.up) + XCTAssertEqual(MessageStyle.TailCorner.bottomLeft.imageOrientation, UIImage.Orientation.upMirrored) + XCTAssertEqual(MessageStyle.TailCorner.topLeft.imageOrientation, UIImage.Orientation.down) + XCTAssertEqual(MessageStyle.TailCorner.topRight.imageOrientation, UIImage.Orientation.downMirrored) + } + + func testTailStyleImageNameSuffix() { + XCTAssertEqual(MessageStyle.TailStyle.curved.imageNameSuffix, "_tail_v2") + XCTAssertEqual(MessageStyle.TailStyle.pointedEdge.imageNameSuffix, "_tail_v1") + } + + func testImageNil() { + XCTAssertNil(MessageStyle.none.image) + } + + func testImageBubble() { + let originalData = (MessageStyle.bubble.image ?? UIImage()).pngData() + let testImage = UIImage(named: "bubble_full", in: assetBundle, compatibleWith: nil) + let testData = stretch(testImage!).withRenderingMode(.alwaysTemplate).pngData() + XCTAssertEqual(originalData, testData) + } + + func testImageBubbleOutline() { + let originalData = (MessageStyle.bubbleOutline(.black).image ?? UIImage()).pngData() + let testImage = UIImage(named: "bubble_outlined", in: assetBundle, compatibleWith: nil) + let testData = stretch(testImage!).withRenderingMode(.alwaysTemplate).pngData() + XCTAssertEqual(originalData, testData) + } + + func testImageBubbleTailCurved() { + var originalData = (MessageStyle.bubbleTail(.bottomLeft, .curved).image ?? UIImage()).pngData() + let testImage = UIImage(named: "bubble_full_tail_v2", in: assetBundle, compatibleWith: nil) + var testData = stretch(transform(image: testImage!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate).pngData() + XCTAssertEqual(originalData, testData) + + originalData = (MessageStyle.bubbleTail(.bottomRight, .curved).image ?? UIImage()).pngData() + testData = stretch(transform(image: testImage!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate).pngData() + XCTAssertEqual(originalData, testData) + + originalData = (MessageStyle.bubbleTail(.topLeft, .curved).image ?? UIImage()).pngData() + testData = stretch(transform(image: testImage!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate).pngData() + XCTAssertEqual(originalData, testData) + + originalData = (MessageStyle.bubbleTail(.topRight, .curved).image ?? UIImage()).pngData() + testData = stretch(transform(image: testImage!, corner: .topRight)!).withRenderingMode(.alwaysTemplate).pngData() + XCTAssertEqual(originalData, testData) + } + + func testImageBubbleTailPointedEdge() { + var originalData = (MessageStyle.bubbleTail(.bottomLeft, .pointedEdge).image ?? UIImage()).pngData() + let testImage = UIImage(named: "bubble_full_tail_v1", in: assetBundle, compatibleWith: nil) + var testData = stretch(transform(image: testImage!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate).pngData() + XCTAssertEqual(originalData, testData) + + originalData = (MessageStyle.bubbleTail(.bottomRight, .pointedEdge).image ?? UIImage()).pngData() + testData = stretch(transform(image: testImage!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate).pngData() + XCTAssertEqual(originalData, testData) + + originalData = (MessageStyle.bubbleTail(.topLeft, .pointedEdge).image ?? UIImage()).pngData() + testData = stretch(transform(image: testImage!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate).pngData() + XCTAssertEqual(originalData, testData) + + originalData = (MessageStyle.bubbleTail(.topRight, .pointedEdge).image ?? UIImage()).pngData() + testData = stretch(transform(image: testImage!, corner: .topRight)!).withRenderingMode(.alwaysTemplate).pngData() + XCTAssertEqual(originalData, testData) + } + + func testImageBubbleTailOutlineCurved() { + var originalData = (MessageStyle.bubbleTailOutline(.red, .bottomLeft, .curved).image ?? UIImage()).pngData() + let testImage = UIImage(named: "bubble_outlined_tail_v2", in: assetBundle, compatibleWith: nil) + var testData = stretch(transform(image: testImage!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate).pngData() + XCTAssertEqual(originalData, testData) + + originalData = (MessageStyle.bubbleTailOutline(.red, .bottomRight, .curved).image ?? UIImage()).pngData() + testData = stretch(transform(image: testImage!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate).pngData() + XCTAssertEqual(originalData, testData) + + originalData = (MessageStyle.bubbleTailOutline(.red, .topLeft, .curved).image ?? UIImage()).pngData() + testData = stretch(transform(image: testImage!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate).pngData() + XCTAssertEqual(originalData, testData) + + originalData = (MessageStyle.bubbleTailOutline(.red, .topRight, .curved).image ?? UIImage()).pngData() + testData = stretch(transform(image: testImage!, corner: .topRight)!).withRenderingMode(.alwaysTemplate).pngData() + XCTAssertEqual(originalData, testData) + } + + func testImageBubbleTailOutlinePointedEdge() { + var originalData = (MessageStyle.bubbleTailOutline(.red, .bottomLeft, .pointedEdge).image ?? UIImage()).pngData() + let testImage = UIImage(named: "bubble_outlined_tail_v1", in: assetBundle, compatibleWith: nil) + var testData = stretch(transform(image: testImage!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate).pngData() + XCTAssertEqual(originalData, testData) + + originalData = (MessageStyle.bubbleTailOutline(.red, .bottomRight, .pointedEdge).image ?? UIImage()).pngData() + testData = stretch(transform(image: testImage!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate).pngData() + XCTAssertEqual(originalData, testData) + + originalData = (MessageStyle.bubbleTailOutline(.red, .topLeft, .pointedEdge).image ?? UIImage()).pngData() + testData = stretch(transform(image: testImage!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate).pngData() + XCTAssertEqual(originalData, testData) + + originalData = (MessageStyle.bubbleTailOutline(.red, .topRight, .pointedEdge).image ?? UIImage()).pngData() + testData = stretch(transform(image: testImage!, corner: .topRight)!).withRenderingMode(.alwaysTemplate).pngData() + XCTAssertEqual(originalData, testData) + } + + func testCachesIdenticalOutlineImagesFromCache() { + let image1 = MessageStyle.bubbleTail(.topLeft, .pointedEdge).image ?? UIImage() + let image2 = MessageStyle.bubbleTail(.topLeft, .pointedEdge).image ?? UIImage() + XCTAssertEqual(image1, image2) + // After clearing cache a new image instance will be loaded from disk + MessageStyle.bubbleImageCache.removeAllObjects() + XCTAssertFalse(image1 === MessageStyle.bubbleTail(.topLeft, .pointedEdge).image) + } + + // MARK: Private + + private let assetBundle = Bundle.messageKitAssetBundle + + private func stretch(_ image: UIImage) -> UIImage { + let center = CGPoint(x: image.size.width / 2, y: image.size.height / 2) + let capInsets = UIEdgeInsets(top: center.y, left: center.x, bottom: center.y, right: center.x) + return image.resizableImage(withCapInsets: capInsets, resizingMode: .stretch) + } + + private func transform(image: UIImage, corner: MessageStyle.TailCorner) -> UIImage? { + guard let cgImage = image.cgImage else { return image } + return UIImage(cgImage: cgImage, scale: image.scale, orientation: corner.imageOrientation) + } +} diff --git a/Tests/MessageKitTests/Model Tests/SenderTests.swift b/Tests/MessageKitTests/Model Tests/SenderTests.swift new file mode 100644 index 000000000..5bb75de24 --- /dev/null +++ b/Tests/MessageKitTests/Model Tests/SenderTests.swift @@ -0,0 +1,39 @@ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import XCTest +@testable import MessageKit + +final class SenderTests: XCTestCase { + func testEqualityBetweenTwoSenders() { + let sender1 = MockUser(senderId: "1", displayName: "Steven") + let sender2 = MockUser(senderId: "1", displayName: "Nathan") + XCTAssertEqual(sender1.senderId, sender2.senderId) + } + + func testNotEqualityBetweenTwoSenders() { + let sender1 = MockUser(senderId: "1", displayName: "Steven") + let sender2 = MockUser(senderId: "2", displayName: "Nathan") + XCTAssertNotEqual(sender1.senderId, sender2.senderId) + } +} diff --git a/Tests/MessageKitTests/Protocols Tests/MessagesDisplayDelegateTests.swift b/Tests/MessageKitTests/Protocols Tests/MessagesDisplayDelegateTests.swift new file mode 100644 index 000000000..2f51d61b9 --- /dev/null +++ b/Tests/MessageKitTests/Protocols Tests/MessagesDisplayDelegateTests.swift @@ -0,0 +1,244 @@ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import XCTest +@testable import MessageKit + +// MARK: - MessagesDisplayDelegateTests + +@MainActor +final class MessagesDisplayDelegateTests: XCTestCase { + // MARK: - Private helper API + + private func makeSUT() -> MockMessagesViewController { + let sut = MockMessagesViewController() + _ = sut.view + sut.beginAppearanceTransition(true, animated: true) + sut.endAppearanceTransition() + sut.viewDidLoad() + sut.view.layoutIfNeeded() + + return sut + } + + // MARK: Internal + + func testBackGroundColorDefaultState() { + let sut = makeSUT() + XCTAssertEqual( + sut.backgroundColor( + for: sut.dataProvider.messages[0], + at: IndexPath(item: 0, section: 0), + in: sut.messagesCollectionView), + UIColor.outgoingMessageBackground) + XCTAssertNotEqual( + sut.backgroundColor( + for: sut.dataProvider.messages[0], + at: IndexPath(item: 0, section: 0), + in: sut.messagesCollectionView), + UIColor.incomingMessageBackground) + XCTAssertEqual( + sut.backgroundColor( + for: sut.dataProvider.messages[1], + at: IndexPath(item: 1, section: 0), + in: sut.messagesCollectionView), + UIColor.incomingMessageBackground) + XCTAssertNotEqual( + sut.backgroundColor( + for: sut.dataProvider.messages[1], + at: IndexPath(item: 1, section: 0), + in: sut.messagesCollectionView), + UIColor.outgoingMessageBackground) + } + + func testBackgroundColorWithoutDataSource_returnsWhiteForDefault() { + let sut = makeSUT() + sut.messagesCollectionView.messagesDataSource = nil + let backgroundColor = sut.backgroundColor( + for: sut.dataProvider.messages[0], + at: IndexPath(item: 0, section: 0), + in: sut.messagesCollectionView) + + XCTAssertEqual(backgroundColor, UIColor.white) + } + + func testBackgroundColorForMessageWithEmoji_returnsClearForDefault() { + let sut = makeSUT() + sut.dataProvider.messages.append(MockMessage( + emoji: "πŸ€”", + user: sut.dataProvider.currentUser, + messageId: "003")) + let backgroundColor = sut.backgroundColor( + for: sut.dataProvider.messages[2], + at: IndexPath(item: 0, section: 0), + in: sut.messagesCollectionView) + + XCTAssertEqual(backgroundColor, .clear) + } + + func testCellTopLabelDefaultState() { + let sut = makeSUT() + XCTAssertNil(sut.dataProvider.cellTopLabelAttributedText( + for: sut.dataProvider.messages[0], + at: IndexPath(item: 0, section: 0))) + } + + func testMessageBottomLabelDefaultState() { + let sut = makeSUT() + XCTAssertNil(sut.dataProvider.messageBottomLabelAttributedText( + for: sut.dataProvider.messages[0], + at: IndexPath(item: 0, section: 0))) + } + + func testMessageStyle_returnsBubleTypeForDefault() { + let sut = makeSUT() + let type = sut.messageStyle( + for: sut.dataProvider.messages[0], + at: IndexPath(item: 0, section: 0), + in: sut.messagesCollectionView) + + let result: Bool + switch type { + case .bubble: + result = true + default: + result = false + } + + XCTAssertTrue(result) + } + + func testMessageHeaderView_isNotNil() { + let sut = makeSUT() + let indexPath = IndexPath(item: 0, section: 1) + XCTAssert(sut.dataProvider != nil) + let headerView = sut.messageHeaderView(for: indexPath, in: sut.messagesCollectionView) + XCTAssertNotNil(headerView) + } +} + +// MARK: - TextMessageDisplayDelegateTests + +@MainActor +class TextMessageDisplayDelegateTests: XCTestCase { + // MARK: - Private helper API + + private func makeSUT() -> MockMessagesViewController { + let sut = MockMessagesViewController() + _ = sut.view + sut.beginAppearanceTransition(true, animated: true) + sut.endAppearanceTransition() + return sut + } + + func testTextColorFromCurrentSender_returnsWhiteForDefault() { + let sut = makeSUT() + let textColor = sut.textColor( + for: sut.dataProvider.messages[0], + at: IndexPath(item: 0, section: 0), + in: sut.messagesCollectionView) + + XCTAssertEqual(textColor, UIColor.outgoingMessageLabel) + } + + func testTextColorFromYou_returnsDarkTextForDefault() { + let sut = makeSUT() + let textColor = sut.textColor( + for: sut.dataProvider.messages[1], + at: IndexPath(item: 0, section: 0), + in: sut.messagesCollectionView) + + XCTAssertEqual(textColor, UIColor.incomingMessageLabel) + } + + func testTextColorWithoutDataSource_returnsDarkTextForDefault() { + let sut = makeSUT() + let dataSource = sut.makeDataSource() + sut.messagesCollectionView.messagesDataSource = dataSource + let textColor = sut.textColor( + for: sut.dataProvider.messages[1], + at: IndexPath(item: 0, section: 0), + in: sut.messagesCollectionView) + + XCTAssertEqual(textColor, UIColor.incomingMessageLabel) + } + + func testEnableDetectors_returnsEmptyForDefault() { + let sut = makeSUT() + let detectors = sut.enabledDetectors( + for: sut.dataProvider.messages[1], + at: IndexPath(item: 0, section: 0), + in: sut.messagesCollectionView) + let expectedDetectors: [DetectorType] = [] + + XCTAssertEqual(detectors, expectedDetectors) + } +} + +// MARK: - MockMessagesViewController + +@MainActor +private class MockMessagesViewController: MessagesViewController, MessagesDisplayDelegate, MessagesLayoutDelegate { + // MARK: Internal + + var dataProvider: MockMessagesDataSource! + + func heightForLocation(message _: MessageType, at _: IndexPath, with _: CGFloat, in _: MessagesCollectionView) -> CGFloat { + 200 + } + + override func viewDidLoad() { + super.viewDidLoad() + + dataProvider = makeDataSource() + messagesCollectionView.messagesDisplayDelegate = self + messagesCollectionView.messagesDataSource = dataProvider + messagesCollectionView.messagesLayoutDelegate = self + messagesCollectionView.reloadData() + } + + func snapshotOptionsForLocation( + message _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) + -> LocationMessageSnapshotOptions + { + LocationMessageSnapshotOptions() + } + + // MARK: Fileprivate + + fileprivate func makeDataSource() -> MockMessagesDataSource { + let dataSource = MockMessagesDataSource() + dataSource.messages.append(MockMessage( + text: "Text 1", + user: dataSource.senders[0], + messageId: "001")) + dataSource.messages.append(MockMessage( + text: "Text 2", + user: dataSource.senders[1], + messageId: "002")) + + return dataSource + } +} diff --git a/Tests/MessageKitTests/Views Tests/AvatarViewTests.swift b/Tests/MessageKitTests/Views Tests/AvatarViewTests.swift new file mode 100644 index 000000000..846b1c01e --- /dev/null +++ b/Tests/MessageKitTests/Views Tests/AvatarViewTests.swift @@ -0,0 +1,92 @@ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation +import XCTest +@testable import MessageKit + +@MainActor +final class AvatarViewTests: XCTestCase { + // MARK: - Private helper API + + private func makeAvatarView() -> AvatarView { + let avatarView = AvatarView() + avatarView.frame.size = CGSize(width: 30, height: 30) + return avatarView + } + + func testNoParams() { + let avatarView = makeAvatarView() + + XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) + // For certain dynamic colors, need to compare cgColor in XCTest + // https://stackoverflow.com/questions/58065340/how-to-compare-two-uidynamicprovidercolor + XCTAssertEqual(avatarView.backgroundColor!.cgColor, UIColor.avatarViewBackground.cgColor) + } + + func testWithImage() { + let avatarView = makeAvatarView() + + let avatar = Avatar(image: UIImage()) + avatarView.set(avatar: avatar) + XCTAssertEqual(avatar.initials, "?") + XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) + XCTAssertEqual(avatarView.backgroundColor!.cgColor, UIColor.avatarViewBackground.cgColor) + } + + func testInitialsOnly() { + let avatarView = makeAvatarView() + + let avatar = Avatar(initials: "DL") + avatarView.set(avatar: avatar) + XCTAssertEqual(avatarView.initials, avatar.initials) + XCTAssertEqual(avatar.initials, "DL") + XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) + XCTAssertEqual(avatarView.backgroundColor!.cgColor, UIColor.avatarViewBackground.cgColor) + } + + func testSetBackground() { + let avatarView = makeAvatarView() + XCTAssertEqual(avatarView.backgroundColor!.cgColor, UIColor.avatarViewBackground.cgColor) + avatarView.backgroundColor = UIColor.red + XCTAssertEqual(avatarView.backgroundColor!, UIColor.red) + } + + func testGetImage() { + let avatarView = makeAvatarView() + + let image = UIImage() + let avatar = Avatar(image: image) + avatarView.set(avatar: avatar) + XCTAssertEqual(avatarView.image, image) + } + + func testRoundedCorners() { + let avatarView = makeAvatarView() + + let avatar = Avatar(image: UIImage()) + avatarView.set(avatar: avatar) + XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) + avatarView.setCorner(radius: 2) + XCTAssertEqual(avatarView.layer.cornerRadius, 2.0) + } +} diff --git a/Tests/ViewsTests/MessageCollectionViewCellTests.swift b/Tests/MessageKitTests/Views Tests/MessageCollectionViewCellTests.swift similarity index 50% rename from Tests/ViewsTests/MessageCollectionViewCellTests.swift rename to Tests/MessageKitTests/Views Tests/MessageCollectionViewCellTests.swift index cbaa1a26c..62190e3ea 100644 --- a/Tests/ViewsTests/MessageCollectionViewCellTests.swift +++ b/Tests/MessageKitTests/Views Tests/MessageCollectionViewCellTests.swift @@ -1,60 +1,52 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +import Foundation import XCTest @testable import MessageKit -final class MessageContentCellTests: XCTestCase { +// MARK: - MessageContentCellTests - var cell: MessageContentCell! +@MainActor +final class MessageContentCellTests: XCTestCase { let frame = CGRect(x: 0, y: 0, width: 100, height: 100) - override func setUp() { - super.setUp() - cell = MessageContentCell(frame: frame) - } - - override func tearDown() { - cell = nil - super.tearDown() - } - func testInit() { + let cell = MessageContentCell(frame: frame) XCTAssertEqual(cell.contentView.autoresizingMask, [.flexibleWidth, .flexibleHeight]) XCTAssert(cell.contentView.subviews.contains(cell.cellTopLabel)) XCTAssert(cell.contentView.subviews.contains(cell.messageBottomLabel)) XCTAssert(cell.contentView.subviews.contains(cell.avatarView)) XCTAssert(cell.contentView.subviews.contains(cell.messageContainerView)) - } func testMessageContainerViewPropertiesSetup() { + let cell = MessageContentCell(frame: frame) XCTAssertTrue(cell.messageContainerView.clipsToBounds) XCTAssertTrue(cell.messageContainerView.layer.masksToBounds) } func testPrepareForReuse() { + let cell = MessageContentCell(frame: frame) cell.prepareForReuse() XCTAssertNil(cell.cellTopLabel.text) XCTAssertNil(cell.cellTopLabel.attributedText) @@ -63,6 +55,7 @@ final class MessageContentCellTests: XCTestCase { } func testApplyLayoutAttributes() { + let cell = MessageContentCell(frame: frame) let layoutAttributes = MessagesCollectionViewLayoutAttributes() layoutAttributes.avatarPosition = AvatarPosition(horizontal: .cellLeading, vertical: .cellBottom) cell.apply(layoutAttributes) @@ -72,16 +65,17 @@ final class MessageContentCellTests: XCTestCase { XCTAssertEqual(cell.cellTopLabel.frame.size, layoutAttributes.cellTopLabelSize) XCTAssertEqual(cell.messageBottomLabel.frame.size, layoutAttributes.messageBottomLabelSize) } - } extension MessageContentCellTests { - - fileprivate class MockMessagesDisplayDelegate: MessagesDisplayDelegate { - - func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions { - return LocationMessageSnapshotOptions() + private class MockMessagesDisplayDelegate: MessagesDisplayDelegate { + func snapshotOptionsForLocation( + message _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) + -> LocationMessageSnapshotOptions + { + LocationMessageSnapshotOptions() } } - } diff --git a/Tests/MessageKitTests/Views Tests/MessagesCollectionViewTests.swift b/Tests/MessageKitTests/Views Tests/MessagesCollectionViewTests.swift new file mode 100644 index 000000000..84e0ca902 --- /dev/null +++ b/Tests/MessageKitTests/Views Tests/MessagesCollectionViewTests.swift @@ -0,0 +1,37 @@ +// MIT License +// +// Copyright (c) 2017-2020 MessageKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import XCTest +@testable import MessageKit + +@MainActor +class MessagesCollectionViewTests: XCTestCase { + let rect = CGRect(x: 0, y: 0, width: 100, height: 100) + let layout = MessagesCollectionViewFlowLayout() + + func testInit() { + let messagesCollectionView = MessagesCollectionView(frame: rect, collectionViewLayout: layout) + XCTAssertEqual(messagesCollectionView.frame, rect) + XCTAssertEqual(messagesCollectionView.collectionViewLayout, layout) + XCTAssertEqual(messagesCollectionView.backgroundColor, UIColor.collectionViewBackground) + } +} diff --git a/Tests/Mocks/MockMessage.swift b/Tests/Mocks/MockMessage.swift deleted file mode 100644 index 5d7e9c446..000000000 --- a/Tests/Mocks/MockMessage.swift +++ /dev/null @@ -1,97 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import Foundation -import CoreLocation -@testable import MessageKit - -struct MockLocationItem: LocationItem { - - var location: CLLocation - var size: CGSize - - init(location: CLLocation) { - self.location = location - self.size = CGSize(width: 240, height: 240) - } - -} - -struct MockMediaItem: MediaItem { - - var url: URL? - var image: UIImage? - var placeholderImage: UIImage - var size: CGSize - - init(image: UIImage) { - self.image = image - self.size = CGSize(width: 240, height: 240) - self.placeholderImage = UIImage() - } - -} - -struct MockMessage: MessageType { - - var messageId: String - var sender: Sender - var sentDate: Date - var kind: MessageKind - - private init(kind: MessageKind, sender: Sender, messageId: String) { - self.kind = kind - self.sender = sender - self.messageId = messageId - self.sentDate = Date() - } - - init(text: String, sender: Sender, messageId: String) { - self.init(kind: .text(text), sender: sender, messageId: messageId) - } - - init(attributedText: NSAttributedString, sender: Sender, messageId: String) { - self.init(kind: .attributedText(attributedText), sender: sender, messageId: messageId) - } - - init(image: UIImage, sender: Sender, messageId: String) { - let mediaItem = MockMediaItem(image: image) - self.init(kind: .photo(mediaItem), sender: sender, messageId: messageId) - } - - init(thumbnail: UIImage, sender: Sender, messageId: String) { - let mediaItem = MockMediaItem(image: thumbnail) - self.init(kind: .video(mediaItem), sender: sender, messageId: messageId) - } - - init(location: CLLocation, sender: Sender, messageId: String) { - let locationItem = MockLocationItem(location: location) - self.init(kind: .location(locationItem), sender: sender, messageId: messageId) - } - - init(emoji: String, sender: Sender, messageId: String) { - self.init(kind: .emoji(emoji), sender: sender, messageId: messageId) - } - -} diff --git a/Tests/Mocks/MockMessagesDataSource.swift b/Tests/Mocks/MockMessagesDataSource.swift deleted file mode 100644 index 566d8d278..000000000 --- a/Tests/Mocks/MockMessagesDataSource.swift +++ /dev/null @@ -1,46 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import Foundation -@testable import MessageKit - -class MockMessagesDataSource: MessagesDataSource { - - var messages: [MessageType] = [] - let senders: [Sender] = [Sender(id: "sender_1", displayName: "Sender 1"), - Sender(id: "sender_2", displayName: "Sender 2")] - - func currentSender() -> Sender { - return senders[0] - } - - func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int { - return messages.count - } - - func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType { - return messages[indexPath.section] - } - -} diff --git a/Tests/ModelTests/MessageKitDateFormatterTests.swift b/Tests/ModelTests/MessageKitDateFormatterTests.swift deleted file mode 100644 index e9b24175a..000000000 --- a/Tests/ModelTests/MessageKitDateFormatterTests.swift +++ /dev/null @@ -1,113 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import XCTest -@testable import MessageKit - -class MessageKitDateFormatterTests: XCTestCase { - - var formatter: DateFormatter! - let attributes = [NSAttributedString.Key.backgroundColor: "red"] - override func setUp() { - super.setUp() - - formatter = DateFormatter() - } - - override func tearDown() { - formatter = nil - super.tearDown() - } - - func testConfigureDateFormatterToday() { - formatter.doesRelativeDateFormatting = true - formatter.dateStyle = .short - formatter.timeStyle = .short - - XCTAssertEqual(MessageKitDateFormatter.shared.string(from: Date()), formatter.string(from: Date())) - } - - func testConfigureDateFormatterTodayAttributed() { - formatter.doesRelativeDateFormatting = true - formatter.dateStyle = .short - formatter.timeStyle = .short - - XCTAssertEqual(MessageKitDateFormatter.shared.attributedString(from: Date(), with: attributes), NSAttributedString(string: formatter.string(from: Date()), attributes: attributes)) - } - - func testConfigureDateFormatterYesterday() { - formatter.doesRelativeDateFormatting = true - formatter.dateStyle = .short - formatter.timeStyle = .short - - let yesterday = (Calendar.current as NSCalendar).date(byAdding: .day, value: -1, to: Date(), options: [])! - XCTAssertEqual(MessageKitDateFormatter.shared.string(from: yesterday), formatter.string(from: yesterday)) - } - - func testConfigureDateFormatterYesterdayAttributed() { - formatter.doesRelativeDateFormatting = true - formatter.dateStyle = .short - formatter.timeStyle = .short - - let yesterday = (Calendar.current as NSCalendar).date(byAdding: .day, value: -1, to: Date(), options: [])! - XCTAssertEqual(MessageKitDateFormatter.shared.attributedString(from: yesterday, - with: attributes), - NSAttributedString(string: formatter.string(from: yesterday), attributes: attributes)) - } - - func testConfigureDateFormatterWeekAndYear() { - //First day of current week - var startOfWeek = Calendar.current.date(from: Calendar.current.dateComponents([.yearForWeekOfYear, .weekOfYear], from: Date()))! - //Check if today or yesterday was the first day of the week, because it will be different format then - if Calendar.current.isDateInToday(startOfWeek) || Calendar.current.isDateInYesterday(startOfWeek) { - formatter.doesRelativeDateFormatting = true - formatter.dateStyle = .short - formatter.timeStyle = .short - - XCTAssertEqual(MessageKitDateFormatter.shared.string(from: startOfWeek), formatter.string(from: startOfWeek)) - } else { - formatter.dateFormat = "EEEE h:mm a" - XCTAssertEqual(MessageKitDateFormatter.shared.string(from: startOfWeek), formatter.string(from: startOfWeek)) - } - - ///Day of last week - startOfWeek = (Calendar.current as NSCalendar).date(byAdding: .day, value: -2, to: startOfWeek, options: [])! - - if Calendar.current.isDate(startOfWeek, equalTo: Date(), toGranularity: .year) { - formatter.dateFormat = "E, d MMM, h:mm a" - } else { - formatter.dateFormat = "MMM d, yyyy, h:mm a" - } - - XCTAssertEqual(MessageKitDateFormatter.shared.string(from: startOfWeek), formatter.string(from: startOfWeek)) - } - - func testConfigureDateFormatterForMoreThanYear() { - formatter.dateFormat = "MMM d, yyyy, h:mm a" - let lastYear = (Calendar.current as NSCalendar).date(byAdding: .year, value: -1, to: Date(), options: [])! - - XCTAssertEqual(formatter.string(from: lastYear), MessageKitDateFormatter.shared.string(from: lastYear)) - } - -} diff --git a/Tests/ModelTests/MessageStyleTests.swift b/Tests/ModelTests/MessageStyleTests.swift deleted file mode 100644 index 7534ac99e..000000000 --- a/Tests/ModelTests/MessageStyleTests.swift +++ /dev/null @@ -1,175 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import XCTest -@testable import MessageKit - -class MessageStyleTests: XCTestCase { - - override func setUp() { - super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() - } - - func testTailCornerImageOrientation() { - XCTAssertEqual(MessageStyle.TailCorner.bottomRight.imageOrientation, UIImage.Orientation.up) - XCTAssertEqual(MessageStyle.TailCorner.bottomLeft.imageOrientation, UIImage.Orientation.upMirrored) - XCTAssertEqual(MessageStyle.TailCorner.topLeft.imageOrientation, UIImage.Orientation.down) - XCTAssertEqual(MessageStyle.TailCorner.topRight.imageOrientation, UIImage.Orientation.downMirrored) - } - - func testTailStyleImageNameSuffix() { - XCTAssertEqual(MessageStyle.TailStyle.curved.imageNameSuffix, "_tail_v2") - XCTAssertEqual(MessageStyle.TailStyle.pointedEdge.imageNameSuffix, "_tail_v1") - } - - func testImageNil() { - XCTAssertNil(MessageStyle.none.image) - } - - func testImageBubble() { - let assetBundle = Bundle.messageKitAssetBundle() - let imagePath = assetBundle.path(forResource: "bubble_full", ofType: "png", inDirectory: "Images") - let originalData = (MessageStyle.bubble.image ?? UIImage()).pngData() - let testData = stretch(UIImage(contentsOfFile: imagePath!)!).withRenderingMode(.alwaysTemplate).pngData() - XCTAssertEqual(originalData, testData) - } - - func testImageBubbleOutline() { - let assetBundle = Bundle.messageKitAssetBundle() - let imagePath = assetBundle.path(forResource: "bubble_outlined", ofType: "png", inDirectory: "Images") - let originalData = (MessageStyle.bubbleOutline(.black).image ?? UIImage()).pngData() - let testData = stretch(UIImage(contentsOfFile: imagePath!)!).withRenderingMode(.alwaysTemplate).pngData() - XCTAssertEqual(originalData, testData) - } - - func testImageBubbleTailCurved() { - let assetBundle = Bundle.messageKitAssetBundle() - let imagePath = assetBundle.path(forResource: "bubble_full_tail_v2", ofType: "png", inDirectory: "Images") - - var originalData = (MessageStyle.bubbleTail(.bottomLeft, .curved).image ?? UIImage()).pngData() - var testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate).pngData() - XCTAssertEqual(originalData, testData) - - originalData = (MessageStyle.bubbleTail(.bottomRight, .curved).image ?? UIImage()).pngData() - testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate).pngData() - XCTAssertEqual(originalData, testData) - - originalData = (MessageStyle.bubbleTail(.topLeft, .curved).image ?? UIImage()).pngData() - testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate).pngData() - XCTAssertEqual(originalData, testData) - - originalData = (MessageStyle.bubbleTail(.topRight, .curved).image ?? UIImage()).pngData() - testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topRight)!).withRenderingMode(.alwaysTemplate).pngData() - XCTAssertEqual(originalData, testData) - } - - func testImageBubbleTailPointedEdge() { - let assetBundle = Bundle.messageKitAssetBundle() - let imagePath = assetBundle.path(forResource: "bubble_full_tail_v1", ofType: "png", inDirectory: "Images") - - var originalData = (MessageStyle.bubbleTail(.bottomLeft, .pointedEdge).image ?? UIImage()).pngData() - var testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate).pngData() - XCTAssertEqual(originalData, testData) - - originalData = (MessageStyle.bubbleTail(.bottomRight, .pointedEdge).image ?? UIImage()).pngData() - testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate).pngData() - XCTAssertEqual(originalData, testData) - - originalData = (MessageStyle.bubbleTail(.topLeft, .pointedEdge).image ?? UIImage()).pngData() - testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate).pngData() - XCTAssertEqual(originalData, testData) - - originalData = (MessageStyle.bubbleTail(.topRight, .pointedEdge).image ?? UIImage()).pngData() - testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topRight)!).withRenderingMode(.alwaysTemplate).pngData() - XCTAssertEqual(originalData, testData) - } - - func testImageBubbleTailOutlineCurved() { - let assetBundle = Bundle.messageKitAssetBundle() - let imagePath = assetBundle.path(forResource: "bubble_outlined_tail_v2", ofType: "png", inDirectory: "Images") - - var originalData = (MessageStyle.bubbleTailOutline(.red, .bottomLeft, .curved).image ?? UIImage()).pngData() - var testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate).pngData() - XCTAssertEqual(originalData, testData) - - originalData = (MessageStyle.bubbleTailOutline(.red, .bottomRight, .curved).image ?? UIImage()).pngData() - testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate).pngData() - XCTAssertEqual(originalData, testData) - - originalData = (MessageStyle.bubbleTailOutline(.red, .topLeft, .curved).image ?? UIImage()).pngData() - testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate).pngData() - XCTAssertEqual(originalData, testData) - - originalData = (MessageStyle.bubbleTailOutline(.red, .topRight, .curved).image ?? UIImage()).pngData() - testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topRight)!).withRenderingMode(.alwaysTemplate).pngData() - XCTAssertEqual(originalData, testData) - } - - func testImageBubbleTailOutlinePointedEdge() { - let assetBundle = Bundle.messageKitAssetBundle() - let imagePath = assetBundle.path(forResource: "bubble_outlined_tail_v1", ofType: "png", inDirectory: "Images") - - var originalData = (MessageStyle.bubbleTailOutline(.red, .bottomLeft, .pointedEdge).image ?? UIImage()).pngData() - var testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate).pngData() - XCTAssertEqual(originalData, testData) - - originalData = (MessageStyle.bubbleTailOutline(.red, .bottomRight, .pointedEdge).image ?? UIImage()).pngData() - testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate).pngData() - XCTAssertEqual(originalData, testData) - - originalData = (MessageStyle.bubbleTailOutline(.red, .topLeft, .pointedEdge).image ?? UIImage()).pngData() - testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate).pngData() - XCTAssertEqual(originalData, testData) - - originalData = (MessageStyle.bubbleTailOutline(.red, .topRight, .pointedEdge).image ?? UIImage()).pngData() - testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topRight)!).withRenderingMode(.alwaysTemplate).pngData() - XCTAssertEqual(originalData, testData) - } - - func testCachesIdenticalOutlineImagesFromCache() { - let image1 = MessageStyle.bubbleTail(.topLeft, .pointedEdge).image ?? UIImage() - let image2 = MessageStyle.bubbleTail(.topLeft, .pointedEdge).image ?? UIImage() - XCTAssertEqual(image1, image2) - // After clearing cache a new image instance will be loaded from disk - MessageStyle.bubbleImageCache.removeAllObjects() - XCTAssertNotEqual(image1, MessageStyle.bubbleTail(.topLeft, .pointedEdge).image) - } - - private func stretch(_ image: UIImage) -> UIImage { - let center = CGPoint(x: image.size.width / 2, y: image.size.height / 2) - let capInsets = UIEdgeInsets(top: center.y, left: center.x, bottom: center.y, right: center.x) - return image.resizableImage(withCapInsets: capInsets, resizingMode: .stretch) - } - - private func transform(image: UIImage, corner: MessageStyle.TailCorner) -> UIImage? { - guard let cgImage = image.cgImage else { return image } - return UIImage(cgImage: cgImage, scale: image.scale, orientation: corner.imageOrientation) - } -} diff --git a/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift b/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift deleted file mode 100644 index 4d2dae9d6..000000000 --- a/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift +++ /dev/null @@ -1,211 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import XCTest - -@testable import MessageKit - -class MessagesDisplayDelegateTests: XCTestCase { - - private var sut: MockMessagesViewController! - - override func setUp() { - super.setUp() - - sut = MockMessagesViewController() - _ = sut.view - sut.beginAppearanceTransition(true, animated: true) - sut.endAppearanceTransition() - sut.view.layoutIfNeeded() - } - - override func tearDown() { - sut = nil - - super.tearDown() - } - - func testBackGroundColorDefaultState() { - XCTAssertEqual(sut.backgroundColor(for: sut.dataProvider.messages[0], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView), - UIColor.outgoingGreen) - XCTAssertNotEqual(sut.backgroundColor(for: sut.dataProvider.messages[0], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView), - UIColor.incomingGray) - XCTAssertEqual(sut.backgroundColor(for: sut.dataProvider.messages[1], - at: IndexPath(item: 1, section: 0), - in: sut.messagesCollectionView), - UIColor.incomingGray) - XCTAssertNotEqual(sut.backgroundColor(for: sut.dataProvider.messages[1], - at: IndexPath(item: 1, section: 0), - in: sut.messagesCollectionView), - UIColor.outgoingGreen) - } - - func testBackgroundColorWithoutDataSource_returnsWhiteForDefault() { - sut.messagesCollectionView.messagesDataSource = nil - let backgroundColor = sut.backgroundColor(for: sut.dataProvider.messages[0], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView) - - XCTAssertEqual(backgroundColor, .white) - } - - func testBackgroundColorForMessageWithEmoji_returnsClearForDefault() { - sut.dataProvider.messages.append(MockMessage(emoji: "πŸ€”", - sender: sut.dataProvider.currentSender(), - messageId: "003")) - let backgroundColor = sut.backgroundColor(for: sut.dataProvider.messages[2], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView) - - XCTAssertEqual(backgroundColor, .clear) - } - - func testCellTopLabelDefaultState() { - XCTAssertNil(sut.dataProvider.cellTopLabelAttributedText(for: sut.dataProvider.messages[0], - at: IndexPath(item: 0, section: 0))) - } - - func testMessageBottomLabelDefaultState() { - XCTAssertNil(sut.dataProvider.messageBottomLabelAttributedText(for: sut.dataProvider.messages[0], - at: IndexPath(item: 0, section: 0))) - } - - func testMessageStyle_returnsBubleTypeForDefault() { - let type = sut.messageStyle(for: sut.dataProvider.messages[0], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView) - - let result: Bool - switch type { - case .bubble: - result = true - default: - result = false - } - - XCTAssertTrue(result) - } - - func testMessageHeaderView_isNotNil() { - let indexPath = IndexPath(item: 0, section: 1) - let headerView = sut.messageHeaderView(for: indexPath, in: sut.messagesCollectionView) - XCTAssertNotNil(headerView) - } - -} - -class TextMessageDisplayDelegateTests: XCTestCase { - - private var sut: MockMessagesViewController! - - override func setUp() { - super.setUp() - - sut = MockMessagesViewController() - _ = sut.view - sut.beginAppearanceTransition(true, animated: true) - sut.endAppearanceTransition() - } - - override func tearDown() { - sut = nil - - super.tearDown() - } - - func testTextColorFromCurrentSender_returnsWhiteForDefault() { - let textColor = sut.textColor(for: sut.dataProvider.messages[0], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView) - - XCTAssertEqual(textColor, .white) - } - - func testTextColorFromYou_returnsDarkTextForDefault() { - let textColor = sut.textColor(for: sut.dataProvider.messages[1], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView) - - XCTAssertEqual(textColor, .darkText) - } - - func testTextColorWithoutDataSource_returnsDarkTextForDefault() { - let dataSource = sut.makeDataSource() - sut.messagesCollectionView.messagesDataSource = dataSource - let textColor = sut.textColor(for: sut.dataProvider.messages[1], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView) - - XCTAssertEqual(textColor, .darkText) - } - - func testEnableDetectors_returnsEmptyForDefault() { - let detectors = sut.enabledDetectors(for: sut.dataProvider.messages[1], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView) - let expectedDetectors: [DetectorType] = [] - - XCTAssertEqual(detectors, expectedDetectors) - } - -} - -private class MockMessagesViewController: MessagesViewController, MessagesDisplayDelegate, MessagesLayoutDelegate { - - func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return 200 - } - - var dataProvider: MockMessagesDataSource! - - override func viewDidLoad() { - super.viewDidLoad() - - dataProvider = makeDataSource() - messagesCollectionView.messagesDisplayDelegate = self - messagesCollectionView.messagesDataSource = dataProvider - messagesCollectionView.messagesLayoutDelegate = self - - } - - fileprivate func makeDataSource() -> MockMessagesDataSource { - let dataSource = MockMessagesDataSource() - dataSource.messages.append(MockMessage(text: "Text 1", - sender: dataSource.senders[0], - messageId: "001")) - dataSource.messages.append(MockMessage(text: "Text 2", - sender: dataSource.senders[1], - messageId: "002")) - - return dataSource - } - - func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions { - return LocationMessageSnapshotOptions() - } -} diff --git a/Tests/SenderSpec.swift b/Tests/SenderSpec.swift deleted file mode 100644 index 56910a3d3..000000000 --- a/Tests/SenderSpec.swift +++ /dev/null @@ -1,49 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import Quick -import Nimble -@testable import MessageKit - -final class SenderSpec: QuickSpec { - - override func spec() { - describe("equality between two Senders") { - context("they have the same id ") { - it("should be equal") { - let sender1 = Sender(id: "1", displayName: "Steven") - let sender2 = Sender(id: "1", displayName: "Nathan") - expect(sender1).to(equal(sender2)) - } - } - context("they have a different id") { - it("should not be equal") { - let sender1 = Sender(id: "1", displayName: "Steven") - let sender2 = Sender(id: "2", displayName: "Nathan") - expect(sender1).toNot(equal(sender2)) - } - } - } - } -} diff --git a/Tests/Supporting Files/Info.plist b/Tests/Supporting Files/Info.plist deleted file mode 100644 index ba72822e8..000000000 --- a/Tests/Supporting Files/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - - diff --git a/Tests/ViewsTests/AvatarViewTests.swift b/Tests/ViewsTests/AvatarViewTests.swift deleted file mode 100644 index a6e9a00ed..000000000 --- a/Tests/ViewsTests/AvatarViewTests.swift +++ /dev/null @@ -1,85 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import XCTest -@testable import MessageKit - -class AvatarViewTests: XCTestCase { - - var avatarView: AvatarView! - - override func setUp() { - super.setUp() - avatarView = AvatarView() - avatarView.frame.size = CGSize(width: 30, height: 30) - } - - override func tearDown() { - super.tearDown() - avatarView = nil - } - - func testNoParams() { - XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) - XCTAssertEqual(avatarView.backgroundColor, UIColor.gray) - } - - func testWithImage() { - let avatar = Avatar(image: UIImage()) - avatarView.set(avatar: avatar) - XCTAssertEqual(avatar.initials, "?") - XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) - XCTAssertEqual(avatarView.backgroundColor, UIColor.gray) - } - - func testInitialsOnly() { - let avatar = Avatar(initials: "DL") - avatarView.set(avatar: avatar) - XCTAssertEqual(avatarView.initials, avatar.initials) - XCTAssertEqual(avatar.initials, "DL") - XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) - XCTAssertEqual(avatarView.backgroundColor, UIColor.gray) - } - - func testSetBackground() { - XCTAssertEqual(avatarView.backgroundColor, UIColor.gray) - avatarView.backgroundColor = UIColor.red - XCTAssertEqual(avatarView.backgroundColor, UIColor.red) - } - - func testGetImage() { - let image = UIImage() - let avatar = Avatar(image: image) - avatarView.set(avatar: avatar) - XCTAssertEqual(avatarView.image, image) - } - - func testRoundedCorners() { - let avatar = Avatar(image: UIImage()) - avatarView.set(avatar: avatar) - XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) - avatarView.setCorner(radius: 2) - XCTAssertEqual(avatarView.layer.cornerRadius, 2.0) - } -} diff --git a/Tests/ViewsTests/MessagesCollectionViewTests.swift b/Tests/ViewsTests/MessagesCollectionViewTests.swift deleted file mode 100644 index a2f35e0cf..000000000 --- a/Tests/ViewsTests/MessagesCollectionViewTests.swift +++ /dev/null @@ -1,50 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import XCTest -@testable import MessageKit - -class MessagesCollectionViewTests: XCTestCase { - - var messagesCollectionView: MessagesCollectionView! - let rect = CGRect(x: 0, y: 0, width: 100, height: 100) - let layout = MessagesCollectionViewFlowLayout() - - override func setUp() { - super.setUp() - messagesCollectionView = MessagesCollectionView(frame: rect, collectionViewLayout: layout) - } - - override func tearDown() { - messagesCollectionView = nil - super.tearDown() - } - - func testInit() { - XCTAssertEqual(messagesCollectionView.frame, rect) - XCTAssertEqual(messagesCollectionView.collectionViewLayout, layout) - XCTAssertEqual(messagesCollectionView.backgroundColor, .white) - } - -} diff --git a/VISION.md b/VISION.md deleted file mode 100644 index e071591fd..000000000 --- a/VISION.md +++ /dev/null @@ -1,25 +0,0 @@ -## MessageKit Vision - -### Goals -- Provide a suitable replacement (but not absolute mirror image) of JSQMessagesViewController. -- Provide β€œsensible defaults, but also customization hooks" - @jessesquires -- Favor a Swift-first, idiomatic API. -- Build a centralized MessageViewController project for iOS. -- Cultivate an inclusive open source community through respectful discussion. - -### Scope -#### Things we will not support: -- Views that live outside of the MessagesViewController such as photo pickers and media players. -- Anything that favors specific networking / caching strategies or specific libraries. - -Instead, MessageKit will provide you with hooks to easily handle these situations as you wish. We believe this is more flexible, maintainable, and reasonable. - -### Technical Considerations -- **iOS version**: -We will strive to support the 3 latest versions of iOS. - -- **Objective-C Compatibility**: -We will not sacrifice functionality or an idiomatic Swift API to support Objective-C, but would love to improve Objective-C compatability where possible. - -- **Layouts**: -We will favor programmatic layouts over `.xib`s where ever possible.