mirror of
https://github.com/MessageKit/MessageKit.git
synced 2026-02-06 19:03:19 +00:00
Merge pull request #1038 from MessageKit/typingIndicatorView
Typing Indicator Support
This commit is contained in:
+11
-3
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 = "<group>"; };
|
||||
382C794121705D2000F4FAF5 /* HorizontalEdgeInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalEdgeInsets.swift; sourceTree = "<group>"; };
|
||||
383B9EB021728BAD008AB91A /* SenderType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SenderType.swift; sourceTree = "<group>"; };
|
||||
388119452253EC30004B26AF /* TypingIndicatorCellSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingIndicatorCellSizeCalculator.swift; sourceTree = "<group>"; };
|
||||
38A2230E221FB8A300D14DAF /* MessageInputBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageInputBar.swift; sourceTree = "<group>"; };
|
||||
38A223102223493400D14DAF /* InputBarAccessoryView.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InputBarAccessoryView.framework; path = Carthage/Build/iOS/InputBarAccessoryView.framework; sourceTree = "<group>"; };
|
||||
38C2AE7B20D4878D00F8079E /* MessageInputBar.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageInputBar.framework; path = Carthage/Build/iOS/MessageInputBar.framework; sourceTree = "<group>"; };
|
||||
38F8062D2173CD4300CDB9DB /* MockUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUser.swift; sourceTree = "<group>"; };
|
||||
38F8063021740D9D00CDB9DB /* TypingIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicator.swift; sourceTree = "<group>"; };
|
||||
38F8063121740D9D00CDB9DB /* BubbleCircle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BubbleCircle.swift; sourceTree = "<group>"; };
|
||||
38F8063421740DAD00CDB9DB /* TypingBubble.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingBubble.swift; sourceTree = "<group>"; };
|
||||
38F8063621740DD500CDB9DB /* TypingIndicatorCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicatorCell.swift; sourceTree = "<group>"; };
|
||||
4C508648221C0BBA0043943C /* AccessoryPosition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessoryPosition.swift; sourceTree = "<group>"; };
|
||||
38F8062D2173CD4300CDB9DB /* MockUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUser.swift; sourceTree = "<group>"; };
|
||||
4C508648221C0BBA0043943C /* AccessoryPosition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessoryPosition.swift; sourceTree = "<group>"; };
|
||||
5073C1142175BE750040EAD5 /* AudioItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioItem.swift; sourceTree = "<group>"; };
|
||||
5073C1182175BE950040EAD5 /* AudioMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioMessageCell.swift; sourceTree = "<group>"; };
|
||||
@@ -249,6 +262,7 @@
|
||||
B7A03F7A1F866B85006AEF79 /* MessageCollectionViewCell.swift */,
|
||||
1F6C040B206A2891007BDE44 /* MessageContentCell.swift */,
|
||||
B7A03F361F866946006AEF79 /* TextMessageCell.swift */,
|
||||
38F8063621740DD500CDB9DB /* TypingIndicatorCell.swift */,
|
||||
);
|
||||
path = Cells;
|
||||
sourceTree = "<group>";
|
||||
@@ -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 = "<group>";
|
||||
@@ -474,6 +491,7 @@
|
||||
1FE783A5206629C2007FA024 /* LocationMessageSizeCalculator.swift */,
|
||||
5073C11C2175BEC60040EAD5 /* AudioMessageSizeCalculator.swift */,
|
||||
0EF0888B206F7E83007F2F58 /* CellSizeCalculator.swift */,
|
||||
388119452253EC30004B26AF /* TypingIndicatorCellSizeCalculator.swift */,
|
||||
);
|
||||
path = Layout;
|
||||
sourceTree = "<group>";
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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:"))
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<T: UICollectionViewCell>(_ cellClass: T.Type) {
|
||||
register(cellClass, forCellWithReuseIdentifier: String(describing: T.self))
|
||||
}
|
||||
|
||||
/// Registers a reusable view for a specific SectionKind
|
||||
public func register<T: UICollectionReusableView>(_ headerFooterClass: T.Type, forSupplementaryViewOfKind kind: String) {
|
||||
register(headerFooterClass,
|
||||
public func register<T: UICollectionReusableView>(_ reusableViewClass: T.Type, forSupplementaryViewOfKind kind: String) {
|
||||
register(reusableViewClass,
|
||||
forSupplementaryViewOfKind: kind,
|
||||
withReuseIdentifier: String(describing: T.self))
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user