// // Created by Jesse Squires // http://www.jessesquires.com // // // Documentation // http://cocoadocs.org/docsets/JSQMessagesViewController // // // GitHub // https://github.com/jessesquires/JSQMessagesViewController // // // License // Copyright (c) 2014 Jesse Squires // Released under an MIT license: http://opensource.org/licenses/MIT // #import "DemoMessagesViewController.h" @implementation DemoMessagesViewController #pragma mark - View lifecycle /** * Override point for customization. * * Customize your view. * Look at the properties on `JSQMessagesViewController` and `JSQMessagesCollectionView` to see what is possible. * * Customize your layout. * Look at the properties on `JSQMessagesCollectionViewFlowLayout` to see what is possible. */ - (void)viewDidLoad { [super viewDidLoad]; self.title = @"JSQMessages"; /** * You MUST set your senderId and display name */ self.senderId = kJSQDemoAvatarIdSquires; self.senderDisplayName = kJSQDemoAvatarDisplayNameSquires; /** * Load up our fake data for the demo */ self.demoData = [[DemoModelData alloc] init]; /** * You can set custom avatar sizes */ if (![NSUserDefaults incomingAvatarSetting]) { self.collectionView.collectionViewLayout.incomingAvatarViewSize = CGSizeZero; } if (![NSUserDefaults outgoingAvatarSetting]) { self.collectionView.collectionViewLayout.outgoingAvatarViewSize = CGSizeZero; } self.showLoadEarlierMessagesHeader = YES; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithImage:[UIImage jsq_defaultTypingIndicatorImage] style:UIBarButtonItemStyleBordered target:self action:@selector(receiveMessagePressed:)]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; if (self.delegateModal) { self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemStop target:self action:@selector(closePressed:)]; } } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; /** * Enable/disable springy bubbles, default is NO. * You must set this from `viewDidAppear:` * Note: this feature is mostly stable, but still experimental */ self.collectionView.collectionViewLayout.springinessEnabled = [NSUserDefaults springinessSetting]; } #pragma mark - Actions - (void)receiveMessagePressed:(UIBarButtonItem *)sender { /** * DEMO ONLY * * The following is simply to simulate received messages for the demo. * Do not actually do this. */ /** * Show the typing indicator to be shown */ self.showTypingIndicator = !self.showTypingIndicator; /** * Scroll to actually view the indicator */ [self scrollToBottomAnimated:YES]; /** * Copy last sent message, this will be the new "received" message */ JSQMessage *copyMessage = [[self.demoData.messages lastObject] copy]; if (!copyMessage) { copyMessage = [JSQTextMessage messageWithSenderId:kJSQDemoAvatarIdJobs displayName:kJSQDemoAvatarDisplayNameJobs text:@"First received!"]; } /** * Allow typing indicator to show */ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSMutableArray *userIds = [[self.demoData.users allKeys] mutableCopy]; [userIds removeObject:self.senderId]; NSString *randomUserId = userIds[arc4random_uniform((int)[userIds count])]; JSQMessage *newMessage = nil; id newMediaData = nil; id newMediaAttachmentCopy = nil; if ([copyMessage isKindOfClass:[JSQMediaMessage class]]) { /** * Last message was a media message */ id copyMediaData = copyMessage.media; if ([copyMediaData isKindOfClass:[JSQPhotoMediaItem class]]) { JSQPhotoMediaItem *photoItemCopy = [((JSQPhotoMediaItem *)copyMediaData) copy]; photoItemCopy.appliesMediaViewMaskAsOutgoing = NO; newMediaAttachmentCopy = [UIImage imageWithCGImage:photoItemCopy.image.CGImage]; /** * Set image to nil to simulate "downloading" the image * and show the placeholder view */ photoItemCopy.image = nil; newMediaData = photoItemCopy; } else if ([copyMediaData isKindOfClass:[JSQLocationMediaItem class]]) { JSQLocationMediaItem *locationItemCopy = [((JSQLocationMediaItem *)copyMediaData) copy]; locationItemCopy.appliesMediaViewMaskAsOutgoing = NO; newMediaAttachmentCopy = [locationItemCopy.location copy]; /** * Set location to nil to simulate "downloading" the location data */ locationItemCopy.location = nil; newMediaData = locationItemCopy; } else if ([copyMediaData isKindOfClass:[JSQVideoMediaItem class]]) { JSQVideoMediaItem *videoItemCopy = [((JSQVideoMediaItem *)copyMediaData) copy]; videoItemCopy.appliesMediaViewMaskAsOutgoing = NO; newMediaAttachmentCopy = [videoItemCopy.fileURL copy]; /** * Reset video item to simulate "downloading" the video */ videoItemCopy.fileURL = nil; videoItemCopy.isReadyToPlay = NO; newMediaData = videoItemCopy; } else { NSLog(@"%s error: unrecognized media item", __PRETTY_FUNCTION__); } newMessage = [JSQMediaMessage messageWithSenderId:randomUserId displayName:self.demoData.users[randomUserId] media:newMediaData]; } else { /** * Last message was a text message */ newMessage = [JSQTextMessage messageWithSenderId:randomUserId displayName:self.demoData.users[randomUserId] text:copyMessage.text]; } /** * Upon receiving a message, you should: * * 1. Play sound (optional) * 2. Add new id object to your data source * 3. Call `finishReceivingMessage` */ [JSQSystemSoundPlayer jsq_playMessageReceivedSound]; [self.demoData.messages addObject:newMessage]; [self finishReceivingMessage]; if ([newMessage isKindOfClass:[JSQMediaMessage class]]) { /** * Simulate "downloading" media */ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ /** * Media is "finished downloading", re-display visible cells * * If media cell is not visible, the next time it is dequeued the view controller will display its new attachment data * * Reload the specific item, or simply call `reloadData` */ if ([newMediaData isKindOfClass:[JSQPhotoMediaItem class]]) { ((JSQPhotoMediaItem *)newMediaData).image = newMediaAttachmentCopy; [self.collectionView reloadData]; } else if ([newMediaData isKindOfClass:[JSQLocationMediaItem class]]) { [((JSQLocationMediaItem *)newMediaData)setLocation:newMediaAttachmentCopy withCompletionHandler:^{ [self.collectionView reloadData]; }]; } else if ([newMediaData isKindOfClass:[JSQVideoMediaItem class]]) { ((JSQVideoMediaItem *)newMediaData).fileURL = newMediaAttachmentCopy; ((JSQVideoMediaItem *)newMediaData).isReadyToPlay = YES; [self.collectionView reloadData]; } else { NSLog(@"%s error: unrecognized media item", __PRETTY_FUNCTION__); } }); } }); } - (void)closePressed:(UIBarButtonItem *)sender { [self.delegateModal didDismissJSQDemoViewController:self]; } #pragma mark - JSQMessagesViewController method overrides - (void)didPressSendButton:(UIButton *)button withMessageText:(NSString *)text senderId:(NSString *)senderId senderDisplayName:(NSString *)senderDisplayName date:(NSDate *)date { /** * Sending a message. Your implementation of this method should do *at least* the following: * * 1. Play sound (optional) * 2. Add new id object to your data source * 3. Call `finishSendingMessage` */ [JSQSystemSoundPlayer jsq_playMessageSentSound]; JSQTextMessage *message = [[JSQTextMessage alloc] initWithSenderId:senderId senderDisplayName:senderDisplayName date:date text:text]; [self.demoData.messages addObject:message]; [self finishSendingMessage]; } - (void)didPressAccessoryButton:(UIButton *)sender { UIActionSheet *sheet = [[UIActionSheet alloc] initWithTitle:@"Media messages" delegate:self cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil otherButtonTitles:@"Send photo", @"Send location", @"Send video", nil]; [sheet showFromToolbar:self.inputToolbar]; } - (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex { if (buttonIndex == actionSheet.cancelButtonIndex) { return; } switch (buttonIndex) { case 0: [self.demoData addPhotoMediaMessage]; break; case 1: { __weak UICollectionView *weakView = self.collectionView; [self.demoData addLocationMediaMessageCompletion:^{ [weakView reloadData]; }]; } break; case 2: [self.demoData addVideoMediaMessage]; break; } [JSQSystemSoundPlayer jsq_playMessageSentSound]; [self finishSendingMessage]; } #pragma mark - JSQMessages CollectionView DataSource - (id)collectionView:(JSQMessagesCollectionView *)collectionView messageDataForItemAtIndexPath:(NSIndexPath *)indexPath { return [self.demoData.messages objectAtIndex:indexPath.item]; } - (id)collectionView:(JSQMessagesCollectionView *)collectionView messageBubbleImageDataForItemAtIndexPath:(NSIndexPath *)indexPath { /** * You may return nil here if you do not want bubbles. * In this case, you should set the background color of your collection view cell's textView. * * Otherwise, return your previously created bubble image data objects. */ JSQMessage *message = [self.demoData.messages objectAtIndex:indexPath.item]; if ([message.senderId isEqualToString:self.senderId]) { return self.demoData.outgoingBubbleImageData; } return self.demoData.incomingBubbleImageData; } - (id)collectionView:(JSQMessagesCollectionView *)collectionView avatarImageDataForItemAtIndexPath:(NSIndexPath *)indexPath { /** * Return `nil` here if you do not want avatars. * If you do return `nil`, be sure to do the following in `viewDidLoad`: * * self.collectionView.collectionViewLayout.incomingAvatarViewSize = CGSizeZero; * self.collectionView.collectionViewLayout.outgoingAvatarViewSize = CGSizeZero; * * It is possible to have only outgoing avatars or only incoming avatars, too. */ /** * Return your previously created avatar image data objects. * * Note: these the avatars will be sized according to these values: * * self.collectionView.collectionViewLayout.incomingAvatarViewSize * self.collectionView.collectionViewLayout.outgoingAvatarViewSize * * Override the defaults in `viewDidLoad` */ JSQMessage *message = [self.demoData.messages objectAtIndex:indexPath.item]; if ([message.senderId isEqualToString:self.senderId]) { if (![NSUserDefaults outgoingAvatarSetting]) { return nil; } } else { if (![NSUserDefaults incomingAvatarSetting]) { return nil; } } return [self.demoData.avatars objectForKey:message.senderId]; } - (NSAttributedString *)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForCellTopLabelAtIndexPath:(NSIndexPath *)indexPath { /** * This logic should be consistent with what you return from `heightForCellTopLabelAtIndexPath:` * The other label text delegate methods should follow a similar pattern. * * Show a timestamp for every 3rd message */ if (indexPath.item % 3 == 0) { JSQMessage *message = [self.demoData.messages objectAtIndex:indexPath.item]; return [[JSQMessagesTimestampFormatter sharedFormatter] attributedTimestampForDate:message.date]; } return nil; } - (NSAttributedString *)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForMessageBubbleTopLabelAtIndexPath:(NSIndexPath *)indexPath { JSQMessage *message = [self.demoData.messages objectAtIndex:indexPath.item]; /** * iOS7-style sender name labels */ if ([message.senderId isEqualToString:self.senderId]) { return nil; } if (indexPath.item - 1 > 0) { JSQMessage *previousMessage = [self.demoData.messages objectAtIndex:indexPath.item - 1]; if ([[previousMessage senderId] isEqualToString:message.senderId]) { return nil; } } /** * Don't specify attributes to use the defaults. */ return [[NSAttributedString alloc] initWithString:message.senderDisplayName]; } - (NSAttributedString *)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForCellBottomLabelAtIndexPath:(NSIndexPath *)indexPath { return nil; } #pragma mark - UICollectionView DataSource - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return [self.demoData.messages count]; } - (UICollectionViewCell *)collectionView:(JSQMessagesCollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { /** * Override point for customizing cells */ JSQMessagesCollectionViewCell *cell = (JSQMessagesCollectionViewCell *)[super collectionView:collectionView cellForItemAtIndexPath:indexPath]; /** * Configure almost *anything* on the cell * * Text colors, label text, label colors, etc. * * * DO NOT set `cell.textView.font` ! * Instead, you need to set `self.collectionView.collectionViewLayout.messageBubbleFont` to the font you want in `viewDidLoad` * * * DO NOT manipulate cell layout information! * Instead, override the properties you want on `self.collectionView.collectionViewLayout` from `viewDidLoad` */ JSQMessage *msg = [self.demoData.messages objectAtIndex:indexPath.item]; if ([msg isKindOfClass:[JSQTextMessage class]]) { if ([msg.senderId isEqualToString:self.senderId]) { cell.textView.textColor = [UIColor blackColor]; } else { cell.textView.textColor = [UIColor whiteColor]; } cell.textView.linkTextAttributes = @{ NSForegroundColorAttributeName : cell.textView.textColor, NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid) }; } return cell; } #pragma mark - JSQMessages collection view flow layout delegate #pragma mark - Adjusting cell label heights - (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout heightForCellTopLabelAtIndexPath:(NSIndexPath *)indexPath { /** * Each label in a cell has a `height` delegate method that corresponds to its text dataSource method */ /** * This logic should be consistent with what you return from `attributedTextForCellTopLabelAtIndexPath:` * The other label height delegate methods should follow similarly * * Show a timestamp for every 3rd message */ if (indexPath.item % 3 == 0) { return kJSQMessagesCollectionViewCellLabelHeightDefault; } return 0.0f; } - (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout heightForMessageBubbleTopLabelAtIndexPath:(NSIndexPath *)indexPath { /** * iOS7-style sender name labels */ JSQMessage *currentMessage = [self.demoData.messages objectAtIndex:indexPath.item]; if ([[currentMessage senderId] isEqualToString:self.senderId]) { return 0.0f; } if (indexPath.item - 1 > 0) { JSQMessage *previousMessage = [self.demoData.messages objectAtIndex:indexPath.item - 1]; if ([[previousMessage senderId] isEqualToString:[currentMessage senderId]]) { return 0.0f; } } return kJSQMessagesCollectionViewCellLabelHeightDefault; } - (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout heightForCellBottomLabelAtIndexPath:(NSIndexPath *)indexPath { return 0.0f; } #pragma mark - Responding to collection view tap events - (void)collectionView:(JSQMessagesCollectionView *)collectionView header:(JSQMessagesLoadEarlierHeaderView *)headerView didTapLoadEarlierMessagesButton:(UIButton *)sender { NSLog(@"Load earlier messages!"); } - (void)collectionView:(JSQMessagesCollectionView *)collectionView didTapAvatarImageView:(UIImageView *)avatarImageView atIndexPath:(NSIndexPath *)indexPath { NSLog(@"Tapped avatar!"); } - (void)collectionView:(JSQMessagesCollectionView *)collectionView didTapMessageBubbleAtIndexPath:(NSIndexPath *)indexPath { NSLog(@"Tapped message bubble!"); } - (void)collectionView:(JSQMessagesCollectionView *)collectionView didTapCellAtIndexPath:(NSIndexPath *)indexPath touchLocation:(CGPoint)touchLocation { NSLog(@"Tapped cell at %@!", NSStringFromCGPoint(touchLocation)); } @end