├── ATLMConversationListViewController.m ├── ATLMConversationViewController.m ├── MEConversationListCell.h ├── MEConversationListCell.m ├── MEConversationViewSwizzle.h ├── MEConversationViewSwizzle.m ├── MEIncomingMessageCollectionViewCell.h ├── MEIncomingMessageCollectionViewCell.m ├── MEOutgoingMessageCollectionViewCell.h ├── MEOutgoingMessageCollectionViewCell.m └── README.md /ATLMConversationListViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ATLMConversationListViewController.m 3 | // Atlas Messenger 4 | // 5 | // Created by Kevin Coleman on 8/29/14. 6 | // Copyright (c) 2014 Layer, Inc. All rights reserved. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | #import "ATLMConversationListViewController.h" 22 | #import "ATLMUser.h" 23 | #import "ATLMConversationViewController.h" 24 | #import "ATLMSettingsViewController.h" 25 | #import "ATLMConversationDetailViewController.h" 26 | #import "ATLMNavigationController.h" 27 | #import "ATLMSplitViewController.h" 28 | #import "LYRIdentity+ATLParticipant.h" 29 | #import "MEConversationListCell.h" 30 | 31 | @interface ATLMConversationListViewController () 32 | 33 | @end 34 | 35 | @implementation ATLMConversationListViewController 36 | 37 | NSString *const ATLMConversationListTableViewAccessibilityLabel = @"Conversation List Table View"; 38 | NSString *const ATLMSettingsButtonAccessibilityLabel = @"Settings Button"; 39 | NSString *const ATLMComposeButtonAccessibilityLabel = @"Compose Button"; 40 | 41 | - (void)viewDidLoad 42 | { 43 | [super viewDidLoad]; 44 | self.tableView.accessibilityLabel = ATLMConversationListTableViewAccessibilityLabel; 45 | self.tableView.isAccessibilityElement = YES; 46 | self.delegate = self; 47 | self.dataSource = self; 48 | self.allowsEditing = YES; 49 | self.cellClass = [MEConversationListCell class]; 50 | 51 | // Left navigation item 52 | UIButton* infoButton= [UIButton buttonWithType:UIButtonTypeInfoLight]; 53 | UIBarButtonItem *infoItem = [[UIBarButtonItem alloc] initWithCustomView:infoButton]; 54 | [infoButton addTarget:self action:@selector(settingsButtonTapped) forControlEvents:UIControlEventTouchUpInside]; 55 | infoButton.accessibilityLabel = ATLMSettingsButtonAccessibilityLabel; 56 | [self.navigationItem setLeftBarButtonItem:infoItem]; 57 | 58 | // Right navigation item 59 | UIBarButtonItem *composeButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCompose target:self action:@selector(composeButtonTapped)]; 60 | composeButton.accessibilityLabel = ATLMComposeButtonAccessibilityLabel; 61 | [self.navigationItem setRightBarButtonItem:composeButton]; 62 | 63 | [self registerNotificationObservers]; 64 | } 65 | 66 | - (void)dealloc 67 | { 68 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 69 | } 70 | 71 | #pragma mark - ATLConversationListViewControllerDelegate 72 | 73 | /** 74 | Atlas - Informs the delegate of a conversation selection. Atlas Messenger pushses a subclass of the `ATLConversationViewController`. 75 | */ 76 | - (void)conversationListViewController:(ATLConversationListViewController *)conversationListViewController didSelectConversation:(LYRConversation *)conversation 77 | { 78 | [self presentControllerWithConversation:conversation]; 79 | } 80 | 81 | /** 82 | Atlas - Informs the delegate a conversation was deleted. Atlas Messenger does not need to react as the superclass will handle removing the conversation in response to a deletion. 83 | */ 84 | - (void)conversationListViewController:(ATLConversationListViewController *)conversationListViewController didDeleteConversation:(LYRConversation *)conversation deletionMode:(LYRDeletionMode)deletionMode 85 | { 86 | NSLog(@"Conversation Successfully Deleted"); 87 | } 88 | 89 | /** 90 | Atlas - Informs the delegate that a conversation deletion attempt failed. Atlas Messenger does not do anything in response. 91 | */ 92 | - (void)conversationListViewController:(ATLConversationListViewController *)conversationListViewController didFailDeletingConversation:(LYRConversation *)conversation deletionMode:(LYRDeletionMode)deletionMode error:(NSError *)error 93 | { 94 | NSLog(@"Conversation Deletion Failed with Error: %@", error); 95 | } 96 | 97 | /** 98 | Atlas - Informs the delegate that a search has been performed. Atlas messenger queries for, and returns objects conforming to the `ATLParticipant` protocol whose `fullName` property contains the search text. 99 | */ 100 | - (void)conversationListViewController:(ATLConversationListViewController *)conversationListViewController didSearchForText:(nonnull NSString *)searchText completion:(nonnull void (^)(NSSet> * _Nonnull))completion 101 | { 102 | LYRQuery *query = [LYRQuery queryWithQueryableClass:[LYRIdentity class]]; 103 | query.predicate = [LYRPredicate predicateWithProperty:@"displayName" predicateOperator:LYRPredicateOperatorLike value:[searchText stringByAppendingString:@"%"]]; 104 | [self.layerClient executeQuery:query completion:^(NSOrderedSet> * _Nullable resultSet, NSError * _Nullable error) { 105 | if (resultSet) { 106 | completion(resultSet.set); 107 | } else { 108 | completion([NSSet set]); 109 | } 110 | }]; 111 | } 112 | 113 | - (id)conversationListViewController:(ATLConversationListViewController *)conversationListViewController avatarItemForConversation:(LYRConversation *)conversation 114 | { 115 | NSMutableSet *participants = conversation.participants.mutableCopy; 116 | [participants removeObject:self.layerClient.authenticatedUser]; 117 | return participants.anyObject; 118 | } 119 | 120 | #pragma mark - ATLConversationListViewControllerDataSource 121 | 122 | /** 123 | Atlas - Returns a label that is used to represent the conversation. Atlas Messenger puts the name representing the `lastMessage.sentByUserID` property first in the string. 124 | */ 125 | - (NSString *)conversationListViewController:(ATLConversationListViewController *)conversationListViewController titleForConversation:(LYRConversation *)conversation 126 | { 127 | // If we have a Conversation name in metadata, return it. 128 | NSString *conversationTitle = conversation.metadata[ATLMConversationMetadataNameKey]; 129 | if (conversationTitle.length) { 130 | return conversationTitle; 131 | } 132 | 133 | NSMutableSet *participants = [conversation.participants mutableCopy]; 134 | NSPredicate *predicate = [NSPredicate predicateWithFormat:@"userID != %@", self.layerClient.authenticatedUser.userID]; 135 | [participants filterUsingPredicate:predicate]; 136 | 137 | if (participants.count == 0) return @"Personal Conversation"; 138 | if (participants.count == 1) return [[participants allObjects][0] displayName]; 139 | 140 | NSMutableArray *firstNames = [NSMutableArray new]; 141 | [participants enumerateObjectsUsingBlock:^(id obj, BOOL *stop) { 142 | id participant = obj; 143 | if (participant.firstName) { 144 | // Put the last message sender's name first 145 | if ([conversation.lastMessage.sender.userID isEqualToString:participant.userID]) { 146 | [firstNames insertObject:participant.firstName atIndex:0]; 147 | } else { 148 | [firstNames addObject:participant.firstName]; 149 | } 150 | } 151 | }]; 152 | NSString *firstNamesString = [firstNames componentsJoinedByString:@", "]; 153 | return firstNamesString; 154 | } 155 | 156 | #pragma mark - Conversation Selection 157 | 158 | // The following method handles presenting the correct `ATLMConversationViewController`, regardeless of the current state of the navigation stack. 159 | - (void)presentControllerWithConversation:(LYRConversation *)conversation 160 | { 161 | ATLMConversationViewController *existingConversationViewController = [self existingConversationViewController]; 162 | if (existingConversationViewController && existingConversationViewController.conversation == conversation) { 163 | if (self.navigationController.topViewController == existingConversationViewController) return; 164 | [self.navigationController popToViewController:existingConversationViewController animated:YES]; 165 | return; 166 | } 167 | 168 | BOOL shouldShowAddressBar = (conversation.participants.count > 2 || !conversation.participants.count); 169 | ATLMConversationViewController *conversationViewController = [ATLMConversationViewController conversationViewControllerWithLayerClient:self.applicationController.layerClient]; 170 | conversationViewController.applicationController = self.applicationController; 171 | conversationViewController.displaysAddressBar = shouldShowAddressBar; 172 | conversationViewController.conversation = conversation; 173 | 174 | [self.applicationController.splitViewController setDetailViewController:conversationViewController]; 175 | } 176 | 177 | #pragma mark - Actions 178 | 179 | - (void)settingsButtonTapped 180 | { 181 | ATLMSettingsViewController *settingsViewController = [[ATLMSettingsViewController alloc] initWithStyle:UITableViewStyleGrouped]; 182 | settingsViewController.applicationController = self.applicationController; 183 | settingsViewController.settingsDelegate = self; 184 | 185 | UINavigationController *controller = [[UINavigationController alloc] initWithRootViewController:settingsViewController]; 186 | [self.navigationController presentViewController:controller animated:YES completion:nil]; 187 | } 188 | 189 | - (void)composeButtonTapped 190 | { 191 | [self presentControllerWithConversation:nil]; 192 | } 193 | 194 | #pragma mark - Conversation Selection From Push Notification 195 | 196 | - (void)selectConversation:(LYRConversation *)conversation 197 | { 198 | if (conversation) { 199 | [self presentControllerWithConversation:conversation]; 200 | } 201 | } 202 | 203 | #pragma mark - ATLMSettingsViewControllerDelegate 204 | 205 | - (void)logoutTappedInSettingsViewController:(ATLMSettingsViewController *)settingsViewController 206 | { 207 | [SVProgressHUD setDefaultMaskType:SVProgressHUDMaskTypeBlack]; 208 | [SVProgressHUD show]; 209 | if (self.applicationController.layerClient.isConnected) { 210 | [self.applicationController.layerClient deauthenticateWithCompletion:^(BOOL success, NSError *error) { 211 | [SVProgressHUD dismiss]; 212 | }]; 213 | } else { 214 | [SVProgressHUD showErrorWithStatus:@"Unable to logout. Layer is not connected"]; 215 | } 216 | } 217 | 218 | - (void)settingsViewControllerDidFinish:(ATLMSettingsViewController *)settingsViewController 219 | { 220 | [settingsViewController dismissViewControllerAnimated:YES completion:nil]; 221 | } 222 | 223 | #pragma mark - Notification Handlers 224 | 225 | - (void)conversationDeleted:(NSNotification *)notification 226 | { 227 | if (self.ATLM_navigationController.isAnimating) { 228 | [self.ATLM_navigationController notifyWhenCompletionEndsUsingBlock:^{ 229 | [self conversationDeleted:notification]; 230 | }]; 231 | return; 232 | } 233 | 234 | ATLMConversationViewController *conversationViewController = [self existingConversationViewController]; 235 | if (!conversationViewController) return; 236 | 237 | LYRConversation *deletedConversation = notification.object; 238 | if (![conversationViewController.conversation isEqual:deletedConversation]) return; 239 | conversationViewController = nil; 240 | [self.navigationController popToViewController:self animated:YES]; 241 | 242 | UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Conversation Deleted" 243 | message:@"The conversation has been deleted." 244 | delegate:nil 245 | cancelButtonTitle:@"OK" 246 | otherButtonTitles:nil]; 247 | [alertView show]; 248 | } 249 | 250 | - (void)conversationParticipantsDidChange:(NSNotification *)notification 251 | { 252 | if (self.ATLM_navigationController.isAnimating) { 253 | [self.ATLM_navigationController notifyWhenCompletionEndsUsingBlock:^{ 254 | [self conversationParticipantsDidChange:notification]; 255 | }]; 256 | return; 257 | } 258 | 259 | NSString *authenticatedUserID = self.applicationController.layerClient.authenticatedUser.userID; 260 | if (!authenticatedUserID) return; 261 | LYRConversation *conversation = notification.object; 262 | if ([[conversation.participants valueForKeyPath:@"userID"] containsObject:authenticatedUserID]) return; 263 | 264 | ATLMConversationViewController *conversationViewController = [self existingConversationViewController]; 265 | if (!conversationViewController) return; 266 | if (![conversationViewController.conversation isEqual:conversation]) return; 267 | 268 | [self.navigationController popToViewController:self animated:YES]; 269 | 270 | UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Removed From Conversation" 271 | message:@"You have been removed from the conversation." 272 | delegate:nil 273 | cancelButtonTitle:@"OK" 274 | otherButtonTitles:nil]; 275 | [alertView show]; 276 | } 277 | 278 | #pragma mark - Helpers 279 | 280 | - (ATLMConversationViewController *)existingConversationViewController 281 | { 282 | if (!self.navigationController) return nil; 283 | 284 | NSUInteger listViewControllerIndex = [self.navigationController.viewControllers indexOfObject:self]; 285 | if (listViewControllerIndex == NSNotFound) return nil; 286 | 287 | NSUInteger nextViewControllerIndex = listViewControllerIndex + 1; 288 | if (nextViewControllerIndex >= self.navigationController.viewControllers.count) return nil; 289 | 290 | id nextViewController = [self.navigationController.viewControllers objectAtIndex:nextViewControllerIndex]; 291 | if (![nextViewController isKindOfClass:[ATLMConversationViewController class]]) return nil; 292 | 293 | return nextViewController; 294 | } 295 | 296 | - (void)registerNotificationObservers 297 | { 298 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(conversationDeleted:) name:ATLMConversationDeletedNotification object:nil]; 299 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(conversationParticipantsDidChange:) name:ATLMConversationParticipantsDidChangeNotification object:nil]; 300 | } 301 | 302 | @end -------------------------------------------------------------------------------- /ATLMConversationViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ATLMConversationViewController.m 3 | // Atlas Messenger 4 | // 5 | // Created by Kevin Coleman on 9/10/14. 6 | // Copyright (c) 2014 Layer, Inc. All rights reserved. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | #import "ATLMConversationViewController.h" 22 | #import "ATLMConversationDetailViewController.h" 23 | #import "ATLMMediaViewController.h" 24 | #import "ATLMUtilities.h" 25 | #import "ATLMParticipantTableViewController.h" 26 | #import "ATLMSplitViewController.h" 27 | #import "LYRIdentity+ATLParticipant.h" 28 | #import "MEConversationViewSwizzle.h" 29 | #import "MEOutgoingMessageCollectionViewCell.h" 30 | #import "MEIncomingMessageCollectionViewCell.h" 31 | 32 | // Makemoji Addition 33 | #import "METextInputView.h" 34 | 35 | static NSDateFormatter *ATLMShortTimeFormatter() 36 | { 37 | static NSDateFormatter *dateFormatter; 38 | if (!dateFormatter) { 39 | dateFormatter = [[NSDateFormatter alloc] init]; 40 | dateFormatter.timeStyle = NSDateFormatterShortStyle; 41 | } 42 | return dateFormatter; 43 | } 44 | 45 | static NSDateFormatter *ATLMDayOfWeekDateFormatter() 46 | { 47 | static NSDateFormatter *dateFormatter; 48 | if (!dateFormatter) { 49 | dateFormatter = [[NSDateFormatter alloc] init]; 50 | dateFormatter.dateFormat = @"EEEE"; // Tuesday 51 | } 52 | return dateFormatter; 53 | } 54 | 55 | static NSDateFormatter *ATLMRelativeDateFormatter() 56 | { 57 | static NSDateFormatter *dateFormatter; 58 | if (!dateFormatter) { 59 | dateFormatter = [[NSDateFormatter alloc] init]; 60 | dateFormatter.dateStyle = NSDateFormatterMediumStyle; 61 | dateFormatter.doesRelativeDateFormatting = YES; 62 | } 63 | return dateFormatter; 64 | } 65 | 66 | static NSDateFormatter *ATLMThisYearDateFormatter() 67 | { 68 | static NSDateFormatter *dateFormatter; 69 | if (!dateFormatter) { 70 | dateFormatter = [[NSDateFormatter alloc] init]; 71 | dateFormatter.dateFormat = @"E, MMM dd,"; // Sat, Nov 29, 72 | } 73 | return dateFormatter; 74 | } 75 | 76 | static NSDateFormatter *ATLMDefaultDateFormatter() 77 | { 78 | static NSDateFormatter *dateFormatter; 79 | if (!dateFormatter) { 80 | dateFormatter = [[NSDateFormatter alloc] init]; 81 | dateFormatter.dateFormat = @"MMM dd, yyyy,"; // Nov 29, 2013, 82 | } 83 | return dateFormatter; 84 | } 85 | 86 | typedef NS_ENUM(NSInteger, ATLMDateProximity) { 87 | ATLMDateProximityToday, 88 | ATLMDateProximityYesterday, 89 | ATLMDateProximityWeek, 90 | ATLMDateProximityYear, 91 | ATLMDateProximityOther, 92 | }; 93 | 94 | static ATLMDateProximity ATLMProximityToDate(NSDate *date) 95 | { 96 | NSCalendar *calendar = [NSCalendar currentCalendar]; 97 | NSDate *now = [NSDate date]; 98 | #pragma GCC diagnostic push 99 | #pragma GCC diagnostic ignored "-Wdeprecated-declarations" 100 | NSCalendarUnit calendarUnits = NSEraCalendarUnit | NSYearCalendarUnit | NSWeekOfMonthCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit; 101 | #pragma GCC diagnostic pop 102 | NSDateComponents *dateComponents = [calendar components:calendarUnits fromDate:date]; 103 | NSDateComponents *todayComponents = [calendar components:calendarUnits fromDate:now]; 104 | if (dateComponents.day == todayComponents.day && 105 | dateComponents.month == todayComponents.month && 106 | dateComponents.year == todayComponents.year && 107 | dateComponents.era == todayComponents.era) { 108 | return ATLMDateProximityToday; 109 | } 110 | 111 | NSDateComponents *componentsToYesterday = [NSDateComponents new]; 112 | componentsToYesterday.day = -1; 113 | NSDate *yesterday = [calendar dateByAddingComponents:componentsToYesterday toDate:now options:0]; 114 | NSDateComponents *yesterdayComponents = [calendar components:calendarUnits fromDate:yesterday]; 115 | if (dateComponents.day == yesterdayComponents.day && 116 | dateComponents.month == yesterdayComponents.month && 117 | dateComponents.year == yesterdayComponents.year && 118 | dateComponents.era == yesterdayComponents.era) { 119 | return ATLMDateProximityYesterday; 120 | } 121 | 122 | if (dateComponents.weekOfMonth == todayComponents.weekOfMonth && 123 | dateComponents.month == todayComponents.month && 124 | dateComponents.year == todayComponents.year && 125 | dateComponents.era == todayComponents.era) { 126 | return ATLMDateProximityWeek; 127 | } 128 | 129 | if (dateComponents.year == todayComponents.year && 130 | dateComponents.era == todayComponents.era) { 131 | return ATLMDateProximityYear; 132 | } 133 | 134 | return ATLMDateProximityOther; 135 | } 136 | 137 | @interface ATLMConversationViewController () 138 | 139 | //Makemoji additions 140 | @property (nonatomic) METextInputView * meTextInputView; 141 | @property (nonatomic) NSMutableArray * messageCells; 142 | 143 | @end 144 | 145 | @implementation ATLMConversationViewController 146 | 147 | NSString *const ATLMConversationViewControllerAccessibilityLabel = @"Conversation View Controller"; 148 | NSString *const ATLMDetailsButtonAccessibilityLabel = @"Details Button"; 149 | NSString *const ATLMDetailsButtonLabel = @"Details"; 150 | 151 | - (void)viewDidLoad 152 | { 153 | [super viewDidLoad]; 154 | self.view.accessibilityLabel = ATLMConversationViewControllerAccessibilityLabel; 155 | self.dataSource = self; 156 | self.delegate = self; 157 | 158 | if (self.conversation) { 159 | [self addDetailsButton]; 160 | } 161 | 162 | [self configureUserInterfaceAttributes]; 163 | [self registerNotificationObservers]; 164 | 165 | // Makemoji Addition: Use this array to keep track of message position since heightForMessage does not return an index path 166 | self.messageCells = [NSMutableArray array]; 167 | 168 | // nil the existing Input Accessory view to remove the message toolbar 169 | 170 | ATLConversationView * conversationView = (ATLConversationView *)self.view; 171 | conversationView.inputAccessoryView = nil; 172 | self.shouldDisplayAvatarItemForOneOtherParticipant = YES; 173 | self.shouldDisplayAvatarItemForAuthenticatedUser = YES; 174 | 175 | 176 | // initialize the Makemoji text input and toolbar 177 | 178 | self.meTextInputView = [[METextInputView alloc] initWithFrame:CGRectZero]; 179 | self.meTextInputView.delegate = self; 180 | [self.view addSubview:self.meTextInputView]; 181 | 182 | // initially hide the toolbar for recipient picker 183 | self.meTextInputView.hidden = YES; 184 | self.collectionView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag; 185 | 186 | // custom collection view cell for displaying HTML messages 187 | [self registerClass:[MEOutgoingMessageCollectionViewCell class] forMessageCellWithReuseIdentifier:@"MEOutgoingMessageCollectionViewCell"]; 188 | [self registerClass:[MEIncomingMessageCollectionViewCell class] forMessageCellWithReuseIdentifier:@"MEIncomingMessageCollectionViewCell"]; 189 | self.edgesForExtendedLayout = UIRectEdgeNone; 190 | 191 | // initial offsets 192 | self.collectionView.scrollIndicatorInsets = UIEdgeInsetsMake(0, 0, (self.view.frame.size.height-self.meTextInputView.frame.origin.y), 0); 193 | self.collectionView.contentInset = UIEdgeInsetsMake(0, 0, (self.view.frame.size.height-self.meTextInputView.frame.origin.y), 0); 194 | 195 | } 196 | 197 | - (void)viewWillAppear:(BOOL)animated 198 | { 199 | [super viewWillAppear:animated]; 200 | [self configureTitle]; 201 | 202 | } 203 | 204 | - (void)viewDidAppear:(BOOL)animated 205 | { 206 | [super viewDidAppear:animated]; 207 | if (self.conversation != nil) { self.meTextInputView.hidden = NO; [self.meTextInputView showKeyboard]; } 208 | } 209 | 210 | - (void)viewWillDisappear:(BOOL)animated 211 | { 212 | [super viewWillDisappear:animated]; 213 | if (![self isMovingFromParentViewController]) { 214 | [self.view resignFirstResponder]; 215 | [self.meTextInputView hideKeyboard]; 216 | } 217 | } 218 | 219 | - (void)dealloc 220 | { 221 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 222 | } 223 | 224 | - (NSString *)conversationViewController:(ATLConversationViewController *)viewController reuseIdentifierForMessage:(LYRMessage *)message { 225 | LYRMessagePart *part = message.parts[0]; 226 | NSString * messageText = [[NSString alloc] initWithData:part.data encoding:NSUTF8StringEncoding]; 227 | if ([part.MIMEType isEqualToString:@"text/plain"] && [METextInputView detectMakemojiMessage:messageText] == YES) { 228 | if ([message.sender.userID isEqualToString:self.layerClient.authenticatedUser.userID]) { 229 | return @"MEOutgoingMessageCollectionViewCell"; 230 | } else { 231 | return @"MEIncomingMessageCollectionViewCell"; 232 | } 233 | } 234 | return nil; 235 | } 236 | 237 | - (CGFloat)conversationViewController:(ATLConversationViewController *)viewController heightForMessage:(LYRMessage *)message withCellWidth:(CGFloat)cellWidth { 238 | LYRMessagePart *part = message.parts[0]; 239 | NSString * messageText = [[NSString alloc] initWithData:part.data encoding:NSUTF8StringEncoding]; 240 | 241 | if ([part.MIMEType isEqualToString:@"text/plain"] && [METextInputView detectMakemojiMessage:messageText] == YES) { 242 | NSString * messageHTML = [METextInputView convertSubstituedToHTML:messageText]; 243 | NSString * messsageIdentifier = [message.identifier absoluteString]; 244 | NSUInteger index; 245 | 246 | if ([self.messageCells containsObject:messsageIdentifier]) { 247 | index = [self.messageCells indexOfObject:[message.identifier absoluteString]]; 248 | } else { 249 | [self.messageCells addObject:messsageIdentifier]; 250 | index = [self.messageCells indexOfObject:[message.identifier absoluteString]]; 251 | } 252 | CGFloat messageHeight = [self.meTextInputView cellHeightForHTML:messageHTML 253 | atIndexPath:[NSIndexPath indexPathForRow:index inSection:0] 254 | maxCellWidth:ATLMaxCellWidth() 255 | cellStyle:MECellStyleSimple]; 256 | CGFloat totalHeight = messageHeight + ATLMessageBubbleLabelVerticalPadding*2; 257 | 258 | if (totalHeight < ATLMessageBubbleDefaultHeight) totalHeight = ATLMessageBubbleDefaultHeight; 259 | return totalHeight; 260 | } 261 | 262 | return 0; 263 | } 264 | 265 | // the chat input frame changed size (keyboard show, expanding input) 266 | -(void)meTextInputView:(METextInputView *)inputView didChangeFrame:(CGRect)frame { 267 | 268 | CGFloat collapsedHeight = self.view.frame.size.height-frame.size.height; 269 | CGFloat heightOffset = (self.view.frame.size.height-self.meTextInputView.frame.origin.y); 270 | CGFloat topOffset = 0; 271 | 272 | if (self.addressBarController != nil && self.addressBarController.view.subviews.count > 0) { 273 | UIView * addressBarView = (UIView *)[self.addressBarController.view.subviews objectAtIndex:0]; 274 | topOffset = addressBarView.frame.size.height; 275 | } 276 | 277 | if (heightOffset != self.collectionView.contentInset.bottom) { 278 | self.collectionView.scrollIndicatorInsets = UIEdgeInsetsMake(topOffset, 0, heightOffset, 0); 279 | self.collectionView.contentInset = UIEdgeInsetsMake(topOffset, 0, heightOffset, 0); 280 | if (frame.origin.y != collapsedHeight) { 281 | [self scrollToBottomAnimated:YES]; 282 | } 283 | } 284 | 285 | } 286 | 287 | -(void)meTextInputView:(METextInputView *)inputView didTapCameraButton:(UIButton*)cameraButton { 288 | [self.meTextInputView hideKeyboard]; 289 | [self.messageInputToolbar.inputToolBarDelegate messageInputToolbar:self.messageInputToolbar didTapLeftAccessoryButton:self.messageInputToolbar.leftAccessoryButton]; 290 | } 291 | 292 | // send button was pressed 293 | -(void)meTextInputView:(METextInputView *)inputView didTapSend:(NSDictionary *)message { 294 | NSData *messageData = [[message objectForKey:@"substitute"] dataUsingEncoding:NSUTF8StringEncoding]; 295 | LYRMessagePart *messagePart = [LYRMessagePart messagePartWithMIMEType:@"text/plain" data:messageData]; 296 | NSError *error = nil; 297 | LYRMessage *layerMessage = [self.layerClient newMessageWithParts:@[ messagePart ] options:nil error:&error]; 298 | BOOL success = [self.conversation sendMessage:layerMessage error:&error]; 299 | } 300 | 301 | 302 | #pragma mark - Accessors 303 | 304 | - (void)setConversation:(LYRConversation *)conversation 305 | { 306 | [super setConversation:conversation]; 307 | [self configureTitle]; 308 | 309 | if (conversation != nil) { 310 | self.meTextInputView.hidden = NO; 311 | [self.meTextInputView showKeyboard]; 312 | } 313 | 314 | } 315 | 316 | #pragma mark - ATLConversationViewControllerDelegate 317 | 318 | /** 319 | Atlas - Informs the delegate of a successful message send. Atlas Messenger adds a `Details` button to the navigation bar if this is the first message sent within a new conversation. 320 | */ 321 | - (void)conversationViewController:(ATLConversationViewController *)viewController didSendMessage:(LYRMessage *)message 322 | { 323 | [self addDetailsButton]; 324 | } 325 | 326 | /** 327 | Atlas - Informs the delegate that a message failed to send. Atlas messeneger display an alert view to inform the user of the failure. 328 | */ 329 | - (void)conversationViewController:(ATLConversationViewController *)viewController didFailSendingMessage:(LYRMessage *)message error:(NSError *)error; 330 | { 331 | NSLog(@"Message Send Failed with Error: %@", error); 332 | UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Messaging Error" 333 | message:error.localizedDescription 334 | delegate:nil 335 | cancelButtonTitle:@"OK" 336 | otherButtonTitles:nil]; 337 | [alertView show]; 338 | } 339 | 340 | /** 341 | Atlas - Informs the delegate that a message was selected. Atlas messenger presents an `ATLImageViewController` if the message contains an image. 342 | */ 343 | - (void)conversationViewController:(ATLConversationViewController *)viewController didSelectMessage:(LYRMessage *)message 344 | { 345 | LYRMessagePart *messagePart = ATLMessagePartForMIMEType(message, ATLMIMETypeImageJPEG); 346 | if (messagePart) { 347 | [self presentMediaViewControllerWithMessage:message]; 348 | return; 349 | } 350 | messagePart = ATLMessagePartForMIMEType(message, ATLMIMETypeImagePNG); 351 | if (messagePart) { 352 | [self presentMediaViewControllerWithMessage:message]; 353 | return; 354 | } 355 | messagePart = ATLMessagePartForMIMEType(message, ATLMIMETypeImageGIF); 356 | if (messagePart) { 357 | [self presentMediaViewControllerWithMessage:message]; 358 | return; 359 | } 360 | messagePart = ATLMessagePartForMIMEType(message, ATLMIMETypeVideoMP4); 361 | if (messagePart) { 362 | [self presentMediaViewControllerWithMessage:message]; 363 | return; 364 | } 365 | } 366 | 367 | - (void)presentMediaViewControllerWithMessage:(LYRMessage *)message 368 | { 369 | ATLMMediaViewController *imageViewController = [[ATLMMediaViewController alloc] initWithMessage:message]; 370 | UINavigationController *controller = [[UINavigationController alloc] initWithRootViewController:imageViewController]; 371 | [self.navigationController presentViewController:controller animated:YES completion:nil]; 372 | } 373 | 374 | #pragma mark - ATLConversationViewControllerDataSource 375 | 376 | /** 377 | Atlas - Returns an object conforming to the `ATLParticipant` protocol whose `userID` property matches the supplied identity. 378 | */ 379 | - (id)conversationViewController:(ATLConversationViewController *)conversationViewController participantForIdentity:(nonnull LYRIdentity *)identity 380 | { 381 | return identity; 382 | } 383 | 384 | /** 385 | Atlas - Returns an `NSAttributedString` object for a given date. The format of this string can be configured to whatever format an application wishes to display. 386 | */ 387 | - (NSAttributedString *)conversationViewController:(ATLConversationViewController *)conversationViewController attributedStringForDisplayOfDate:(NSDate *)date 388 | { 389 | NSDateFormatter *dateFormatter; 390 | ATLMDateProximity dateProximity = ATLMProximityToDate(date); 391 | switch (dateProximity) { 392 | case ATLMDateProximityToday: 393 | case ATLMDateProximityYesterday: 394 | dateFormatter = ATLMRelativeDateFormatter(); 395 | break; 396 | case ATLMDateProximityWeek: 397 | dateFormatter = ATLMDayOfWeekDateFormatter(); 398 | break; 399 | case ATLMDateProximityYear: 400 | dateFormatter = ATLMThisYearDateFormatter(); 401 | break; 402 | case ATLMDateProximityOther: 403 | dateFormatter = ATLMDefaultDateFormatter(); 404 | break; 405 | } 406 | 407 | NSString *dateString = [dateFormatter stringFromDate:date]; 408 | NSString *timeString = [ATLMShortTimeFormatter() stringFromDate:date]; 409 | 410 | NSMutableAttributedString *dateAttributedString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@ %@", dateString, timeString]]; 411 | [dateAttributedString addAttribute:NSForegroundColorAttributeName value:[UIColor grayColor] range:NSMakeRange(0, dateAttributedString.length)]; 412 | [dateAttributedString addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:11] range:NSMakeRange(0, dateAttributedString.length)]; 413 | [dateAttributedString addAttribute:NSFontAttributeName value:[UIFont boldSystemFontOfSize:11] range:NSMakeRange(0, dateString.length)]; 414 | return dateAttributedString; 415 | } 416 | 417 | /** 418 | Atlas - Returns an `NSAttributedString` object for given recipient state. The state string will only be displayed below the latest message that was sent by the currently authenticated user. 419 | */ 420 | - (NSAttributedString *)conversationViewController:(ATLConversationViewController *)conversationViewController attributedStringForDisplayOfRecipientStatus:(NSDictionary *)recipientStatus 421 | { 422 | NSMutableDictionary *mutableRecipientStatus = [recipientStatus mutableCopy]; 423 | if ([mutableRecipientStatus valueForKey:self.applicationController.layerClient.authenticatedUser.userID]) { 424 | [mutableRecipientStatus removeObjectForKey:self.applicationController.layerClient.authenticatedUser.userID]; 425 | } 426 | 427 | NSString *statusString = [NSString new]; 428 | if (mutableRecipientStatus.count > 1) { 429 | __block NSUInteger readCount = 0; 430 | __block BOOL delivered = NO; 431 | __block BOOL sent = NO; 432 | __block BOOL pending = NO; 433 | [mutableRecipientStatus enumerateKeysAndObjectsUsingBlock:^(NSString *userID, NSNumber *statusNumber, BOOL *stop) { 434 | LYRRecipientStatus status = statusNumber.integerValue; 435 | switch (status) { 436 | case LYRRecipientStatusInvalid: 437 | break; 438 | case LYRRecipientStatusPending: 439 | pending = YES; 440 | break; 441 | case LYRRecipientStatusSent: 442 | sent = YES; 443 | break; 444 | case LYRRecipientStatusDelivered: 445 | delivered = YES; 446 | break; 447 | case LYRRecipientStatusRead: 448 | readCount += 1; 449 | break; 450 | } 451 | }]; 452 | if (readCount) { 453 | NSString *participantString = readCount > 1 ? @"Participants" : @"Participant"; 454 | statusString = [NSString stringWithFormat:@"Read by %lu %@", (unsigned long)readCount, participantString]; 455 | } else if (pending) { 456 | statusString = @"Pending"; 457 | }else if (delivered) { 458 | statusString = @"Delivered"; 459 | } else if (sent) { 460 | statusString = @"Sent"; 461 | } 462 | } else { 463 | __block NSString *blockStatusString = [NSString new]; 464 | [mutableRecipientStatus enumerateKeysAndObjectsUsingBlock:^(NSString *userID, NSNumber *statusNumber, BOOL *stop) { 465 | if ([userID isEqualToString:self.applicationController.layerClient.authenticatedUser.userID]) return; 466 | LYRRecipientStatus status = statusNumber.integerValue; 467 | switch (status) { 468 | case LYRRecipientStatusInvalid: 469 | blockStatusString = @"Not Sent"; 470 | break; 471 | case LYRRecipientStatusPending: 472 | blockStatusString = @"Pending"; 473 | break; 474 | case LYRRecipientStatusSent: 475 | blockStatusString = @"Sent"; 476 | break; 477 | case LYRRecipientStatusDelivered: 478 | blockStatusString = @"Delivered"; 479 | break; 480 | case LYRRecipientStatusRead: 481 | blockStatusString = @"Read"; 482 | break; 483 | } 484 | }]; 485 | statusString = blockStatusString; 486 | } 487 | return [[NSAttributedString alloc] initWithString:statusString attributes:@{NSFontAttributeName : [UIFont boldSystemFontOfSize:11]}]; 488 | } 489 | 490 | #pragma mark - ATLAddressBarControllerDelegate 491 | 492 | /** 493 | Atlas - Informs the delegate that the user tapped the `addContacts` icon in the `ATLAddressBarViewController`. Atlas Messenger presents an `ATLParticipantPickerController`. 494 | */ 495 | - (void)addressBarViewController:(ATLAddressBarViewController *)addressBarViewController didTapAddContactsButton:(UIButton *)addContactsButton 496 | { 497 | NSSet *selectedParticipantIDs = [addressBarViewController.selectedParticipants valueForKey:@"userID"]; 498 | if (!selectedParticipantIDs) { 499 | selectedParticipantIDs = [NSSet new]; 500 | } 501 | 502 | LYRQuery *query = [LYRQuery queryWithQueryableClass:[LYRIdentity class]]; 503 | query.predicate = [LYRPredicate predicateWithProperty:@"userID" predicateOperator:LYRPredicateOperatorIsNotIn value:selectedParticipantIDs]; 504 | NSError *error; 505 | NSOrderedSet *identities = [self.layerClient executeQuery:query error:&error]; 506 | if (error) { 507 | ATLMAlertWithError(error); 508 | } 509 | 510 | ATLMParticipantTableViewController *controller = [ATLMParticipantTableViewController participantTableViewControllerWithParticipants:identities.set sortType:ATLParticipantPickerSortTypeFirstName]; 511 | controller.blockedParticipantIdentifiers = [self.layerClient.policies valueForKey:@"sentByUserID"]; 512 | controller.delegate = self; 513 | controller.allowsMultipleSelection = NO; 514 | 515 | UINavigationController *navigationController =[[UINavigationController alloc] initWithRootViewController:controller]; 516 | [self.navigationController presentViewController:navigationController animated:YES completion:nil]; 517 | [self.meTextInputView hideKeyboard]; 518 | } 519 | 520 | /** 521 | Atlas - Informs the delegate that the user is searching for participants. Atlas Messengers queries for participants whose `fullName` property contains the given search string. 522 | */ 523 | - (void)addressBarViewController:(ATLAddressBarViewController *)addressBarViewController searchForParticipantsMatchingText:(NSString *)searchText completion:(void (^)(NSArray *participants))completion 524 | { 525 | LYRQuery *query = [LYRQuery queryWithQueryableClass:[LYRIdentity class]]; 526 | query.predicate = [LYRPredicate predicateWithProperty:@"displayName" predicateOperator:LYRPredicateOperatorLike value:[searchText stringByAppendingString:@"%"]]; 527 | [self.layerClient executeQuery:query completion:^(NSOrderedSet> * _Nullable resultSet, NSError * _Nullable error) { 528 | if (resultSet) { 529 | completion(resultSet.array); 530 | } else { 531 | completion([NSArray array]); 532 | } 533 | }]; 534 | } 535 | 536 | /** 537 | Atlas - Informs the delegate that the user tapped on the `ATLAddressBarViewController` while it was disabled. Atlas Messenger presents an `ATLConversationDetailViewController` in response. 538 | */ 539 | - (void)addressBarViewControllerDidSelectWhileDisabled:(ATLAddressBarViewController *)addressBarViewController 540 | { 541 | [self detailsButtonTapped]; 542 | } 543 | 544 | #pragma mark - ATLParticipantTableViewControllerDelegate 545 | 546 | /** 547 | Atlas - Informs the delegate that the user selected an participant. Atlas Messenger in turn, informs the `ATLAddressBarViewController` of the selection. 548 | */ 549 | - (void)participantTableViewController:(ATLParticipantTableViewController *)participantTableViewController didSelectParticipant:(id)participant 550 | { 551 | [self.addressBarController selectParticipant:participant]; 552 | [self.navigationController dismissViewControllerAnimated:YES completion:nil]; 553 | } 554 | 555 | /** 556 | Atlas - Informs the delegate that the user is searching for participants. Atlas Messengers queries for participants whose `fullName` property contains the give search string. 557 | */ 558 | - (void)participantTableViewController:(ATLParticipantTableViewController *)participantTableViewController didSearchWithString:(NSString *)searchText completion:(void (^)(NSSet *))completion 559 | { 560 | LYRQuery *query = [LYRQuery queryWithQueryableClass:[LYRIdentity class]]; 561 | query.predicate = [LYRPredicate predicateWithProperty:@"displayName" predicateOperator:LYRPredicateOperatorLike value:searchText]; 562 | [self.layerClient executeQuery:query completion:^(NSOrderedSet> * _Nullable resultSet, NSError * _Nullable error) { 563 | if (resultSet) { 564 | completion(resultSet.set); 565 | } else { 566 | completion([NSSet set]); 567 | } 568 | }]; 569 | } 570 | 571 | #pragma mark - LSConversationDetailViewControllerDelegate 572 | 573 | /** 574 | Atlas - Informs the delegate that the user has tapped the `Share My Current Location` button. Atlas Messenger sends a message into the current conversation with the current location. 575 | */ 576 | - (void)conversationDetailViewControllerDidSelectShareLocation:(ATLMConversationDetailViewController *)conversationDetailViewController 577 | { 578 | [self sendLocationMessage]; 579 | [self.navigationController popViewControllerAnimated:YES]; 580 | } 581 | 582 | /** 583 | Atlas - Informs the delegate that the conversation has changed. Atlas Messenger updates its conversation and the current view controller's title in response. 584 | */ 585 | - (void)conversationDetailViewController:(ATLMConversationDetailViewController *)conversationDetailViewController didChangeConversation:(LYRConversation *)conversation 586 | { 587 | self.conversation = conversation; 588 | [self configureTitle]; 589 | [self.meTextInputView showKeyboard]; 590 | } 591 | 592 | #pragma mark - Details Button Actions 593 | 594 | - (void)addDetailsButton 595 | { 596 | if (self.navigationItem.rightBarButtonItem) return; 597 | 598 | UIBarButtonItem *detailsButtonItem = [[UIBarButtonItem alloc] initWithTitle:ATLMDetailsButtonLabel 599 | style:UIBarButtonItemStylePlain 600 | target:self 601 | action:@selector(detailsButtonTapped)]; 602 | detailsButtonItem.accessibilityLabel = ATLMDetailsButtonAccessibilityLabel; 603 | self.navigationItem.rightBarButtonItem = detailsButtonItem; 604 | } 605 | 606 | - (void)detailsButtonTapped 607 | { 608 | ATLMConversationDetailViewController *detailViewController = [ATLMConversationDetailViewController conversationDetailViewControllerWithConversation:self.conversation]; 609 | detailViewController.detailDelegate = self; 610 | detailViewController.applicationController = self.applicationController; 611 | [self.navigationController pushViewController:detailViewController animated:YES]; 612 | } 613 | 614 | #pragma mark - Notification Handlers 615 | 616 | - (void)conversationMetadataDidChange:(NSNotification *)notification 617 | { 618 | if (!self.conversation) return; 619 | if (!notification.object) return; 620 | if (![notification.object isEqual:self.conversation]) return; 621 | 622 | [self configureTitle]; 623 | } 624 | 625 | #pragma mark - Helpers 626 | 627 | - (void)configureTitle 628 | { 629 | if ([self.conversation.metadata valueForKey:ATLMConversationMetadataNameKey]) { 630 | NSString *conversationTitle = [self.conversation.metadata valueForKey:ATLMConversationMetadataNameKey]; 631 | if (conversationTitle.length) { 632 | self.title = conversationTitle; 633 | } else { 634 | self.title = [self defaultTitle]; 635 | } } else { 636 | self.title = [self defaultTitle]; 637 | } 638 | } 639 | 640 | - (NSString *)defaultTitle 641 | { 642 | if (!self.conversation) { 643 | return @"New Message"; 644 | } 645 | 646 | NSMutableSet *otherParticipants = [self.conversation.participants mutableCopy]; 647 | NSPredicate *predicate = [NSPredicate predicateWithFormat:@"userID != %@", self.layerClient.authenticatedUser.userID]; 648 | [otherParticipants filterUsingPredicate:predicate]; 649 | 650 | if (otherParticipants.count == 0) { 651 | return @"Personal"; 652 | } else if (otherParticipants.count == 1) { 653 | LYRIdentity *otherIdentity = [otherParticipants anyObject]; 654 | id participant = [self conversationViewController:self participantForIdentity:otherIdentity]; 655 | return participant ? participant.firstName : @"Message"; 656 | } else if (otherParticipants.count > 1) { 657 | NSUInteger participantCount = 0; 658 | id knownParticipant; 659 | for (LYRIdentity *identity in otherParticipants) { 660 | id participant = [self conversationViewController:self participantForIdentity:identity]; 661 | if (participant) { 662 | participantCount += 1; 663 | knownParticipant = participant; 664 | } 665 | } 666 | if (participantCount == 1) { 667 | return knownParticipant.firstName; 668 | } else if (participantCount > 1) { 669 | return @"Group"; 670 | } 671 | } 672 | return @"Message"; 673 | } 674 | 675 | #pragma mark - Link Tap Handler 676 | 677 | - (void)userDidTapLink:(NSNotification *)notification 678 | { 679 | [[UIApplication sharedApplication] openURL:notification.object]; 680 | } 681 | 682 | - (void)configureUserInterfaceAttributes 683 | { 684 | [[ATLIncomingMessageCollectionViewCell appearance] setBubbleViewColor:ATLLightGrayColor()]; 685 | [[ATLIncomingMessageCollectionViewCell appearance] setMessageTextColor:[UIColor blackColor]]; 686 | [[ATLIncomingMessageCollectionViewCell appearance] setMessageLinkTextColor:ATLBlueColor()]; 687 | 688 | [[ATLOutgoingMessageCollectionViewCell appearance] setBubbleViewColor:ATLBlueColor()]; 689 | [[ATLOutgoingMessageCollectionViewCell appearance] setMessageTextColor:[UIColor whiteColor]]; 690 | [[ATLOutgoingMessageCollectionViewCell appearance] setMessageLinkTextColor:[UIColor whiteColor]]; 691 | } 692 | 693 | - (void)registerNotificationObservers 694 | { 695 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userDidTapLink:) name:ATLUserDidTapLinkNotification object:nil]; 696 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(conversationMetadataDidChange:) name:ATLMConversationMetadataDidChangeNotification object:nil]; 697 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(deviceOrientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil]; 698 | } 699 | 700 | #pragma mark - Device Orientation 701 | 702 | - (void)deviceOrientationDidChange:(NSNotification *)notification 703 | { 704 | [self.collectionView.collectionViewLayout invalidateLayout]; 705 | } 706 | 707 | @end 708 | -------------------------------------------------------------------------------- /MEConversationListCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // MEConverstaionListCell.h 3 | // Atlas Messenger 4 | // 5 | // Created by steve on 3/14/16. 6 | // Copyright © 2016 Layer, Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "MEMessageView.h" 11 | #import 12 | 13 | @interface MEConversationListCell : ATLConversationTableViewCell 14 | @property MEMessageView * messageView; 15 | @property NSString * lastMessageText; 16 | @property BOOL isMakemojiText; 17 | @end 18 | -------------------------------------------------------------------------------- /MEConversationListCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // MEConverstaionListCell.m 3 | // Atlas Messenger 4 | // 5 | // Created by steve on 3/14/16. 6 | // Copyright © 2016 Layer, Inc. All rights reserved. 7 | // 8 | 9 | #import "MEConversationListCell.h" 10 | #import "METextInputView.h" 11 | 12 | @implementation MEConversationListCell 13 | 14 | - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier 15 | { 16 | self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; 17 | if (self) { 18 | [self commonInit]; 19 | } 20 | return self; 21 | } 22 | 23 | - (id)initWithCoder:(NSCoder *)aDecoder 24 | { 25 | self = [super initWithCoder:aDecoder]; 26 | if (self) { 27 | [self commonInit]; 28 | } 29 | return self; 30 | } 31 | 32 | -(void)commonInit { 33 | self.messageView = [[MEMessageView alloc] initWithFrame:CGRectZero]; 34 | self.messageView.autoresizingMask = UIViewAutoresizingNone; 35 | self.messageView.clipsToBounds = YES; 36 | [self.contentView addSubview:self.messageView]; 37 | } 38 | 39 | -(void)layoutSubviews { 40 | [super layoutSubviews]; 41 | UILabel * foundLabel = [self findLastMessageLabel]; 42 | foundLabel.hidden = NO; 43 | if (self.isMakemojiText == YES) { 44 | if (self.messageView.subviews.count > 0) { 45 | foundLabel.hidden = YES; 46 | UIView * messageLabel = [self.messageView.subviews objectAtIndex:0]; 47 | messageLabel.autoresizingMask = UIViewAutoresizingNone; 48 | self.messageView.frame = CGRectMake(30, 28.5, self.contentView.frame.size.width-70, 41); 49 | messageLabel.frame = CGRectMake(0, 0, self.messageView.frame.size.width, 40); 50 | } 51 | } 52 | } 53 | 54 | - (void)updateWithLastMessageText:(NSString *)lastMessageText { 55 | [super updateWithLastMessageText:lastMessageText]; 56 | self.lastMessageText = lastMessageText; 57 | self.isMakemojiText = NO; 58 | if ([METextInputView detectMakemojiMessage:lastMessageText] == YES) { 59 | NSString * messageHTML = [METextInputView convertSubstituedToHTML:lastMessageText]; 60 | UIColor * messageTextColor = self.lastMessageLabelColor; 61 | NSString * fontSize = [NSString stringWithFormat:@"font-size:%ipx;", 16]; 62 | NSString * fontColor = [NSString stringWithFormat:@"color:%@;", [self hexStringFromColor:messageTextColor]]; 63 | messageHTML = [messageHTML stringByReplacingOccurrencesOfString:@"font-size:16px;" withString:fontSize]; 64 | messageHTML = [messageHTML stringByReplacingOccurrencesOfString:@"color:#000000;" withString:fontColor]; 65 | self.isMakemojiText = YES; 66 | [self.messageView setHTMLString:messageHTML]; 67 | } 68 | 69 | } 70 | 71 | -(UILabel *)findLastMessageLabel { 72 | UILabel * foundLabel; 73 | for (id view in self.contentView.subviews) { 74 | if ([view isKindOfClass:[UILabel class]]) { 75 | UILabel * sublabel = (UILabel *)view; 76 | if ([sublabel.text isEqualToString:self.lastMessageText]) { 77 | foundLabel = view; 78 | } 79 | } 80 | } 81 | return foundLabel; 82 | } 83 | 84 | - (NSString *)hexStringFromColor:(UIColor *)color { 85 | CGColorSpaceModel colorSpace = CGColorSpaceGetModel(CGColorGetColorSpace(color.CGColor)); 86 | const CGFloat *components = CGColorGetComponents(color.CGColor); 87 | CGFloat r, g, b, a; 88 | if (colorSpace == kCGColorSpaceModelMonochrome) { 89 | r = components[0]; 90 | g = components[0]; 91 | b = components[0]; 92 | a = components[1]; 93 | } 94 | else if (colorSpace == kCGColorSpaceModelRGB) { 95 | r = components[0]; 96 | g = components[1]; 97 | b = components[2]; 98 | a = components[3]; 99 | } 100 | 101 | return [NSString stringWithFormat:@"#%02lX%02lX%02lX", 102 | lroundf(r * 255), 103 | lroundf(g * 255), 104 | lroundf(b * 255)]; 105 | } 106 | 107 | @end 108 | -------------------------------------------------------------------------------- /MEConversationViewSwizzle.h: -------------------------------------------------------------------------------- 1 | // 2 | // Swizzle.h 3 | // Atlas Messenger 4 | // 5 | // Created by steve on 2/27/16. 6 | // Copyright © 2016 Layer, Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ATLConversationView (Tracking) 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /MEConversationViewSwizzle.m: -------------------------------------------------------------------------------- 1 | // 2 | // Swizzle.m 3 | // Atlas Messenger 4 | // 5 | // Created by steve on 2/27/16. 6 | // Copyright © 2016 Layer, Inc. All rights reserved. 7 | // 8 | 9 | #import "MEConversationViewSwizzle.h" 10 | #import "JRSwizzle.h" 11 | 12 | @implementation ATLConversationView (Tracking) 13 | 14 | + (void)initialize { 15 | [super initialize]; 16 | [self jr_swizzleMethod:@selector(canBecomeFirstResponder) 17 | withMethod:@selector(SNcanBecomeFirstResponder) 18 | error:nil]; 19 | } 20 | 21 | - (BOOL)SNcanBecomeFirstResponder { 22 | return NO; 23 | } 24 | 25 | 26 | 27 | @end 28 | 29 | -------------------------------------------------------------------------------- /MEIncomingMessageCollectionViewCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // MEIncomingMessageCollectionViewCell.h 3 | // Atlas Messenger 4 | // 5 | // Created by steve on 3/7/16. 6 | // Copyright © 2016 Layer, Inc. All rights reserved. 7 | // 8 | 9 | #import "MEMessageView.h" 10 | #import "MEOutgoingMessageCollectionViewCell.h" 11 | 12 | @interface MEIncomingMessageCollectionViewCell : MEOutgoingMessageCollectionViewCell 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /MEIncomingMessageCollectionViewCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // MEIncomingMessageCollectionViewCell.m 3 | // Atlas Messenger 4 | // 5 | // Created by steve on 3/7/16. 6 | // Copyright © 2016 Layer, Inc. All rights reserved. 7 | // 8 | 9 | #import "MEIncomingMessageCollectionViewCell.h" 10 | #import "METextInputView.h" 11 | 12 | @implementation MEIncomingMessageCollectionViewCell 13 | 14 | - (void)presentMessage:(LYRMessage *)message { 15 | self.bubbleView.backgroundColor = [[ATLIncomingMessageCollectionViewCell appearance] bubbleViewColor]; 16 | LYRMessagePart *part = message.parts[0]; 17 | 18 | if ([part.MIMEType isEqualToString:@"text/plain"]) { 19 | NSString * messageString = [[NSString alloc] initWithData:part.data encoding:NSUTF8StringEncoding]; 20 | NSString * messageHTML = [METextInputView convertSubstituedToHTML:messageString]; 21 | UIColor * messageTextColor = [[ATLIncomingMessageCollectionViewCell appearance] messageTextColor]; 22 | NSString * fontSize = [NSString stringWithFormat:@"font-size:%ipx;", 16]; 23 | NSString * fontColor = [NSString stringWithFormat:@"color:%@;", [self hexStringFromColor:messageTextColor]]; 24 | messageHTML = [messageHTML stringByReplacingOccurrencesOfString:@"font-size:16px;" withString:fontSize]; 25 | messageHTML = [messageHTML stringByReplacingOccurrencesOfString:@"color:#000000;" withString:fontColor]; 26 | [self setHTMLString:messageHTML]; 27 | messageString = nil; 28 | 29 | } 30 | } 31 | 32 | -(void)layoutSubviews { 33 | [super layoutSubviews]; 34 | self.avatarImageView.frame = CGRectMake(ATLMessageBubbleLabelHorizontalPadding, ATLMessageBubbleLabelVerticalPadding, 27, 27); 35 | if (!self.superview) { return; } 36 | if (self.shouldDisplayAvatar == NO) { self.avatarImageView.frame = CGRectZero; } 37 | 38 | CGFloat leadIn = ATLMessageCellHorizontalMargin+self.avatarImageView.frame.size.width; 39 | 40 | self.bubbleView.frame = CGRectMake(leadIn, 0, self.bubbleView.frame.size.width, self.bubbleView.frame.size.height); 41 | self.messageView.frame = CGRectMake(leadIn+ATLMessageBubbleLabelHorizontalPadding, ATLMessageBubbleLabelVerticalPadding, self.messageView.frame.size.width, self.messageView.frame.size.height); 42 | 43 | } 44 | 45 | 46 | @end -------------------------------------------------------------------------------- /MEOutgoingMessageCollectionViewCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // MEAtlasCollectionViewCell.h 3 | // Atlas Messenger 4 | // 5 | // Created by steve on 2/27/16. 6 | // Copyright © 2016 Layer, Inc. All rights reserved. 7 | // 8 | 9 | #import "MEMessageView.h" 10 | #import "MECollectionViewCell.h" 11 | #import 12 | 13 | @interface MEOutgoingMessageCollectionViewCell : MECollectionViewCell 14 | @property UIView * bubbleView; 15 | @property ATLAvatarImageView * avatarImageView; 16 | @property (nonatomic) BOOL shouldDisplayAvatar; 17 | 18 | - (NSString *)hexStringFromColor:(UIColor *)color; 19 | @end 20 | -------------------------------------------------------------------------------- /MEOutgoingMessageCollectionViewCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // MEAtlasCollectionViewCell.m 3 | // Atlas Messenger 4 | // 5 | // Created by steve on 2/27/16. 6 | // Copyright © 2016 Layer, Inc. All rights reserved. 7 | // 8 | 9 | #import "MEOutgoingMessageCollectionViewCell.h" 10 | #import "METextInputView.h" 11 | 12 | @implementation MEOutgoingMessageCollectionViewCell 13 | 14 | - (id)initWithFrame:(CGRect)frame 15 | { 16 | self = [super initWithFrame:frame]; 17 | if (self) { 18 | [self meCommonInit]; 19 | } 20 | return self; 21 | } 22 | 23 | - (id)initWithCoder:(NSCoder *)aDecoder 24 | { 25 | self = [super initWithCoder:aDecoder]; 26 | if (self) { 27 | [self meCommonInit]; 28 | } 29 | return self; 30 | } 31 | 32 | -(void)meCommonInit { 33 | self.bubbleView = [[UIView alloc] initWithFrame:CGRectZero]; 34 | self.bubbleView.layer.cornerRadius = 16; 35 | [self.contentView addSubview:self.bubbleView]; 36 | [self.contentView sendSubviewToBack:self.bubbleView]; 37 | 38 | self.avatarImageView = [[ATLAvatarImageView alloc] init]; 39 | self.avatarImageView.translatesAutoresizingMaskIntoConstraints = NO; 40 | self.avatarImageView.hidden = YES; 41 | self.avatarImageView.frame = CGRectZero; 42 | [self.contentView addSubview:self.avatarImageView]; 43 | [self.contentView bringSubviewToFront:self.avatarImageView]; 44 | 45 | } 46 | 47 | - (void)updateWithSender:(id)sender { 48 | if (sender) { 49 | self.avatarImageView.hidden = NO; 50 | self.avatarImageView.avatarItem = sender; 51 | } else { 52 | self.avatarImageView.hidden = YES; 53 | } 54 | } 55 | 56 | - (void)shouldDisplayAvatarItem:(BOOL)shouldDisplayAvatarItem { 57 | self.shouldDisplayAvatar = shouldDisplayAvatarItem; 58 | } 59 | 60 | - (void)presentMessage:(LYRMessage *)message { 61 | self.bubbleView.backgroundColor = [[ATLOutgoingMessageCollectionViewCell appearance] bubbleViewColor]; 62 | LYRMessagePart *part = message.parts[0]; 63 | 64 | if ([part.MIMEType isEqualToString:@"text/plain"]) { 65 | NSString * messageString = [[NSString alloc] initWithData:part.data encoding:NSUTF8StringEncoding]; 66 | NSString * messageHTML = [METextInputView convertSubstituedToHTML:messageString]; 67 | UIColor * messageTextColor = [[ATLOutgoingMessageCollectionViewCell appearance] messageTextColor]; 68 | NSString * fontSize = [NSString stringWithFormat:@"font-size:%ipx;", 16]; 69 | NSString * fontColor = [NSString stringWithFormat:@"color:%@;", [self hexStringFromColor:messageTextColor]]; 70 | messageHTML = [messageHTML stringByReplacingOccurrencesOfString:@"font-size:16px;" withString:fontSize]; 71 | messageHTML = [messageHTML stringByReplacingOccurrencesOfString:@"color:#000000;" withString:fontColor]; 72 | [self setHTMLString:messageHTML]; 73 | messageString = nil; 74 | 75 | } 76 | 77 | } 78 | 79 | -(void)layoutSubviews { 80 | [super layoutSubviews]; 81 | self.avatarImageView.frame = CGRectMake(self.contentView.frame.size.width-27-ATLMessageBubbleLabelHorizontalPadding, ATLMessageBubbleLabelVerticalPadding, 27, 27); 82 | if (!self.superview) { return; } 83 | if (self.shouldDisplayAvatar == NO) { self.avatarImageView.frame = CGRectZero; } 84 | CGFloat maxBubbleWidth = ATLMaxCellWidth() + (ATLMessageBubbleLabelHorizontalPadding*2); 85 | CGFloat textHeight = self.contentView.frame.size.height-(ATLMessageBubbleLabelVerticalPadding*2); 86 | CGSize textSize = [self.messageView suggestedSizeForTextForSize:CGSizeMake(ATLMaxCellWidth(), textHeight)]; 87 | CGFloat bubbleWidth = maxBubbleWidth; 88 | textSize.width += (ATLMessageBubbleLabelHorizontalPadding * 2); 89 | if (textSize.width < maxBubbleWidth) { bubbleWidth = textSize.width; } 90 | 91 | CGFloat leadIn = self.contentView.frame.size.width - bubbleWidth - ATLMessageCellHorizontalMargin - self.avatarImageView.frame.size.width; 92 | 93 | self.bubbleView.frame = CGRectMake(leadIn, 0, bubbleWidth, self.contentView.frame.size.height); 94 | self.messageView.frame = CGRectMake(leadIn+ATLMessageBubbleLabelHorizontalPadding, ATLMessageBubbleLabelVerticalPadding, self.bubbleView.frame.size.width-(ATLMessageBubbleLabelVerticalPadding*2), self.bubbleView.frame.size.height-(ATLMessageBubbleLabelVerticalPadding*2)); 95 | } 96 | 97 | - (NSString *)hexStringFromColor:(UIColor *)color { 98 | CGColorSpaceModel colorSpace = CGColorSpaceGetModel(CGColorGetColorSpace(color.CGColor)); 99 | const CGFloat *components = CGColorGetComponents(color.CGColor); 100 | CGFloat r, g, b, a; 101 | if (colorSpace == kCGColorSpaceModelMonochrome) { 102 | r = components[0]; 103 | g = components[0]; 104 | b = components[0]; 105 | a = components[1]; 106 | } 107 | else if (colorSpace == kCGColorSpaceModelRGB) { 108 | r = components[0]; 109 | g = components[1]; 110 | b = components[2]; 111 | a = components[3]; 112 | } 113 | 114 | return [NSString stringWithFormat:@"#%02lX%02lX%02lX", 115 | lroundf(r * 255), 116 | lroundf(g * 255), 117 | lroundf(b * 255)]; 118 | } 119 | 120 | @end 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MakemojiSDK-AtlasDemo 2 | 3 | This repo provides a example of how to use the MakemojiSDK text input and toolbar from within the Atlas Messenger app. This can be used as a basis for using Makemoji within your Atlas powered apps. 4 | 5 | You will first want to follow the instructions for installing the MakemojiSDK found here: [http://makemoji.com/docs](http://makemoji.com/docs) 6 | --------------------------------------------------------------------------------