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/workflows/ci_pr_example.yml b/.github/workflows/ci_pr_example.yml index f50d6a2fc..d98c63fda 100644 --- a/.github/workflows/ci_pr_example.yml +++ b/.github/workflows/ci_pr_example.yml @@ -2,14 +2,19 @@ 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-latest + runs-on: macos-15 steps: - name: Checkout the Git repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 10 - name: Build and run example project - run: ./GitHubActions/build.sh example + run: make build_example diff --git a/.github/workflows/ci_pr_framework.yml b/.github/workflows/ci_pr_framework.yml index 345da23e7..08bd9b031 100644 --- a/.github/workflows/ci_pr_framework.yml +++ b/.github/workflows/ci_pr_framework.yml @@ -2,14 +2,17 @@ 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-latest - env: - DEVELOPER_DIR: /Applications/Xcode_12.app/Contents/Developer + runs-on: macos-15 steps: - name: Checkout the Git repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Build framework - run: ./GitHubActions/build.sh framework + run: make framework diff --git a/.github/workflows/ci_pr_tests.yml b/.github/workflows/ci_pr_tests.yml index 03ced7067..cd53f5472 100644 --- a/.github/workflows/ci_pr_tests.yml +++ b/.github/workflows/ci_pr_tests.yml @@ -2,17 +2,17 @@ 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-latest - env: - DEVELOPER_DIR: /Applications/Xcode_12.app/Contents/Developer + runs-on: macos-15 steps: - name: Checkout the Git repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Build and run tests - run: ./GitHubActions/build.sh tests - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1.0.13 - + run: make test diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml deleted file mode 100644 index 1d84a1390..000000000 --- a/.github/workflows/danger.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Danger - -on: - pull_request: - branches: - - master - -jobs: - danger: - name: Run Danger - runs-on: macOS-latest - steps: - - name: Checkout the Git repository - uses: actions/checkout@v2 - with: - fetch-depth: 0 -# - name: Cache Gems -# uses: actions/cache@v1 -# with: -# path: vendor/bundle -# key: ${{ runner.os }}-danger-${{ env.cache-name }}-gems-${{ hashFiles('**/Gemfile.lock') }} -# restore-keys: | -# ${{ runner.os }}-danger-${{ env.cache-name }}-gems- -# ${{ runner.os }}-danger-${{ env.cache-name }}- -# ${{ runner.os }}-danger- -# - name: Run build script -# run: gem install bundler && bundle install && bundle exec danger --fail-on-errors=true -# env: -# DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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 6e844af0b..65697789f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ build/ DerivedData .build/ +docs/ +docc/ ## Various settings *.pbxuser 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/.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 f0172b388..62d8e9482 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,18 +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_top_level_acl -explicit_top_level_acl: error -included: - - Sources + 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/CHANGELOG.md b/CHANGELOG.md index 874421802..9cdd2b6c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,138 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa ## 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) @@ -17,6 +149,8 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa ### 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 @@ -127,7 +261,7 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa ### Deprecated -- Deprecated `SenderType.id` in favour 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) +- 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 @@ -218,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) @@ -236,7 +375,7 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa - 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. @@ -244,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. @@ -396,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). @@ -602,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. @@ -636,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). @@ -656,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. @@ -692,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. @@ -723,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. @@ -741,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/Dangerfile b/Dangerfile index 5d75a1084..339e23910 100644 --- a/Dangerfile +++ b/Dangerfile @@ -1,7 +1,7 @@ # # MIT License # -# Copyright (c) 2017-2020 MessageKit +# 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 @@ -29,6 +29,11 @@ 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") @@ -42,9 +47,9 @@ 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.") + warn("Big Pull Request - Please consider splitting up your changes into smaller Pull Requests.") end swiftlint.config_file = '.swiftlint.yml' -swiftlint.binary_path = 'Example/Pods/SwiftLint/swiftlint' +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 94% rename from CHANGELOG_GUIDELINES.md rename to Documentation/CHANGELOG_GUIDELINES.md index 8851728d7..3470e2e49 100644 --- a/CHANGELOG_GUIDELINES.md +++ b/Documentation/CHANGELOG_GUIDELINES.md @@ -43,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 index bdff909a2..db52220f0 100644 --- a/Documentation/CUSTOM_CELLS.md +++ b/Documentation/CUSTOM_CELLS.md @@ -46,6 +46,10 @@ 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 @@ -69,7 +73,10 @@ internal class ConversationViewController: MessagesViewController { 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) diff --git a/Documentation/FAQs.md b/Documentation/FAQs.md index ae5ee5219..ae6943dde 100644 --- a/Documentation/FAQs.md +++ b/Documentation/FAQs.md @@ -6,6 +6,7 @@ - [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? @@ -108,4 +109,8 @@ func didTapMessage(in cell: MessageCollectionViewCell) { ## 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 `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 syncronous database lookups, keychain access, networking, etc. in your MessageKit delegate methods. You should instead cache what you need. +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/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 89dd180fb..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 } @@ -35,7 +35,7 @@ Third, each message must have a `sentDate` which represents the `Date` that each Fourth, each message must specify what kind of message it is through the `kind: MessageKind` property: ### [MessageKind](https://github.com/MessageKit/MessageKit#default-cells) -`MessageData` has 8 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`. @@ -105,7 +105,7 @@ let messages: [MessageType] = [] extension ChatViewController: MessagesDataSource { - func currentSender() -> SenderType { + var currentSender: SenderType { return Sender(senderId: "any_unique_id", displayName: "Steven") } 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 d58ccf9f5..49465a9fd 100644 --- a/Example/ChatExample.xcodeproj/project.pbxproj +++ b/Example/ChatExample.xcodeproj/project.pbxproj @@ -3,12 +3,15 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ - 13CCA04625793058005C19BB /* MessageKit in Frameworks */ = {isa = PBXBuildFile; productRef = 13CCA04525793058005C19BB /* MessageKit */; }; - 13CCA06325793E24005C19BB /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 13CCA06225793E24005C19BB /* Kingfisher */; }; + 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 */; }; @@ -26,7 +29,6 @@ 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 */; }; @@ -42,6 +44,11 @@ 882B5E901CF7D56000B6E160 /* ChatExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882B5E8E1CF7D56000B6E160 /* ChatExampleUITests.swift */; }; 882B5E951CF7D56E00B6E160 /* ChatExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882B5E931CF7D56E00B6E160 /* ChatExampleTests.swift */; }; 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 */ @@ -62,7 +69,11 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 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 = ""; }; @@ -80,7 +91,6 @@ 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 = ""; }; @@ -101,6 +111,11 @@ 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 = ""; }; + 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 */ @@ -108,9 +123,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 13CCA06325793E24005C19BB /* Kingfisher in Frameworks */, + 13EFA5D927AC5634003002CC /* Kingfisher in Frameworks */, 1C5433DF24C38DBF00A5383B /* SwiftUI.framework in Frameworks */, - 13CCA04625793058005C19BB /* MessageKit in Frameworks */, + 13EFA5D727AC5631003002CC /* MessageKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -132,6 +147,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 13EFA5CF27AC5368003002CC /* Packages */ = { + isa = PBXGroup; + children = ( + 13EFA5D027AC5368003002CC /* MessageKit */, + ); + name = Packages; + sourceTree = ""; + }; 1C5433E024C3A32400A5383B /* SwiftUI */ = { isa = PBXGroup; children = ( @@ -145,7 +168,10 @@ isa = PBXGroup; children = ( 1C5433E024C3A32400A5383B /* SwiftUI */, + 032A15E125965E1100E00FE3 /* CameraInputBarAccessoryView.swift */, 385C2920211FF32E0010B4BA /* CustomCell.swift */, + 9B49D541263DA6F9008804B5 /* CustomMessageContentCell.swift */, + 9B49D546263DAA29008804B5 /* CustomTextMessageContentCell.swift */, 385C2921211FF32E0010B4BA /* TableViewCells.swift */, ); path = Views; @@ -154,6 +180,8 @@ 385C2929211FF33D0010B4BA /* Models */ = { isa = PBXGroup; children = ( + 9B49D539263D9606008804B5 /* CustomLayoutSizeCalculator.swift */, + 9B49D52C263C3E1F008804B5 /* CustomTextLayoutSizeCalculator.swift */, 385C2926211FF33B0010B4BA /* MockMessage.swift */, 385C2925211FF33A0010B4BA /* MockSocket.swift */, 383B9EB22172A1C4008AB91A /* MockUser.swift */, @@ -172,6 +200,7 @@ 385C2933211FF3670010B4BA /* Extensions */ = { isa = PBXGroup; children = ( + 032A15DC25965D9A00E00FE3 /* AlertService.swift */, 385C292A211FF3450010B4BA /* Settings+UserDefaults.swift */, 385C292F211FF3630010B4BA /* UIColor+Extensions.swift */, 385C2930211FF3630010B4BA /* UIViewController+Extensions.swift */, @@ -195,12 +224,13 @@ 38CCCC582258419300DD5482 /* AutocompleteExampleViewController.swift */, 385C2940211FF38F0010B4BA /* BasicExampleViewController.swift */, 385C2941211FF38F0010B4BA /* ChatViewController.swift */, + 9B49D527263C31FC008804B5 /* CustomLayoutExampleViewController.swift */, 385C293F211FF38F0010B4BA /* LaunchViewController.swift */, 385C293D211FF38F0010B4BA /* MessageContainerController.swift */, - 385C293E211FF38F0010B4BA /* NavigationController.swift */, - 385C293C211FF38E0010B4BA /* SettingsViewController.swift */, - 63ECDABD24182A470016C349 /* MessageSubviewViewController.swift */, 63ECDAC1241889630016C349 /* MessageSubviewContainerViewController.swift */, + 63ECDABD24182A470016C349 /* MessageSubviewViewController.swift */, + 385C293C211FF38E0010B4BA /* SettingsViewController.swift */, + 138A04D328254AD300E2780F /* CustomInputBarExampleViewController.swift */, ); path = "View Controllers"; sourceTree = ""; @@ -238,6 +268,7 @@ 882B5E771CF7D53600B6E160 /* Sources */, 882B5E921CF7D56D00B6E160 /* Tests */, 882B5E8D1CF7D56000B6E160 /* UITests */, + 13EFA5CF27AC5368003002CC /* Packages */, 882B5E341CF7D4B900B6E160 /* Products */, 955AE3B8EFF8FDB8F572D3F7 /* Frameworks */, ); @@ -307,17 +338,17 @@ 882B5E301CF7D4B900B6E160 /* Frameworks */, 882B5E2F1CF7D4B900B6E160 /* Sources */, 882B5E311CF7D4B900B6E160 /* Resources */, - 7E0AFF9D207BB460004DFD4C /* ShellScript */, ); buildRules = ( ); dependencies = ( - 13CCA051257932DD005C19BB /* PBXTargetDependency */, + 13EFA5D527AC557F003002CC /* PBXTargetDependency */, + 13EFA5D327AC557A003002CC /* PBXTargetDependency */, ); name = ChatExample; packageProductDependencies = ( - 13CCA04525793058005C19BB /* MessageKit */, - 13CCA06225793E24005C19BB /* Kingfisher */, + 13EFA5D627AC5631003002CC /* MessageKit */, + 13EFA5D827AC5634003002CC /* Kingfisher */, ); productName = ChatExample; productReference = 882B5E331CF7D4B900B6E160 /* ChatExample.app */; @@ -327,7 +358,6 @@ isa = PBXNativeTarget; buildConfigurationList = 882B5E601CF7D4B900B6E160 /* Build configuration list for PBXNativeTarget "ChatExampleTests" */; buildPhases = ( - 9652D78EC571EF07E339DB3D /* [CP] Check Pods Manifest.lock */, 882B5E451CF7D4B900B6E160 /* Sources */, 882B5E461CF7D4B900B6E160 /* Frameworks */, 882B5E471CF7D4B900B6E160 /* Resources */, @@ -346,7 +376,6 @@ isa = PBXNativeTarget; buildConfigurationList = 882B5E631CF7D4B900B6E160 /* Build configuration list for PBXNativeTarget "ChatExampleUITests" */; buildPhases = ( - A41A5803A6544DC04D2BDB94 /* [CP] Check Pods Manifest.lock */, 882B5E501CF7D4B900B6E160 /* Sources */, 882B5E511CF7D4B900B6E160 /* Frameworks */, 882B5E521CF7D4B900B6E160 /* Resources */, @@ -437,73 +466,16 @@ }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - 7E0AFF9D207BB460004DFD4C /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "#\"${PODS_ROOT}/SwiftLint/swiftlint\"\n"; - }; - 9652D78EC571EF07E339DB3D /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - 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; - }; - A41A5803A6544DC04D2BDB94 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - 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; - }; -/* 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 */, @@ -514,8 +486,11 @@ 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 */, @@ -525,6 +500,7 @@ 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 */, ); @@ -549,9 +525,13 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 13CCA051257932DD005C19BB /* PBXTargetDependency */ = { + 13EFA5D327AC557A003002CC /* PBXTargetDependency */ = { isa = PBXTargetDependency; - productRef = 13CCA050257932DD005C19BB /* MessageKit */; + productRef = 13EFA5D227AC557A003002CC /* Kingfisher */; + }; + 13EFA5D527AC557F003002CC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = 13EFA5D427AC557F003002CC /* MessageKit */; }; 882B5E4B1CF7D4B900B6E160 /* PBXTargetDependency */ = { isa = PBXTargetDependency; @@ -626,7 +606,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -679,7 +659,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_VERSION = 5.0; @@ -693,13 +673,13 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 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 = 5.0; }; name = Debug; }; @@ -708,6 +688,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = "$(SRCROOT)/Sources/Resources/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -716,7 +697,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 5.0; }; name = Release; }; @@ -725,6 +705,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; INFOPLIST_FILE = Tests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -732,7 +713,6 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.ChatExampleTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ChatExample.app/ChatExample"; }; name = Debug; @@ -742,6 +722,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; INFOPLIST_FILE = Tests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -751,7 +732,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ChatExample.app/ChatExample"; }; name = Release; @@ -760,6 +740,7 @@ isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = UITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -767,7 +748,6 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.ChatExampleUITests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; TEST_TARGET_NAME = ChatExample; }; name = Debug; @@ -776,6 +756,7 @@ isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = UITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -785,7 +766,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 5.0; TEST_TARGET_NAME = ChatExample; }; name = Release; @@ -837,21 +817,26 @@ repositoryURL = "https://github.com/onevcat/Kingfisher"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 5.15.8; + minimumVersion = 8.0.0; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 13CCA04525793058005C19BB /* MessageKit */ = { + 13EFA5D227AC557A003002CC /* Kingfisher */ = { + isa = XCSwiftPackageProductDependency; + package = 13CCA06125793E24005C19BB /* XCRemoteSwiftPackageReference "Kingfisher" */; + productName = Kingfisher; + }; + 13EFA5D427AC557F003002CC /* MessageKit */ = { isa = XCSwiftPackageProductDependency; productName = MessageKit; }; - 13CCA050257932DD005C19BB /* MessageKit */ = { + 13EFA5D627AC5631003002CC /* MessageKit */ = { isa = XCSwiftPackageProductDependency; productName = MessageKit; }; - 13CCA06225793E24005C19BB /* Kingfisher */ = { + 13EFA5D827AC5634003002CC /* Kingfisher */ = { isa = XCSwiftPackageProductDependency; package = 13CCA06125793E24005C19BB /* XCRemoteSwiftPackageReference "Kingfisher" */; productName = Kingfisher; diff --git a/Example/ChatExample.xcworkspace/contents.xcworkspacedata b/Example/ChatExample.xcworkspace/contents.xcworkspacedata index 208b510b2..66edd0cf9 100644 --- a/Example/ChatExample.xcworkspace/contents.xcworkspacedata +++ b/Example/ChatExample.xcworkspace/contents.xcworkspacedata @@ -2,9 +2,6 @@ - - + location = "group:ChatExample.xcodeproj"> diff --git a/Example/MessageKit b/Example/MessageKit deleted file mode 120000 index 443220724..000000000 --- a/Example/MessageKit +++ /dev/null @@ -1 +0,0 @@ -../../MessageKit \ No newline at end of file diff --git a/Example/Sources/AppDelegate.swift b/Example/Sources/AppDelegate.swift index 5895d4cac..604e3eb43 100644 --- a/Example/Sources/AppDelegate.swift +++ b/Example/Sources/AppDelegate.swift @@ -1,52 +1,52 @@ -/* - 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. - */ +// 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 = splitViewController + window?.makeKeyAndVisible() - let masterViewController = NavigationController(rootViewController: LaunchViewController()) - let detailViewController = NavigationController() - let splitViewController = UISplitViewController() - splitViewController.viewControllers = [masterViewController, detailViewController] - splitViewController.preferredDisplayMode = .allVisible - - 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 + if UserDefaults.isFirstLaunch() { + // Enable Text Messages + UserDefaults.standard.set(true, forKey: "Text Messages") } + return true + } } diff --git a/Example/Sources/AudioController/BasicAudioController.swift b/Example/Sources/AudioController/BasicAudioController.swift index 3924dce77..9f239d3d8 100644 --- a/Example/Sources/AudioController/BasicAudioController.swift +++ b/Example/Sources/AudioController/BasicAudioController.swift @@ -1,214 +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. - */ +// 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 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 playing a sound - case playing + /// The audio controller is currently in pause state + case pause - /// The audio controller is currently in pause state - case pause - - /// The audio controller is not playing any sound and audioPlayer is nil - case stopped + /// 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. -open class BasicAudioController: NSObject, AVAudioPlayerDelegate { - - /// 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 - - // The `MessagesCollectionView` where the playing cell exist - public weak var messageCollectionView: MessagesCollectionView? - - /// The `Timer` that update playing progress - internal var progressTimer: Timer? - - // MARK: - Init Methods - - public init(messageCollectionView: MessagesCollectionView) { - self.messageCollectionView = messageCollectionView - super.init() +@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) } - - // MARK: - Methods - - /// Used to configure the audio cell UI: - /// 1. play button selected state; - /// 2. progresssView 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 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 becasue 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) } - - /// 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 message: 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) } - - /// 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 + 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) + } - /// 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() + } - // MARK: - Fire Methods - @objc private func didFireProgressTimer(_ timer: 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() - } - } - } + open func audioPlayerDecodeErrorDidOccur(_: AVAudioPlayer, error _: Error?) { + 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) - } + // MARK: Public - // MARK: - AVAudioPlayerDelegate - open func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { - stopAnyOngoingPlaying() - } + // The `MessagesCollectionView` where the playing cell exist + public weak var messageCollectionView: MessagesCollectionView? + + // MARK: Internal + + /// The `Timer` that update playing progress + internal var progressTimer: Timer? - open func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) { + // 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 b368af714..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-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. - */ +// 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 = 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. - */ - 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. - */ - 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. - */ - 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. - */ - 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 bb92f9549..ffe348807 100644 --- a/Example/Sources/Data Generation/SampleData.swift +++ b/Example/Sources/Data Generation/SampleData.swift @@ -1,286 +1,315 @@ -/* - 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-2019 MessageKit +import AVFoundation +import CoreLocation +import MessageKit +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: +final internal class SampleData { + // MARK: Lifecycle - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. + private init() { } - 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: Internal -import UIKit -import MessageKit -import CoreLocation -import AVFoundation + 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 + } -final internal class SampleData { + static let shared = SampleData() - static let shared = SampleData() + 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") - private init() {} + 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 - } + 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"]), + ] - 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") + var now = Date() - 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"]) - ] + 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")!, + ] - var currentSender: MockUser { - return steven - } + let emojis = [ + "👍", + "😂😂😂", + "👋👋👋", + "😱😱😱", + "😃😃😃", + "❤️", + ] - 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 attributes = ["Font1", "Font2", "Font3", "Font4", "Color", "Combo"] - 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 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")! - ] + let sounds: [URL] = [ + Bundle.main.url(forResource: "sound1", withExtension: "m4a")!, + Bundle.main.url(forResource: "sound2", withExtension: "m4a")!, + ] - 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")! - ) - } + 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")!) + } - 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) - } + var currentSender: MockUser { + steven + } - 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 randomMessageType() -> MessageTypes { - return MessageTypes.allCases.compactMap { - guard UserDefaults.standard.bool(forKey: "\($0.rawValue)" + " Messages") else { return nil } - return $0 - }.random()! + 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") } - // swiftlint:disable cyclomatic_complexity - func randomMessage(allowedSenders: [MockUser]) -> MockMessage { - let uniqueID = UUID().uuidString - let user = allowedSenders.random()! - let date = dateAddingRandomTime() + return NSAttributedString(attributedString: mutableAttributedString) + } - 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) - } + 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 } - // swiftlint:enable cyclomatic_complexity + } + + func randomMessageType() -> MessageTypes { + MessageTypes.allCases.compactMap { + guard UserDefaults.standard.bool(forKey: "\($0.rawValue)" + " Messages") else { return nil } + return $0 + }.random()! + } - func getMessages(count: Int, completion: ([MockMessage]) -> Void) { - var messages: [MockMessage] = [] - // Disable Custom Messages - UserDefaults.standard.set(false, forKey: "Custom Messages") - for _ in 0.. 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) } - - func getMessages(count: Int) -> [MockMessage] { - 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: [MockUser], 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: 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) - } + 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 index 37e25fac9..86e2c60bb 100644 --- a/Example/Sources/Extensions/Settings+UserDefaults.swift +++ b/Example/Sources/Extensions/Settings+UserDefaults.swift @@ -1,54 +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. - */ +// 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" - - // 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 + 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() } - - 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 + 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 8711dbbc0..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-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. - */ +// 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 eff46fa30..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-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. - */ +// 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/Layout/CustomMessageFlowLayout.swift b/Example/Sources/Layout/CustomMessageFlowLayout.swift index ecf406789..e2d7d4526 100644 --- a/Example/Sources/Layout/CustomMessageFlowLayout.swift +++ b/Example/Sources/Layout/CustomMessageFlowLayout.swift @@ -1,68 +1,71 @@ -/* - 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. - */ +// 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 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 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) + 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 12a365ba1..f47760936 100644 --- a/Example/Sources/Models/MockMessage.swift +++ b/Example/Sources/Models/MockMessage.swift @@ -1,170 +1,177 @@ -/* - 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. - */ +// 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 +import AVFoundation import CoreLocation +import Foundation import MessageKit -import AVFoundation - -private struct CoordinateItem: LocationItem { +import UIKit - var location: CLLocation - var size: CGSize +// MARK: - CoordinateItem - init(location: CLLocation) { - self.location = location - self.size = CGSize(width: 240, height: 240) - } +private struct CoordinateItem: LocationItem { + var location: CLLocation + var size: CGSize + init(location: CLLocation) { + self.location = location + size = CGSize(width: 240, height: 240) + } } -private struct ImageMediaItem: MediaItem { +// MARK: - ImageMediaItem - 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() - } - - init(imageURL: URL) { - self.url = imageURL - self.size = CGSize(width: 240, height: 240) - self.placeholderImage = UIImage(imageLiteralResourceName: "image_message_placeholder") - } +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 MockAudiotem: AudioItem { - - var url: URL - var size: CGSize - var duration: Float +// MARK: - MockAudioItem - init(url: URL) { - self.url = url - self.size = CGSize(width: 160, height: 35) - // compute duration - let audioAsset = AVURLAsset(url: url) - self.duration = Float(CMTimeGetSeconds(audioAsset.duration)) - } +private struct MockAudioItem: AudioItem { + var url: URL + var size: CGSize + var duration: Float + 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] = []) { - self.displayName = name - self.initials = initials - self.phoneNumbers = phoneNumbers - self.emails = emails - } - + 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 + } } +// MARK: - MockLinkItem + struct MockLinkItem: LinkItem { - let text: String? - let attributedText: NSAttributedString? - let url: URL - let title: String? - let teaser: String - let thumbnailImage: UIImage + let text: String? + let attributedText: NSAttributedString? + let url: URL + let title: String? + let teaser: String + let thumbnailImage: UIImage } -internal struct MockMessage: MessageType { +// MARK: - MockMessage - var messageId: String - var sender: SenderType { - return user - } - var sentDate: Date - var kind: MessageKind - - var user: MockUser - - private init(kind: MessageKind, user: MockUser, messageId: String, date: Date) { - self.kind = kind - self.user = user - self.messageId = messageId - self.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 = MockAudiotem(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) - } +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 bdf1b63f3..41c9f1b11 100644 --- a/Example/Sources/Models/MockSocket.swift +++ b/Example/Sources/Models/MockSocket.swift @@ -1,85 +1,94 @@ -/* - 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. - */ +// 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: [MockUser] = [] - - private init() {} - - @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 - } - - @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?() - } + // 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 index e4f2362fd..4274b2c2e 100644 --- a/Example/Sources/Models/MockUser.swift +++ b/Example/Sources/Models/MockUser.swift @@ -1,31 +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. - */ +// 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 + var senderId: String + var displayName: String } diff --git a/Example/Sources/Resources/Assets.xcassets/Contents.json b/Example/Sources/Resources/Assets.xcassets/Contents.json index da4a164c9..73c00596a 100644 --- a/Example/Sources/Resources/Assets.xcassets/Contents.json +++ b/Example/Sources/Resources/Assets.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} 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/View Controllers/AdvancedExampleViewController.swift b/Example/Sources/View Controllers/AdvancedExampleViewController.swift index 9e79232a2..d575e64cc 100644 --- a/Example/Sources/View Controllers/AdvancedExampleViewController.swift +++ b/Example/Sources/View Controllers/AdvancedExampleViewController.swift @@ -1,441 +1,530 @@ -/* - 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. - */ +// 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 MapKit -import MessageKit import InputBarAccessoryView import Kingfisher +import MapKit +import MessageKit +import UIKit -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") - } - - 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 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 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() - } - } - } - } - - 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.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() - } +// MARK: - AdvancedExampleViewController - 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 - } - let color = isOverLimit ? .red : UIColor(white: 0.6, alpha: 1) - item.setTitleColor(color, for: .normal) - } - let bottomItems = [.flexibleSpace, charCountButton] - - configureInputBarPadding() - - 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 neccesary - /// 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 - - } - - // 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].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) { - updateTitleView(title: "MessageKit", subtitle: isHidden ? "2 Online" : "Typing...") - setTypingIndicatorViewHidden(isHidden, animated: true, whilePerforming: updates) { [weak self] success in - if success, self?.isLastSectionVisible() == true { - self?.messagesCollectionView.scrollToLastItem(animated: true) - } - } - } - - 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 { - 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) +final class AdvancedExampleViewController: ChatViewController { + // 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") + } + + // 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) + } + + 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) + } + + // 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() } + } } - - // 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") - } + } - // 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 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 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) - } - - // 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]) + } + } + } + + 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) + } + } + } + + // 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 + } + + // MARK: Private + + 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)]) + let color = isOverLimit ? .red : UIColor(white: 0.6, alpha: 1) + item.setTitleColor(color, for: .normal) + } + let bottomItems = [.flexibleSpace, charCountButton] + + configureInputBarPadding() + + 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 } - 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 - } + self.navigationController?.present(actionSheet, animated: true, completion: nil) + } + } } -// MARK: - MessagesDisplayDelegate +// MARK: MessagesDisplayDelegate extension AdvancedExampleViewController: 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 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) + } + } + + 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) + { + 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() + } + } + + // 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: - 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] { - 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 message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] { - return [.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag] - } - - // 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 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 - 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) - } +// MARK: MessagesLayoutDelegate - func configureMediaMessageImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { - if case MessageKind.photo(let media) = message.kind, let imageURL = media.url { - imageView.kf.setImage(with: imageURL) - } else { - imageView.kf.cancelDownloadTask() - } - } - - // 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 snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions { - - return LocationMessageSnapshotOptions(showsBuildings: true, showsPointsOfInterest: true, span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)) +extension AdvancedExampleViewController: MessagesLayoutDelegate { + func cellTopLabelHeight(for _: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat { + if isTimeLabelVisible(at: indexPath) { + return 18 } + return 0 + } - // MARK: - Audio Messages - - func audioTintColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { - return self.isFromCurrentSender(message: message) ? .white : .primaryColor + 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 configureAudioCell(_ cell: AudioMessageCell, message: MessageType) { - audioController.configureAudioCell(cell, message: message) // this is needed especily when the cell is reconfigure while is playing sound - } - + func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat { + (!isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message)) ? 16 : 0 + } } -// 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 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 - } - } +// 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 index 63ce2acfa..c5b2de5da 100644 --- a/Example/Sources/View Controllers/AutocompleteExampleViewController.swift +++ b/Example/Sources/View Controllers/AutocompleteExampleViewController.swift @@ -1,389 +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. - */ +// 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 MessageKit import InputBarAccessoryView import Kingfisher +import MessageKit +import UIKit -final class AutocompleteExampleViewController: ChatViewController { - - 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 hastagAutocompletes: [AutocompleteCompletion] = { - var array: [AutocompleteCompletion] = [] - for _ in 1...100 { - array.append(AutocompleteCompletion(text: Lorem.word(), context: nil)) - } - return array - }() - - // Completions loaded async that get appeneded 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) - } - - 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 - } - } - - @objc - func joinChat() { - configureMessageInputBarForChat() - } - - // 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].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) - } - } - } - - 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") - } - } - - // 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 - } +// MARK: - AutocompleteExampleViewController - // Async autocomplete requires the manager to reload - func inputBar(_ inputBar: InputBarAccessoryView, textViewTextDidChangeTo text: 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() - } - } - } +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") + } + } } -extension AutocompleteExampleViewController: AutocompleteManagerDelegate, AutocompleteManagerDataSource { +// MARK: AutocompleteManagerDelegate, AutocompleteManagerDataSource - // MARK: - AutocompleteManagerDataSource - - func autocompleteManager(_ manager: AutocompleteManager, autocompleteSourceFor prefix: String) -> [AutocompleteCompletion] { - - if prefix == "@" { - return SampleData.shared.senders - .map { user in - return AutocompleteCompletion(text: user.displayName, - context: ["id": user.senderId]) - } - } else if prefix == "#" { - return hastagAutocompletes + 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 { return $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(_ manager: AutocompleteManager, shouldBecomeVisible: Bool) { - setAutocompleteManager(active: shouldBecomeVisible) - } - - // Optional - func autocompleteManager(_ manager: AutocompleteManager, shouldRegister prefix: String, at range: NSRange) -> Bool { - return true - } - - // Optional - func autocompleteManager(_ manager: AutocompleteManager, shouldUnregister prefix: String) -> Bool { - return true - } - - // Optional - func autocompleteManager(_ manager: AutocompleteManager, shouldComplete prefix: String, with text: String) -> Bool { - return 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() +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]) } - messageInputBar.invalidateIntrinsicContentSize() - } + } 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 +// MARK: MessagesDisplayDelegate extension AutocompleteExampleViewController: 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] { - 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 message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] { - return [.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag] - } - - // 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 messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle { - return .bubble - } - - 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 - 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: IndexPath, in messagesCollectionView: MessagesCollectionView) { - if case MessageKind.photo(let media) = message.kind, let imageURL = media.url { - imageView.kf.setImage(with: imageURL) - } else { - imageView.kf.cancelDownloadTask() - } - } + // 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 +// MARK: MessagesLayoutDelegate extension AutocompleteExampleViewController: MessagesLayoutDelegate { - - func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - if isTimeLabelVisible(at: indexPath) { - return 18 - } - return 0 - } - - 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 : 0 - } + func cellTopLabelHeight(for _: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat { + if isTimeLabelVisible(at: indexPath) { + return 18 } + return 0 + } - func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return (!isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message)) ? 16 : 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 d43cb8d43..9f1994faa 100644 --- a/Example/Sources/View Controllers/BasicExampleViewController.swift +++ b/Example/Sources/View Controllers/BasicExampleViewController.swift @@ -1,140 +1,169 @@ -/* - 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. - */ +// 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 Kingfisher +import UIKit + +// MARK: - BasicExampleViewController class BasicExampleViewController: ChatViewController { - override func configureMessageCollectionView() { - super.configureMessageCollectionView() - messagesCollectionView.messagesLayoutDelegate = self - messagesCollectionView.messagesDisplayDelegate = self - } + 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] { - switch detector { - case .hashtag, .mention: return [.foregroundColor: UIColor.blue] - default: return MessageLabel.defaultAttributes - } - } - - func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] { - return [.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag] - } - - // 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) - } + } - func configureMediaMessageImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { - if case MessageKind.photo(let media) = message.kind, let imageURL = media.url { - imageView.kf.setImage(with: imageURL) - } else { - imageView.kf.cancelDownloadTask() - } - } - - // 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 snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions { - - return LocationMessageSnapshotOptions(showsBuildings: true, showsPointsOfInterest: true, span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)) - } + func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) { + let avatar = SampleData.shared.getAvatarFor(sender: message.sender) + avatarView.set(avatar: avatar) + } - // MARK: - Audio Messages + 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 audioTintColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { - return 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 especily when the cell is reconfigure while is playing sound - } + // 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 cellBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return 17 - } - - 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 b5e71b8b4..aeb2171ae 100644 --- a/Example/Sources/View Controllers/ChatViewController.swift +++ b/Example/Sources/View Controllers/ChatViewController.swift @@ -1,355 +1,372 @@ -/* -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. -*/ +// +// 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 MessageKit import InputBarAccessoryView +import MessageKit +import UIKit + +// MARK: - ChatViewController /// A base class for the example controllers class ChatViewController: MessagesViewController, MessagesDataSource { - - // MARK: - Public properties - - /// The `BasicAudioController` controll the AVAudioPlayer state (play, pause, stop) and udpate 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: - Private properties - - private let formatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateStyle = .medium - return formatter - }() - - // MARK: - Lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - - configureMessageCollectionView() - configureMessageInputBar() - loadFirstMessages() - title = "MessageKit" - } - - 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() - } - - override var preferredStatusBarStyle: UIStatusBarStyle { - return .lightContent - } - - 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() - } - } - } - } - - @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 - - scrollsToLastItemOnKeyboardBeginsEditing = true // default false - maintainPositionOnKeyboardFrameChanged = 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) - } - - // MARK: - MessagesDataSource - - func currentSender() -> SenderType { - 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 cellBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { - return NSAttributedString(string: "Read", attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10), NSAttributedString.Key.foregroundColor: UIColor.darkGray]) - } - - 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 didTapImage(in cell: MessageCollectionViewCell) { - print("Image tapped") - } - - func didTapCellTopLabel(in cell: MessageCollectionViewCell) { - print("Top cell label tapped") - } - - func didTapCellBottomLabel(in cell: MessageCollectionViewCell) { - print("Bottom cell label tapped") - } - - func didTapMessageTopLabel(in cell: MessageCollectionViewCell) { - print("Top message label tapped") - } - - func didTapMessageBottomLabel(in cell: 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 - } - 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 - } - 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 cell: AudioMessageCell) { - print("Did start playing audio sound") - } - - func didPauseAudio(in cell: AudioMessageCell) { - print("Did pause audio sound") - } - - func didStopAudio(in cell: AudioMessageCell) { - print("Did stop audio sound") - } - - func didTapAccessoryView(in cell: MessageCollectionViewCell) { - print("Accessory view 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 + } + 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 + } + 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 didSelectTransitInformation(_ transitInformation: [String: String]) { - print("TransitInformation Selected: \(transitInformation)") - } + func didSelectAddress(_ addressComponents: [String: String]) { + print("Address Selected: \(addressComponents)") + } - func didSelectHashtag(_ hashtag: String) { - print("Hashtag selected: \(hashtag)") - } + func didSelectDate(_ date: Date) { + print("Date Selected: \(date)") + } - func didSelectMention(_ mention: String) { - print("Mention selected: \(mention)") - } - - func didSelectCustom(_ pattern: String, match: String?) { - print("Custom data detector patter selected: \(pattern)") - } -} + func didSelectPhoneNumber(_ phoneNumber: String) { + print("Phone Number Selected: \(phoneNumber)") + } -// MARK: - MessageInputBarDelegate + func didSelectURL(_ url: URL) { + print("URL Selected: \(url)") + } -extension ChatViewController: InputBarAccessoryViewDelegate { + func didSelectTransitInformation(_ transitInformation: [String: String]) { + print("TransitInformation Selected: \(transitInformation)") + } - @objc - func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) { - processInputBar(messageInputBar) - } + func didSelectHashtag(_ hashtag: String) { + print("Hashtag selected: \(hashtag)") + } - 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 + func didSelectMention(_ mention: String) { + print("Mention selected: \(mention)") + } - let substring = attributedText.attributedSubstring(from: range) - let context = substring.attribute(.autocompletedContext, at: 0, effectiveRange: nil) - print("Autocompleted: `", substring, "` with context: ", context ?? []) - } + func didSelectCustom(_ pattern: String, match _: String?) { + print("Custom data detector patter selected: \(pattern)") + } +} - 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: InputBarAccessoryViewDelegate - 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) - } - } - } +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 ?? "-") + } + + 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: 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 55d110d8c..90c42b894 100644 --- a/Example/Sources/View Controllers/LaunchViewController.swift +++ b/Example/Sources/View Controllers/LaunchViewController.swift @@ -1,120 +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. - */ +// 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 SafariServices import SwiftUI +import UIKit final internal class LaunchViewController: UITableViewController { - - override var preferredStatusBarStyle: UIStatusBarStyle { - return .lightContent - } + // MARK: Lifecycle - let cells = ["Basic Example", "Advanced Example", "Autocomplete Example", "Embedded Example", "Subview Example", "SwiftUI 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) - navigationController?.navigationBar.prefersLargeTitles = true - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - 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 - - // swiftlint:disable cyclomatic_complexity - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let cell = cells[indexPath.row] - switch cell { - case "Basic Example": - let viewController = BasicExampleViewController() - let detailViewController = NavigationController(rootViewController: viewController) - splitViewController?.showDetailViewController(detailViewController, sender: self) - case "Advanced Example": - let viewController = AdvancedExampleViewController() - let detailViewController = NavigationController(rootViewController: viewController) - splitViewController?.showDetailViewController(detailViewController, sender: self) - case "Autocomplete Example": - let viewController = AutocompleteExampleViewController() - let detailViewController = NavigationController(rootViewController: viewController) - splitViewController?.showDetailViewController(detailViewController, sender: self) - case "Embedded Example": - navigationController?.pushViewController(MessageContainerController(), animated: true) - case "SwiftUI Example": - if #available(iOS 13, *) { - navigationController?.pushViewController(UIHostingController(rootView: SwiftUIExampleView()), animated: true) - } - case "Settings": - let viewController = SettingsViewController() - let detailViewController = NavigationController(rootViewController: viewController) - splitViewController?.showDetailViewController(detailViewController, sender: self) - case "Subview Example": - let viewController = MessageSubviewContainerViewController() - let detailViewController = NavigationController(rootViewController: viewController) - splitViewController?.showDetailViewController(detailViewController, sender: self) - 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) - webViewController.preferredControlTintColor = .primaryColor - splitViewController?.showDetailViewController(webViewController, sender: self) + } + + 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 e48d65840..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-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. - */ +// 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) - 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 index 4ddbb3737..ed6e116c0 100644 --- a/Example/Sources/View Controllers/MessageSubviewContainerViewController.swift +++ b/Example/Sources/View Controllers/MessageSubviewContainerViewController.swift @@ -1,57 +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. -*/ +// +// 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 { - return .lightContent - } - - let messageSubviewViewController = MessageSubviewViewController() - - override func viewDidLoad() { - super.viewDidLoad() - - messageSubviewViewController.willMove(toParent: self) - addChild(messageSubviewViewController) - view.addSubview(messageSubviewViewController.view) - messageSubviewViewController.didMove(toParent: self) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationController?.navigationBar.isTranslucent = true - navigationController?.navigationBar.barTintColor = .clear - } + override var preferredStatusBarStyle: UIStatusBarStyle { + .lightContent + } - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - navigationController?.navigationBar.isTranslucent = false - navigationController?.navigationBar.barTintColor = .primaryColor - } + 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 index 0481d5e34..04d128891 100644 --- a/Example/Sources/View Controllers/MessageSubviewViewController.swift +++ b/Example/Sources/View Controllers/MessageSubviewViewController.swift @@ -1,51 +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. -*/ +// +// 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 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() - private var keyboardManager = KeyboardManager() + 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) + } - private let subviewInputBar = InputBarAccessoryView() + 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 viewDidLoad() { - super.viewDidLoad() - subviewInputBar.delegate = self - // Take into account the height of the bottom input bar - additionalBottomInset = 88 - } + override func inputBar(_: InputBarAccessoryView, didPressSendButtonWith _: String) { + processInputBar(subviewInputBar) + } - override func didMove(toParent parent: UIViewController?) { - super.didMove(toParent: parent) - parent?.view.addSubview(subviewInputBar) - keyboardManager.bind(inputAccessoryView: subviewInputBar) - } + // MARK: Private - override func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) { - processInputBar(subviewInputBar) - } + 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 c47cace57..000000000 --- a/Example/Sources/View Controllers/NavigationController.swift +++ /dev/null @@ -1,61 +0,0 @@ -/* - 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 - -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] - 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] - 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] - navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.black] - } - } - -} diff --git a/Example/Sources/View Controllers/SettingsViewController.swift b/Example/Sources/View Controllers/SettingsViewController.swift index 137e1f3aa..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-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. - */ +// 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 + +// 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", "Photo from URL Messages", "Video Messages", "Audio Messages", "Emoji Messages", "Location Messages", "Url Messages", "Phone Messages", "ShareContact 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) - UserDefaults.standard.synchronize() + } + + @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 a229f344b..55330ddb4 100644 --- a/Example/Sources/Views/CustomCell.swift +++ b/Example/Sources/Views/CustomCell.swift @@ -1,64 +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. - */ +// 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 index ce4a7fb9e..c6f6fcbae 100644 --- a/Example/Sources/Views/SwiftUI/MessagesView.swift +++ b/Example/Sources/Views/SwiftUI/MessagesView.swift @@ -1,147 +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. - */ +// 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 SwiftUI -import MessageKit 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) - } + 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) + } } -@available(iOS 13.0, *) +// MARK: - MessagesView + struct MessagesView: UIViewControllerRepresentable { - - @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.maintainPositionOnKeyboardFrameChanged = true // default false - messagesVC.showMessageTimestampOnSwipeLeft = true // default false - - return messagesVC - } - - func updateUIViewController(_ uiViewController: MessagesViewController, context: Context) { - uiViewController.messagesCollectionView.reloadData() - scrollToBottom(uiViewController) - } - - 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 inital scroll flash by - uiViewController.messagesCollectionView.scrollToLastItem(animated: self.initialized) - self.initialized = true - } - } - - func makeCoordinator() -> Coordinator { - return Coordinator(messages: $messages) - } - - final class Coordinator { - - let formatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateStyle = .medium - return formatter - }() - - var messages: Binding<[MessageType]> - init(messages: Binding<[MessageType]>) { - self.messages = messages - } - + // 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 + } + } } -@available(iOS 13.0, *) +// MARK: - MessagesView.Coordinator + MessagesDataSource + extension MessagesView.Coordinator: MessagesDataSource { - func currentSender() -> SenderType { - return SampleData.shared.currentSender - } - - func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType { - return messages.wrappedValue[indexPath.section] - } - - func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int { - return messages.wrappedValue.count - } - - 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)]) - } + var currentSender: SenderType { + SampleData.shared.currentSender + } - func messageTimestampLabelAttributedText(for message: MessageType, at indexPath: 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]) - } + 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]) + } } -@available(iOS 13.0, *) +// 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 = "" - } + 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 = "" + } } -@available(iOS 13.0, *) +// MARK: - MessagesView.Coordinator + MessagesLayoutDelegate, MessagesDisplayDelegate + extension MessagesView.Coordinator: MessagesLayoutDelegate, MessagesDisplayDelegate { - 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) - } - 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 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 index 41d3b0f26..b4fd02bc0 100644 --- a/Example/Sources/Views/SwiftUI/SwiftUIExampleView.swift +++ b/Example/Sources/Views/SwiftUI/SwiftUIExampleView.swift @@ -6,38 +6,53 @@ // Copyright © 2020 MessageKit. All rights reserved. // -import SwiftUI import MessageKit +import SwiftUI + +// MARK: - SwiftUIExampleView -@available(iOS 13.0, *) struct SwiftUIExampleView: View { - - @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) - } - - private func connectToMessageSocket() { - MockSocket.shared.connect(with: [SampleData.shared.nathan, SampleData.shared.wu]).onNewMessage { message in - self.messages.append(message) - } + // 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 func cleanupSocket() { - MockSocket.shared.disconnect() + 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() + } } -@available(iOS 13.0, *) +// MARK: - SwiftUIExampleView_Previews + struct SwiftUIExampleView_Previews: PreviewProvider { - static var previews: some View { - SwiftUIExampleView() - } + static var previews: some View { + SwiftUIExampleView() + } } diff --git a/Example/Sources/Views/TableViewCells.swift b/Example/Sources/Views/TableViewCells.swift index c03081d81..fb4cc5d0a 100644 --- a/Example/Sources/Views/TableViewCells.swift +++ b/Example/Sources/Views/TableViewCells.swift @@ -1,62 +1,63 @@ -/* - 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. - */ +// 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 df4df7276..8c810b170 100644 --- a/Example/UITests/ChatExampleUITests.swift +++ b/Example/UITests/ChatExampleUITests.swift @@ -20,27 +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. - XCUIApplication().launch() - - // 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 index f2e566d0c..a8a0b8f14 100644 --- a/Gemfile +++ b/Gemfile @@ -22,5 +22,5 @@ # SOFTWARE. source 'https://rubygems.org' -gem 'danger', '~> 6.2' -gem 'danger-swiftlint', '~> 0.24' +gem 'danger', '~> 8.6' +gem 'danger-swiftlint', '~> 0.29' diff --git a/Gemfile.lock b/Gemfile.lock index 62e89c6a6..57adcd5ab 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,9 @@ GEM remote: https://rubygems.org/ specs: - addressable (2.7.0) - public_suffix (>= 2.0.2, < 5.0) - claide (1.0.3) + addressable (2.8.1) + public_suffix (>= 2.0.2, < 6.0) + claide (1.1.0) claide-plugins (0.9.2) cork nap @@ -11,58 +11,81 @@ GEM colored2 (3.1.2) cork (0.3.0) colored2 (~> 3.1) - danger (6.3.2) + danger (8.6.1) claide (~> 1.0) claide-plugins (>= 0.9.2) colored2 (~> 3.1) cork (~> 0.1) - faraday (~> 0.9) + faraday (>= 0.9.0, < 2.0) faraday-http-cache (~> 2.0) - git (~> 1.6) - kramdown (~> 2.0) + git (~> 1.7) + kramdown (~> 2.3) kramdown-parser-gfm (~> 1.0) no_proxy_fix octokit (~> 4.7) - terminal-table (~> 1) - danger-swiftlint (0.24.5) + terminal-table (>= 1, < 4) + danger-swiftlint (0.30.2) danger rake (> 10) thor (~> 0.19) - faraday (0.17.4) - multipart-post (>= 1.2, < 3) - faraday-http-cache (2.2.0) + 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) - git (1.8.1) + 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.3.1) + kramdown (2.4.0) rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) - multipart-post (2.1.1) + multipart-post (2.2.3) nap (1.1.0) no_proxy_fix (0.1.2) - octokit (4.20.0) - faraday (>= 0.9) - sawyer (~> 0.8.0, >= 0.5.3) + octokit (4.25.1) + faraday (>= 1, < 3) + sawyer (~> 0.9) open4 (1.3.4) - public_suffix (4.0.6) - rake (13.0.3) + public_suffix (5.0.1) + rake (13.0.6) rchardet (1.8.0) - rexml (3.2.4) - sawyer (0.8.2) + rexml (3.3.9) + ruby2_keywords (0.0.5) + sawyer (0.9.2) addressable (>= 2.3.5) - faraday (> 0.8, < 2.0) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) + faraday (>= 0.17.3, < 3) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) thor (0.20.3) - unicode-display_width (1.7.0) + unicode-display_width (2.3.0) PLATFORMS ruby DEPENDENCIES - danger (~> 6.2) - danger-swiftlint (~> 0.24) + danger (~> 8.6) + danger-swiftlint (~> 0.29) BUNDLED WITH 2.1.4 diff --git a/GitHubActions/build.sh b/GitHubActions/build.sh deleted file mode 100755 index a040ab0fd..000000000 --- a/GitHubActions/build.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash - -# -# 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. - -set -e -function trap_handler { - echo -e "\n\nOh no! You walked directly into the slavering fangs of a lurking grue!" - echo "**** You have died ****" - exit 255 -} -trap trap_handler INT TERM EXIT - -MODE="$1" - -if [ "$MODE" = "tests" -o "$MODE" = "all" ]; then - echo "Running MessageKit tests." - set -o pipefail && xcodebuild test -scheme MessageKit -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 11" | xcpretty -c - success="1" -fi - -if [ "$MODE" = "framework" -o "$MODE" = "all" ]; then - echo "Building MessageKit Framework." - set -o pipefail && xcodebuild build -scheme MessageKit -destination "platform=iOS Simulator,name=iPhone 11" | xcpretty -c - success="1" -fi - -if [ "$MODE" = "example" -o "$MODE" = "all" ]; then - echo "Building & testing MessageKit Example app." - cd Example - set -o pipefail && xcodebuild build analyze -scheme ChatExample -destination "platform=iOS Simulator,name=iPhone 11" CODE_SIGNING_REQUIRED=NO | xcpretty -c - success="1" -fi - -if [ "$success" = "1" ]; then -trap - EXIT -exit 0 -fi - -echo "Unrecognised mode '$MODE'." diff --git a/LICENSE.md b/LICENSE.md index 20cc288d5..4731930ad 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017-2020 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 35fdfe813..000000000 --- a/MessageKit.podspec +++ /dev/null @@ -1,21 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'MessageKit' - s.version = '3.5.1' - 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.swift_version = '5.3' - - s.ios.deployment_target = '12.0' - s.ios.resource_bundle = { 'MessageKit' => 'Sources/Assets.xcassets' } - - s.dependency 'InputBarAccessoryView', '~> 5.3.0' - -end diff --git a/Package.swift b/Package.swift index eaf578e2c..ca0bcbd50 100644 --- a/Package.swift +++ b/Package.swift @@ -1,40 +1,39 @@ -// swift-tools-version:5.3 -// The swift-tools-version declares the minimum version of Swift required to build this package. +// swift-tools-version:6.0 -/* - 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. - */ +// 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(.v12)], + platforms: [.iOS(.v14)], products: [ - .library(name: "MessageKit", targets: ["MessageKit"]), + .library(name: "MessageKit", targets: ["MessageKit"]) ], dependencies: [ - .package(url: "https://github.com/nathantannar4/InputBarAccessoryView", .upToNextMajor(from: "5.3.0")) + .package(url: "https://github.com/nathantannar4/InputBarAccessoryView", .upToNextMajor(from: "7.0.0")), ], targets: [ + // MARK: - MessageKit + .target( name: "MessageKit", dependencies: ["InputBarAccessoryView"], @@ -42,7 +41,7 @@ let package = Package( exclude: ["Supporting/Info.plist", "Supporting/MessageKit.h"], swiftSettings: [SwiftSetting.define("IS_SPM")] ), - .testTarget(name: "MessageKitTests", dependencies: ["MessageKit"]) + .testTarget(name: "MessageKitTests", dependencies: ["MessageKit"]), ], - swiftLanguageVersions: [.v5] + swiftLanguageModes: [.v6] ) diff --git a/README.md b/README.md index a9109f5ba..260d48555 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,25 @@

