diff --git a/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessageViewModel.swift b/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessageViewModel.swift index 3bf0d431c..9406c0312 100644 --- a/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessageViewModel.swift +++ b/ChattoAdditions/Source/Chat Items/BaseMessage/BaseMessageViewModel.swift @@ -133,6 +133,6 @@ public class MessageViewModelDefaultBuilder { public func createMessageViewModel(message: MessageModelProtocol) -> MessageViewModelProtocol { // Override to use default avatarImage - return MessageViewModel(dateFormatter: self.dynamicType.dateFormatter, showsTail: false, messageModel: message, avatarImage: nil) + return MessageViewModel(dateFormatter: MessageViewModelDefaultBuilder.dateFormatter, showsTail: false, messageModel: message, avatarImage: nil) } } diff --git a/ChattoAdditions/Source/Chat Items/PhotoMessages/Views/PhotoMessageCollectionViewCellDefaultStyle.swift b/ChattoAdditions/Source/Chat Items/PhotoMessages/Views/PhotoMessageCollectionViewCellDefaultStyle.swift index e7e5b1c42..30a3057e7 100644 --- a/ChattoAdditions/Source/Chat Items/PhotoMessages/Views/PhotoMessageCollectionViewCellDefaultStyle.swift +++ b/ChattoAdditions/Source/Chat Items/PhotoMessages/Views/PhotoMessageCollectionViewCellDefaultStyle.swift @@ -45,19 +45,19 @@ public class PhotoMessageCollectionViewCellDefaultStyle: PhotoMessageCollectionV lazy private var baseStyle = BaseMessageCollectionViewCellDefaultStyle() lazy private var maskImageIncomingTail: UIImage = { - return UIImage(named: "bubble-incoming-tail", inBundle: NSBundle(forClass: self.dynamicType), compatibleWithTraitCollection: nil)! + return UIImage(named: "bubble-incoming-tail", inBundle: NSBundle(forClass: PhotoMessageCollectionViewCellDefaultStyle.self), compatibleWithTraitCollection: nil)! }() lazy private var maskImageIncomingNoTail: UIImage = { - return UIImage(named: "bubble-incoming", inBundle: NSBundle(forClass: self.dynamicType), compatibleWithTraitCollection: nil)! + return UIImage(named: "bubble-incoming", inBundle: NSBundle(forClass: PhotoMessageCollectionViewCellDefaultStyle.self), compatibleWithTraitCollection: nil)! }() lazy private var maskImageOutgoingTail: UIImage = { - return UIImage(named: "bubble-outgoing-tail", inBundle: NSBundle(forClass: self.dynamicType), compatibleWithTraitCollection: nil)! + return UIImage(named: "bubble-outgoing-tail", inBundle: NSBundle(forClass: PhotoMessageCollectionViewCellDefaultStyle.self), compatibleWithTraitCollection: nil)! }() lazy private var maskImageOutgoingNoTail: UIImage = { - return UIImage(named: "bubble-outgoing", inBundle: NSBundle(forClass: self.dynamicType), compatibleWithTraitCollection: nil)! + return UIImage(named: "bubble-outgoing", inBundle: NSBundle(forClass: PhotoMessageCollectionViewCellDefaultStyle.self), compatibleWithTraitCollection: nil)! }() lazy private var placeholderBackgroundIncoming: UIImage = { @@ -69,7 +69,7 @@ public class PhotoMessageCollectionViewCellDefaultStyle: PhotoMessageCollectionV }() lazy private var placeholderIcon: UIImage = { - return UIImage(named: "photo-bubble-placeholder-icon", inBundle: NSBundle(forClass: self.dynamicType), compatibleWithTraitCollection: nil)! + return UIImage(named: "photo-bubble-placeholder-icon", inBundle: NSBundle(forClass: PhotoMessageCollectionViewCellDefaultStyle.self), compatibleWithTraitCollection: nil)! }() public func maskingImage(viewModel viewModel: PhotoMessageViewModelProtocol) -> UIImage { diff --git a/ChattoAdditions/Source/Input/Photos/LiveCameraCell.swift b/ChattoAdditions/Source/Input/Photos/LiveCameraCell.swift index 8bef834a5..7ea068d26 100644 --- a/ChattoAdditions/Source/Input/Photos/LiveCameraCell.swift +++ b/ChattoAdditions/Source/Input/Photos/LiveCameraCell.swift @@ -88,9 +88,9 @@ class LiveCameraCell: UICollectionViewCell { private func updateIcon() { switch self.authorizationStatus { case .NotDetermined, .Authorized: - self.iconImageView.image = UIImage(named: Constants.cameraImageName, inBundle: NSBundle(forClass: self.dynamicType), compatibleWithTraitCollection: nil) + self.iconImageView.image = UIImage(named: Constants.cameraImageName, inBundle: NSBundle(forClass: LiveCameraCell.self), compatibleWithTraitCollection: nil) case .Restricted, .Denied: - self.iconImageView.image = UIImage(named: Constants.lockedCameraImageName, inBundle: NSBundle(forClass: self.dynamicType), compatibleWithTraitCollection: nil) + self.iconImageView.image = UIImage(named: Constants.lockedCameraImageName, inBundle: NSBundle(forClass: LiveCameraCell.self), compatibleWithTraitCollection: nil) } self.setNeedsLayout() } diff --git a/ChattoAdditions/Source/Input/Photos/PhotosChatInputItem.swift b/ChattoAdditions/Source/Input/Photos/PhotosChatInputItem.swift index 83d4dd687..2207cc71b 100644 --- a/ChattoAdditions/Source/Input/Photos/PhotosChatInputItem.swift +++ b/ChattoAdditions/Source/Input/Photos/PhotosChatInputItem.swift @@ -34,9 +34,9 @@ import Foundation lazy private var internalTabView: UIButton = { var button = UIButton(type: .Custom) button.exclusiveTouch = true - button.setImage(UIImage(named: "camera-icon-unselected", inBundle: NSBundle(forClass: self.dynamicType), compatibleWithTraitCollection: nil), forState: .Normal) - button.setImage(UIImage(named: "camera-icon-selected", inBundle: NSBundle(forClass: self.dynamicType), compatibleWithTraitCollection: nil), forState: .Highlighted) - button.setImage(UIImage(named: "camera-icon-selected", inBundle: NSBundle(forClass: self.dynamicType), compatibleWithTraitCollection: nil), forState: .Selected) + button.setImage(UIImage(named: "camera-icon-unselected", inBundle: NSBundle(forClass: PhotosChatInputItem.self), compatibleWithTraitCollection: nil), forState: .Normal) + button.setImage(UIImage(named: "camera-icon-selected", inBundle: NSBundle(forClass: PhotosChatInputItem.self), compatibleWithTraitCollection: nil), forState: .Highlighted) + button.setImage(UIImage(named: "camera-icon-selected", inBundle: NSBundle(forClass: PhotosChatInputItem.self), compatibleWithTraitCollection: nil), forState: .Selected) return button }() diff --git a/ChattoAdditions/Source/Input/Photos/PhotosInputCell.swift b/ChattoAdditions/Source/Input/Photos/PhotosInputCell.swift index ebb023066..00f6ebb28 100644 --- a/ChattoAdditions/Source/Input/Photos/PhotosInputCell.swift +++ b/ChattoAdditions/Source/Input/Photos/PhotosInputCell.swift @@ -45,7 +45,7 @@ class PhotosInputPlaceholderCell: UICollectionViewCell { private func commonInit() { self.imageView = UIImageView() self.imageView.contentMode = .Center - self.imageView.image = UIImage(named: Constants.imageName, inBundle: NSBundle(forClass: self.dynamicType), compatibleWithTraitCollection: nil) + self.imageView.image = UIImage(named: Constants.imageName, inBundle: NSBundle(forClass: PhotosInputPlaceholderCell.self), compatibleWithTraitCollection: nil) self.contentView.addSubview(self.imageView) self.contentView.backgroundColor = Constants.backgroundColor } diff --git a/ChattoAdditions/Source/Input/Text/TextChatInputItem.swift b/ChattoAdditions/Source/Input/Text/TextChatInputItem.swift index 0f04daeb7..18b00f138 100644 --- a/ChattoAdditions/Source/Input/Text/TextChatInputItem.swift +++ b/ChattoAdditions/Source/Input/Text/TextChatInputItem.swift @@ -30,9 +30,9 @@ import Foundation lazy private var internalTabView: UIButton = { var button = UIButton(type: .Custom) button.exclusiveTouch = true - button.setImage(UIImage(named: "text-icon-unselected", inBundle: NSBundle(forClass: self.dynamicType), compatibleWithTraitCollection: nil), forState: .Normal) - button.setImage(UIImage(named: "text-icon-selected", inBundle: NSBundle(forClass: self.dynamicType), compatibleWithTraitCollection: nil), forState: .Highlighted) - button.setImage(UIImage(named: "text-icon-selected", inBundle: NSBundle(forClass: self.dynamicType), compatibleWithTraitCollection: nil), forState: .Selected) + button.setImage(UIImage(named: "text-icon-unselected", inBundle: NSBundle(forClass: TextChatInputItem.self), compatibleWithTraitCollection: nil), forState: .Normal) + button.setImage(UIImage(named: "text-icon-selected", inBundle: NSBundle(forClass: TextChatInputItem.self), compatibleWithTraitCollection: nil), forState: .Highlighted) + button.setImage(UIImage(named: "text-icon-selected", inBundle: NSBundle(forClass: TextChatInputItem.self), compatibleWithTraitCollection: nil), forState: .Selected) return button }() diff --git a/ChattoApp/ChattoApp.xcodeproj/project.pbxproj b/ChattoApp/ChattoApp.xcodeproj/project.pbxproj index 32d3b4fc3..984b7e8db 100644 --- a/ChattoApp/ChattoApp.xcodeproj/project.pbxproj +++ b/ChattoApp/ChattoApp.xcodeproj/project.pbxproj @@ -31,6 +31,7 @@ C3F91DCC1C75EFE300D461D2 /* SendingStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3F91DC91C75EFE300D461D2 /* SendingStatusCollectionViewCell.swift */; }; C3F91DCD1C75EFE300D461D2 /* SendingStatusCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C3F91DCA1C75EFE300D461D2 /* SendingStatusCollectionViewCell.xib */; }; C3F91DCE1C75EFE300D461D2 /* SendingStatusPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3F91DCB1C75EFE300D461D2 /* SendingStatusPresenter.swift */; }; + FE1A697B1C93FF10004DF45A /* TimestampSeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1A697A1C93FF10004DF45A /* TimestampSeparatorView.swift */; }; FE2D050B1C915ADB006F902B /* BaseMessageCollectionViewCellAvatarStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2D050A1C915ADB006F902B /* BaseMessageCollectionViewCellAvatarStyle.swift */; }; /* End PBXBuildFile section */ @@ -97,6 +98,7 @@ C3F91DC91C75EFE300D461D2 /* SendingStatusCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendingStatusCollectionViewCell.swift; sourceTree = ""; }; C3F91DCA1C75EFE300D461D2 /* SendingStatusCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SendingStatusCollectionViewCell.xib; sourceTree = ""; }; C3F91DCB1C75EFE300D461D2 /* SendingStatusPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendingStatusPresenter.swift; sourceTree = ""; }; + FE1A697A1C93FF10004DF45A /* TimestampSeparatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimestampSeparatorView.swift; sourceTree = ""; }; FE2D050A1C915ADB006F902B /* BaseMessageCollectionViewCellAvatarStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseMessageCollectionViewCellAvatarStyle.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -211,6 +213,7 @@ C3F91DC81C75EFE300D461D2 /* Sending status */, C3F91DB11C75EF9E00D461D2 /* Text Messages */, FE2D050A1C915ADB006F902B /* BaseMessageCollectionViewCellAvatarStyle.swift */, + FE1A697A1C93FF10004DF45A /* TimestampSeparatorView.swift */, ); path = Source; sourceTree = ""; @@ -452,6 +455,7 @@ C3F91DC61C75EF9E00D461D2 /* DemoTextMessageViewModel.swift in Sources */, C3F91DC01C75EF9E00D461D2 /* DemoPhotoMessageHandler.swift in Sources */, C3F91DBC1C75EF9E00D461D2 /* DemoChatViewController.swift in Sources */, + FE1A697B1C93FF10004DF45A /* TimestampSeparatorView.swift in Sources */, C3F91DBD1C75EF9E00D461D2 /* FakeDataSource.swift in Sources */, C3F91DB61C75EF9E00D461D2 /* AppDelegate.swift in Sources */, C3F91DCE1C75EFE300D461D2 /* SendingStatusPresenter.swift in Sources */, diff --git a/ChattoApp/ChattoApp/Source/ChatItemsDemoDecorator.swift b/ChattoApp/ChattoApp/Source/ChatItemsDemoDecorator.swift index 560bd64e1..ba7a8db02 100644 --- a/ChattoApp/ChattoApp/Source/ChatItemsDemoDecorator.swift +++ b/ChattoApp/ChattoApp/Source/ChatItemsDemoDecorator.swift @@ -38,11 +38,13 @@ final class ChatItemsDemoDecorator: ChatItemsDecoratorProtocol { for (index, chatItem) in chatItems.enumerate() { let next: ChatItemProtocol? = (index + 1 < chatItems.count) ? chatItems[index + 1] : nil + let prev: ChatItemProtocol? = (index > 0) ? chatItems[index - 1] : nil let bottomMargin = self.separationAfterItem(chatItem, next: next) var showsTail = false var additionalItems = [DecoratedChatItem]() + var showWeekDayDateStamp = false if let currentMessage = chatItem as? MessageModelProtocol { if let nextMessage = next as? MessageModelProtocol { showsTail = currentMessage.senderId != nextMessage.senderId @@ -50,6 +52,12 @@ final class ChatItemsDemoDecorator: ChatItemsDecoratorProtocol { showsTail = true } + if let prevMsg = prev as? MessageModelProtocol { + showWeekDayDateStamp = NSCalendar.currentCalendar().compareDate(currentMessage.date, toDate: prevMsg.date, toUnitGranularity: NSCalendarUnit.Day) != NSComparisonResult.OrderedSame + } else { + showWeekDayDateStamp = true + } + if self.showsStatusForMessage(currentMessage) { additionalItems.append( DecoratedChatItem( @@ -57,8 +65,15 @@ final class ChatItemsDemoDecorator: ChatItemsDecoratorProtocol { decorationAttributes: nil) ) } + + if showWeekDayDateStamp { + let dateTimeStamp = DecoratedChatItem(chatItem: WeekDayDatestamp(date: currentMessage.date), decorationAttributes: ChatItemDecorationAttributes(bottomMargin: 0, showsTail: false)) + decoratedChatItems.append(dateTimeStamp) + } } + + decoratedChatItems.append(DecoratedChatItem( chatItem: chatItem, decorationAttributes: ChatItemDecorationAttributes(bottomMargin: bottomMargin, showsTail: showsTail)) diff --git a/ChattoApp/ChattoApp/Source/DemoChatViewController.swift b/ChattoApp/ChattoApp/Source/DemoChatViewController.swift index 8fb18d9b0..8381bedc3 100644 --- a/ChattoApp/ChattoApp/Source/DemoChatViewController.swift +++ b/ChattoApp/ChattoApp/Source/DemoChatViewController.swift @@ -41,7 +41,7 @@ class DemoChatViewController: BaseChatViewController { override func viewDidLoad() { super.viewDidLoad() - let image = UIImage(named: "bubble-incoming-tail-border", inBundle: NSBundle(forClass: self.dynamicType), compatibleWithTraitCollection: nil)?.bma_tintWithColor(UIColor.blueColor()) + let image = UIImage(named: "bubble-incoming-tail-border", inBundle: NSBundle(forClass: DemoChatViewController.self), compatibleWithTraitCollection: nil)?.bma_tintWithColor(UIColor.blueColor()) super.chatItemsDecorator = ChatItemsDemoDecorator() let addIncomingMessageButton = UIBarButtonItem(image: image, style: .Plain, target: self, action: "addRandomIncomingMessage") self.navigationItem.rightBarButtonItem = addIncomingMessageButton @@ -83,7 +83,8 @@ class DemoChatViewController: BaseChatViewController { interactionHandler: DemoPhotoMessageHandler(baseHandler: self.baseMessageHandler) ) ], - SendingStatusModel.chatItemType: [SendingStatusPresenterBuilder()] + SendingStatusModel.chatItemType: [SendingStatusPresenterBuilder()], + WeekDayDatestamp.chatItemType: [WeekDayDatestampPresenterBuilder()] ] } diff --git a/ChattoApp/ChattoApp/Source/TimestampSeparatorView.swift b/ChattoApp/ChattoApp/Source/TimestampSeparatorView.swift new file mode 100644 index 000000000..de86e9968 --- /dev/null +++ b/ChattoApp/ChattoApp/Source/TimestampSeparatorView.swift @@ -0,0 +1,148 @@ +/* +The MIT License (MIT) + +Copyright (c) 2016-present Zhao Wang. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +import Foundation +import UIKit +import Chatto + +extension NSDate { + // Have a time stamp formatter to avoid keep creating new ones. This improves performance + private static let weekdayAndDateStampDateFormatter: NSDateFormatter = { + let dateFormatter = NSDateFormatter() + dateFormatter.timeZone = NSTimeZone.localTimeZone() + dateFormatter.dateFormat = "EEEE, MMM dd yyyy" // "Monday, Mar 7 2016" + return dateFormatter + }() + + func toWeekDayAndDateString() -> String { + return NSDate.weekdayAndDateStampDateFormatter.stringFromDate(self) + } +} + +class WeekDayDatestamp: ChatItemProtocol { + let uid: String + let type: String = WeekDayDatestamp.chatItemType + + static var chatItemType: ChatItemType { + return "WeekDayDatestamp" + } + + init(date: NSDate) { + let datestampString = date.toWeekDayAndDateString() + self.uid = datestampString.uppercaseString + } +} + +class WeekDayDatestampCollectionViewCell: UICollectionViewCell { + private let label: UILabel = UILabel() + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + commonInit() + } + + private func commonInit() { + label.font = UIFont.systemFontOfSize(12) + label.textAlignment = .Center + label.textColor = UIColor.grayColor() + + self.contentView.addSubview(label) + } + + var text: String = "" { + didSet { + if oldValue != text { + setTextOnLabel(text) + } + } + } + + private func setTextOnLabel(text: String) { + label.text = text + label.sizeToFit() + setNeedsLayout() + } + + override func layoutSubviews() { + super.layoutSubviews() + label.center = self.contentView.center + } +} + +public class WeekDayDatestampPresenterBuilder: ChatItemPresenterBuilderProtocol { + + public func canHandleChatItem(chatItem: ChatItemProtocol) -> Bool { + return chatItem is WeekDayDatestamp + } + + public func createPresenterWithChatItem(chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol { + assert(self.canHandleChatItem(chatItem)) + return WeekDayDatestampPresenter(weekDayDatestamp: chatItem as! WeekDayDatestamp) + } + + public var presenterType: ChatItemPresenterProtocol.Type { + return WeekDayDatestampPresenter.self + } +} + +class WeekDayDatestampPresenter: ChatItemPresenterProtocol { + + let weekDayDatestamp: WeekDayDatestamp + init (weekDayDatestamp: WeekDayDatestamp) { + self.weekDayDatestamp = weekDayDatestamp + } + + private static let cellReuseIdentifier = WeekDayDatestampCollectionViewCell.self.description() + + static func registerCells(collectionView: UICollectionView) { + collectionView.registerClass(WeekDayDatestampCollectionViewCell.self, forCellWithReuseIdentifier: cellReuseIdentifier) + } + + func dequeueCell(collectionView collectionView: UICollectionView, indexPath: NSIndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCellWithReuseIdentifier(WeekDayDatestampPresenter.cellReuseIdentifier, forIndexPath: indexPath) + return cell + } + + func configureCell(cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) { + guard let datestamp = cell as? WeekDayDatestampCollectionViewCell else { + assert(false, "expecting status cell") + return + } + + datestamp.text = weekDayDatestamp.uid + } + + var canCalculateHeightInBackground: Bool { + return true + } + + func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat { + return 24 + } +}