diff --git a/CHANGELOG.md b/CHANGELOG.md index a637d4de..7aba7f2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,19 +10,27 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa ### Changed -- **Breaking Change** Add support for share contact. [#1013](https://github.com/MessageKit/MessageKit/pull/1013) by [@moldovaniosif](https://github.com/moldovaniosif) +- **Breaking Change** Deprecated the Sender struct in favor of the `SenderType` protocol. +[#909](https://github.com/MessageKit/MessageKit/pull/909) by [@nathantannar4](https://github.com/nathantannar4) - **Breaking Change** Deprecated the Sender struct in favor of the `SenderType` protocol. [#909](https://github.com/MessageKit/MessageKit/pull/909) by [@nathantannar4](https://github.com/nathantannar4) -- **Breaking Change** Add support for audio messages [#892](https://github.com/MessageKit/MessageKit/pull/892) by [@moldovaniosif](https://github.com/moldovaniosif). Added new protocols `AudioControllerDelegate`, `AudioItem` a new cell `AudioMessageCell` and a new controller `BasicAudioController`. +- **Breaking Change** Add support for audio messages. Added new protocols `AudioControllerDelegate`, `AudioItem` a new cell `AudioMessageCell` and a new controller `BasicAudioController`. +[#892](https://github.com/MessageKit/MessageKit/pull/892) by [@moldovaniosif](https://github.com/moldovaniosif). -- **Breaking Change** Moved `handleTapGesture` method to `MessageCollectionViewCell` [#950](https://github.com/MessageKit/MessageKit/pull/950) by [@nathantannar4](https://github.com/nathantannar4) +- **Breaking Change** Moved `handleTapGesture` method to `MessageCollectionViewCell` +[#950](https://github.com/MessageKit/MessageKit/pull/950) by [@nathantannar4](https://github.com/nathantannar4) - **Breaking Change** Renamed function `layoutBottomLabel(with:)` to `layoutMessageBottomLabel(with:)` in `MessageContentCell` class. [#920](https://github.com/MessageKit/MessageKit/pull/920) by [@maxxx777](https://github.com/maxxx777) ### Added +- **Breaking Change** Add support for share contact. [#1013](https://github.com/MessageKit/MessageKit/pull/1013) by [@moldovaniosif](https://github.com/moldovaniosif) + +- Added typing indicator support, `func setTypingIndicatorViewHidden(_ isHidden: Bool, animated: Bool, whilePerforming updates: (() -> Void)? = nil, completion: ((Bool) -> Void)? = nil)`. Return a custom typing view by conforming to `MessagesDisplayDelegate` or use the [default appearance](https://github.com/nathantannar4/TypingIndicator). Customize the size with `MessagesLayoutDelegate` . +[#989](https://github.com/MessageKit/MessageKit/pull/911) by [@nathantannar4](https://github.com/nathantannar4) + - Added `AccessoryPosition` class. [#989](https://github.com/MessageKit/MessageKit/pull/989) by [@subdiox](https://github.com/subdiox) diff --git a/Example/Sources/AppDelegate.swift b/Example/Sources/AppDelegate.swift index ea28caaa..d5efe9f4 100644 --- a/Example/Sources/AppDelegate.swift +++ b/Example/Sources/AppDelegate.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + 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 diff --git a/Example/Sources/AudioController/BasicAudioController.swift b/Example/Sources/AudioController/BasicAudioController.swift index 4ff00d2b..3924dce7 100644 --- a/Example/Sources/AudioController/BasicAudioController.swift +++ b/Example/Sources/AudioController/BasicAudioController.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + 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 diff --git a/Example/Sources/Data Generation/Lorem.swift b/Example/Sources/Data Generation/Lorem.swift index 61b7df48..47597730 100755 --- a/Example/Sources/Data Generation/Lorem.swift +++ b/Example/Sources/Data Generation/Lorem.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + 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 diff --git a/Example/Sources/Data Generation/SampleData.swift b/Example/Sources/Data Generation/SampleData.swift index d537beae..69ee97d4 100644 --- a/Example/Sources/Data Generation/SampleData.swift +++ b/Example/Sources/Data Generation/SampleData.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + 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 diff --git a/Example/Sources/Extensions/UIColor+Extensions.swift b/Example/Sources/Extensions/UIColor+Extensions.swift index f951c17c..8711dbbc 100644 --- a/Example/Sources/Extensions/UIColor+Extensions.swift +++ b/Example/Sources/Extensions/UIColor+Extensions.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + 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 diff --git a/Example/Sources/Extensions/UIViewController+Extensions.swift b/Example/Sources/Extensions/UIViewController+Extensions.swift index 2d734549..eff46fa3 100644 --- a/Example/Sources/Extensions/UIViewController+Extensions.swift +++ b/Example/Sources/Extensions/UIViewController+Extensions.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + 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 diff --git a/Example/Sources/Layout/CustomMessageFlowLayout.swift b/Example/Sources/Layout/CustomMessageFlowLayout.swift index 004ec703..74b0633e 100644 --- a/Example/Sources/Layout/CustomMessageFlowLayout.swift +++ b/Example/Sources/Layout/CustomMessageFlowLayout.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + 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 @@ -30,9 +30,9 @@ open class CustomMessagesFlowLayout: MessagesCollectionViewFlowLayout { open lazy var customMessageSizeCalculator = CustomMessageSizeCalculator(layout: self) open override func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator { -// if isSectionReservedForTypingBubble(indexPath.section) { -// return typingMessageSizeCalculator -// } + if isSectionReservedForTypingIndicator(indexPath.section) { + return typingIndicatorSizeCalculator + } let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) if case .custom = message.kind { return customMessageSizeCalculator diff --git a/Example/Sources/Models/MockMessage.swift b/Example/Sources/Models/MockMessage.swift index 54e758c6..ef6d0f84 100644 --- a/Example/Sources/Models/MockMessage.swift +++ b/Example/Sources/Models/MockMessage.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + 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 diff --git a/Example/Sources/Models/MockSocket.swift b/Example/Sources/Models/MockSocket.swift index c3bc2996..b23c5bcf 100644 --- a/Example/Sources/Models/MockSocket.swift +++ b/Example/Sources/Models/MockSocket.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + 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 diff --git a/Example/Sources/Models/MockUser.swift b/Example/Sources/Models/MockUser.swift index def55314..e4f2362f 100644 --- a/Example/Sources/Models/MockUser.swift +++ b/Example/Sources/Models/MockUser.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + 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 diff --git a/Example/Sources/Settings+UserDefaults.swift b/Example/Sources/Settings+UserDefaults.swift index edddcc72..f638cf4c 100644 --- a/Example/Sources/Settings+UserDefaults.swift +++ b/Example/Sources/Settings+UserDefaults.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + 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 diff --git a/Example/Sources/View Controllers/AdvancedExampleViewController.swift b/Example/Sources/View Controllers/AdvancedExampleViewController.swift index ed7a2d3a..1c7f9d3b 100644 --- a/Example/Sources/View Controllers/AdvancedExampleViewController.swift +++ b/Example/Sources/View Controllers/AdvancedExampleViewController.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + 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 @@ -37,10 +37,6 @@ final class AdvancedExampleViewController: ChatViewController { super.viewDidLoad() updateTitleView(title: "MessageKit", subtitle: "2 Online") - - // Customize the typing bubble! These are the default values -// typingBubbleBackgroundColor = UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1) -// typingBubbleDotColor = .lightGray } override func viewDidAppear(_ animated: Bool) { @@ -48,12 +44,11 @@ final class AdvancedExampleViewController: ChatViewController { MockSocket.shared.connect(with: [SampleData.shared.steven, SampleData.shared.wu]) .onTypingStatus { [weak self] in - self?.setTypingIndicatorHidden(false) + self?.setTypingIndicatorViewHidden(false) }.onNewMessage { [weak self] message in - self?.setTypingIndicatorHidden(true, performUpdates: { -// self?.insertMessage(message) + self?.setTypingIndicatorViewHidden(true, performUpdates: { + self?.insertMessage(message) }) - self?.insertMessage(message) } } @@ -186,14 +181,13 @@ final class AdvancedExampleViewController: ChatViewController { return messageList[indexPath.section].user == messageList[indexPath.section + 1].user } - func setTypingIndicatorHidden(_ isHidden: Bool, performUpdates updates: (() -> Void)? = nil) { + func setTypingIndicatorViewHidden(_ isHidden: Bool, performUpdates updates: (() -> Void)? = nil) { updateTitleView(title: "MessageKit", subtitle: isHidden ? "2 Online" : "Typing...") -// setTypingBubbleHidden(isHidden, animated: true, whilePerforming: updates) { [weak self] (_) in -// if self?.isLastSectionVisible() == true { -// self?.messagesCollectionView.scrollToBottom(animated: true) -// } -// } -// messagesCollectionView.scrollToBottom(animated: true) + setTypingIndicatorViewHidden(isHidden, animated: true, whilePerforming: updates) { [weak self] success in + if success, self?.isLastSectionVisible() == true { + self?.messagesCollectionView.scrollToBottom(animated: true) + } + } } private func makeButton(named: String) -> InputBarButtonItem { @@ -228,10 +222,10 @@ final class AdvancedExampleViewController: ChatViewController { fatalError("Ouch. nil data source for messages") } -// guard !isSectionReservedForTypingBubble(indexPath.section) else { -// return super.collectionView(collectionView, cellForItemAt: indexPath) -// } - + 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) diff --git a/Example/Sources/View Controllers/BasicExampleViewController.swift b/Example/Sources/View Controllers/BasicExampleViewController.swift index 8cdfa841..a498e237 100644 --- a/Example/Sources/View Controllers/BasicExampleViewController.swift +++ b/Example/Sources/View Controllers/BasicExampleViewController.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + 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 diff --git a/Example/Sources/View Controllers/ChatViewController.swift b/Example/Sources/View Controllers/ChatViewController.swift index c21ca4ca..d257e1c0 100644 --- a/Example/Sources/View Controllers/ChatViewController.swift +++ b/Example/Sources/View Controllers/ChatViewController.swift @@ -1,7 +1,7 @@ /* MIT License -Copyright (c) 2017-2018 MessageKit +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 @@ -112,6 +112,10 @@ class ChatViewController: MessagesViewController, MessagesDataSource { 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 diff --git a/Example/Sources/View Controllers/LaunchViewController.swift b/Example/Sources/View Controllers/LaunchViewController.swift index 109db593..fbfb71d2 100644 --- a/Example/Sources/View Controllers/LaunchViewController.swift +++ b/Example/Sources/View Controllers/LaunchViewController.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + 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 diff --git a/Example/Sources/View Controllers/MessageContainerController.swift b/Example/Sources/View Controllers/MessageContainerController.swift index bf170616..e48d6584 100644 --- a/Example/Sources/View Controllers/MessageContainerController.swift +++ b/Example/Sources/View Controllers/MessageContainerController.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + 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 diff --git a/Example/Sources/View Controllers/NavigationController.swift b/Example/Sources/View Controllers/NavigationController.swift index cb41b0cb..cc2d7eb2 100644 --- a/Example/Sources/View Controllers/NavigationController.swift +++ b/Example/Sources/View Controllers/NavigationController.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + 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 diff --git a/Example/Sources/View Controllers/SettingsViewController.swift b/Example/Sources/View Controllers/SettingsViewController.swift index 302d105c..92235e52 100644 --- a/Example/Sources/View Controllers/SettingsViewController.swift +++ b/Example/Sources/View Controllers/SettingsViewController.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + 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 diff --git a/Example/Sources/Views/CustomCell.swift b/Example/Sources/Views/CustomCell.swift index c23b0205..a229f344 100644 --- a/Example/Sources/Views/CustomCell.swift +++ b/Example/Sources/Views/CustomCell.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + 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 diff --git a/Example/Sources/Views/TableViewCells.swift b/Example/Sources/Views/TableViewCells.swift index 615ffc13..c03081d8 100644 --- a/Example/Sources/Views/TableViewCells.swift +++ b/Example/Sources/Views/TableViewCells.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + 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 diff --git a/LICENSE.md b/LICENSE.md index edefe0f8..21660f30 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017-2018 MessageKit +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 diff --git a/MessageKit.podspec~ b/MessageKit.podspec~ deleted file mode 100644 index 4a4007e6..00000000 --- a/MessageKit.podspec~ +++ /dev/null @@ -1,27 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'MessageKit' - s.version = '3.0.0' - s.license = { :type => "MIT", :file => "LICENSE.md" } - - s.summary = 'An elegant messages UI library for iOS.' - s.homepage = 'https://github.com/MessageKit/MessageKit' - s.social_media_url = 'https://twitter.com/_SD10_' - s.author = { "Steven Deutsch" => "stevensdeutsch@yahoo.com" } - - s.source = { :git => 'https://github.com/MessageKit/MessageKit.git', :tag => s.version } - s.source_files = 'Sources/**/*.swift' - - s.pod_target_xcconfig = { - "SWIFT_VERSION" => "4.2", - } - - s.swift_version = '4.2' - - s.ios.deployment_target = '9.0' - s.ios.resource_bundle = { 'MessageKitAssets' => 'Assets/MessageKitAssets.bundle/Images' } - - s.requires_arc = true - - s.dependency 'InputBarAccessoryView' - -end diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index b4d75c9c..57ad9a52 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -36,9 +36,15 @@ 1FF377AC20087DA2004FD648 /* MessagesViewController+Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */; }; 382C794221705D2000F4FAF5 /* HorizontalEdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382C794121705D2000F4FAF5 /* HorizontalEdgeInsets.swift */; }; 383B9EB121728BAD008AB91A /* SenderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383B9EB021728BAD008AB91A /* SenderType.swift */; }; + 388119462253EC30004B26AF /* TypingIndicatorCellSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388119452253EC30004B26AF /* TypingIndicatorCellSizeCalculator.swift */; }; 38A2230F221FB8A300D14DAF /* MessageInputBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A2230E221FB8A300D14DAF /* MessageInputBar.swift */; }; 38A223112223493500D14DAF /* InputBarAccessoryView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38A223102223493400D14DAF /* InputBarAccessoryView.framework */; }; 38F8062F2173CD8F00CDB9DB /* MockUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F8062D2173CD4300CDB9DB /* MockUser.swift */; }; + 38F8063221740D9E00CDB9DB /* TypingIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F8063021740D9D00CDB9DB /* TypingIndicator.swift */; }; + 38F8063321740D9E00CDB9DB /* BubbleCircle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F8063121740D9D00CDB9DB /* BubbleCircle.swift */; }; + 38F8063521740DAD00CDB9DB /* TypingBubble.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F8063421740DAD00CDB9DB /* TypingBubble.swift */; }; + 38F8063721740DD500CDB9DB /* TypingIndicatorCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F8063621740DD500CDB9DB /* TypingIndicatorCell.swift */; }; + 4C508649221C0BBA0043943C /* AccessoryPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C508648221C0BBA0043943C /* AccessoryPosition.swift */; }; 4C508649221C0BBA0043943C /* AccessoryPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C508648221C0BBA0043943C /* AccessoryPosition.swift */; }; 5073C1152175BE750040EAD5 /* AudioItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5073C1142175BE750040EAD5 /* AudioItem.swift */; }; 5073C1192175BE960040EAD5 /* AudioMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5073C1182175BE950040EAD5 /* AudioMessageCell.swift */; }; @@ -143,10 +149,17 @@ 1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Keyboard.swift"; sourceTree = ""; }; 382C794121705D2000F4FAF5 /* HorizontalEdgeInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalEdgeInsets.swift; sourceTree = ""; }; 383B9EB021728BAD008AB91A /* SenderType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SenderType.swift; sourceTree = ""; }; + 388119452253EC30004B26AF /* TypingIndicatorCellSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingIndicatorCellSizeCalculator.swift; sourceTree = ""; }; 38A2230E221FB8A300D14DAF /* MessageInputBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageInputBar.swift; sourceTree = ""; }; 38A223102223493400D14DAF /* InputBarAccessoryView.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InputBarAccessoryView.framework; path = Carthage/Build/iOS/InputBarAccessoryView.framework; sourceTree = ""; }; 38C2AE7B20D4878D00F8079E /* MessageInputBar.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageInputBar.framework; path = Carthage/Build/iOS/MessageInputBar.framework; sourceTree = ""; }; 38F8062D2173CD4300CDB9DB /* MockUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUser.swift; sourceTree = ""; }; + 38F8063021740D9D00CDB9DB /* TypingIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicator.swift; sourceTree = ""; }; + 38F8063121740D9D00CDB9DB /* BubbleCircle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BubbleCircle.swift; sourceTree = ""; }; + 38F8063421740DAD00CDB9DB /* TypingBubble.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingBubble.swift; sourceTree = ""; }; + 38F8063621740DD500CDB9DB /* TypingIndicatorCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicatorCell.swift; sourceTree = ""; }; + 4C508648221C0BBA0043943C /* AccessoryPosition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessoryPosition.swift; sourceTree = ""; }; + 38F8062D2173CD4300CDB9DB /* MockUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUser.swift; sourceTree = ""; }; 4C508648221C0BBA0043943C /* AccessoryPosition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessoryPosition.swift; sourceTree = ""; }; 5073C1142175BE750040EAD5 /* AudioItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioItem.swift; sourceTree = ""; }; 5073C1182175BE950040EAD5 /* AudioMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioMessageCell.swift; sourceTree = ""; }; @@ -249,6 +262,7 @@ B7A03F7A1F866B85006AEF79 /* MessageCollectionViewCell.swift */, 1F6C040B206A2891007BDE44 /* MessageContentCell.swift */, B7A03F361F866946006AEF79 /* TextMessageCell.swift */, + 38F8063621740DD500CDB9DB /* TypingIndicatorCell.swift */, ); path = Cells; sourceTree = ""; @@ -425,11 +439,14 @@ 2EB618F01F84676A007FBA0E /* Cells */, 2EB618F11F846899007FBA0E /* HeadersFooters */, B7A03F3E1F86694F006AEF79 /* AvatarView.swift */, + 38F8063121740D9D00CDB9DB /* BubbleCircle.swift */, + 38F8063421740DAD00CDB9DB /* TypingBubble.swift */, 1FE783A7206633C0007FA024 /* InsetLabel.swift */, B7A03F431F86694F006AEF79 /* MessageContainerView.swift */, B7A03F3F1F86694F006AEF79 /* MessageLabel.swift */, B7A03F451F86694F006AEF79 /* MessagesCollectionView.swift */, B7A03F441F86694F006AEF79 /* PlayButtonView.swift */, + 38F8063021740D9D00CDB9DB /* TypingIndicator.swift */, ); path = Views; sourceTree = ""; @@ -474,6 +491,7 @@ 1FE783A5206629C2007FA024 /* LocationMessageSizeCalculator.swift */, 5073C11C2175BEC60040EAD5 /* AudioMessageSizeCalculator.swift */, 0EF0888B206F7E83007F2F58 /* CellSizeCalculator.swift */, + 388119452253EC30004B26AF /* TypingIndicatorCellSizeCalculator.swift */, ); path = Layout; sourceTree = ""; @@ -624,10 +642,12 @@ B7A03F5B1F8669CA006AEF79 /* MessageType.swift in Sources */, B7A03F601F8669CA006AEF79 /* MessagesDisplayDelegate.swift in Sources */, 1FE783A8206633C0007FA024 /* InsetLabel.swift in Sources */, + 38F8063321740D9E00CDB9DB /* BubbleCircle.swift in Sources */, B7A03F5C1F8669CA006AEF79 /* MessageCellDelegate.swift in Sources */, 5073C11D2175BEC60040EAD5 /* AudioMessageSizeCalculator.swift in Sources */, 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */, 1F6C040E206A2AF4007BDE44 /* MessageReusableView.swift in Sources */, + 388119462253EC30004B26AF /* TypingIndicatorCellSizeCalculator.swift in Sources */, B7A03F4B1F86694F006AEF79 /* MessageContainerView.swift in Sources */, B7A03F281F866895006AEF79 /* LocationMessageSnapshotOptions.swift in Sources */, B7A03F6C1F8669EB006AEF79 /* UIView+Extensions.swift in Sources */, @@ -639,6 +659,7 @@ B7A03F461F86694F006AEF79 /* AvatarView.swift in Sources */, 1FCA6D30201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift in Sources */, B7A03F3D1F866946006AEF79 /* MediaMessageCell.swift in Sources */, + 38F8063721740DD500CDB9DB /* TypingIndicatorCell.swift in Sources */, 1FE783A220662905007FA024 /* TextMessageSizeCalculator.swift in Sources */, B7A03F2E1F866895006AEF79 /* MessageKind.swift in Sources */, B7A03F7B1F866B85006AEF79 /* MessageCollectionViewCell.swift in Sources */, @@ -662,6 +683,7 @@ B7A03F251F866895006AEF79 /* NSConstraintLayoutSet.swift in Sources */, 0EE91E661FDEC888005420A2 /* CGRect+Extensions.swift in Sources */, B7A03F181F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift in Sources */, + 38F8063221740D9E00CDB9DB /* TypingIndicator.swift in Sources */, B7A03F2A1F866895006AEF79 /* MessageStyle.swift in Sources */, B7A03F4D1F86694F006AEF79 /* MessagesCollectionView.swift in Sources */, 0EF0888C206F7E83007F2F58 /* CellSizeCalculator.swift in Sources */, @@ -669,6 +691,7 @@ 1FE783A4206629A5007FA024 /* MediaMessageSizeCalculator.swift in Sources */, B7A03F731F866A06006AEF79 /* MessageKit+Availability.swift in Sources */, 1FE7839E20662835007FA024 /* MessageSizeCalculator.swift in Sources */, + 38F8063521740DAD00CDB9DB /* TypingBubble.swift in Sources */, 1FE783A6206629C2007FA024 /* LocationMessageSizeCalculator.swift in Sources */, B7A03F2D1F866895006AEF79 /* LabelAlignment.swift in Sources */, 1F6C040C206A2891007BDE44 /* MessageContentCell.swift in Sources */, diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift index b022aaf9..ed4f9cd8 100644 --- a/Sources/Controllers/MessagesViewController.swift +++ b/Sources/Controllers/MessagesViewController.swift @@ -70,6 +70,10 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { } } + public var isTypingIndicatorHidden: Bool { + return messagesCollectionView.isTypingIndicatorHidden + } + public var selectedIndexPathForMenu: IndexPath? private var isFirstLayout: Bool = true @@ -171,24 +175,87 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { } } + // 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) } - return collectionView.messagesDataSource?.numberOfSections(in: collectionView) ?? 0 + 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 } - /// Note: - /// If you override this method, remember to call MessagesDataSource's customCell(for:at:in:) for MessageKind.custom messages, if necessary + /// 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 { @@ -199,6 +266,10 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { 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 { @@ -262,9 +333,17 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { 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) @@ -272,11 +351,19 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { 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 { @@ -289,6 +376,9 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { } 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:")) } diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index a7d60456..b5e76738 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -62,18 +62,18 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { 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() } @@ -92,6 +92,26 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { NotificationCenter.default.addObserver(self, selector: #selector(MessagesCollectionViewFlowLayout.handleOrientationChange(_:)), name: UIDevice.orientationDidChangeNotification, 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]? { @@ -148,10 +168,17 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { 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) - /// - Note: - /// If you override this method, remember to call MessageLayoutDelegate's customCellSizeCalculator(for:at:in:) method for MessageKind.custom messages, if necessary + /// 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: diff --git a/Sources/Layout/TypingIndicatorCellSizeCalculator.swift b/Sources/Layout/TypingIndicatorCellSizeCalculator.swift new file mode 100644 index 00000000..3e77663f --- /dev/null +++ b/Sources/Layout/TypingIndicatorCellSizeCalculator.swift @@ -0,0 +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. + */ + +import UIKit + +open class TypingCellSizeCalculator: CellSizeCalculator { + + open var height: CGFloat = 62 + + public 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.horizontal + contentInset.horizontal + return CGSize(width: collectionViewWidth - inset, height: height) + } +} diff --git a/Sources/Models/AccessoryPosition.swift b/Sources/Models/AccessoryPosition.swift index d0496220..04092026 100644 --- a/Sources/Models/AccessoryPosition.swift +++ b/Sources/Models/AccessoryPosition.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017-2018 MessageKit + 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 diff --git a/Sources/Protocols/MessagesDataSource.swift b/Sources/Protocols/MessagesDataSource.swift index 0d219ec9..9db208f7 100644 --- a/Sources/Protocols/MessagesDataSource.swift +++ b/Sources/Protocols/MessagesDataSource.swift @@ -113,6 +113,14 @@ public protocol MessagesDataSource: AnyObject { /// - 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 { @@ -144,4 +152,8 @@ public extension MessagesDataSource { 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) + } } diff --git a/Sources/Protocols/MessagesLayoutDelegate.swift b/Sources/Protocols/MessagesLayoutDelegate.swift index 57c4ec33..3bdcb363 100644 --- a/Sources/Protocols/MessagesLayoutDelegate.swift +++ b/Sources/Protocols/MessagesLayoutDelegate.swift @@ -48,6 +48,25 @@ public protocol MessagesLayoutDelegate: AnyObject { /// 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: + /// - messagesCollectionView: The `MessagesCollectionView` in which this view will be displayed. + /// + /// - Note: + /// The default value returned by this method is the width of the `messagesCollectionView` and + /// a height of 52. + func typingIndicatorViewSize(in messagesCollectionView: MessagesCollectionView) -> 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: @@ -114,6 +133,14 @@ public extension MessagesLayoutDelegate { return .zero } + func typingIndicatorViewSize(in messagesCollectionView: MessagesCollectionView) -> CGSize { + return CGSize(width: messagesCollectionView.bounds.width, height: 48) + } + + func typingIndicatorViewTopInset(in messagesCollectionView: MessagesCollectionView) -> CGFloat { + return 15 + } + func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { return 0 } diff --git a/Sources/Views/BubbleCircle.swift b/Sources/Views/BubbleCircle.swift new file mode 100644 index 00000000..c0c276ad --- /dev/null +++ b/Sources/Views/BubbleCircle.swift @@ -0,0 +1,49 @@ +/* + 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 + } + +} diff --git a/Sources/Views/Cells/TypingIndicatorCell.swift b/Sources/Views/Cells/TypingIndicatorCell.swift new file mode 100644 index 00000000..7e8c5d94 --- /dev/null +++ b/Sources/Views/Cells/TypingIndicatorCell.swift @@ -0,0 +1,66 @@ +/* + MIT License + + Copyright (c) 2017-2019 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +/// A subclass of `MessageCollectionViewCell` used to display the typing indicator. +open class TypingIndicatorCell: MessageCollectionViewCell { + + // MARK: - Subviews + + 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) + } + +} diff --git a/Sources/Views/MessagesCollectionView.swift b/Sources/Views/MessagesCollectionView.swift index c9d97bc5..22e75474 100644 --- a/Sources/Views/MessagesCollectionView.swift +++ b/Sources/Views/MessagesCollectionView.swift @@ -36,12 +36,23 @@ open class MessagesCollectionView: UICollectionView { open weak var messageCellDelegate: MessageCellDelegate? + open var isTypingIndicatorHidden: Bool { + return messagesCollectionViewFlowLayout.isTypingIndicatorViewHidden + } + 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) { @@ -67,6 +78,7 @@ open class MessagesCollectionView: UICollectionView { register(LocationMessageCell.self) register(AudioMessageCell.self) register(ContactMessageCell.self) + register(TypingIndicatorCell.self) register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader) register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter) } @@ -113,14 +125,36 @@ open class MessagesCollectionView: UICollectionView { 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(_ headerFooterClass: T.Type, forSupplementaryViewOfKind kind: String) { - register(headerFooterClass, + public func register(_ reusableViewClass: T.Type, forSupplementaryViewOfKind kind: String) { + register(reusableViewClass, forSupplementaryViewOfKind: kind, withReuseIdentifier: String(describing: T.self)) } diff --git a/Sources/Views/TypingBubble.swift b/Sources/Views/TypingBubble.swift new file mode 100644 index 00000000..82cfb239 --- /dev/null +++ b/Sources/Views/TypingBubble.swift @@ -0,0 +1,155 @@ +/* + 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 = .incomingGray + } + + // 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: - 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) } + } + +} diff --git a/Sources/Views/TypingIndicator.swift b/Sources/Views/TypingIndicator.swift new file mode 100644 index 00000000..60872b37 --- /dev/null +++ b/Sources/Views/TypingIndicator.swift @@ -0,0 +1,165 @@ +/* + 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.lightGray { + 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: - Layout + + 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 { + 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) + } + } + +}