- - - A community-driven replacement for JSQMessagesViewController https://messagekit.github.io +

- -[![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) - -[![codecov](https://codecov.io/gh/MessageKit/MessageKit/branch/master/graph/badge.svg)](https://codecov.io/gh/MessageKit/MessageKit) - -Swift - - -CocoaPods - - -Xcode - - -MIT - - -Contributions Welcome - - +

+ A community-driven replacement for JSQMessagesViewController +

+

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

+

@@ -38,34 +32,9 @@ - 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 5.3 -pod 'MessageKit' -``` - -> For Swift 5.0 use version 3.3.0 - -```ruby -# Swift 5.0 -pod 'MessageKit', '~> 3.3.0' -``` - -> For Swift 4.2 use version 3.0.0 - -```ruby -# Swift 4.2 -pod 'MessageKit', '~> 3.0.0' -``` - -### [Swift Package Manager](https://swift.org/package-manager/) +### [Swift Package Manager](https://swift.org/package-manager/) - **Recommended** 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 @@ -73,21 +42,33 @@ You can [just add](https://developer.apple.com/documentation/xcode/adding_packag ``` https://github.com/MessageKit/MessageKit ``` -Older versions of Swift and XCode don't support MessageKit via SPM. +Older versions of Swift and Xcode don't support MessageKit via SPM. ### [Manual](https://github.com/MessageKit/MessageKit/blob/master/Documentation/MANUAL_INSTALLATION.md) ## Requirements -- **iOS 12** or later -- **Swift 5.3** 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.0 +> For iOS 9 and iOS 10 please use version 3.1.1 ## Getting Started +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). + +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. + +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

@@ -98,21 +79,13 @@ Each default cell is a subclass of [`MessageContentCell`](https://github.com/Mes 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. -### MessageInputBar Structure +### InputBarAccessoryView 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. - -### Guides - -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). - -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. - -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 @@ -150,11 +123,16 @@ If you choose to use the `.custom` kind you are responsible for all of the cells ## 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. +- 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. @@ -177,6 +155,9 @@ 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) @@ -191,7 +172,12 @@ Add your app to the list of apps using this library and make a pull request. - [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 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/Controllers/MessagesViewController+Keyboard.swift b/Sources/Controllers/MessagesViewController+Keyboard.swift index 59e0926e1..974fa1077 100644 --- a/Sources/Controllers/MessagesViewController+Keyboard.swift +++ b/Sources/Controllers/MessagesViewController+Keyboard.swift @@ -1,144 +1,152 @@ -/* - 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. - */ - +// 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 UIKit import InputBarAccessoryView +import UIKit -internal extension MessagesViewController { - - // MARK: - Register / Unregister Observers - - 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) - } - - 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 scrollsToLastItemOnKeyboardBeginsEditing || scrollsToLastItemOnKeyboardBeginsEditing { - guard - let inputTextView = notification.object as? InputTextView, - inputTextView === messageInputBar.inputTextView - else { - return - } - if scrollsToLastItemOnKeyboardBeginsEditing { - messagesCollectionView.scrollToLastItem() - } else { - messagesCollectionView.scrollToLastItem(animated: true) - } - } - } - - @objc - private func handleKeyboardDidChangeState(_ notification: Notification) { - guard !isMessagesControllerBeingDismissed else { return } - - guard let keyboardStartFrameInScreenCoords = notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? CGRect else { return } - guard !keyboardStartFrameInScreenCoords.isEmpty || UIDevice.current.userInterfaceIdiom != .pad 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 - } - - guard self.presentedViewController == nil else { - // This is important to skip notifications from child modal controllers in iOS >= 13.0 - 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 - - UIView.performWithoutAnimation { - messageCollectionViewBottomInset = newBottomInset - } - - if maintainPositionOnKeyboardFrameChanged && differenceOfBottomInset != 0 { - let contentOffset = CGPoint(x: messagesCollectionView.contentOffset.x, y: messagesCollectionView.contentOffset.y + differenceOfBottomInset) - // Changing contentOffset to bigger number than the contentSize will result in a jump of content - // https://github.com/MessageKit/MessageKit/issues/1486 - guard contentOffset.y <= messagesCollectionView.contentSize.height else { - return - } - messagesCollectionView.setContentOffset(contentOffset, animated: false) - } - } - - // MARK: - Inset Computation - - 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 || (messagesCollectionView.frame.maxY - intersection.maxY) > 0.001 { - // 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) +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 } - - func requiredInitialScrollViewBottomInset() -> CGFloat { - let inputAccessoryViewHeight = inputAccessoryView?.frame.height ?? 0 - return max(0, inputAccessoryViewHeight + additionalBottomInset - automaticallyAddedBottomInset) - } - - /// 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 { - return messagesCollectionView.adjustedContentInset.bottom - messagesCollectionView.contentInset.bottom + } + + // 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 } + messagesCollectionView.scrollToLastItem() + } } diff --git a/Sources/Controllers/MessagesViewController+Menu.swift b/Sources/Controllers/MessagesViewController+Menu.swift index 67e5b8cc4..7aa07a36d 100644 --- a/Sources/Controllers/MessagesViewController+Menu.swift +++ b/Sources/Controllers/MessagesViewController+Menu.swift @@ -1,100 +1,113 @@ -/* - 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. - */ +// 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 -internal extension MessagesViewController { +extension MessagesViewController { + // MARK: Internal - // MARK: - Register / Unregister Observers + // MARK: - Register / Unregister Observers - func addMenuControllerObservers() { - NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(_:)), name: UIMenuController.willShowMenuNotification, object: nil) - } - - 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 f9c4d5c5c..41965d5a4 100644 --- a/Sources/Controllers/MessagesViewController.swift +++ b/Sources/Controllers/MessagesViewController.swift @@ -1,508 +1,403 @@ -/* - 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. - */ - +// 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 UIKit import InputBarAccessoryView +import UIKit /// A subclass of `UIViewController` with a `MessagesCollectionView` object /// that is used to display conversation interfaces. -open class MessagesViewController: UIViewController, -UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, UIGestureRecognizerDelegate { - - /// 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() - - /// 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 - open var scrollsToLastItemOnKeyboardBeginsEditing: Bool = false - - /// 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`. - /// NOTE: This is related to `scrollToBottom` whereas the above flag is related to `scrollToLastItem` - check each function for differences - @available(*, deprecated, message: "Control scrolling to bottom on keyboardBeginEditing by using scrollsToLastItemOnKeyboardBeginsEditing instead", renamed: "scrollsToLastItemOnKeyboardBeginsEditing") - 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 - - /// Display the date of message by swiping left. - /// The default value of this property is `false`. - open var showMessageTimestampOnSwipeLeft: Bool = false { - didSet { - messagesCollectionView.showMessageTimestampOnSwipeLeft = showMessageTimestampOnSwipeLeft - if showMessageTimestampOnSwipeLeft { - addPanGesture() - } else { - removePanGesture() - } - } - } - - /// Pan gesture for display the date of message by swiping left. - private var panGesture: UIPanGestureRecognizer? - - open override var canBecomeFirstResponder: Bool { - return true - } - - open override var inputAccessoryView: UIView? { - return messageInputBar - } - - open override var shouldAutorotate: Bool { - return false - } - - /// 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 - } - } - - public var isTypingIndicatorHidden: Bool { - return messagesCollectionView.isTypingIndicatorHidden - } - - public var selectedIndexPathForMenu: IndexPath? - - private var isFirstLayout: Bool = true - - internal var isMessagesControllerBeingDismissed: Bool = false - - internal var messageCollectionViewBottomInset: CGFloat = 0 { - didSet { - messagesCollectionView.contentInset.bottom = messageCollectionViewBottomInset - messagesCollectionView.scrollIndicatorInsets.bottom = messageCollectionViewBottomInset - } - } - - // MARK: - View Life Cycle - - open override func viewDidLoad() { - super.viewDidLoad() - setupDefaults() - setupSubviews() - setupConstraints() - setupDelegates() - addMenuControllerObservers() - addObservers() - } - - open override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - if !isFirstLayout { - addKeyboardObservers() - } - } - - open override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - isMessagesControllerBeingDismissed = false - } - - open override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - isMessagesControllerBeingDismissed = true - removeKeyboardObservers() - } - - open override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - isMessagesControllerBeingDismissed = false - } - - open override func viewDidLayoutSubviews() { - // Hack to prevent animation of the contentInset after viewDidAppear - if isFirstLayout { - defer { isFirstLayout = false } - addKeyboardObservers() - messageCollectionViewBottomInset = requiredInitialScrollViewBottomInset() - } - } - - open override func viewSafeAreaInsetsDidChange() { - super.viewSafeAreaInsetsDidChange() - messageCollectionViewBottomInset = requiredInitialScrollViewBottomInset() - } - - // MARK: - Initializers - - deinit { - removeMenuControllerObservers() - removeObservers() - clearMemoryCache() - } - - // MARK: - Methods [Private] - - /// Display time of message by swiping the cell - private func addPanGesture() { - panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) - guard let panGesture = panGesture else { - return - } - panGesture.delegate = self - messagesCollectionView.addGestureRecognizer(panGesture) - messagesCollectionView.clipsToBounds = false - } - - private func removePanGesture() { - guard let panGesture = panGesture else { - return - } - panGesture.delegate = nil - self.panGesture = nil - messagesCollectionView.removeGestureRecognizer(panGesture) - messagesCollectionView.clipsToBounds = true - } - - @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 - } - } - - private func setupDefaults() { - extendedLayoutIncludesOpaqueBars = true - view.backgroundColor = .collectionViewBackground - messagesCollectionView.keyboardDismissMode = .interactive - messagesCollectionView.alwaysBounceVertical = true - messagesCollectionView.backgroundColor = .collectionViewBackground - if #available(iOS 13.0, *) { - messagesCollectionView.automaticallyAdjustsScrollIndicatorInsets = false - } - } - - private func setupDelegates() { - messagesCollectionView.delegate = self - messagesCollectionView.dataSource = self - } - - private func setupSubviews() { - view.addSubview(messagesCollectionView) - } - - private func setupConstraints() { - messagesCollectionView.translatesAutoresizingMaskIntoConstraints = false - - let top = messagesCollectionView.topAnchor.constraint(equalTo: view.topAnchor) - let bottom = messagesCollectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor) - let leading = messagesCollectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor) - let trailing = messagesCollectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) - NSLayoutConstraint.activate([top, bottom, trailing, leading]) - } - - // MARK: - Typing Indicator API - - /// 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 - 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 - messagesCollectionView.setTypingIndicatorViewHidden(isHidden) - - if animated { - messagesCollectionView.performBatchUpdates({ [weak self] in - self?.performUpdatesForTypingIndicatorVisability(at: section) - updates?() - }, completion: completion) - } else { - performUpdatesForTypingIndicatorVisability(at: section) - updates?() - completion?(true) - } - } - - /// 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]) - } - } - - /// 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 { - return !messagesCollectionView.isTypingIndicatorHidden && section == self.numberOfSections(in: messagesCollectionView) - 1 - } - - // 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 - } - - 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) - } - - guard let messagesDataSource = messagesCollectionView.messagesDataSource else { - fatalError(MessageKitError.nilMessagesDataSource) - } - - if isSectionReservedForTypingIndicator(indexPath.section) { - return messagesDataSource.typingIndicator(at: indexPath, in: messagesCollectionView) - } - - 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 .audio: - let cell = messagesCollectionView.dequeueReusableCell(AudioMessageCell.self, for: indexPath) - cell.configure(with: message, at: indexPath, and: messagesCollectionView) - return cell - case .contact: - 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) - } - - guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { - fatalError(MessageKitError.nilMessagesDisplayDelegate) - } - - 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(_ 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 collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { - - guard let messagesCollectionView = collectionView as? MessagesCollectionView else { - fatalError(MessageKitError.notMessagesCollectionView) - } - guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { - fatalError(MessageKitError.nilMessagesLayoutDelegate) - } - if isSectionReservedForTypingIndicator(section) { - return .zero - } - return layoutDelegate.headerViewSize(for: section, in: messagesCollectionView) - } - - open func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { - guard let cell = cell as? TypingIndicatorCell else { return } - cell.typingBubble.startAnimating() - } - - 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) - } - if isSectionReservedForTypingIndicator(section) { - return .zero - } - return layoutDelegate.footerViewSize(for: section, in: messagesCollectionView) - } - - open func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool { - guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return false } - - if isSectionReservedForTypingIndicator(indexPath.section) { - 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 - } - } - - open func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool { - if isSectionReservedForTypingIndicator(indexPath.section) { - return false - } - return (action == NSSelectorFromString("copy:")) - } - - 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 - } - } - - // 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() - } - - // MARK: - UIGestureRecognizerDelegate - - /// check pan gesture direction - public 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) - } +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() + } + } + } + + /// 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() + } + } + + /// 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 + } + + 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) + } + + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) + } + + if isSectionReservedForTypingIndicator(indexPath.section) { + return messagesDataSource.typingIndicator(at: indexPath, in: messagesCollectionView) + } + + 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) + } + + guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { + fatalError(MessageKitError.nilMessagesDisplayDelegate) + } + + 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) + } + guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { + fatalError(MessageKitError.nilMessagesLayoutDelegate) + } + if isSectionReservedForTypingIndicator(section) { + return .zero + } + 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) + } + guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { + fatalError(MessageKitError.nilMessagesLayoutDelegate) + } + if isSectionReservedForTypingIndicator(section) { + return .zero + } + return layoutDelegate.footerViewSize(for: section, in: messagesCollectionView) + } + + open func collectionView(_: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool { + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return false } + + if isSectionReservedForTypingIndicator(indexPath.section) { + 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 + } + } + + open func collectionView( + _: UICollectionView, + canPerformAction action: Selector, + forItemAt indexPath: IndexPath, + withSender _: Any?) + -> Bool + { + if isSectionReservedForTypingIndicator(indexPath.section) { + return false + } + return (action == NSSelectorFromString("copy:")) + } + + open func collectionView(_: UICollectionView, performAction _: Selector, forItemAt indexPath: IndexPath, withSender _: 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 + } + } + + // MARK: Public + + public var selectedIndexPathForMenu: IndexPath? + + // MARK: Internal + + // MARK: - Internal properties + + internal let state: State = .init() + + // MARK: Private + + // MARK: - Private methods + + private func setupDefaults() { + extendedLayoutIncludesOpaqueBars = true + view.backgroundColor = .collectionViewBackground + messagesCollectionView.keyboardDismissMode = .interactive + messagesCollectionView.alwaysBounceVertical = true + messagesCollectionView.backgroundColor = .collectionViewBackground + } + + private func setupSubviews() { + view.addSubviews(messagesCollectionView, inputContainerView) + } + + private func setupConstraints() { + messagesCollectionView.translatesAutoresizingMaskIntoConstraints = false + /// Constraints of inputContainerView are managed by keyboardManager + inputContainerView.translatesAutoresizingMaskIntoConstraints = false + + 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), + ]) + } + + private func setupDelegates() { + messagesCollectionView.delegate = self + messagesCollectionView.dataSource = self + } + + private func setupInputBar(for kind: MessageInputBarKind) { + inputContainerView.subviews.forEach { $0.removeFromSuperview() } + + func pinViewToInputContainer(_ view: UIView) { + view.translatesAutoresizingMaskIntoConstraints = false + inputContainerView.addSubviews(view) + + 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), + ]) + } + + 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 4fe47c26a..aa4ca7d4f 100644 --- a/Sources/Extensions/Bundle+Extensions.swift +++ b/Sources/Extensions/Bundle+Extensions.swift @@ -1,40 +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. - */ +// 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 { - #if IS_SPM - static var messageKitAssetBundle: Bundle = Bundle.module - #else - static var messageKitAssetBundle: Bundle { - guard let url = Bundle(for: MessagesViewController.self).url(forResource: "MessageKit", withExtension: "bundle"), - let resourcesBundle = Bundle(url: url) - else { - fatalError(MessageKitError.couldNotLoadAssetsBundle) - } - return resourcesBundle - } - #endif +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 5efc13426..2ead04b23 100644 --- a/Sources/Extensions/CGRect+Extensions.swift +++ b/Sources/Extensions/CGRect+Extensions.swift @@ -1,32 +1,30 @@ -/* - 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. - */ +// 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) { - 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 index af3de70ac..bd9a31b21 100644 --- a/Sources/Extensions/MessageKind+textMessageKind.swift +++ b/Sources/Extensions/MessageKind+textMessageKind.swift @@ -1,38 +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. - */ +// 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 MessageKind { - 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)") - } +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 7ac6e4bb5..87b15671f 100644 --- a/Sources/Extensions/NSAttributedString+Extensions.swift +++ b/Sources/Extensions/NSAttributedString+Extensions.swift @@ -1,37 +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. - */ +// 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 -internal extension NSAttributedString { - - func width(considering height: CGFloat) -> CGFloat { - - let constraintBox = CGSize(width: .greatestFiniteMagnitude, height: height) - let rect = self.boundingRect(with: constraintBox, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil) - return rect.width - - } +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 01882f5e3..e5df7ad80 100644 --- a/Sources/Extensions/UIColor+Extensions.swift +++ b/Sources/Extensions/UIColor+Extensions.swift @@ -1,57 +1,58 @@ -/* - 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. - */ +// 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 -internal extension UIColor { +@MainActor +extension UIColor { + // MARK: Internal - private static func colorFromAssetBundle(named: String) -> UIColor { - guard let color = UIColor(named: named, in: Bundle.messageKitAssetBundle, compatibleWith: nil) else { - fatalError(MessageKitError.couldNotFindColorAsset) - } - return color - } - - static var incomingMessageBackground: UIColor { colorFromAssetBundle(named: "incomingMessageBackground") } - - static var outgoingMessageBackground: UIColor { colorFromAssetBundle(named: "outgoingMessageBackground") } - - static var incomingMessageLabel: UIColor { colorFromAssetBundle(named: "incomingMessageLabel") } - - static var outgoingMessageLabel: UIColor { colorFromAssetBundle(named: "outgoingMessageLabel") } - - static var incomingAudioMessageTint: UIColor { colorFromAssetBundle(named: "incomingAudioMessageTint") } - - static var outgoingAudioMessageTint: UIColor { colorFromAssetBundle(named: "outgoingAudioMessageTint") } - - static var collectionViewBackground: UIColor { colorFromAssetBundle(named: "collectionViewBackground") } - - static var typingIndicatorDot: UIColor { colorFromAssetBundle(named: "typingIndicatorDot") } - - static var label: UIColor { colorFromAssetBundle(named: "label") } - - static var avatarViewBackground: UIColor { colorFromAssetBundle(named: "avatarViewBackground") } + internal static var incomingMessageBackground: UIColor { colorFromAssetBundle(named: "incomingMessageBackground") } + + internal static var outgoingMessageBackground: UIColor { colorFromAssetBundle(named: "outgoingMessageBackground") } + + internal static var incomingMessageLabel: UIColor { colorFromAssetBundle(named: "incomingMessageLabel") } + + internal static var outgoingMessageLabel: UIColor { colorFromAssetBundle(named: "outgoingMessageLabel") } + + internal static var incomingAudioMessageTint: UIColor { colorFromAssetBundle(named: "incomingAudioMessageTint") } + + 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 ece1eb7c0..c26740de5 100644 --- a/Sources/Extensions/UIEdgeInsets+Extensions.swift +++ b/Sources/Extensions/UIEdgeInsets+Extensions.swift @@ -1,38 +1,34 @@ -/* - 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. - */ +// 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 -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 index 35969b869..aa371f67e 100644 --- a/Sources/Extensions/UIImage+Extensions.swift +++ b/Sources/Extensions/UIImage+Extensions.swift @@ -1,38 +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. - */ +// 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 + case play + case pause + case disclosure } /// This extension provide a way to access image resources with in framework -internal extension UIImage { - static func messageKitImageWith(type: ImageType) -> UIImage? { - UIImage(named: type.rawValue, in: Bundle.messageKitAssetBundle, compatibleWith: nil) - } +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 42bbbbd9c..d741de44b 100644 --- a/Sources/Extensions/UIView+Extensions.swift +++ b/Sources/Extensions/UIView+Extensions.swift @@ -1,128 +1,142 @@ -/* - 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. - */ +// 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 -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, 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 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 let centerY = centerY { - let constraint = centerYAnchor.constraint(equalTo: centerY, constant: centerYConstant) - constraint.identifier = "centerY" - constraints.append(constraint) - } - - 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) - } - - 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 index 07a239243..b08ed2995 100644 --- a/Sources/Layout/AudioMessageSizeCalculator.swift +++ b/Sources/Layout/AudioMessageSizeCalculator.swift @@ -1,44 +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. - */ +// 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) -> CGSize { - switch message.kind { - case .audio(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 .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 3bff16cb3..85ffbffed 100644 --- a/Sources/Layout/CellSizeCalculator.swift +++ b/Sources/Layout/CellSizeCalculator.swift @@ -1,50 +1,53 @@ -/* - 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. - */ +// 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 index 266a5cf4b..9d2166b29 100644 --- a/Sources/Layout/ContactMessageSizeCalculator.swift +++ b/Sources/Layout/ContactMessageSizeCalculator.swift @@ -1,74 +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. - */ +// 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 { - - 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) - - internal func contactLabelInsets(for message: MessageType) -> UIEdgeInsets { - let dataSource = messagesLayout.messagesDataSource - let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? outgoingMessageNameLabelInsets : incomingMessageNameLabelInsets - } - - open override func messageContainerMaxWidth(for message: MessageType) -> CGFloat { - let maxWidth = super.messageContainerMaxWidth(for: message) - let textInsets = contactLabelInsets(for: message) - return maxWidth - textInsets.horizontal - } - - open override func messageContainerSize(for message: MessageType) -> CGSize { - let maxWidth = messageContainerMaxWidth(for: message) - - 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: 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 index b9af0018f..b18f241a0 100644 --- a/Sources/Layout/LinkPreviewMessageSizeCalculator.swift +++ b/Sources/Layout/LinkPreviewMessageSizeCalculator.swift @@ -1,102 +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. - */ +// 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 - static let imageViewSize: CGFloat = 60 - static let imageViewMargin: CGFloat = 8 + public override init(layout: MessagesCollectionViewFlowLayout?) { + let titleFont = UIFont.systemFont(ofSize: 13, weight: .semibold) + let titleFontMetrics = UIFontMetrics(forTextStyle: .footnote) + self.titleFont = titleFontMetrics.scaledFont(for: titleFont) - public var titleFont: UIFont - public var teaserFont: UIFont = .preferredFont(forTextStyle: .caption2) - public var domainFont: UIFont + let domainFont = UIFont.systemFont(ofSize: 12, weight: .semibold) + let domainFontMetrics = UIFontMetrics(forTextStyle: .caption1) + self.domainFont = domainFontMetrics.scaledFont(for: domainFont) - public override init(layout: MessagesCollectionViewFlowLayout?) { - let titleFont = UIFont.systemFont(ofSize: 13, weight: .semibold) - let titleFontMetrics = UIFontMetrics(forTextStyle: .footnote) - self.titleFont = titleFontMetrics.scaledFont(for: titleFont) + super.init(layout: layout) + } - let domainFont = UIFont.systemFont(ofSize: 12, weight: .semibold) - let domainFontMetrics = UIFontMetrics(forTextStyle: .caption1) - self.domainFont = domainFontMetrics.scaledFont(for: domainFont) + // MARK: Open - super.init(layout: layout) + 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 messageContainerMaxWidth(for message: MessageType) -> CGFloat { - switch message.kind { - case .linkPreview: - let maxWidth = super.messageContainerMaxWidth(for: message) - return max(maxWidth, (layout?.collectionView?.bounds.width ?? 0) * 0.75) - default: - return super.messageContainerMaxWidth(for: message) - } + 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)") } - open override func messageContainerSize(for message: MessageType) -> 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)) - var containerSize = super.messageContainerSize(for: message) - containerSize.width = max(containerSize.width, messageContainerMaxWidth(for: message)) + let labelInsets: UIEdgeInsets = messageLabelInsets(for: message) - let labelInsets: UIEdgeInsets = messageLabelInsets(for: message) + let minHeight = containerSize.height + LinkPreviewMessageSizeCalculator.imageViewSize + let previewMaxWidth = containerSize + .width - + ( + LinkPreviewMessageSizeCalculator.imageViewSize + LinkPreviewMessageSizeCalculator.imageViewMargin + labelInsets + .horizontal) - 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.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.teaser, attributes: [.font: teaserFont]), - containerSize: &containerSize, - maxWidth: previewMaxWidth) + calculateContainerSize( + with: NSAttributedString(string: linkItem.url.host ?? "", attributes: [.font: domainFont]), + 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 - containerSize.height = max(minHeight, containerSize.height) + labelInsets.vertical + return containerSize + } - 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) + } - 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 } -private extension LinkPreviewMessageSizeCalculator { - private func calculateContainerSize(with attibutedString: NSAttributedString, containerSize: inout CGSize, maxWidth: CGFloat) { - guard !attibutedString.string.isEmpty else { return } - let size = labelSize(for: attibutedString, considering: maxWidth) - containerSize.height += size.height - } +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 e862b9c67..56ce3329c 100644 --- a/Sources/Layout/LocationMessageSizeCalculator.swift +++ b/Sources/Layout/LocationMessageSizeCalculator.swift @@ -1,44 +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. - */ +// 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 46ff9f1e5..c6eebaf91 100644 --- a/Sources/Layout/MediaMessageSizeCalculator.swift +++ b/Sources/Layout/MediaMessageSizeCalculator.swift @@ -1,49 +1,46 @@ -/* - 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. - */ +// 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 dad694114..099487215 100644 --- a/Sources/Layout/MessageSizeCalculator.swift +++ b/Sources/Layout/MessageSizeCalculator.swift @@ -1,305 +1,353 @@ -/* - 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. - */ +// 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 avatarLeadingTrailingPadding: CGFloat = 0 - - 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 incomingCellBottomLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42)) - public var outgoingCellBottomLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42)) - - 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 - - public var incomingAccessoryViewPosition: AccessoryPosition = .messageCenter - public var outgoingAccessoryViewPosition: AccessoryPosition = .messageCenter + 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.avatarLeadingTrailingPadding = avatarLeadingTrailingPadding - - attributes.messageContainerPadding = messageContainerPadding(for: message) - attributes.messageContainerSize = messageContainerSize(for: message) - 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) - - attributes.messageBottomLabelAlignment = messageBottomLabelAlignment(for: message) - attributes.messageBottomLabelSize = messageBottomLabelSize(for: message, at: indexPath) - - attributes.accessoryViewSize = accessoryViewSize(for: message) - attributes.accessoryViewPadding = accessoryViewPadding(for: message) - attributes.accessoryViewPosition = accessoryViewPosition(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 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).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) - } + 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: - Message time label + open func messageContainerSize(for _: MessageType, at _: IndexPath) -> CGSize { + // Returns .zero by default + .zero + } - 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 - } - let size = attributedText.size() - return CGSize(width: size.width, height: size.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 + } - // 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: Public - // MARK: - Bottom Message Label + public var incomingAvatarSize = CGSize(width: 30, height: 30) + public var outgoingAvatarSize = CGSize(width: 30, height: 30) - 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) - } + public var incomingAvatarPosition = AvatarPosition(vertical: .cellBottom) + public var outgoingAvatarPosition = AvatarPosition(vertical: .cellBottom) - open func messageBottomLabelAlignment(for message: MessageType) -> LabelAlignment { - let dataSource = messagesLayout.messagesDataSource - let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? outgoingMessageBottomLabelAlignment : incomingMessageBottomLabelAlignment - } + public var avatarLeadingTrailingPadding: CGFloat = 0 - // MARK: - Accessory View + 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 func accessoryViewSize(for message: MessageType) -> CGSize { - let dataSource = messagesLayout.messagesDataSource - let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? outgoingAccessoryViewSize : incomingAccessoryViewSize - } + public var incomingCellTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) + public var outgoingCellTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) - 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 - } + public var incomingCellBottomLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42)) + public var outgoingCellBottomLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42)) - // MARK: - MessageContainer - - open func messageContainerPadding(for message: MessageType) -> UIEdgeInsets { - let dataSource = messagesLayout.messagesDataSource - let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) - return isFromCurrentSender ? outgoingMessagePadding : incomingMessagePadding - } + public var incomingMessageTopLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42)) + public var outgoingMessageTopLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42)) - open func messageContainerSize(for message: MessageType) -> CGSize { - // Returns .zero by default - return .zero - } + public var incomingMessageBottomLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42)) + public var outgoingMessageBottomLabelAlignment = 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 - avatarLeadingTrailingPadding - } + public var incomingAccessoryViewSize = CGSize.zero + public var outgoingAccessoryViewSize = CGSize.zero - // MARK: - Helpers + public var incomingAccessoryViewPadding = HorizontalEdgeInsets.zero + public var outgoingAccessoryViewPadding = HorizontalEdgeInsets.zero - 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 incomingAccessoryViewPosition: AccessoryPosition = .messageCenter + public var outgoingAccessoryViewPosition: AccessoryPosition = .messageCenter - 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 + // MARK: - Helpers - return rect.size + 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 8970e8ce1..fa280b932 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -1,333 +1,357 @@ -/* - 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. - */ +// 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 Foundation import UIKit -import AVFoundation /// The layout object used by `MessagesCollectionView` to determine the size of all /// framework provided `MessageCollectionViewCell` subclasses. open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { - - open override class var layoutAttributesClass: AnyClass { - return MessagesCollectionViewLayoutAttributes.self - } - - /// The `MessagesCollectionView` that owns this layout object. - public var messagesCollectionView: MessagesCollectionView { - guard let messagesCollectionView = collectionView as? MessagesCollectionView else { - fatalError(MessageKitError.layoutUsedOnForeignType) - } - return messagesCollectionView - } - - /// The `MessagesDataSource` for the layout's collection view. - public var messagesDataSource: MessagesDataSource { - guard let messagesDataSource = messagesCollectionView.messagesDataSource else { - fatalError(MessageKitError.nilMessagesDataSource) - } - return messagesDataSource - } - - /// The `MessagesLayoutDelegate` for the layout's collection view. - public var messagesLayoutDelegate: MessagesLayoutDelegate { - guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { - fatalError(MessageKitError.nilMessagesLayoutDelegate) - } - return messagesLayoutDelegate - } - - public var itemWidth: CGFloat { - guard let collectionView = collectionView else { return 0 } - return collectionView.frame.width - sectionInset.left - sectionInset.right - } - - public private(set) var isTypingIndicatorViewHidden: Bool = true - - // 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: - 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: UIApplication.didChangeStatusBarOrientationNotification, object: nil) - } - - // 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 - } - - /// 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 { - return !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 - } - for attributes in attributesArray where attributes.representedElementCategory == .cell { - let cellSizeCalculator = cellSizeCalculatorForItem(at: attributes.indexPath) - cellSizeCalculator.configure(attributes: attributes) - } - return attributesArray - } - - 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: - Layout Invalidation - - open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { - return collectionView?.bounds.width != newBounds.width - } - - 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 - } - - @objc - private func handleOrientationChange(_ notification: Notification) { - invalidateLayout() - } - - // 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) - - /// 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 textMessageSizeCalculator - case .attributedText: - return attributedTextMessageSizeCalculator - case .emoji: - return emojiMessageSizeCalculator - case .photo: - return photoMessageSizeCalculator - case .video: - return videoMessageSizeCalculator - case .location: - return locationMessageSizeCalculator - case .audio: - return audioMessageSizeCalculator - case .contact: - return contactMessageSizeCalculator - case .linkPreview: - return linkPreviewMessageSizeCalculator - case .custom: - return messagesLayoutDelegate.customCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) - } - } - - 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 `avatarLeadingTrailingPadding` of all `MessageSizeCalculator`s - public func setAvatarLeadingTrailingPadding(_ newPadding: CGFloat) { - messageSizeCalculators().forEach { $0.avatarLeadingTrailingPadding = newPadding } - } - - /// 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 } - } - - /// Get all `MessageSizeCalculator`s - open func messageSizeCalculators() -> [MessageSizeCalculator] { - return [textMessageSizeCalculator, - attributedTextMessageSizeCalculator, - emojiMessageSizeCalculator, - photoMessageSizeCalculator, - videoMessageSizeCalculator, - locationMessageSizeCalculator, - audioMessageSizeCalculator, - contactMessageSizeCalculator, - linkPreviewMessageSizeCalculator - ] - } - + // 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 + } + + // 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 + } + for attributes in attributesArray where attributes.representedElementCategory == .cell { + let cellSizeCalculator = cellSizeCalculatorForItem(at: attributes.indexPath) + cellSizeCalculator.configure(attributes: attributes) + } + return attributesArray + } + + 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: - Layout Invalidation + + open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { + collectionView?.bounds.width != newBounds.width + } + + 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 + } + + /// 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) + } + } + + open func sizeForItem(at indexPath: IndexPath) -> CGSize { + let calculator = cellSizeCalculatorForItem(at: indexPath) + return calculator.sizeForItem(at: indexPath) + } + + /// Get all `MessageSizeCalculator`s + open func messageSizeCalculators() -> [MessageSizeCalculator] { + [ + textMessageSizeCalculator, + attributedTextMessageSizeCalculator, + emojiMessageSizeCalculator, + photoMessageSizeCalculator, + videoMessageSizeCalculator, + locationMessageSizeCalculator, + audioMessageSizeCalculator, + contactMessageSizeCalculator, + linkPreviewMessageSizeCalculator, + ] + } + + // MARK: Public + + public private(set) var isTypingIndicatorViewHidden = true + + /// The `MessagesCollectionView` that owns this layout object. + public var messagesCollectionView: MessagesCollectionView { + guard let messagesCollectionView = collectionView as? MessagesCollectionView else { + fatalError(MessageKitError.layoutUsedOnForeignType) + } + return messagesCollectionView + } + + /// The `MessagesDataSource` for the layout's collection view. + public var messagesDataSource: MessagesDataSource { + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) + } + return messagesDataSource + } + + /// The `MessagesLayoutDelegate` for the layout's collection view. + public var messagesLayoutDelegate: MessagesLayoutDelegate { + guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { + fatalError(MessageKitError.nilMessagesLayoutDelegate) + } + return messagesLayoutDelegate + } + + public var itemWidth: CGFloat { + guard let collectionView = collectionView else { return 0 } + return collectionView.frame.width - sectionInset.left - sectionInset.right + } + + /// 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 `avatarLeadingTrailingPadding` of all `MessageSizeCalculator`s + public func setAvatarLeadingTrailingPadding(_ newPadding: CGFloat) { + messageSizeCalculators().forEach { $0.avatarLeadingTrailingPadding = newPadding } + } + + /// 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 e182a63c9..ff8fefad6 100644 --- a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift +++ b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift @@ -1,119 +1,91 @@ -/* - 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. - */ +// 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) - public var avatarLeadingTrailingPadding: CGFloat = 0 + 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 cellBottomLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) - public var cellBottomLabelSize: 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 messageTimeLabelSize: CGSize = .zero + public var cellTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) + public var cellTopLabelSize: CGSize = .zero - public var accessoryViewSize: CGSize = .zero - public var accessoryViewPadding: HorizontalEdgeInsets = .zero - public var accessoryViewPosition: AccessoryPosition = .messageCenter + public var cellBottomLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) + public var cellBottomLabelSize: CGSize = .zero - public var linkPreviewFonts = LinkPreviewFonts(titleFont: .preferredFont(forTextStyle: .footnote), - teaserFont: .preferredFont(forTextStyle: .caption2), - domainFont: .preferredFont(forTextStyle: .caption1)) - - // MARK: - Methods + public var messageTopLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) + public var messageTopLabelSize: 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.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 messageBottomLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) + public var messageBottomLabelSize: 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 == avatarPosition - && attributes.avatarLeadingTrailingPadding == avatarLeadingTrailingPadding - && attributes.messageContainerSize == messageContainerSize - && attributes.messageContainerPadding == messageContainerPadding - && attributes.messageLabelFont == messageLabelFont - && attributes.messageLabelInsets == messageLabelInsets - && attributes.cellTopLabelAlignment == cellTopLabelAlignment - && attributes.cellTopLabelSize == cellTopLabelSize - && attributes.cellBottomLabelAlignment == cellBottomLabelAlignment - && attributes.cellBottomLabelSize == cellBottomLabelSize - && attributes.messageTimeLabelSize == messageTimeLabelSize - && attributes.messageTopLabelAlignment == messageTopLabelAlignment - && attributes.messageTopLabelSize == messageTopLabelSize - && attributes.messageBottomLabelAlignment == messageBottomLabelAlignment - && attributes.messageBottomLabelSize == messageBottomLabelSize - && attributes.accessoryViewSize == accessoryViewSize - && attributes.accessoryViewPadding == accessoryViewPadding - && attributes.accessoryViewPosition == accessoryViewPosition - && attributes.linkPreviewFonts == linkPreviewFonts - } else { - return false - } - } + 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 c7e667137..85cb3890f 100644 --- a/Sources/Layout/TextMessageSizeCalculator.swift +++ b/Sources/Layout/TextMessageSizeCalculator.swift @@ -1,92 +1,95 @@ -/* - 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. - */ +// 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 - 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)") - } + 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 index 551465da9..f8d687956 100644 --- a/Sources/Layout/TypingIndicatorCellSizeCalculator.swift +++ b/Sources/Layout/TypingIndicatorCellSizeCalculator.swift @@ -1,38 +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. - */ +// 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 + } - public init(layout: MessagesCollectionViewFlowLayout? = nil) { - super.init() - self.layout = layout - } + // MARK: Open - open override func sizeForItem(at indexPath: IndexPath) -> CGSize { - guard let layout = layout as? MessagesCollectionViewFlowLayout else { return .zero } - return layout.messagesLayoutDelegate.typingIndicatorViewSize(for: layout) - } + 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 index cd2c486e9..59c09c5ce 100644 --- a/Sources/Models/AccessoryPosition.swift +++ b/Sources/Models/AccessoryPosition.swift @@ -1,48 +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. - */ +// 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 { - - /// 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 +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 5c7bc47d9..42872a755 100644 --- a/Sources/Models/Avatar.swift +++ b/Sources/Models/Avatar.swift @@ -1,48 +1,44 @@ -/* - 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. - */ +// 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 /// 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 - } - + // 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 = "?" + + // MARK: - Initializer + + 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 f3c11e954..9f6000a26 100644 --- a/Sources/Models/AvatarPosition.swift +++ b/Sources/Models/AvatarPosition.swift @@ -1,99 +1,92 @@ -/* - 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. - */ +// 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: Equatable { - - /// 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) - } - + // MARK: Lifecycle + + 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 + + /// 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 + } + + // The vertical position + public var vertical: Vertical + + // The horizontal position + public var horizontal: Horizontal } // MARK: - Equatable Conformance -public extension AvatarPosition { - - static func == (lhs: AvatarPosition, rhs: AvatarPosition) -> Bool { - return lhs.vertical == rhs.vertical && lhs.horizontal == rhs.horizontal - } - +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 1b711a779..7fd0cb228 100644 --- a/Sources/Models/DetectorType.swift +++ b/Sources/Models/DetectorType.swift @@ -1,77 +1,80 @@ -/* - 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-2019 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: +public enum DetectorType: Hashable, Sendable { + case address + case date + case phoneNumber + case url + case transitInformation + case custom(NSRegularExpression) - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. + // MARK: Public - 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. - */ + // 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: [])) -import Foundation + /// Simply check if the detector type is a .custom + public var isCustom: Bool { + switch self { + case .custom: return true + default: return false + } + } -public enum DetectorType: Hashable { + /// The hashValue of the `DetectorType` so we can conform to `Hashable` and be sorted. + public func hash(into hasher: inout Hasher) { + hasher.combine(toInt()) + } - case address - case date - case phoneNumber - case url - case transitInformation - case custom(NSRegularExpression) + // MARK: Internal - // swiftlint:disable force_try - public static var hashtag = DetectorType.custom(try! NSRegularExpression(pattern: "#[a-zA-Z0-9]{4,}", options: [])) - public static var mention = DetectorType.custom(try! NSRegularExpression(pattern: "@[a-zA-Z0-9]{4,}", options: [])) - // swiftlint:enable force_try + // 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 - } + 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 } + } - /// Simply check if the detector type is a .custom - public var isCustom: Bool { - switch self { - case .custom: return true - default: return false - } - } + // MARK: Private - ///The hashValue of the `DetectorType` so we can conform to `Hashable` and be sorted. - public func hash(into hasher: inout Hasher) { - hasher.combine(toInt()) + /// 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 } - - /// 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 695f4e506..86cfc3f2d 100644 --- a/Sources/Models/HorizontalEdgeInsets.swift +++ b/Sources/Models/HorizontalEdgeInsets.swift @@ -1,58 +1,55 @@ -/* - 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. - */ +// 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: Equatable { +// 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) + } } // MARK: Equatable Conformance -public extension HorizontalEdgeInsets { - - 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 { - return left + right - } +extension HorizontalEdgeInsets { + internal var horizontal: CGFloat { + left + right + } } diff --git a/Sources/Models/LabelAlignment.swift b/Sources/Models/LabelAlignment.swift index 314650750..fc9616dcf 100644 --- a/Sources/Models/LabelAlignment.swift +++ b/Sources/Models/LabelAlignment.swift @@ -1,47 +1,43 @@ -/* - 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. - */ +// 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: Equatable { +// 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 -public extension LabelAlignment { - - 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 index 89936a81e..8d6c14733 100644 --- a/Sources/Models/LinkPreviewFonts.swift +++ b/Sources/Models/LinkPreviewFonts.swift @@ -1,32 +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. - */ +// 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 + let titleFont: UIFont + let teaserFont: UIFont + let domainFont: UIFont } diff --git a/Sources/Models/LocationMessageSnapshotOptions.swift b/Sources/Models/LocationMessageSnapshotOptions.swift index ccef50c51..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-2019 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 b8dbea23d..cb11f2d12 100644 --- a/Sources/Models/MessageKind.swift +++ b/Sources/Models/MessageKind.swift @@ -1,75 +1,71 @@ -/* - 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. - */ +// 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) - /// An audio message. - case audio(AudioItem) - - /// A contact message. - case contact(ContactItem) + /// An audio message. + case audio(AudioItem) - /// A link preview message. - case linkPreview(LinkItem) + /// A contact message. + case contact(ContactItem) - /// 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?) + /// A link preview message. + case linkPreview(LinkItem) - // MARK: - Not supported yet + /// 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 system(String) -// +// // case placeholder - } diff --git a/Sources/Models/MessageKitDateFormatter.swift b/Sources/Models/MessageKitDateFormatter.swift index 76e1f94e6..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-2019 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 53c2d025e..2ac4ca46b 100644 --- a/Sources/Models/MessageKitError.swift +++ b/Sources/Models/MessageKitError.swift @@ -1,38 +1,36 @@ -/* - MIT License +// 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. - 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 struct MessageKitError { - 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." +internal enum MessageKitError { + 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 c5d6a3275..ed1dbeba4 100644 --- a/Sources/Models/MessageStyle.swift +++ b/Sources/Models/MessageStyle.swift @@ -1,148 +1,157 @@ -/* - 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. - */ +// 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 + // MARK: - TailStyle - public enum TailStyle { + public enum TailStyle { + case curved + case pointedEdge - 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? { - if let imageCacheKey = imageCacheKey, let cachedImage = MessageStyle.bubbleImageCache.object(forKey: imageCacheKey as NSString) { - return cachedImage - } + 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) + } - guard - let imageName = imageName, - var image = UIImage(named: imageName, in: Bundle.messageKitAssetBundle, compatibleWith: nil) - else { - return nil - } + return strechAndCache(image: image) + } - 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) - if let imageCacheKey = imageCacheKey { - MessageStyle.bubbleImageCache.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 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 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, .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 791409285..000000000 --- a/Sources/Models/NSConstraintLayoutSet.swift +++ /dev/null @@ -1,81 +0,0 @@ -/* - 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 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 c44b8be16..000000000 --- a/Sources/Models/Sender.swift +++ /dev/null @@ -1,57 +0,0 @@ -/* - 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 object that groups the metadata of a messages sender. -@available(*, deprecated, message: "`Sender` has been replaced with the `SenderType` protocol in 3.0.0") -public struct Sender: SenderType { - - // MARK: - Properties - - /// The unique String identifier for the sender. - /// - /// Note: This value must be unique across all senders. - public let senderId: String - - @available(*, deprecated, renamed: "senderId", message: "`id` has been renamed `senderId` as defined in the `SenderType` protocol") - public var id: String { - return senderId - } - - /// The display name of a sender. - public let displayName: String - - // MARK: - Intializers - - public init(senderId: String, displayName: String) { - self.senderId = senderId - self.displayName = displayName - } - - @available(*, deprecated, message: "`id` has been renamed `senderId` as defined in the `SenderType` protocol") - public init(id: String, displayName: String) { - self.init(senderId: id, displayName: displayName) - } -} diff --git a/Sources/Protocols/AudioItem.swift b/Sources/Protocols/AudioItem.swift index cae71fdaa..db24e7c58 100644 --- a/Sources/Protocols/AudioItem.swift +++ b/Sources/Protocols/AudioItem.swift @@ -1,41 +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. - */ +// 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 -import class AVFoundation.AVAudioPlayer /// 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 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 } + /// 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 index f43e842d9..9a40fc78d 100644 --- a/Sources/Protocols/ContactItem.swift +++ b/Sources/Protocols/ContactItem.swift @@ -1,41 +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. - */ +// 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 } + /// 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 index d8b16a388..5d3342a6f 100644 --- a/Sources/Protocols/LinkItem.swift +++ b/Sources/Protocols/LinkItem.swift @@ -1,67 +1,66 @@ -/* - 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-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 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. - /// 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 LinkeItem.attributedText. - - /// The message text. - var text: String? { get } + /// The message text. + var text: String? { get } - /// The message attributed text. - var attributedText: NSAttributedString? { get } + /// The message attributed text. + var attributedText: NSAttributedString? { get } - /// The URL. - var url: URL { get } + /// The URL. + var url: URL { get } - /// The title. - var title: String? { get } + /// The title. + var title: String? { get } - /// The teaser text. - var teaser: String { get } + /// The teaser text. + var teaser: String { get } - /// The thumbnail image. - var thumbnailImage: UIImage { get } + /// The thumbnail image. + var thumbnailImage: UIImage { get } } -public extension LinkItem { - var textKind: MessageKind { - let kind: MessageKind - if let text = self.text { - kind = .text(text) - } else if let attributedText = self.attributedText { - kind = .attributedText(attributedText) - } else { - fatalError("LinkItem must have \"text\" or \"attributedText\"") - } - return kind +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 10ba2c0f5..8789bfa8a 100644 --- a/Sources/Protocols/LocationItem.swift +++ b/Sources/Protocols/LocationItem.swift @@ -1,38 +1,34 @@ -/* - 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. - */ +// 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 -import class CoreLocation.CLLocation /// 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 86525c3e4..28160d4b8 100644 --- a/Sources/Protocols/MediaItem.swift +++ b/Sources/Protocols/MediaItem.swift @@ -1,43 +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. - */ +// 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 db2d71189..a243a773d 100644 --- a/Sources/Protocols/MessageCellDelegate.swift +++ b/Sources/Protocols/MessageCellDelegate.swift @@ -1,192 +1,188 @@ -/* - 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. - */ +// 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 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) - + /// 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 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) { } - func didTapBackground(in cell: MessageCollectionViewCell) {} + public func didTapAvatar(in _: MessageCollectionViewCell) { } - func didTapMessage(in cell: MessageCollectionViewCell) {} + public func didTapCellTopLabel(in _: MessageCollectionViewCell) { } - func didTapAvatar(in cell: MessageCollectionViewCell) {} + public func didTapCellBottomLabel(in _: MessageCollectionViewCell) { } - func didTapCellTopLabel(in cell: MessageCollectionViewCell) {} - - func didTapCellBottomLabel(in cell: MessageCollectionViewCell) {} + public func didTapMessageTopLabel(in _: MessageCollectionViewCell) { } - func didTapMessageTopLabel(in cell: MessageCollectionViewCell) {} - - func didTapImage(in cell: MessageCollectionViewCell) {} + public func didTapImage(in _: MessageCollectionViewCell) { } - func didTapPlayButton(in cell: AudioMessageCell) {} + public func didTapPlayButton(in _: AudioMessageCell) { } - func didStartAudio(in cell: AudioMessageCell) {} + public func didStartAudio(in _: AudioMessageCell) { } - func didPauseAudio(in cell: AudioMessageCell) {} + public func didPauseAudio(in _: AudioMessageCell) { } - func didStopAudio(in cell: AudioMessageCell) {} + public func didStopAudio(in _: AudioMessageCell) { } - func didTapMessageBottomLabel(in cell: MessageCollectionViewCell) {} - - func didTapAccessoryView(in cell: MessageCollectionViewCell) {} + public func didTapMessageBottomLabel(in _: MessageCollectionViewCell) { } + public func didTapAccessoryView(in _: MessageCollectionViewCell) { } } diff --git a/Sources/Protocols/MessageLabelDelegate.swift b/Sources/Protocols/MessageLabelDelegate.swift index bdd4ee66e..a1b7da6e5 100644 --- a/Sources/Protocols/MessageLabelDelegate.swift +++ b/Sources/Protocols/MessageLabelDelegate.swift @@ -1,99 +1,95 @@ -/* - 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. - */ +// 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]) + + /// 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?) } -public extension MessageLabelDelegate { - - func didSelectAddress(_ addressComponents: [String: String]) {} +extension MessageLabelDelegate { + public func didSelectAddress(_: [String: String]) { } - func didSelectDate(_ date: Date) {} + public func didSelectDate(_: Date) { } - func didSelectPhoneNumber(_ phoneNumber: String) {} + public func didSelectPhoneNumber(_: String) { } - func didSelectURL(_ url: URL) {} - - func didSelectTransitInformation(_ transitInformation: [String: String]) {} + public func didSelectURL(_: URL) { } - func didSelectMention(_ mention: String) {} + public func didSelectTransitInformation(_: [String: String]) { } - func didSelectHashtag(_ hashtag: String) {} + public func didSelectMention(_: String) { } - func didSelectCustom(_ pattern: String, match: String?) {} + public func didSelectHashtag(_: String) { } + public func didSelectCustom(_: String, match _: String?) { } } diff --git a/Sources/Protocols/MessageType.swift b/Sources/Protocols/MessageType.swift index 664be32a4..d7c91786a 100644 --- a/Sources/Protocols/MessageType.swift +++ b/Sources/Protocols/MessageType.swift @@ -1,43 +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. - */ +// 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: SenderType { 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 efb0a1357..831d1cf2f 100644 --- a/Sources/Protocols/MessagesDataSource.swift +++ b/Sources/Protocols/MessagesDataSource.swift @@ -1,183 +1,265 @@ -/* - 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. - */ +// 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 `SenderType` of new messages in the `MessagesCollectionView`. - func currentSender() -> SenderType - - /// 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? - - /// 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 + /// 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.senderId == currentSender().senderId - } - - func numberOfItems(inSection section: Int, in messagesCollectionView: MessagesCollectionView) -> Int { - return 1 - } - - func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { - return nil - } - - func cellBottomLabelAttributedText(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 messageTimestampLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { - let sentDate = message.sentDate - let sentDateString = MessageKitDateFormatter.shared.string(from: sentDate) - let timeLabelFont: UIFont = .boldSystemFont(ofSize: 10) - let timeLabelColor: UIColor - if #available(iOS 13, *) { - timeLabelColor = .systemGray - } else { - timeLabelColor = .darkGray - } - return NSAttributedString(string: sentDateString, attributes: [NSAttributedString.Key.font: timeLabelFont, NSAttributedString.Key.foregroundColor: timeLabelColor]) - } - - func customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell { - fatalError(MessageKitError.customDataUnresolvedCell) - } - - func typingIndicator(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell { - return messagesCollectionView.dequeueReusableCell(TypingIndicatorCell.self, for: indexPath) - } +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 2f8d0bdfc..81a5d6722 100644 --- a/Sources/Protocols/MessagesDisplayDelegate.swift +++ b/Sources/Protocols/MessagesDisplayDelegate.swift @@ -1,331 +1,408 @@ -/* - 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. - */ +// 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 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 `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 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: - Audio Message - - /// Used to configure the audio cell UI: - /// 1. play button selected state; - /// 2. progresssView 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 mintues 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) + // 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 - } - - 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) ? .outgoingMessageBackground : .incomingMessageBackground - } - } - - func messageHeaderView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView { - return messagesCollectionView.dequeueReusableHeaderView(MessageReusableView.self, for: indexPath) - } - - 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 = "?" - } - - func configureAccessoryView(_ accessoryView: UIView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {} - - // MARK: - Text Messages 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) ? .outgoingMessageLabel : .incomingMessageLabel - } - - func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] { - return [] +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 detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any] { - return MessageLabel.defaultAttributes + } + + 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) } - - // MARK: - Location Messages Defaults - - func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions { - return LocationMessageSnapshotOptions() + 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 annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? { - return MKPinAnnotationView(annotation: nil, reuseIdentifier: nil) + 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 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) { - } + // MARK: - LinkPreview Message Defaults - // MARK: - Audio Message Defaults - - func configureAudioCell(_ cell: AudioMessageCell, message: MessageType) { - - } - - func audioTintColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { - guard let dataSource = messagesCollectionView.messagesDataSource else { - fatalError(MessageKitError.nilMessagesDataSource) - } - return dataSource.isFromCurrentSender(message: message) ? .outgoingAudioMessageTint : .incomingAudioMessageTint - } - - func audioProgressTextFormat(_ duration: Float, for audioCell: AudioMessageCell, in messageCollectionView: MessagesCollectionView) -> String { - var retunValue = "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 { - retunValue = String(format: "0:%.02d", Int(duration.rounded(.up))) - } else if duration < 3600 { - retunValue = String(format: "%.02d:%.02d", Int(duration/60), Int(duration) % 60) - } else { - let hours = Int(duration/3600) - let remainingMinutsInSeconds = Int(duration) - hours*3600 - retunValue = String(format: "%.02d:%.02d:%.02d", hours, Int(remainingMinutsInSeconds/60), Int(remainingMinutsInSeconds) % 60) - } - return retunValue - } - - // MARK: - LinkPreview Message Defaults - - func configureLinkPreviewImageView(_ 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 a3d9ff5a8..015c45c30 100644 --- a/Sources/Protocols/MessagesLayoutDelegate.swift +++ b/Sources/Protocols/MessagesLayoutDelegate.swift @@ -1,166 +1,413 @@ -/* - 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. - */ +// 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 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 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 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) - } - - func typingIndicatorViewTopInset(in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return 15 - } - - func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return 0 - } - - func cellBottomLabelHeight(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 index 4c5c19a7b..e22605f0e 100644 --- a/Sources/Protocols/SenderType.swift +++ b/Sources/Protocols/SenderType.swift @@ -1,38 +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. - */ +// 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 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 } + /// The display name of a sender. + var displayName: String { get } } diff --git a/Sources/Supporting/MessageInputBar.swift b/Sources/Supporting/MessageInputBar.swift deleted file mode 100644 index 7c6f7dbf1..000000000 --- a/Sources/Supporting/MessageInputBar.swift +++ /dev/null @@ -1,32 +0,0 @@ -/* - 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 InputBarAccessoryView - -@available(*, unavailable, renamed: "InputBarAccessoryView") -public typealias MessageInputBar = InputBarAccessoryView - -@available(*, unavailable, renamed: "InputBarAccessoryViewDelegate") -public typealias MessageInputBarDelegate = InputBarAccessoryViewDelegate diff --git a/Sources/Supporting/MessageKit+Availability.swift b/Sources/Supporting/MessageKit+Availability.swift deleted file mode 100644 index 03d3083fe..000000000 --- a/Sources/Supporting/MessageKit+Availability.swift +++ /dev/null @@ -1,73 +0,0 @@ -/* - 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 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 6e5bd2068..3fe8ed56e 100644 --- a/Sources/Views/AvatarView.swift +++ b/Sources/Views/AvatarView.swift @@ -1,198 +1,218 @@ -/* - 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. - */ +// 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 = .avatarViewBackground - 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 index c0c276ad8..93acf6f0b 100644 --- a/Sources/Views/BubbleCircle.swift +++ b/Sources/Views/BubbleCircle.swift @@ -1,49 +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. - */ +// 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 applys 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 - } - + /// 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 index 59f473c30..e32ecd8cd 100644 --- a/Sources/Views/Cells/AudioMessageCell.swift +++ b/Sources/Views/Cells/AudioMessageCell.swift @@ -1,141 +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. - */ +// 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 UIKit /// A subclass of `MessageContentCell` used to display video and audio messages. open class AudioMessageCell: MessageContentCell { - - /// 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 lable 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: .gray) - activityIndicatorView.hidesWhenStopped = true - activityIndicatorView.isHidden = true - return activityIndicatorView - }() - - public lazy var progressView: UIProgressView = { - let progressView = UIProgressView(progressViewStyle: .default) - progressView.progress = 0.0 - return progressView - }() - - // MARK: - Methods - - /// 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) + // 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) } + } - open override func setupSubviews() { - super.setupSubviews() - messageContainerView.addSubview(playButton) - messageContainerView.addSubview(activityIndicatorView) - messageContainerView.addSubview(durationLabel) - messageContainerView.addSubview(progressView) - setupConstraints() - } + // MARK: - Configure Cell - open override func prepareForReuse() { - super.prepareForReuse() - progressView.progress = 0 - playButton.isSelected = false - activityIndicatorView.stopAnimating() - playButton.isHidden = false - durationLabel.text = "0:00" - } + open override func configure( + with message: MessageType, + at indexPath: IndexPath, + and messagesCollectionView: MessagesCollectionView) + { + super.configure(with: message, at: indexPath, and: messagesCollectionView) - /// 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) - } + guard let dataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) } - // MARK: - Configure Cell - - open override func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) { - super.configure(with: message, at: indexPath, and: messagesCollectionView) + let playButtonLeftConstraint = messageContainerView.constraints.filter { $0.identifier == "left" }.first + let durationLabelRightConstraint = messageContainerView.constraints.filter { $0.identifier == "right" }.first - 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 - } + 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) - } + 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 let .audio(audioItem) = message.kind { - durationLabel.text = displayDelegate.audioProgressTextFormat(audioItem.duration, for: self, in: messagesCollectionView) - } + let tintColor = displayDelegate.audioTintColor(for: message, at: indexPath, in: messagesCollectionView) + playButton.imageView?.tintColor = tintColor + durationLabel.textColor = tintColor + progressView.tintColor = tintColor - displayDelegate.configureAudioCell(self, message: message) + 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 index 0ffbb8502..cc3e64cc0 100644 --- a/Sources/Views/Cells/ContactMessageCell.swift +++ b/Sources/Views/Cells/ContactMessageCell.swift @@ -1,143 +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-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 { - - 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 - }() - - // MARK: - Methods - open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { - super.apply(layoutAttributes) - guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { - return - } - nameLabel.font = attributes.messageLabelFont - } + // MARK: Open - open override func setupSubviews() { - super.setupSubviews() - messageContainerView.addSubview(initialsContainerView) - messageContainerView.addSubview(nameLabel) - messageContainerView.addSubview(disclosureImageView) - initialsContainerView.addSubview(initialsLabel) - setupConstraints() + // MARK: - Methods + open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { + super.apply(layoutAttributes) + guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { + return } - - open override func prepareForReuse() { - super.prepareForReuse() - nameLabel.text = "" - initialsLabel.text = "" + 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) } - - 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) + 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 } - - // 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 let .contact(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 - return constraint.identifier == ConstraintsID.initialsContainerLeftConstraint.rawValue - }.first - let disclosureRightConstraint = messageContainerView.constraints.filter { (constraint) -> Bool in - return 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 + // 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 index e14a0172c..6d2df58f7 100644 --- a/Sources/Views/Cells/LinkPreviewMessageCell.swift +++ b/Sources/Views/Cells/LinkPreviewMessageCell.swift @@ -1,96 +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. - */ +// 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 { - 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 - }() - - private var linkURL: URL? - - 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 + // 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 } - 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 + guard case MessageKind.linkPreview(let linkItem) = message.kind else { + fatalError("LinkPreviewMessageCell received unhandled MessageDataType: \(message.kind)") } - 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) + 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 23dcfb22f..44601b1b8 100644 --- a/Sources/Views/Cells/LocationMessageCell.swift +++ b/Sources/Views/Cells/LocationMessageCell.swift @@ -1,111 +1,126 @@ -/* - 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. - */ +// 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 b66d8f8a0..f93779352 100644 --- a/Sources/Views/Cells/MediaMessageCell.swift +++ b/Sources/Views/Cells/MediaMessageCell.swift @@ -1,96 +1,96 @@ -/* - 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. - */ +// 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 image view display the media content. - open var imageView: UIImageView = { - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFill - return imageView - }() - - // 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)) + /// 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 + }() + + // 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)) + } + + 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 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(imageView) - messageContainerView.addSubview(playButtonView) - setupConstraints() - } - - open override func prepareForReuse() { - super.prepareForReuse() - self.imageView.image = nil + 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 } - open override func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) { - super.configure(with: message, at: indexPath, and: messagesCollectionView) + displayDelegate.configureMediaMessageImageView(imageView, for: message, at: indexPath, in: messagesCollectionView) + } - guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { - fatalError(MessageKitError.nilMessagesDisplayDelegate) - } - - 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 - } - - displayDelegate.configureMediaMessageImageView(imageView, for: message, at: indexPath, in: messagesCollectionView) - } - - /// Handle tap gesture on contentView and its subviews. - open override func handleTapGesture(_ gesture: UIGestureRecognizer) { - let touchLocation = gesture.location(in: imageView) + /// Handle tap gesture on contentView and its subviews. + open override func handleTapGesture(_ gesture: UIGestureRecognizer) { + let touchLocation = gesture.location(in: imageView) - guard imageView.frame.contains(touchLocation) else { - super.handleTapGesture(gesture) - return - } - delegate?.didTapImage(in: self) + 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 51855a1cf..41cbd1f02 100644 --- a/Sources/Views/Cells/MessageCollectionViewCell.swift +++ b/Sources/Views/Cells/MessageCollectionViewCell.swift @@ -1,45 +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. - */ +// 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) + } - /// Handle tap gesture on contentView and its subviews. - open func handleTapGesture(_ gesture: UIGestureRecognizer) { - // Should be overridden - } + // 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 618d0eaea..471f36c3b 100644 --- a/Sources/Views/Cells/MessageContentCell.swift +++ b/Sources/Views/Cells/MessageContentCell.swift @@ -1,365 +1,373 @@ -/* - 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. - */ +// 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 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 = InsetLabel() - - // 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() - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - setupSubviews() - } - - open func setupSubviews() { - contentView.addSubview(accessoryView) - contentView.addSubview(cellTopLabel) - contentView.addSubview(messageTopLabel) - contentView.addSubview(messageBottomLabel) - contentView.addSubview(cellBottomLabel) - contentView.addSubview(messageContainerView) - contentView.addSubview(avatarView) - contentView.addSubview(messageTimestampLabel) + // 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) } - - open override func prepareForReuse() { - super.prepareForReuse() - cellTopLabel.text = nil - cellBottomLabel.text = nil - messageTopLabel.text = nil - messageBottomLabel.text = nil - messageTimestampLabel.attributedText = nil + guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { + fatalError(MessageKitError.nilMessagesDisplayDelegate) } - // 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) + 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) } - - /// 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 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 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) } - /// 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) - } + 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 } - /// 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) + 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 `ContentView`'s tap gesture, return false when `ContentView` doesn't needs to handle gesture - open func cellContentView(canHandle touchPoint: CGPoint) -> Bool { - return false + 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) } - // 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) - } - - 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) + 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 } - /// 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 - } - } - - 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) - } - - messageContainerView.frame = CGRect(origin: origin, size: attributes.messageContainerSize) + // 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) } - /// 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 - } - - // 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) - } - - /// 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: UIScreen.main.bounds.width + 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) - } + 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 f79c1018f..08fcf381b 100644 --- a/Sources/Views/Cells/TextMessageCell.swift +++ b/Sources/Views/Cells/TextMessageCell.swift @@ -1,102 +1,102 @@ -/* - 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. - */ +// 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) - } - 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 - } - 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 index 7e8c5d948..368d25b50 100644 --- a/Sources/Views/Cells/TypingIndicatorCell.swift +++ b/Sources/Views/Cells/TypingIndicatorCell.swift @@ -1,66 +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. - */ +// 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: - Subviews + // MARK: Lifecycle - public var insets = UIEdgeInsets(top: 15, left: 0, bottom: 0, right: 0) - - public let typingBubble = TypingBubble() - - // MARK: - Initialization - - public override init(frame: CGRect) { - super.init(frame: frame) - setupSubviews() - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setupSubviews() - } - - open func setupSubviews() { - addSubview(typingBubble) - } - - open override func prepareForReuse() { - super.prepareForReuse() - if typingBubble.isAnimating { - typingBubble.stopAnimating() - } - } - - // MARK: - Layout - - open override func layoutSubviews() { - super.layoutSubviews() - typingBubble.frame = bounds.inset(by: insets) + 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/HeadersFooters/MessageReusableView.swift b/Sources/Views/HeadersFooters/MessageReusableView.swift index d8becac6c..77654ed72 100644 --- a/Sources/Views/HeadersFooters/MessageReusableView.swift +++ b/Sources/Views/HeadersFooters/MessageReusableView.swift @@ -1,40 +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. - */ +// 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 - // MARK: - Initializers - - public override init(frame: CGRect) { - super.init(frame: frame) - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } + 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 5cdfa1729..9e6c7b62f 100644 --- a/Sources/Views/InsetLabel.swift +++ b/Sources/Views/InsetLabel.swift @@ -1,38 +1,34 @@ -/* - 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. - */ +// 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 index 22b51c6c3..28fc1b1c1 100644 --- a/Sources/Views/LinkPreviewView.swift +++ b/Sources/Views/LinkPreviewView.swift @@ -1,112 +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. - */ +// 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 { - 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 - }() - - 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 - }() - - 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 aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + // 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 cbecffa4f..e816f0d19 100644 --- a/Sources/Views/MessageContainerView.swift +++ b/Sources/Views/MessageContainerView.swift @@ -1,88 +1,89 @@ -/* - 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. - */ +// 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 90a78c05f..036bc3565 100644 --- a/Sources/Views/MessageLabel.swift +++ b/Sources/Views/MessageLabel.swift @@ -1,551 +1,581 @@ -/* - 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. - */ +// 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 - }() - - internal 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 override var text: String? { - didSet { - setTextStorage(attributedText, shouldParse: true) - } - } + open weak var delegate: MessageLabelDelegate? - open override var font: UIFont! { - didSet { - setTextStorage(attributedText, shouldParse: false) - } - } + open internal(set) var addressAttributes: [NSAttributedString.Key: Any] = defaultAttributes - open override var textColor: UIColor! { - didSet { - setTextStorage(attributedText, shouldParse: false) - } - } - - open override var lineBreakMode: NSLineBreakMode { - didSet { - textContainer.lineBreakMode = lineBreakMode - if !isConfiguring { setNeedsDisplay() } - } - } - - open override var numberOfLines: Int { - didSet { - textContainer.maximumNumberOfLines = numberOfLines - if !isConfiguring { setNeedsDisplay() } - } - } - - open override var textAlignment: NSTextAlignment { - didSet { - setTextStorage(attributedText, shouldParse: false) - } - } - - open var textInsets: UIEdgeInsets = .zero { - didSet { - if !isConfiguring { setNeedsDisplay() } - } - } + open internal(set) var dateAttributes: [NSAttributedString.Key: Any] = defaultAttributes - open override var intrinsicContentSize: CGSize { - var size = super.intrinsicContentSize - size.width += textInsets.horizontal - size.height += textInsets.vertical - return size - } - - internal var messageLabelFont: UIFont? + open internal(set) var phoneNumberAttributes: [NSAttributedString.Key: Any] = defaultAttributes - private var attributesNeedUpdate = false + open internal(set) var urlAttributes: [NSAttributedString.Key: Any] = defaultAttributes - public static var defaultAttributes: [NSAttributedString.Key: Any] = { - return [ - NSAttributedString.Key.foregroundColor: UIColor.darkText, - NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue, - NSAttributedString.Key.underlineColor: UIColor.darkText - ] - }() + open internal(set) var transitInformationAttributes: [NSAttributedString.Key: Any] = defaultAttributes - open internal(set) var addressAttributes: [NSAttributedString.Key: Any] = defaultAttributes + open internal(set) var hashtagAttributes: [NSAttributedString.Key: Any] = defaultAttributes - open internal(set) var dateAttributes: [NSAttributedString.Key: Any] = defaultAttributes + open internal(set) var mentionAttributes: [NSAttributedString.Key: Any] = defaultAttributes - open internal(set) var phoneNumberAttributes: [NSAttributedString.Key: Any] = defaultAttributes + open internal(set) var customAttributes: [NSRegularExpression: [NSAttributedString.Key: Any]] = [:] - 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]] = [:] - - 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]) - } + open var enabledDetectors: [DetectorType] = [] { + didSet { + setTextStorage(attributedText, shouldParse: true) } + } - // MARK: - Initializers - - public override init(frame: CGRect) { - super.init(frame: frame) - setupView() + open override var attributedText: NSAttributedString? { + didSet { + setTextStorage(attributedText, shouldParse: true) } + } - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setupView() + open override var text: String? { + didSet { + setTextStorage(attributedText, shouldParse: true) } + } - // MARK: - Open Methods - - open override func drawText(in rect: CGRect) { - - let insetRect = rect.inset(by: textInsets) - textContainer.size = CGSize(width: insetRect.width, height: rect.height) - - let origin = insetRect.origin - let range = layoutManager.glyphRange(for: textContainer) - - layoutManager.drawBackground(forGlyphRange: range, at: origin) - layoutManager.drawGlyphs(forGlyphRange: range, at: origin) + // swiftlint:disable:next implicitly_unwrapped_optional + open override var font: UIFont! { + didSet { + setTextStorage(attributedText, shouldParse: false) } + } - // MARK: - Public Methods - - public func configure(block: () -> Void) { - isConfiguring = true - block() - if attributesNeedUpdate { - updateAttributes(for: enabledDetectors) - } - attributesNeedUpdate = false - isConfiguring = false - setNeedsDisplay() + // swiftlint:disable:next implicitly_unwrapped_optional + open override var textColor: UIColor! { + didSet { + setTextStorage(attributedText, shouldParse: false) } + } - // MARK: - Private Methods - - 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) - } - - for (detector, rangeTuples) in rangesForDetectors { - if enabledDetectors.contains(detector) { - let attributes = detectorAttributes(for: detector) - rangeTuples.forEach { (range, _) in - mutableText.addAttributes(attributes, range: range) - } - } - } - - let modifiedText = NSAttributedString(attributedString: mutableText) - textStorage.setAttributedString(modifiedText) - - if !isConfiguring { setNeedsDisplay() } - + open override var lineBreakMode: NSLineBreakMode { + didSet { + textContainer.lineBreakMode = lineBreakMode + if !isConfiguring { setNeedsDisplay() } } - - 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 - } - - 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) - } + open override var numberOfLines: Int { + didSet { + textContainer.maximumNumberOfLines = numberOfLines + if !isConfiguring { setNeedsDisplay() } } + } - 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 - } - + open override var textAlignment: NSTextAlignment { + didSet { + setTextStorage(attributedText, shouldParse: false) } + } - 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 + open var textInsets: UIEdgeInsets = .zero { + didSet { + if !isConfiguring { setNeedsDisplay() } } + } - // 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: 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) - } + open override var intrinsicContentSize: CGSize { + var size = super.intrinsicContentSize + size.width += textInsets.horizontal + size.height += textInsets.vertical + return size + } - guard enabledDetectors.contains(.url) else { - return matches - } + // MARK: - Open Methods - // Enumerate NSAttributedString NSLinks and append ranges - var results: [NSTextCheckingResult] = matches + open override func drawText(in rect: CGRect) { + let insetRect = rect.inset(by: textInsets) + textContainer.size = CGSize(width: insetRect.width, height: insetRect.height) - 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) - } + let origin = insetRect.origin + let range = layoutManager.glyphRange(for: textContainer) - return results - } + layoutManager.drawBackground(forGlyphRange: range, at: origin) + layoutManager.drawGlyphs(forGlyphRange: range, at: origin) + } - 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 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") - } + 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: - Public Methods + + public func configure(block: () -> Void) { + isConfiguring = true + block() + if attributesNeedUpdate { + updateAttributes(for: enabledDetectors) + } + attributesNeedUpdate = false + isConfiguring = false + setNeedsDisplay() + } + + // MARK: Internal + + internal lazy var rangesForDetectors: [DetectorType: [(NSRange, MessageTextCheckingType)]] = [:] + + internal var messageLabelFont: UIFont? + + // MARK: Private + + // 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 var isConfiguring = false + + private var attributesNeedUpdate = false + + // MARK: - Private Methods + + 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) + } + + for (detector, rangeTuples) in rangesForDetectors { + if enabledDetectors.contains(detector) { + let attributes = detectorAttributes(for: detector) + rangeTuples.forEach { range, _ in + mutableText.addAttributes(attributes, range: range) + } + } } - // MARK: - Gesture Handling + let modifiedText = NSAttributedString(attributedString: mutableText) + textStorage.setAttributedString(modifiedText) - private func stringIndex(at location: CGPoint) -> Int? { - guard textStorage.length > 0 else { return nil } + if !isConfiguring { setNeedsDisplay() } + } - var location = location + private func paragraphStyle(for text: NSAttributedString) -> NSParagraphStyle { + guard text.length > 0 else { return NSParagraphStyle() } - location.x -= textInsets.left - location.y -= textInsets.top + var range = NSRange(location: 0, length: text.length) + let existingStyle = text.attribute(.paragraphStyle, at: 0, effectiveRange: &range) as? NSMutableParagraphStyle + let style = existingStyle ?? NSMutableParagraphStyle() - let index = layoutManager.glyphIndex(for: location, in: textContainer) + style.lineBreakMode = lineBreakMode + style.alignment = textAlignment - let lineRect = layoutManager.lineFragmentUsedRect(forGlyphAt: index, effectiveRange: nil) - - var characterIndex: Int? - - if lineRect.contains(location) { - characterIndex = layoutManager.characterIndexForGlyph(at: index) - } - - return characterIndex - - } + return style + } - open func handleGesture(_ touchLocation: CGPoint) -> Bool { + private func updateAttributes(for detectors: [DetectorType]) { + guard let attributedText = attributedText, attributedText.length > 0 else { return } + let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText) - guard let index = stringIndex(at: touchLocation) else { return false } + for detector in detectors { + guard let rangeTuples = rangesForDetectors[detector] else { continue } - for (detectorType, ranges) in rangesForDetectors { - for (range, value) in ranges { - if range.contains(index) { - handleGesture(for: detectorType, value: value) - return true - } - } + 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) } - return false - } - // swiftlint:disable cyclomatic_complexity - 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) - case let .custom(pattern, match): - guard let match = match else { return } - switch detectorType { - case .hashtag: - handleHashtag(match) - case .mention: - handleMention(match) - default: - handleCustom(pattern, match: match) - } - } - } - // swiftlint:enable cyclomatic_complexity - - private func handleAddress(_ addressComponents: [String: String]) { - delegate?.didSelectAddress(addressComponents) - } - - private func handleDate(_ date: Date) { - delegate?.didSelectDate(date) - } + 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) + } + + 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) + } + + 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 handleURL(_ url: URL) { - delegate?.didSelectURL(url) + 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) + } } - private func handlePhoneNumber(_ phoneNumber: String) { - delegate?.didSelectPhoneNumber(phoneNumber) - } - - private func handleTransitInformation(_ components: [String: String]) { - delegate?.didSelectTransitInformation(components) - } - - private func handleHashtag(_ hashtag: String) { - delegate?.didSelectHashtag(hashtag) - } - - private func handleMention(_ mention: String) { - delegate?.didSelectMention(mention) - } - - private func handleCustom(_ pattern: String, match: String) { - delegate?.didSelectCustom(pattern, match: match) - } - + 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: - Gesture Handling + + private func stringIndex(at location: CGPoint) -> Int? { + guard textStorage.length > 0 else { return nil } + + var location = location + + location.x -= textInsets.left + location.y -= textInsets.top + + let index = layoutManager.glyphIndex(for: location, in: textContainer) + + let lineRect = layoutManager.lineFragmentUsedRect(forGlyphAt: index, effectiveRange: nil) + + var characterIndex: Int? + + if lineRect.contains(location) { + characterIndex = layoutManager.characterIndexForGlyph(at: index) + } + + 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) + } + } + } + + // swiftlint:enable cyclomatic_complexity + + 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 handleHashtag(_ hashtag: String) { + delegate?.didSelectHashtag(hashtag) + } + + private func handleMention(_ mention: String) { + delegate?.didSelectMention(mention) + } + + private func handleCustom(_ pattern: String, match: String) { + delegate?.didSelectCustom(pattern, match: match) + } } +// 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?) + 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 bbf5de975..a551ef2f9 100644 --- a/Sources/Views/MessagesCollectionView.swift +++ b/Sources/Views/MessagesCollectionView.swift @@ -1,217 +1,225 @@ -/* - 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. - */ +// 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? - - open weak var messagesDisplayDelegate: MessagesDisplayDelegate? - - open weak var messagesLayoutDelegate: MessagesLayoutDelegate? - - open weak var messageCellDelegate: MessageCellDelegate? - - open var isTypingIndicatorHidden: Bool { - return messagesCollectionViewFlowLayout.isTypingIndicatorViewHidden - } - - /// Display the date of message by swiping left. - /// The default value of this property is `false`. - internal var showMessageTimestampOnSwipeLeft: Bool = false - - 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) - } - - open var messagesCollectionViewFlowLayout: MessagesCollectionViewFlowLayout { - guard let layout = collectionViewLayout as? MessagesCollectionViewFlowLayout else { - fatalError(MessageKitError.layoutUsedOnForeignType) - } - return layout - } - - // MARK: - Initializers - - public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { - super.init(frame: frame, collectionViewLayout: layout) - backgroundColor = .collectionViewBackground - registerReusableViews() - setupGestureRecognizers() - } - - required public init?(coder aDecoder: NSCoder) { - super.init(frame: .zero, collectionViewLayout: MessagesCollectionViewFlowLayout()) - } - - public convenience init() { - self.init(frame: .zero, collectionViewLayout: MessagesCollectionViewFlowLayout()) - } - - // 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) - } - - @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) - } - - // 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 numberOfSections > 0 else { return } - - let lastSection = numberOfSections - 1 - let lastItemIndex = numberOfItems(inSection: lastSection) - 1 - - guard lastItemIndex >= 0 else { return } - - let indexPath = IndexPath(row: lastItemIndex, section: lastSection) - scrollToItem(at: indexPath, at: pos, animated: animated) - } - - // NOTE: This method seems to cause crash in certain cases - https://github.com/MessageKit/MessageKit/issues/725 - // Could try using `scrollToLastItem` above - @available(*, deprecated, message: "Scroll to bottom by using scrollToLastItem(:) instead", renamed: "scrollToLastItem") - public func scrollToBottom(animated: Bool = false) { - performBatchUpdates(nil) { [weak self] _ in - guard let self = self else { return } - let collectionViewContentHeight = self.collectionViewLayout.collectionViewContentSize.height - 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) - } - - // 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) { - messagesCollectionViewFlowLayout.setTypingIndicatorViewHidden(isHidden) - } - - /// 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 { - return 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 headerFooterClass: 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))") - } - 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))") - } - 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 override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { + super.init(frame: frame, collectionViewLayout: layout) + backgroundColor = .collectionViewBackground + registerReusableViews() + setupGestureRecognizers() + } + required public init?(coder _: NSCoder) { + super.init(frame: .zero, collectionViewLayout: MessagesCollectionViewFlowLayout()) + } + + public convenience init() { + self.init(frame: .zero, collectionViewLayout: MessagesCollectionViewFlowLayout()) + } + + // MARK: Open + + // MARK: - Properties + + open weak var messagesDataSource: MessagesDataSource? + + open weak var messagesDisplayDelegate: MessagesDisplayDelegate? + + open weak var messagesLayoutDelegate: MessagesLayoutDelegate? + + open weak var messageCellDelegate: MessageCellDelegate? + + 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))") + } + 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))") + } + 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 + } + + // MARK: Internal + + /// Display the date of message by swiping left. + /// The default value of this property is `false`. + internal var showMessageTimestampOnSwipeLeft = false + + // 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) { + messagesCollectionViewFlowLayout.setTypingIndicatorViewHidden(isHidden) + } + + // MARK: Private + + private var indexPathForLastItem: IndexPath? { + guard numberOfSections > 0 else { return nil } + + 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 aba215d41..c78d96671 100644 --- a/Sources/Views/PlayButtonView.swift +++ b/Sources/Views/PlayButtonView.swift @@ -1,132 +1,136 @@ -/* - 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. - */ +// 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 blurView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) - 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(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 - } - + // 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 index 004b7beb5..aef03025b 100644 --- a/Sources/Views/TypingBubble.swift +++ b/Sources/Views/TypingBubble.swift @@ -1,155 +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. - */ +// 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: - Properties - - open var isPulseEnabled: Bool = true - - public private(set) var isAnimating: Bool = false - - open override var backgroundColor: UIColor? { - set { - [contentBubble, cornerBubble, tinyBubble].forEach { $0.backgroundColor = newValue } - } - get { - return contentBubble.backgroundColor - } - } - - private struct AnimationKeys { - static let pulse = "typingBubble.pulse" - } - - // 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: - 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 - } - - public override init(frame: CGRect) { - super.init(frame: frame) - setupSubviews() - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setupSubviews() - } - - 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 - 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: 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 } } - - // 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) } - } + get { + contentBubble.backgroundColor } - - open func stopAnimating() { - defer { isAnimating = false } - guard isAnimating else { return } - typingIndicator.stopAnimating() - [contentBubble, cornerBubble, tinyBubble].forEach { $0.layer.removeAnimation(forKey: AnimationKeys.pulse) } + } + + // 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 index a287781ce..355600987 100644 --- a/Sources/Views/TypingIndicator.swift +++ b/Sources/Views/TypingIndicator.swift @@ -1,165 +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. - */ +// 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: - Properties - - /// The offset that each dot will transform by during the bounce animation - public var bounceOffset: CGFloat = 2.5 - - /// A convenience accessor for the `backgroundColor` of each dot - open var dotColor: UIColor = UIColor.typingIndicatorDot { - didSet { - dots.forEach { $0.backgroundColor = dotColor } - } - } - - /// A flag that determines if the bounce animation is added in `startAnimating()` - public var isBounceEnabled: Bool = false - - /// A flag that determines if the opacity animation is added in `startAnimating()` - public var isFadeEnabled: Bool = true - - /// A flag indicating the animation state - public private(set) var isAnimating: Bool = false - - /// Keys for each animation layer - private struct AnimationKeys { - static let offset = "typingIndicator.offset" - static let bounce = "typingIndicator.bounce" - static let opacity = "typingIndicator.opacity" - } - - /// 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 - } - - // MARK: - Subviews - - public let stackView = UIStackView() - - public let dots: [BubbleCircle] = { - return [BubbleCircle(), BubbleCircle(), BubbleCircle()] - }() - - // MARK: - Initialization - - public override init(frame: CGRect) { - super.init(frame: frame) - setupView() - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setupView() - } - - /// 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) + // 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 } } - - // MARK: - Layout - - open override func layoutSubviews() { - super.layoutSubviews() - stackView.frame = bounds - stackView.spacing = bounds.width > 0 ? 5 : 0 + } + + /// 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 } - - // 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 { - 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 = delay + 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) } - - /// 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/LinuxMain.swift b/Tests/LinuxMain.swift index 17f629526..616d3b476 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1,30 +1,28 @@ -/* - 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. - */ +// 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 import MessageKitTests +import XCTest var tests = [XCTestCaseEntry]() tests += MessageKitTests.allTests() diff --git a/Tests/MessageKitTests/Controllers Test/MessageLabelTests.swift b/Tests/MessageKitTests/Controllers Test/MessageLabelTests.swift index 2439a80e9..78cae636b 100644 --- a/Tests/MessageKitTests/Controllers Test/MessageLabelTests.swift +++ b/Tests/MessageKitTests/Controllers Test/MessageLabelTests.swift @@ -1,151 +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. - */ +// 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 MessageLabelTests: XCTestCase { +// MARK: - MessageLabelTests - let mentionsList = ["@julienkode", "@facebook", "@google", "@1234"] - let hashtagsList = ["#julienkode", "#facebook", "#google", "#1234"] - - func testMentionDetection() { - let messageLabel: MessageLabel = MessageLabel() - let detector: DetectorType = 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) - } +@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" - func testHashtagDetection() { - let messageLabel: MessageLabel = MessageLabel() - let detector: DetectorType = 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 = MessageLabel() - let detector: DetectorType = 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) - } + 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))] - func testSyncBetweenAttributedAndText() { - let messageLabel: 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 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 ({ (range, 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 - } - + 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 -private 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) - } +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 index 491327062..417bcea11 100644 --- a/Tests/MessageKitTests/Controllers Test/MessagesViewControllerTests.swift +++ b/Tests/MessageKitTests/Controllers Test/MessagesViewControllerTests.swift @@ -1,62 +1,53 @@ -/* - 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. - */ +// 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 import CoreLocation +import XCTest @testable import MessageKit -final class MessagesViewControllerTests: XCTestCase { +// MARK: - MessagesViewControllerTests - var sut: MessagesViewController! - // swiftlint:disable weak_delegate - private var layoutDelegate = MockLayoutDelegate() - // swiftlint:enable weak_delegate - - // MARK: - Overridden Methods +@MainActor +final class MessagesViewControllerTests: XCTestCase { - override func setUp() { - super.setUp() + // MARK: - Private helper API - sut = MessagesViewController() + 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() - } - override func tearDown() { - sut = nil - - super.tearDown() + return sut } // MARK: - Test func testNumberOfSectionWithoutData_isZero() { let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() sut.messagesCollectionView.messagesDataSource = messagesDataSource XCTAssertEqual(sut.messagesCollectionView.numberOfSections, 0) @@ -64,6 +55,7 @@ final class MessagesViewControllerTests: XCTestCase { func testNumberOfSection_isNumberOfMessages() { let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() sut.messagesCollectionView.messagesDataSource = messagesDataSource messagesDataSource.messages = makeMessages(for: messagesDataSource.senders) @@ -77,6 +69,7 @@ final class MessagesViewControllerTests: XCTestCase { func testNumberOfItemInSection_isOne() { let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() sut.messagesCollectionView.messagesDataSource = messagesDataSource messagesDataSource.messages = makeMessages(for: messagesDataSource.senders) @@ -88,15 +81,18 @@ final class MessagesViewControllerTests: XCTestCase { 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")) + 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)) + let cell = sut.messagesCollectionView.dataSource?.collectionView( + sut.messagesCollectionView, + cellForItemAt: IndexPath(item: 0, section: 0)) XCTAssertNotNil(cell) XCTAssertTrue(cell is TextMessageCell) @@ -104,17 +100,20 @@ final class MessagesViewControllerTests: XCTestCase { 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")) + 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)) + let cell = sut.messagesCollectionView.dataSource?.collectionView( + sut.messagesCollectionView, + cellForItemAt: IndexPath(item: 0, section: 0)) XCTAssertNotNil(cell) XCTAssertTrue(cell is TextMessageCell) @@ -122,15 +121,18 @@ final class MessagesViewControllerTests: XCTestCase { 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")) + 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)) + let cell = sut.messagesCollectionView.dataSource?.collectionView( + sut.messagesCollectionView, + cellForItemAt: IndexPath(item: 0, section: 0)) XCTAssertNotNil(cell) XCTAssertTrue(cell is MediaMessageCell) @@ -138,15 +140,18 @@ final class MessagesViewControllerTests: XCTestCase { 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")) + 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)) + let cell = sut.messagesCollectionView.dataSource?.collectionView( + sut.messagesCollectionView, + cellForItemAt: IndexPath(item: 0, section: 0)) XCTAssertNotNil(cell) XCTAssertTrue(cell is MediaMessageCell) @@ -154,15 +159,18 @@ final class MessagesViewControllerTests: XCTestCase { 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")) + 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)) + let cell = sut.messagesCollectionView.dataSource?.collectionView( + sut.messagesCollectionView, + cellForItemAt: IndexPath(item: 0, section: 0)) XCTAssertNotNil(cell) XCTAssertTrue(cell is LocationMessageCell) @@ -170,16 +178,19 @@ final class MessagesViewControllerTests: XCTestCase { func testCellForItemWithAudioData_returnsAudioMessageCell() { let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() sut.messagesCollectionView.messagesDataSource = messagesDataSource - messagesDataSource.messages.append(MockMessage(audioURL: URL.init(fileURLWithPath: ""), - duration: 4.0, - user: messagesDataSource.senders[0], - messageId: "test_id")) + 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)) + let cell = sut.messagesCollectionView.dataSource?.collectionView( + sut.messagesCollectionView, + cellForItemAt: IndexPath(item: 0, section: 0)) XCTAssertNotNil(cell) XCTAssertTrue(cell is AudioMessageCell) @@ -187,37 +198,34 @@ final class MessagesViewControllerTests: XCTestCase { 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()) + 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")) + 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)) + let cell = sut.messagesCollectionView.dataSource?.collectionView( + sut.messagesCollectionView, + cellForItemAt: IndexPath(item: 0, section: 0)) XCTAssertNotNil(cell) XCTAssertTrue(cell is LinkPreviewMessageCell) } - // MARK: - Assistants - - private func makeMessages(for senders: [MockUser]) -> [MessageType] { - return [MockMessage(text: "Text 1", user: senders[0], messageId: "test_id_1"), - MockMessage(text: "Text 2", user: senders[1], messageId: "test_id_2")] - } - // MARK: - Setups - + func testSubviewsSetup() { let controller = MessagesViewController() XCTAssertTrue(controller.view.subviews.contains(controller.messagesCollectionView)) @@ -229,36 +237,53 @@ final class MessagesViewControllerTests: XCTestCase { XCTAssertTrue(controller.messagesCollectionView.delegate is MessagesViewController) XCTAssertTrue(controller.messagesCollectionView.dataSource is MessagesViewController) } - + func testDefaultPropertyValues() { let controller = MessagesViewController() - XCTAssertTrue(controller.canBecomeFirstResponder) - XCTAssertFalse(controller.shouldAutorotate) - XCTAssertNotNil(controller.inputAccessoryView) 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"), + ] + } } -private class MockLayoutDelegate: MessagesLayoutDelegate, MessagesDisplayDelegate { +// MARK: - MockLayoutDelegate +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 heightForLocation(message _: MessageType, at _: IndexPath, with _: CGFloat, in _: MessagesCollectionView) -> CGFloat { + 0.0 } - func heightForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return 10.0 + func heightForMedia(message _: MessageType, at _: IndexPath, with _: CGFloat, in _: MessagesCollectionView) -> CGFloat { + 10.0 } - - func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions { - return LocationMessageSnapshotOptions() + + func snapshotOptionsForLocation( + message _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) + -> LocationMessageSnapshotOptions + { + LocationMessageSnapshotOptions() } } diff --git a/Tests/MessageKitTests/MessageKitTests.swift b/Tests/MessageKitTests/MessageKitTests.swift index 7f2624469..1b5169b52 100644 --- a/Tests/MessageKitTests/MessageKitTests.swift +++ b/Tests/MessageKitTests/MessageKitTests.swift @@ -1,33 +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. - */ +// 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 var allTests = [ - "" + static let allTests = [ + "", ] } diff --git a/Tests/MessageKitTests/Mocks/MockMessage.swift b/Tests/MessageKitTests/Mocks/MockMessage.swift index 113166996..efca37dfa 100644 --- a/Tests/MessageKitTests/Mocks/MockMessage.swift +++ b/Tests/MessageKitTests/Mocks/MockMessage.swift @@ -1,133 +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. - */ +// 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 -import CoreLocation -import AVFoundation @testable import MessageKit -struct MockLocationItem: LocationItem { +// MARK: - MockLocationItem - var location: CLLocation - var size: CGSize - - init(location: CLLocation) { - self.location = location - self.size = CGSize(width: 240, height: 240) - } +struct MockLocationItem: LocationItem { + var location: CLLocation + var size: CGSize + init(location: CLLocation) { + self.location = location + 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() - } +// 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() + } } -private struct MockAudiotem: AudioItem { - - var url: URL - var size: CGSize - var duration: Float - - init(url: URL, duration: Float) { - self.url = url - self.size = CGSize(width: 160, height: 35) - self.duration = duration - } +// 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 + let text: String? + let attributedText: NSAttributedString? + let url: URL + let title: String? + let teaser: String + let thumbnailImage: UIImage } -struct MockMessage: MessageType { +// MARK: - MockMessage - var messageId: String - var sender: SenderType { - return user - } - var sentDate: Date - var kind: MessageKind - var user: MockUser - - private init(kind: MessageKind, user: MockUser, messageId: String) { - self.kind = kind - self.user = user - self.messageId = messageId - self.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) - } +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 index 42ecdb9e4..dc9da0138 100644 --- a/Tests/MessageKitTests/Mocks/MockMessagesDataSource.swift +++ b/Tests/MessageKitTests/Mocks/MockMessagesDataSource.swift @@ -1,52 +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. - */ +// 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") + MockUser(senderId: "sender_2", displayName: "Sender 2"), ] var currentUser: MockUser { - return senders[0] + senders[0] } - func currentSender() -> SenderType { - return currentUser + var currentSender: SenderType { + currentUser } - func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int { - return messages.count + func numberOfSections(in _: MessagesCollectionView) -> Int { + messages.count } - func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType { - return messages[indexPath.section] + 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 index 9c048f0a6..a91b3f8c8 100644 --- a/Tests/MessageKitTests/Mocks/MockUser.swift +++ b/Tests/MessageKitTests/Mocks/MockUser.swift @@ -1,31 +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. - */ +// 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 + var senderId: String + var displayName: String } diff --git a/Tests/MessageKitTests/Model Tests/AvatarTests.swift b/Tests/MessageKitTests/Model Tests/AvatarTests.swift index a3ed9a6f0..f87804af6 100644 --- a/Tests/MessageKitTests/Model Tests/AvatarTests.swift +++ b/Tests/MessageKitTests/Model Tests/AvatarTests.swift @@ -1,35 +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. - */ +// 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, "?") - } + 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 index 81ac4c47c..9fa84b1c6 100644 --- a/Tests/MessageKitTests/Model Tests/DetectorTypeTests.swift +++ b/Tests/MessageKitTests/Model Tests/DetectorTypeTests.swift @@ -1,47 +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. - */ +// 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) - } + 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 index 5f7ca643e..154a8c144 100644 --- a/Tests/MessageKitTests/Model Tests/MessageKitDateFormatterTests.swift +++ b/Tests/MessageKitTests/Model Tests/MessageKitDateFormatterTests.swift @@ -1,114 +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. - */ +// 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)) + 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)) } - 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)) - } + /// Day of last week + startOfWeek = (Calendar.current as NSCalendar).date(byAdding: .day, value: -2, to: startOfWeek, options: [])! - 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)) + 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" } - 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(MessageKitDateFormatter.shared.string(from: startOfWeek), formatter.string(from: startOfWeek)) + } - XCTAssertEqual(formatter.string(from: lastYear), MessageKitDateFormatter.shared.string(from: lastYear)) - } + 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 index ccd44064a..dc12297a2 100644 --- a/Tests/MessageKitTests/Model Tests/MessageStyleTests.swift +++ b/Tests/MessageKitTests/Model Tests/MessageStyleTests.swift @@ -1,157 +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. - */ +// 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 { - private let assetBundle = Bundle.messageKitAssetBundle - - 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) - } - - 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) - } + // 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 index f2ddb6d8a..5bb75de24 100644 --- a/Tests/MessageKitTests/Model Tests/SenderTests.swift +++ b/Tests/MessageKitTests/Model Tests/SenderTests.swift @@ -1,40 +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. - */ +// 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) - } + 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 index 9eec24fb0..2f51d61b9 100644 --- a/Tests/MessageKitTests/Protocols Tests/MessagesDisplayDelegateTests.swift +++ b/Tests/MessageKitTests/Protocols Tests/MessagesDisplayDelegateTests.swift @@ -1,188 +1,212 @@ -/* - 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. - */ +// 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 MessagesDisplayDelegateTests: XCTestCase { - - private var sut: MockMessagesViewController! +// MARK: - MessagesDisplayDelegateTests - override func setUp() { - super.setUp() +@MainActor +final class MessagesDisplayDelegateTests: XCTestCase { + // MARK: - Private helper API - sut = MockMessagesViewController() + private func makeSUT() -> MockMessagesViewController { + let sut = MockMessagesViewController() _ = sut.view sut.beginAppearanceTransition(true, animated: true) sut.endAppearanceTransition() + sut.viewDidLoad() sut.view.layoutIfNeeded() - } - - override func tearDown() { - sut = nil - super.tearDown() + return sut } + // MARK: Internal + func testBackGroundColorDefaultState() { - 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) + 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) + 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() { - 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) + 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() { - XCTAssertNil(sut.dataProvider.cellTopLabelAttributedText(for: sut.dataProvider.messages[0], - at: IndexPath(item: 0, section: 0))) + let sut = makeSUT() + 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))) + let sut = makeSUT() + 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 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 + 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) } - } -class TextMessageDisplayDelegateTests: XCTestCase { - - private var sut: MockMessagesViewController! +// MARK: - TextMessageDisplayDelegateTests - override func setUp() { - super.setUp() +@MainActor +class TextMessageDisplayDelegateTests: XCTestCase { + // MARK: - Private helper API - sut = MockMessagesViewController() + private func makeSUT() -> MockMessagesViewController { + let sut = MockMessagesViewController() _ = sut.view sut.beginAppearanceTransition(true, animated: true) sut.endAppearanceTransition() - } - - override func tearDown() { - sut = nil - - super.tearDown() + return sut } func testTextColorFromCurrentSender_returnsWhiteForDefault() { - let textColor = sut.textColor(for: sut.dataProvider.messages[0], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView) + 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 textColor = sut.textColor(for: sut.dataProvider.messages[1], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView) + 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) + 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 detectors = sut.enabledDetectors(for: sut.dataProvider.messages[1], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView) + 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) } - } -private class MockMessagesViewController: MessagesViewController, MessagesDisplayDelegate, MessagesLayoutDelegate { +// MARK: - MockMessagesViewController - func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return 200 - } +@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() @@ -190,22 +214,31 @@ private class MockMessagesViewController: MessagesViewController, MessagesDispla 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")) + 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 } - - func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions { - return LocationMessageSnapshotOptions() - } } diff --git a/Tests/MessageKitTests/Views Tests/AvatarViewTests.swift b/Tests/MessageKitTests/Views Tests/AvatarViewTests.swift index de3c070d2..846b1c01e 100644 --- a/Tests/MessageKitTests/Views Tests/AvatarViewTests.swift +++ b/Tests/MessageKitTests/Views Tests/AvatarViewTests.swift @@ -1,47 +1,42 @@ -/* - 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. - */ +// 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 - var avatarView: AvatarView! - - override func setUp() { - super.setUp() - avatarView = AvatarView() + private func makeAvatarView() -> AvatarView { + let avatarView = AvatarView() avatarView.frame.size = CGSize(width: 30, height: 30) - } - - override func tearDown() { - super.tearDown() - avatarView = nil + 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 @@ -49,6 +44,8 @@ final class AvatarViewTests: XCTestCase { } func testWithImage() { + let avatarView = makeAvatarView() + let avatar = Avatar(image: UIImage()) avatarView.set(avatar: avatar) XCTAssertEqual(avatar.initials, "?") @@ -57,6 +54,8 @@ final class AvatarViewTests: XCTestCase { } func testInitialsOnly() { + let avatarView = makeAvatarView() + let avatar = Avatar(initials: "DL") avatarView.set(avatar: avatar) XCTAssertEqual(avatarView.initials, avatar.initials) @@ -66,12 +65,15 @@ final class AvatarViewTests: XCTestCase { } 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) @@ -79,6 +81,8 @@ final class AvatarViewTests: XCTestCase { } func testRoundedCorners() { + let avatarView = makeAvatarView() + let avatar = Avatar(image: UIImage()) avatarView.set(avatar: avatar) XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) diff --git a/Tests/MessageKitTests/Views Tests/MessageCollectionViewCellTests.swift b/Tests/MessageKitTests/Views Tests/MessageCollectionViewCellTests.swift index b34e47b1a..62190e3ea 100644 --- a/Tests/MessageKitTests/Views Tests/MessageCollectionViewCellTests.swift +++ b/Tests/MessageKitTests/Views Tests/MessageCollectionViewCellTests.swift @@ -1,61 +1,52 @@ -/* - 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. - */ +// 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) @@ -64,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) @@ -73,14 +65,17 @@ final class MessageContentCellTests: XCTestCase { XCTAssertEqual(cell.cellTopLabel.frame.size, layoutAttributes.cellTopLabelSize) XCTAssertEqual(cell.messageBottomLabel.frame.size, layoutAttributes.messageBottomLabelSize) } - } extension MessageContentCellTests { private class MockMessagesDisplayDelegate: MessagesDisplayDelegate { - func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions { - return LocationMessageSnapshotOptions() + 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 index 27fd32e39..84e0ca902 100644 --- a/Tests/MessageKitTests/Views Tests/MessagesCollectionViewTests.swift +++ b/Tests/MessageKitTests/Views Tests/MessagesCollectionViewTests.swift @@ -1,50 +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. - */ +// 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 { - - 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() { + let messagesCollectionView = MessagesCollectionView(frame: rect, collectionViewLayout: layout) XCTAssertEqual(messagesCollectionView.frame, rect) XCTAssertEqual(messagesCollectionView.collectionViewLayout, layout) XCTAssertEqual(messagesCollectionView.backgroundColor, UIColor.collectionViewBackground) } - } 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.