/* Copyright 2015 OpenMarket Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #import "MXKRoomBubbleCellDataWithAppendingMode.h" static NSAttributedString *messageSeparator = nil; @implementation MXKRoomBubbleCellDataWithAppendingMode #pragma mark - MXKRoomBubbleCellDataStoring - (instancetype)initWithEvent:(MXEvent *)event andRoomState:(MXRoomState *)roomState andRoomDataSource:(MXKRoomDataSource *)roomDataSource2 { self = [super initWithEvent:event andRoomState:roomState andRoomDataSource:roomDataSource2]; if (self) { // Set default settings self.maxComponentCount = 10; } return self; } - (BOOL)addEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState { // We group together text messages from the same user (attachments are not merged). if ([event.sender isEqualToString:self.senderId] && (self.attachment == nil) && (self.bubbleComponents.count < self.maxComponentCount)) { // Attachments (image, video, sticker ...) cannot be added here if ([roomDataSource.eventFormatter isSupportedAttachment:event]) { return NO; } // Check sender information NSString *eventSenderName = [roomDataSource.eventFormatter senderDisplayNameForEvent:event withRoomState:roomState]; NSString *eventSenderAvatar = [roomDataSource.eventFormatter senderAvatarUrlForEvent:event withRoomState:roomState]; if ((self.senderDisplayName || eventSenderName) && ([self.senderDisplayName isEqualToString:eventSenderName] == NO)) { return NO; } if ((self.senderAvatarUrl || eventSenderAvatar) && ([self.senderAvatarUrl isEqualToString:eventSenderAvatar] == NO)) { return NO; } // Take into account here the rendered bubbles pagination if (roomDataSource.bubblesPagination == MXKRoomDataSourceBubblesPaginationPerDay) { // Event must be sent the same day than the existing bubble. NSString *bubbleDateString = [roomDataSource.eventFormatter dateStringFromDate:self.date withTime:NO]; NSString *eventDateString = [roomDataSource.eventFormatter dateStringFromEvent:event withTime:NO]; if (bubbleDateString && eventDateString && ![bubbleDateString isEqualToString:eventDateString]) { return NO; } } // Create new message component MXKRoomBubbleComponent *addedComponent = [[MXKRoomBubbleComponent alloc] initWithEvent:event roomState:roomState eventFormatter:roomDataSource.eventFormatter session:self.mxSession]; if (addedComponent) { [self addComponent:addedComponent]; } // else the event is ignored, we consider it as handled return YES; } return NO; } - (BOOL)mergeWithBubbleCellData:(id)bubbleCellData { if ([self hasSameSenderAsBubbleCellData:bubbleCellData]) { MXKRoomBubbleCellData *cellData = (MXKRoomBubbleCellData*)bubbleCellData; // Only text messages are merged (Attachments are not merged). if ((self.attachment == nil) && (cellData.attachment == nil)) { // Take into account here the rendered bubbles pagination if (roomDataSource.bubblesPagination == MXKRoomDataSourceBubblesPaginationPerDay) { // bubble components must be sent the same day than self. NSString *selfDateString = [roomDataSource.eventFormatter dateStringFromDate:self.date withTime:NO]; NSString *bubbleDateString = [roomDataSource.eventFormatter dateStringFromDate:bubbleCellData.date withTime:NO]; if (![bubbleDateString isEqualToString:selfDateString]) { return NO; } } // Add all components of the provided message for (MXKRoomBubbleComponent* component in cellData.bubbleComponents) { [self addComponent:component]; } return YES; } } return NO; } - (NSAttributedString*)attributedTextMessageWithHighlightedEvent:(NSString*)eventId tintColor:(UIColor*)tintColor { // Create attributed string NSMutableAttributedString *customAttributedTextMsg; NSAttributedString *componentString; @synchronized(bubbleComponents) { for (MXKRoomBubbleComponent* component in bubbleComponents) { componentString = component.attributedTextMessage; if (componentString) { if ([component.event.eventId isEqualToString:eventId]) { NSMutableAttributedString *customComponentString = [[NSMutableAttributedString alloc] initWithAttributedString:componentString]; UIColor *color = tintColor ? tintColor : [UIColor lightGrayColor]; [customComponentString addAttribute:NSBackgroundColorAttributeName value:color range:NSMakeRange(0, customComponentString.length)]; componentString = customComponentString; } if (!customAttributedTextMsg) { customAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:componentString]; } else { // Append attributed text [customAttributedTextMsg appendAttributedString:[MXKRoomBubbleCellDataWithAppendingMode messageSeparator]]; [customAttributedTextMsg appendAttributedString:componentString]; } } } } return customAttributedTextMsg; } #pragma mark - - (void)prepareBubbleComponentsPosition { // Set position of the first component [super prepareBubbleComponentsPosition]; @synchronized(bubbleComponents) { // Check whether the position of other components need to be refreshed if (!self.attachment && shouldUpdateComponentsPosition && bubbleComponents.count > 1) { // Init attributed string with the first text component not nil. MXKRoomBubbleComponent *component = bubbleComponents.firstObject; CGFloat positionY = component.position.y; NSMutableAttributedString *attributedString; NSUInteger index = 0; for (; index < bubbleComponents.count; index++) { component = [bubbleComponents objectAtIndex:index]; component.position = CGPointMake(0, positionY); if (component.attributedTextMessage) { attributedString = [[NSMutableAttributedString alloc] initWithAttributedString:component.attributedTextMessage]; [attributedString appendAttributedString:[MXKRoomBubbleCellDataWithAppendingMode messageSeparator]]; break; } } for (index++; index < bubbleComponents.count; index++) { // Append the next text component component = [bubbleComponents objectAtIndex:index]; if (component.attributedTextMessage) { [attributedString appendAttributedString:component.attributedTextMessage]; // Compute the height of the resulting string CGFloat cumulatedHeight = [self rawTextHeight:attributedString]; // Deduce the position of the beginning of this component CGFloat positionY = MXKROOMBUBBLECELLDATA_TEXTVIEW_DEFAULT_VERTICAL_INSET + (cumulatedHeight - [self rawTextHeight:component.attributedTextMessage]); component.position = CGPointMake(0, positionY); [attributedString appendAttributedString:[MXKRoomBubbleCellDataWithAppendingMode messageSeparator]]; } else { // Apply the current vertical position on this empty component. component.position = CGPointMake(0, positionY); } } } } shouldUpdateComponentsPosition = NO; } #pragma mark - - (NSString*)textMessage { NSString *rawText = nil; if (self.attributedTextMessage) { // Append all components text message NSMutableString *currentTextMsg; @synchronized(bubbleComponents) { for (MXKRoomBubbleComponent* component in bubbleComponents) { if (component.textMessage == nil) { continue; } if (!currentTextMsg) { currentTextMsg = [NSMutableString stringWithString:component.textMessage]; } else { // Append text message [currentTextMsg appendString:@"\n"]; [currentTextMsg appendString:component.textMessage]; } } } rawText = currentTextMsg; } return rawText; } - (void)setAttributedTextMessage:(NSAttributedString *)inAttributedTextMessage { super.attributedTextMessage = inAttributedTextMessage; // Position of each components should be computed again shouldUpdateComponentsPosition = YES; } - (NSAttributedString*)attributedTextMessage { @synchronized(bubbleComponents) { if (self.hasAttributedTextMessage && !attributedTextMessage.length) { // Create attributed string NSMutableAttributedString *currentAttributedTextMsg; for (MXKRoomBubbleComponent* component in bubbleComponents) { if (component.attributedTextMessage) { if (!currentAttributedTextMsg) { currentAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:component.attributedTextMessage]; } else { // Append attributed text [currentAttributedTextMsg appendAttributedString:[MXKRoomBubbleCellDataWithAppendingMode messageSeparator]]; [currentAttributedTextMsg appendAttributedString:component.attributedTextMessage]; } } } self.attributedTextMessage = currentAttributedTextMsg; } } return attributedTextMessage; } - (void)setMaxTextViewWidth:(CGFloat)inMaxTextViewWidth { CGFloat previousMaxWidth = self.maxTextViewWidth; [super setMaxTextViewWidth:inMaxTextViewWidth]; // Check change if (previousMaxWidth != self.maxTextViewWidth) { // Position of each components should be computed again shouldUpdateComponentsPosition = YES; } } #pragma mark - + (NSAttributedString *)messageSeparator { @synchronized(self) { if(messageSeparator == nil) { messageSeparator = [[NSAttributedString alloc] initWithString:@"\n\n" attributes:@{NSForegroundColorAttributeName : [UIColor blackColor], NSFontAttributeName: [UIFont systemFontOfSize:4]}]; } } return messageSeparator; } #pragma mark - Privates - (void)addComponent:(MXKRoomBubbleComponent*)addedComponent { @synchronized(bubbleComponents) { // Check date of existing components to insert this new one NSUInteger index = bubbleComponents.count; // Component without date is added at the end by default if (addedComponent.date) { while (index) { MXKRoomBubbleComponent *msgComponent = [bubbleComponents objectAtIndex:(--index)]; if (msgComponent.date && [msgComponent.date compare:addedComponent.date] != NSOrderedDescending) { // New component will be inserted here index ++; break; } } } // Insert new component [bubbleComponents insertObject:addedComponent atIndex:index]; // Indicate that the data's text message layout should be recomputed. [self invalidateTextLayout]; } } @end