element-ios/matrixConsole/Model/RoomMessage.m
giomfo 8fb7e57e2c BugFix SYIOS-60 - In a self chat, Console takes ages to paginate back - even if messages are in cache
Optimize room message update (limit text content size measurement).
2015-01-20 12:41:27 +01:00

520 lines
22 KiB
Objective-C

/*
Copyright 2014 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 "RoomMessage.h"
#import "MatrixHandler.h"
#import "AppSettings.h"
NSString *const kRoomMessageLocalPreviewKey = @"kRoomMessageLocalPreviewKey";
NSString *const kRoomMessageUploadIdKey = @"kRoomMessageUploadIdKey";
static NSAttributedString *messageSeparator = nil;
@interface RoomMessage() {
// Array of RoomMessageComponent
NSMutableArray *messageComponents;
// Current text message reset at each component change (see attributedTextMessage property)
NSMutableAttributedString *currentAttributedTextMsg;
BOOL shouldUpdateComponentsHeight;
}
+ (NSAttributedString *)messageSeparator;
@end
@implementation RoomMessage
@synthesize uploadProgress;
- (id)initWithEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState {
if (self = [super init]) {
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
_senderId = event.userId;
_senderName = [mxHandler senderDisplayNameForEvent:event withRoomState:roomState];
_senderAvatarUrl = [mxHandler senderAvatarUrlForEvent:event withRoomState:roomState];
_maxTextViewWidth = ROOM_MESSAGE_DEFAULT_MAX_TEXTVIEW_WIDTH;
_contentSize = CGSizeZero;
self.uploadProgress = -1;
currentAttributedTextMsg = nil;
// Set message type (consider text by default), and check attachment if any
_messageType = RoomMessageTypeText;
if ([mxHandler isSupportedAttachment:event]) {
// Note: event.eventType is equal here to MXEventTypeRoomMessage
NSString *msgtype = event.content[@"msgtype"];
if ([msgtype isEqualToString:kMXMessageTypeImage]) {
_messageType = RoomMessageTypeImage;
// Retrieve content url/info
_attachmentURL = event.content[@"url"];
_attachmentInfo = event.content[@"info"];
// Handle thumbnail url/info
_thumbnailURL = event.content[@"thumbnail_url"];
_thumbnailInfo = event.content[@"thumbnail_info"];
if (!_thumbnailURL) {
// Suppose _attachmentURL is a matrix content uri, we use SDK to get the well adapted thumbnail from server
_thumbnailURL = [mxHandler thumbnailURLForContent:_attachmentURL inViewSize:self.contentSize withMethod:MXThumbnailingMethodScale];
}
} else if ([msgtype isEqualToString:kMXMessageTypeAudio]) {
// Not supported yet
//_messageType = RoomMessageTypeAudio;
} else if ([msgtype isEqualToString:kMXMessageTypeVideo]) {
_messageType = RoomMessageTypeVideo;
// Retrieve content url/info
_attachmentURL = event.content[@"url"];
_attachmentInfo = event.content[@"info"];
if (_attachmentInfo) {
// Get video thumbnail info
_thumbnailURL = _attachmentInfo[@"thumbnail_url"];
_thumbnailInfo = _attachmentInfo[@"thumbnail_info"];
}
} else if ([msgtype isEqualToString:kMXMessageTypeLocation]) {
// Not supported yet
// _messageType = RoomMessageTypeLocation;
}
// Retrieve local preview url (if any)
_previewURL = event.content[kRoomMessageLocalPreviewKey];
// Retrieve upload id (if any)
_uploadId = event.content[kRoomMessageUploadIdKey];
}
// Set first component of the current message
RoomMessageComponent *msgComponent = [[RoomMessageComponent alloc] initWithEvent:event andRoomState:roomState];
if (msgComponent) {
messageComponents = [NSMutableArray array];
[messageComponents addObject:msgComponent];
if (_messageType == RoomMessageTypeText) {
// Set text range
msgComponent.range = NSMakeRange(0, msgComponent.textMessage.length);
// Compute the height of the text component
msgComponent.height = [self rawTextHeight:self.attributedTextMessage];
shouldUpdateComponentsHeight = NO;
}
} else {
// Ignore this event
self = nil;
}
}
return self;
}
- (void)dealloc {
messageComponents = nil;
}
- (BOOL)addEvent:(MXEvent *)event withRoomState:(MXRoomState*)roomState {
// We group together text messages from the same user
if ([event.userId isEqualToString:_senderId] && (_messageType == RoomMessageTypeText)) {
// Attachments (image, video ...) cannot be added here
MatrixHandler *mxHandler = [MatrixHandler sharedHandler];
if ([mxHandler isSupportedAttachment:event]) {
return NO;
}
// Check sender information
NSString *eventSenderName = [mxHandler senderDisplayNameForEvent:event withRoomState:roomState];
NSString *eventSenderAvatar = [mxHandler senderAvatarUrlForEvent:event withRoomState:roomState];
if ((_senderName || eventSenderName) &&
([_senderName isEqualToString:eventSenderName] == NO)) {
return NO;
}
if ((_senderAvatarUrl || eventSenderAvatar) &&
([_senderAvatarUrl isEqualToString:eventSenderAvatar] == NO)) {
return NO;
}
// Create new message component
RoomMessageComponent *addedComponent = [[RoomMessageComponent alloc] initWithEvent:event andRoomState:roomState];
if (addedComponent) {
[self addComponent:addedComponent];
}
// else the event is ignored, we consider it as handled
return YES;
}
return NO;
}
- (BOOL)replaceLocalEventId:(NSString *)localEventId withEventId:(NSString *)eventId {
NSUInteger index = messageComponents.count;
while (index--) {
RoomMessageComponent* msgComponent = [messageComponents objectAtIndex:index];
if ([msgComponent.eventId isEqualToString:localEventId]) {
msgComponent.eventId = eventId;
// Refresh global attributed string (if any) to take into account potential component style change
if (currentAttributedTextMsg) {
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:msgComponent.textMessage attributes:[msgComponent stringAttributes]];
[currentAttributedTextMsg replaceCharactersInRange:msgComponent.range withAttributedString:attributedString];
// Reset content size
_contentSize = CGSizeZero;
} // else let the getter "attributedTextMessage" build it
return YES;
}
}
// here the provided eventId has not been found
return NO;
}
- (BOOL)removeEvent:(NSString *)eventId {
if (_messageType == RoomMessageTypeText) {
NSUInteger index = messageComponents.count;
while (index--) {
RoomMessageComponent* msgComponent = [messageComponents objectAtIndex:index];
if ([msgComponent.eventId isEqualToString:eventId]) {
[messageComponents removeObjectAtIndex:index];
// Refresh global attributed string (if any)
if (currentAttributedTextMsg) {
if (!messageComponents.count) {
// The message is now empty - Reset
self.attributedTextMessage = nil;
} else {
// Update
[currentAttributedTextMsg deleteCharactersInRange:msgComponent.range];
// Remove a separator
NSRange separatorRange = NSMakeRange(0, [RoomMessage messageSeparator].length);
if (msgComponent.range.location) {
separatorRange.location = msgComponent.range.location - [RoomMessage messageSeparator].length;
}
[currentAttributedTextMsg deleteCharactersInRange:separatorRange];
// Reset content size
_contentSize = CGSizeZero;
}
} // else let the getter "attributedTextMessage" build it
// Adjust range for components displayed after this removed component
NSUInteger deletedLength = msgComponent.range.length + [RoomMessage messageSeparator].length;
for (; index < messageComponents.count; index++) {
msgComponent = [messageComponents objectAtIndex:index];
NSRange range = msgComponent.range;
NSAssert(range.location >= deletedLength, @"RoomMessage: the ranges of msg components are corrupted");
if (range.location >= deletedLength) {
range.location -= deletedLength;
} else {
range.location = 0;
}
msgComponent.range = range;
}
// Height of each components should be updated
shouldUpdateComponentsHeight = YES;
return YES;
}
}
// here the provided eventId has not been found
}
return NO;
}
- (RoomMessageComponent*)componentWithEventId:(NSString *)eventId {
for (RoomMessageComponent* msgComponent in messageComponents) {
if ([msgComponent.eventId isEqualToString:eventId]) {
return msgComponent;
}
}
return nil;
}
- (BOOL)containsEventId:(NSString *)eventId {
return nil != [self componentWithEventId:eventId];
}
- (BOOL)hasSameSenderAsRoomMessage:(RoomMessage*)roomMessage {
// NOTE: same sender means here same id, same name and same avatar
// Check first user id
if ([_senderId isEqualToString:roomMessage.senderId] == NO) {
return NO;
}
// Check sender name
if ((_senderName.length || roomMessage.senderName.length) && ([_senderName isEqualToString:roomMessage.senderName] == NO)) {
return NO;
}
// Check avatar url
if ((_senderAvatarUrl.length || roomMessage.senderAvatarUrl.length) && ([_senderAvatarUrl isEqualToString:roomMessage.senderAvatarUrl] == NO)) {
return NO;
}
return YES;
}
- (BOOL)mergeWithRoomMessage:(RoomMessage*)roomMessage {
if ([self hasSameSenderAsRoomMessage:roomMessage]) {
if ((_messageType == RoomMessageTypeText) && (roomMessage.messageType == RoomMessageTypeText)) {
// Add all components of the provided message
for (RoomMessageComponent* msgComponent in roomMessage.components) {
[self addComponent:msgComponent];
}
return YES;
}
}
return NO;
}
- (void)checkComponentsHeight {
// Check conditions to ignore this action
if (_messageType != RoomMessageTypeText || !shouldUpdateComponentsHeight || !messageComponents.count) {
return;
}
// This method should run on main thread
if ([NSThread currentThread] != [NSThread mainThread]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self checkComponentsHeight];
});
return;
}
// Compute height of the first component
RoomMessageComponent *msgComponent = [messageComponents objectAtIndex:0];
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:msgComponent.textMessage attributes:[msgComponent stringAttributes]];
CGSize textContentSize = [self textContentSize:attributedString];
if (textContentSize.height) {
msgComponent.height = textContentSize.height - (2 * ROOM_MESSAGE_TEXTVIEW_MARGIN);
} else {
msgComponent.height = 0;
}
// Compute height of other components
for (NSUInteger index = 1; index < messageComponents.count; index++) {
CGFloat previousContentHeight = textContentSize.height ? textContentSize.height : (2 * ROOM_MESSAGE_TEXTVIEW_MARGIN);
msgComponent = [messageComponents objectAtIndex:index];
// Append attributed text
[attributedString appendAttributedString:[RoomMessage messageSeparator]];
[attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:msgComponent.textMessage attributes:[msgComponent stringAttributes]]];
textContentSize = [self textContentSize:attributedString];
msgComponent.height = textContentSize.height - previousContentHeight;
}
shouldUpdateComponentsHeight = NO;
}
#pragma mark - Properties
- (void)setMaxTextViewWidth:(CGFloat)maxTextViewWidth {
if (_messageType == RoomMessageTypeText) {
// Check change
if (_maxTextViewWidth != maxTextViewWidth) {
_maxTextViewWidth = maxTextViewWidth;
// Reset content size
_contentSize = CGSizeZero;
// Invalidate existing height for components
shouldUpdateComponentsHeight = YES;
}
}
}
- (CGSize)contentSize {
if (CGSizeEqualToSize(_contentSize, CGSizeZero)) {
if (_messageType == RoomMessageTypeText) {
if ([NSThread currentThread] != [NSThread mainThread]) {
dispatch_sync(dispatch_get_main_queue(), ^{
_contentSize = [self textContentSize:self.attributedTextMessage];
});
} else {
_contentSize = [self textContentSize:self.attributedTextMessage];
}
} else if (_messageType == RoomMessageTypeImage || _messageType == RoomMessageTypeVideo) {
CGFloat width, height;
width = height = 40;
if (_thumbnailInfo || _attachmentInfo) {
if (_thumbnailInfo) {
width = [_thumbnailInfo[@"w"] integerValue];
height = [_thumbnailInfo[@"h"] integerValue];
} else {
width = [_attachmentInfo[@"w"] integerValue];
height = [_attachmentInfo[@"h"] integerValue];
}
if (width > ROOM_MESSAGE_MAX_ATTACHMENTVIEW_WIDTH || height > ROOM_MESSAGE_MAX_ATTACHMENTVIEW_WIDTH) {
if (width > height) {
height = (height * ROOM_MESSAGE_MAX_ATTACHMENTVIEW_WIDTH) / width;
height = floorf(height / 2) * 2;
width = ROOM_MESSAGE_MAX_ATTACHMENTVIEW_WIDTH;
} else {
width = (width * ROOM_MESSAGE_MAX_ATTACHMENTVIEW_WIDTH) / height;
width = floorf(width / 2) * 2;
height = ROOM_MESSAGE_MAX_ATTACHMENTVIEW_WIDTH;
}
}
}
_contentSize = CGSizeMake(width, height);
} else {
_contentSize = CGSizeMake(40, 40);
}
}
return _contentSize;
}
- (NSArray*)components {
return [messageComponents copy];
}
- (void)setAttributedTextMessage:(NSAttributedString *)inAttributedTextMessage {
if (!inAttributedTextMessage.length) {
currentAttributedTextMsg = nil;
} else {
currentAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:inAttributedTextMessage];
}
// Reset content size
_contentSize = CGSizeZero;
}
- (NSAttributedString*)attributedTextMessage {
if (!currentAttributedTextMsg && messageComponents.count) {
// Create attributed string
for (RoomMessageComponent* msgComponent in messageComponents) {
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:msgComponent.textMessage attributes:[msgComponent stringAttributes]];
if (!currentAttributedTextMsg) {
currentAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:attributedString];
} else {
// Append attributed text
[currentAttributedTextMsg appendAttributedString:[RoomMessage messageSeparator]];
[currentAttributedTextMsg appendAttributedString:attributedString];
}
}
}
return currentAttributedTextMsg;
}
- (BOOL)startsWithSenderName {
if (_messageType == RoomMessageTypeText) {
if (messageComponents.count) {
RoomMessageComponent *msgComponent = [messageComponents objectAtIndex:0];
return msgComponent.startsWithSenderName;
}
}
return NO;
}
- (BOOL)isUploadInProgress {
if (_messageType != RoomMessageTypeText) {
if (messageComponents.count) {
RoomMessageComponent *msgComponent = [messageComponents firstObject];
return (msgComponent.style == RoomMessageComponentStyleInProgress);
}
}
return NO;
}
#pragma mark - Privates
- (void)addComponent:(RoomMessageComponent*)addedComponent {
// Check date of existing components to insert this new one
NSUInteger addedTextLength = addedComponent.textMessage.length;
NSUInteger addedTextLocation = 0;
NSUInteger separatorLocation = 0;
NSUInteger index = messageComponents.count;
while (index) {
RoomMessageComponent *msgComponent = [messageComponents objectAtIndex:(--index)];
if ([msgComponent.date compare:addedComponent.date] == NSOrderedDescending) {
// New component will be inserted before this one -> Adjust text range
NSRange range = msgComponent.range;
range.location += (addedTextLength + [RoomMessage messageSeparator].length);
msgComponent.range = range;
} else {
// New component will be inserted here
index ++;
addedTextLocation = msgComponent.range.location + msgComponent.range.length;
break;
}
}
// Insert new component
[messageComponents insertObject:addedComponent atIndex:index];
// Adjust added component location
if (addedTextLocation) {
// We will insert separator before adding new component
separatorLocation = addedTextLocation;
addedTextLocation += [RoomMessage messageSeparator].length;
} else {
// The new component is inserted in first position, separator will be inserted after it
separatorLocation = addedTextLength;
}
// Update global attributed string (if any)
if (currentAttributedTextMsg) {
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:addedComponent.textMessage attributes:[addedComponent stringAttributes]];
if (addedTextLocation) {
// Add separator before added text component
[currentAttributedTextMsg insertAttributedString:[RoomMessage messageSeparator] atIndex:separatorLocation];
[currentAttributedTextMsg insertAttributedString:attributedString atIndex:addedTextLocation];
} else {
// The new component is inserted in first position
[currentAttributedTextMsg insertAttributedString:attributedString atIndex:addedTextLocation];
// Check whether a separator is required
if (messageComponents.count > 1) {
[currentAttributedTextMsg insertAttributedString:[RoomMessage messageSeparator] atIndex:separatorLocation];
}
}
// Reset content size
_contentSize = CGSizeZero;
} // Else let the getter "attributedTextMessage" build it
// Set text range
addedComponent.range = NSMakeRange(addedTextLocation, addedTextLength);
// Height of each components should be computed again
shouldUpdateComponentsHeight = YES;
}
#pragma mark - Text measuring
// Return the raw height of the provided text by removing any margin
- (CGFloat)rawTextHeight: (NSAttributedString*)attributedText {
__block CGSize textSize;
if ([NSThread currentThread] != [NSThread mainThread]) {
dispatch_sync(dispatch_get_main_queue(), ^{
textSize = [self textContentSize:attributedText];
});
} else {
textSize = [self textContentSize:attributedText];
}
if (textSize.height) {
// Return the actual height of the text by removing textview margin from content height
return (textSize.height - (2 * ROOM_MESSAGE_TEXTVIEW_MARGIN));
}
return 0;
}
// Return the content size of a text view initialized with the provided attributed text
// CAUTION: This method runs only on main thread
- (CGSize)textContentSize: (NSAttributedString*)attributedText {
if (attributedText.length) {
// Use a TextView template
UITextView *dummyTextView = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, _maxTextViewWidth, MAXFLOAT)];
dummyTextView.attributedText = attributedText;
return [dummyTextView sizeThatFits:dummyTextView.frame.size];
}
return CGSizeZero;
}
#pragma mark -
+ (NSAttributedString *)messageSeparator {
@synchronized(self) {
if(messageSeparator == nil) {
messageSeparator = [[NSAttributedString alloc] initWithString:@"\r\n\r\n" attributes:@{NSForegroundColorAttributeName : [UIColor blackColor],
NSFontAttributeName: [UIFont systemFontOfSize:4]}];
}
}
return messageSeparator;
}
@end