element-ios/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m

996 lines
34 KiB
Objective-C

/*
Copyright 2018-2024 New Vector Ltd.
Copyright 2017 Vector Creations Ltd
Copyright 2015 OpenMarket Ltd
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
#define MXKROOMBUBBLECELLDATA_MAX_ATTACHMENTVIEW_WIDTH 192
#define MXKROOMBUBBLECELLDATA_DEFAULT_MAX_TEXTVIEW_WIDTH 200
@import MatrixSDK;
#import "MXKRoomBubbleCellData.h"
#import "MXKTools.h"
#import "GeneratedInterface-Swift.h"
@implementation MXKRoomBubbleCellData
@synthesize senderId, targetId, roomId, senderDisplayName, senderAvatarUrl, senderAvatarPlaceholder, targetDisplayName, targetAvatarUrl, targetAvatarPlaceholder, isEncryptedRoom, isPaginationFirstBubble, shouldHideSenderInformation, date, isIncoming, isAttachmentWithThumbnail, isAttachmentWithIcon, attachment;
@synthesize textMessage, attributedTextMessage, attributedTextMessageWithoutPositioningSpace;
@synthesize shouldHideSenderName, isTyping, showBubbleDateTime, showBubbleReceipts, useCustomDateTimeLabel, useCustomReceipts, useCustomUnsentButton, hasNoDisplay;
@synthesize tag;
@synthesize collapsable, collapsed, collapsedAttributedTextMessage, prevCollapsableCellData, nextCollapsableCellData, collapseState;
#pragma mark - MXKRoomBubbleCellDataStoring
- (instancetype)initWithEvent:(MXEvent *)event andRoomState:(MXRoomState *)roomState andRoomDataSource:(MXKRoomDataSource *)roomDataSource
{
self = [self init];
if (self)
{
self->roomDataSource = roomDataSource;
// Initialize read receipts
self.readReceipts = [NSMutableDictionary dictionary];
// Create the bubble component based on matrix event
MXKRoomBubbleComponent *firstComponent = [[MXKRoomBubbleComponent alloc] initWithEvent:event
roomState:roomState
andLatestRoomState:roomDataSource.roomState
eventFormatter:roomDataSource.eventFormatter
session:roomDataSource.mxSession];
if (firstComponent)
{
bubbleComponents = [NSMutableArray array];
[bubbleComponents addObject:firstComponent];
senderId = event.sender;
if ([event.type isEqualToString:kMXEventTypeStringRoomMember])
{
MXRoomMemberEventContent *content = [MXRoomMemberEventContent modelFromJSON:event.content];
if (![content.membership isEqualToString:kMXMembershipStringJoin])
{
targetId = event.stateKey;
}
else
{
targetId = event.sender;
}
}
else
{
targetId = nil;
}
roomId = roomDataSource.roomId;
// If `roomScreenUseOnlyLatestUserAvatarAndName`is enabled, the avatar and name are
// displayed from the latest room state perspective rather than the historical.
MXRoomState *latestRoomState = roomDataSource.roomState;
MXRoomState *displayRoomState = RiotSettings.shared.roomScreenUseOnlyLatestUserAvatarAndName ? latestRoomState : roomState;
[self setRoomState:displayRoomState];
senderAvatarPlaceholder = nil;
targetAvatarPlaceholder = nil;
// Encryption status should always rely on the `MXRoomState`
// from the event rather than the latest.
isEncryptedRoom = roomState.isEncrypted;
isIncoming = ([event.sender isEqualToString:roomDataSource.mxSession.myUser.userId] == NO);
// Check attachment if any
if ([roomDataSource.eventFormatter isSupportedAttachment:event])
{
// Note: event.eventType is equal here to MXEventTypeRoomMessage or MXEventTypeSticker
attachment = [[MXKAttachment alloc] initWithEvent:event andMediaManager:roomDataSource.mxSession.mediaManager];
if (attachment && attachment.type == MXKAttachmentTypeImage)
{
// Check the current thumbnail orientation. Rotate the current content size (if need)
if (attachment.thumbnailOrientation == UIImageOrientationLeft || attachment.thumbnailOrientation == UIImageOrientationRight)
{
_contentSize = CGSizeMake(_contentSize.height, _contentSize.width);
}
}
}
// Report the attributed string (This will initialize _contentSize attribute)
self.attributedTextMessage = firstComponent.attributedTextMessage;
// Initialize rendering attributes
_maxTextViewWidth = MXKROOMBUBBLECELLDATA_DEFAULT_MAX_TEXTVIEW_WIDTH;
}
else
{
// Ignore this event
self = nil;
}
}
return self;
}
- (void)dealloc
{
roomDataSource = nil;
bubbleComponents = nil;
}
- (void)refreshProfilesIfNeeded:(MXRoomState *)latestRoomState
{
if (RiotSettings.shared.roomScreenUseOnlyLatestUserAvatarAndName)
{
[self setRoomState:latestRoomState];
}
}
/**
Sets the `MXRoomState` for a buble cell. This allows to adapt the display
of a cell with a different room state than its historical. This won't update critical
flag/status, such as `isEncryptedRoom`.
@param roomState the `MXRoomState` to use for this cell.
*/
- (void)setRoomState:(MXRoomState *)roomState;
{
MXEvent* firstEvent = self.events.firstObject;
if (firstEvent == nil || roomState == nil)
{
return;
}
senderDisplayName = [roomDataSource.eventFormatter senderDisplayNameForEvent:firstEvent
withRoomState:roomState];
senderAvatarUrl = [roomDataSource.eventFormatter senderAvatarUrlForEvent:firstEvent
withRoomState:roomState];
targetDisplayName = [roomDataSource.eventFormatter targetDisplayNameForEvent:firstEvent
withRoomState:roomState];
targetAvatarUrl = [roomDataSource.eventFormatter targetAvatarUrlForEvent:firstEvent
withRoomState:roomState];
}
- (NSUInteger)updateEvent:(NSString *)eventId withEvent:(MXEvent *)event
{
NSUInteger count = 0;
@synchronized(bubbleComponents)
{
// Retrieve the component storing the event and update it
for (NSUInteger index = 0; index < bubbleComponents.count; index++)
{
MXKRoomBubbleComponent *roomBubbleComponent = [bubbleComponents objectAtIndex:index];
if ([roomBubbleComponent.event.eventId isEqualToString:eventId])
{
[roomBubbleComponent updateWithEvent:event
roomState:roomDataSource.roomState
andLatestRoomState:nil
session:self.mxSession];
if (!roomBubbleComponent.textMessage.length)
{
[bubbleComponents removeObjectAtIndex:index];
}
// Indicate that the text message layout should be recomputed.
[self invalidateTextLayout];
// Handle here attachment update.
// For example: the case of update of attachment event happens when an echo is replaced by its true event
// received back by the events stream.
if (attachment)
{
// Check the current content url, to update it with the actual one
// Retrieve content url/info
NSString *eventContentURL = event.content[@"url"];
if (event.content[@"file"][@"url"])
{
eventContentURL = event.content[@"file"][@"url"];
}
if (!eventContentURL.length)
{
// The attachment has been redacted.
attachment = nil;
_contentSize = CGSizeZero;
}
else if (![attachment.eventId isEqualToString:event.eventId] || ![attachment.contentURL isEqualToString:eventContentURL])
{
MXKAttachment *updatedAttachment = [[MXKAttachment alloc] initWithEvent:event andMediaManager:roomDataSource.mxSession.mediaManager];
// Sanity check on attachment type
if (updatedAttachment && attachment.type == updatedAttachment.type)
{
// Re-use the current image as preview to prevent the cell from flashing
updatedAttachment.previewImage = [attachment getCachedThumbnail];
if (!updatedAttachment.previewImage && attachment.type == MXKAttachmentTypeImage)
{
updatedAttachment.previewImage = [MXMediaManager loadPictureFromFilePath:attachment.cacheFilePath];
}
// Clean the cache by removing the useless data
if (![updatedAttachment.cacheFilePath isEqualToString:attachment.cacheFilePath])
{
[[NSFileManager defaultManager] removeItemAtPath:attachment.cacheFilePath error:nil];
}
if (![updatedAttachment.thumbnailCachePath isEqualToString:attachment.thumbnailCachePath])
{
[[NSFileManager defaultManager] removeItemAtPath:attachment.thumbnailCachePath error:nil];
}
// Update the current attachment description
attachment = updatedAttachment;
if (attachment.type == MXKAttachmentTypeImage)
{
// Reset content size
_contentSize = CGSizeZero;
// Check the current thumbnail orientation. Rotate the current content size (if need)
if (attachment.thumbnailOrientation == UIImageOrientationLeft || attachment.thumbnailOrientation == UIImageOrientationRight)
{
_contentSize = CGSizeMake(_contentSize.height, _contentSize.width);
}
}
}
else
{
MXLogDebug(@"[MXKRoomBubbleCellData] updateEvent: Warning: Does not support change of attachment type");
}
}
}
else if ([roomDataSource.eventFormatter isSupportedAttachment:event])
{
// The event is updated to an event with attachement
attachment = [[MXKAttachment alloc] initWithEvent:event andMediaManager:roomDataSource.mxSession.mediaManager];
if (attachment && attachment.type == MXKAttachmentTypeImage)
{
// Check the current thumbnail orientation. Rotate the current content size (if need)
if (attachment.thumbnailOrientation == UIImageOrientationLeft || attachment.thumbnailOrientation == UIImageOrientationRight)
{
_contentSize = CGSizeMake(_contentSize.height, _contentSize.width);
}
}
}
break;
}
}
count = bubbleComponents.count;
}
return count;
}
- (NSUInteger)removeEvent:(NSString *)eventId
{
NSUInteger count = 0;
@synchronized(bubbleComponents)
{
for (MXKRoomBubbleComponent *roomBubbleComponent in bubbleComponents)
{
if ([roomBubbleComponent.event.eventId isEqualToString:eventId])
{
[bubbleComponents removeObject:roomBubbleComponent];
// Indicate that the text message layout should be recomputed.
[self invalidateTextLayout];
break;
}
}
count = bubbleComponents.count;
}
return count;
}
- (NSUInteger)removeEventsFromEvent:(NSString*)eventId removedEvents:(NSArray<MXEvent*>**)removedEvents;
{
NSMutableArray *cuttedEvents = [NSMutableArray array];
@synchronized(bubbleComponents)
{
NSInteger componentIndex = [self bubbleComponentIndexForEventId:eventId];
if (NSNotFound != componentIndex)
{
NSArray *newBubbleComponents = [bubbleComponents subarrayWithRange:NSMakeRange(0, componentIndex)];
for (NSUInteger i = componentIndex; i < bubbleComponents.count; i++)
{
MXKRoomBubbleComponent *roomBubbleComponent = bubbleComponents[i];
[cuttedEvents addObject:roomBubbleComponent.event];
}
bubbleComponents = [NSMutableArray arrayWithArray:newBubbleComponents];
// Indicate that the text message layout should be recomputed.
[self invalidateTextLayout];
}
}
*removedEvents = cuttedEvents;
return bubbleComponents.count;
}
- (BOOL)hasSameSenderAsBubbleCellData:(id<MXKRoomBubbleCellDataStoring>)bubbleCellData
{
// Sanity check: accept only object of MXKRoomBubbleCellData classes or sub-classes
NSParameterAssert([bubbleCellData isKindOfClass:[MXKRoomBubbleCellData class]]);
// NOTE: Same sender means here same id, same display name and same avatar
// Check first user id
if ([senderId isEqualToString:bubbleCellData.senderId] == NO)
{
return NO;
}
// Check sender name
if ((senderDisplayName.length || bubbleCellData.senderDisplayName.length) && ([senderDisplayName isEqualToString:bubbleCellData.senderDisplayName] == NO))
{
return NO;
}
// Check avatar url
if ((senderAvatarUrl.length || bubbleCellData.senderAvatarUrl.length) && ([senderAvatarUrl isEqualToString:bubbleCellData.senderAvatarUrl] == NO))
{
return NO;
}
return YES;
}
- (MXKRoomBubbleComponent*) getFirstBubbleComponent
{
MXKRoomBubbleComponent* first = nil;
@synchronized(bubbleComponents)
{
if (bubbleComponents.count)
{
first = [bubbleComponents firstObject];
}
}
return first;
}
- (MXKRoomBubbleComponent*)getFirstBubbleComponentWithDisplay
{
// Look for the first component which is actually displayed (some event are ignored in room history display).
MXKRoomBubbleComponent* first = nil;
@synchronized(bubbleComponents)
{
for (MXKRoomBubbleComponent *component in bubbleComponents)
{
if (component.attributedTextMessage)
{
first = component;
break;
}
}
}
return first;
}
- (MXKRoomBubbleComponent*)getLastBubbleComponentWithDisplay
{
// Look for the first component which is actually displayed (some event are ignored in room history display).
MXKRoomBubbleComponent* lastVisibleComponent = nil;
@synchronized(bubbleComponents)
{
for (MXKRoomBubbleComponent *component in bubbleComponents.reverseObjectEnumerator)
{
if (component.attributedTextMessage)
{
lastVisibleComponent = component;
break;
}
}
}
return lastVisibleComponent;
}
- (NSAttributedString*)attributedTextMessageWithHighlightedEvent:(NSString*)eventId tintColor:(UIColor*)tintColor
{
NSAttributedString *customAttributedTextMsg;
// By default only one component is supported, consider here the first component
MXKRoomBubbleComponent *firstComponent = [self getFirstBubbleComponent];
if (firstComponent)
{
customAttributedTextMsg = firstComponent.attributedTextMessage;
// Sanity check
if (customAttributedTextMsg && [firstComponent.event.eventId isEqualToString:eventId])
{
NSMutableAttributedString *customComponentString = [[NSMutableAttributedString alloc] initWithAttributedString:customAttributedTextMsg];
UIColor *color = tintColor ? tintColor : [UIColor lightGrayColor];
[customComponentString addAttribute:NSBackgroundColorAttributeName value:color range:NSMakeRange(0, customComponentString.length)];
customAttributedTextMsg = customComponentString;
}
}
return customAttributedTextMsg;
}
- (void)highlightPatternInTextMessage:(NSString*)pattern
withBackgroundColor:(UIColor *)backgroundColor
foregroundColor:(UIColor*)foregroundColor
andFont:(UIFont*)patternFont
{
highlightedPattern = pattern;
highlightedPatternBackgroundColor = backgroundColor;
highlightedPatternForegroundColor = foregroundColor;
highlightedPatternFont = patternFont;
// Indicate that the text message layout should be recomputed.
[self invalidateTextLayout];
}
- (void)setShouldHideSenderInformation:(BOOL)inShouldHideSenderInformation
{
shouldHideSenderInformation = inShouldHideSenderInformation;
}
- (BOOL)hasThreadRoot
{
@synchronized (bubbleComponents)
{
for (MXKRoomBubbleComponent *component in bubbleComponents)
{
if (component.thread)
{
return YES;
}
}
}
return NO;
}
#pragma mark -
- (void)invalidateTextLayout
{
self.attributedTextMessage = nil;
}
- (void)prepareBubbleComponentsPosition
{
// Consider here only the first component if any
MXKRoomBubbleComponent *firstComponent = [self getFirstBubbleComponent];
if (firstComponent)
{
CGFloat positionY = (attachment == nil || attachment.type == MXKAttachmentTypeFile || attachment.type == MXKAttachmentTypeAudio || attachment.type == MXKAttachmentTypeVoiceMessage) ? MXKROOMBUBBLECELLDATA_TEXTVIEW_DEFAULT_VERTICAL_INSET : 0;
firstComponent.position = CGPointMake(0, positionY);
}
}
- (NSInteger)bubbleComponentIndexForEventId:(NSString *)eventId
{
return [self.bubbleComponents indexOfObjectPassingTest:^BOOL(MXKRoomBubbleComponent * _Nonnull bubbleComponent, NSUInteger idx, BOOL * _Nonnull stop) {
if ([bubbleComponent.event.eventId isEqualToString:eventId])
{
*stop = YES;
return YES;
}
return NO;
}];
}
#pragma mark - Text measuring
// Return the raw height of the provided text by removing any margin
- (CGFloat)rawTextHeight: (NSAttributedString*)attributedText
{
return [self rawTextHeight:attributedText withMaxWidth:_maxTextViewWidth];
}
// Return the raw height of the provided text by removing any vertical margin/inset and constraining the width.
- (CGFloat)rawTextHeight: (NSAttributedString*)attributedText withMaxWidth:(CGFloat)maxTextViewWidth
{
__block CGSize textSize;
if ([NSThread currentThread] != [NSThread mainThread])
{
dispatch_sync(dispatch_get_main_queue(), ^{
textSize = [self textContentSize:attributedText removeVerticalInset:YES maxTextViewWidth:maxTextViewWidth];
});
}
else
{
textSize = [self textContentSize:attributedText removeVerticalInset:YES maxTextViewWidth:maxTextViewWidth];
}
return textSize.height;
}
- (CGSize)textContentSize:(NSAttributedString*)attributedText removeVerticalInset:(BOOL)removeVerticalInset
{
return [self textContentSize:attributedText removeVerticalInset:removeVerticalInset maxTextViewWidth:_maxTextViewWidth];
}
- (CGSize)textContentSize:(NSAttributedString*)attributedText removeVerticalInset:(BOOL)removeVerticalInset maxTextViewWidth:(CGFloat)maxTextViewWidth
{
static UITextView* measurementTextView = nil;
static UITextView* measurementTextViewWithoutInset = nil;
if (attributedText.length)
{
if (!measurementTextView)
{
measurementTextView = [[UITextView alloc] init];
measurementTextViewWithoutInset = [[UITextView alloc] init];
// Remove the container inset: this operation impacts only the vertical margin.
// Note: consider textContainer.lineFragmentPadding to remove horizontal margin
measurementTextViewWithoutInset.textContainerInset = UIEdgeInsetsZero;
}
// Select the right text view for measurement
UITextView *selectedTextView = (removeVerticalInset ? measurementTextViewWithoutInset : measurementTextView);
selectedTextView.frame = CGRectMake(0, 0, maxTextViewWidth, 0);
selectedTextView.attributedText = attributedText;
// Force the layout manager to layout the text, fixes problems starting iOS 16
[selectedTextView.layoutManager ensureLayoutForTextContainer:selectedTextView.textContainer];
CGSize size = [selectedTextView sizeThatFits:selectedTextView.frame.size];
// Manage the case where a string attribute has a single paragraph with a left indent
// In this case, [UITextView sizeThatFits] ignores the indent and return the width
// of the text only.
// So, add this indent afterwards
NSRange textRange = NSMakeRange(0, attributedText.length);
NSRange longestEffectiveRange;
NSParagraphStyle *paragraphStyle = [attributedText attribute:NSParagraphStyleAttributeName atIndex:0 longestEffectiveRange:&longestEffectiveRange inRange:textRange];
if (NSEqualRanges(textRange, longestEffectiveRange))
{
size.width = size.width + paragraphStyle.headIndent;
}
return size;
}
return CGSizeZero;
}
#pragma mark - Properties
- (MXSession*)mxSession
{
return roomDataSource.mxSession;
}
- (NSArray*)bubbleComponents
{
NSArray* copy;
@synchronized(bubbleComponents)
{
copy = [bubbleComponents copy];
}
return copy;
}
- (NSString*)textMessage
{
return self.attributedTextMessage.string;
}
- (void)setAttributedTextMessage:(NSAttributedString *)inAttributedTextMessage
{
attributedTextMessage = inAttributedTextMessage;
if (attributedTextMessage.length && highlightedPattern)
{
[self highlightPattern];
}
// Reset content size
_contentSize = CGSizeZero;
}
- (NSAttributedString*)attributedTextMessage
{
if (self.hasAttributedTextMessage && !attributedTextMessage.length)
{
// By default only one component is supported, consider here the first component
MXKRoomBubbleComponent *firstComponent = [self getFirstBubbleComponent];
if (firstComponent)
{
attributedTextMessage = firstComponent.attributedTextMessage;
if (attributedTextMessage.length && highlightedPattern)
{
[self highlightPattern];
}
}
}
return attributedTextMessage;
}
- (BOOL)hasAttributedTextMessage
{
// Determine if the event formatter will return at least one string for the events in this cell.
// No string means that the event formatter has been configured so that it did not accept all events
// of the cell.
BOOL hasAttributedTextMessage = NO;
@synchronized(bubbleComponents)
{
for (MXKRoomBubbleComponent *roomBubbleComponent in bubbleComponents)
{
if (roomBubbleComponent.attributedTextMessage)
{
hasAttributedTextMessage = YES;
break;
}
}
}
return hasAttributedTextMessage;
}
- (BOOL)hasLink
{
@synchronized (bubbleComponents) {
for (MXKRoomBubbleComponent *component in bubbleComponents)
{
if (component.link)
{
return YES;
}
}
}
return NO;
}
- (MXKRoomBubbleComponentDisplayFix)displayFix
{
MXKRoomBubbleComponentDisplayFix displayFix = MXKRoomBubbleComponentDisplayFixNone;
@synchronized(bubbleComponents)
{
for (MXKRoomBubbleComponent *component in self.bubbleComponents)
{
displayFix |= component.displayFix;
}
}
return displayFix;
}
- (BOOL)shouldHideSenderName
{
BOOL res = NO;
MXKRoomBubbleComponent *firstDisplayedComponent = [self getFirstBubbleComponentWithDisplay];
NSString *senderDisplayName = self.senderDisplayName;
if (firstDisplayedComponent)
{
res = (firstDisplayedComponent.event.isEmote || (firstDisplayedComponent.event.isState && senderDisplayName && [firstDisplayedComponent.textMessage hasPrefix:senderDisplayName]));
}
return res;
}
- (BOOL)canInvitePeople
{
NSInteger requiredLevel = roomDataSource.roomState.powerLevels.invite;
NSInteger myLevel = [roomDataSource.roomState.powerLevels powerLevelOfUserWithUserID:roomDataSource.mxSession.myUserId];
return myLevel >= requiredLevel;
}
- (NSArray*)events
{
NSMutableArray* eventsArray;
@synchronized(bubbleComponents)
{
eventsArray = [NSMutableArray arrayWithCapacity:bubbleComponents.count];
for (MXKRoomBubbleComponent *roomBubbleComponent in bubbleComponents)
{
if (roomBubbleComponent.event)
{
[eventsArray addObject:roomBubbleComponent.event];
}
}
}
return eventsArray;
}
- (NSDate*)date
{
MXKRoomBubbleComponent *firstDisplayedComponent = [self getFirstBubbleComponentWithDisplay];
if (firstDisplayedComponent)
{
return firstDisplayedComponent.date;
}
return nil;
}
- (BOOL)hasNoDisplay
{
BOOL noDisplay = YES;
// Check whether at least one component has a string description.
@synchronized(bubbleComponents)
{
if (self.collapsed)
{
// Collapsed cells have no display except their cell header
noDisplay = !self.collapsedAttributedTextMessage;
}
else
{
for (MXKRoomBubbleComponent *roomBubbleComponent in bubbleComponents)
{
if (roomBubbleComponent.attributedTextMessage)
{
noDisplay = NO;
break;
}
}
}
}
return (noDisplay && !attachment);
}
- (BOOL)isAttachmentWithThumbnail
{
return (attachment && (attachment.type == MXKAttachmentTypeImage || attachment.type == MXKAttachmentTypeVideo || attachment.type == MXKAttachmentTypeSticker));
}
- (BOOL)isAttachmentWithIcon
{
// Not supported yet (TODO for audio, file).
return NO;
}
- (BOOL)isAttachment
{
if (!self.attachment)
{
return NO;
}
if (!attachment.contentURL || !attachment.contentInfo) {
return NO;
}
switch (self.attachment.type) {
case MXKAttachmentTypeFile:
case MXKAttachmentTypeAudio:
case MXKAttachmentTypeVoiceMessage:
return YES;
default:
return NO;
}
}
- (void)setMaxTextViewWidth:(CGFloat)inMaxTextViewWidth
{
// Check change
if (inMaxTextViewWidth != _maxTextViewWidth)
{
_maxTextViewWidth = inMaxTextViewWidth;
// Reset content size
_contentSize = CGSizeZero;
}
}
- (CGSize)contentSize
{
if (CGSizeEqualToSize(_contentSize, CGSizeZero))
{
if (attachment == nil)
{
// Here the bubble is a text message
if ([NSThread currentThread] != [NSThread mainThread])
{
dispatch_sync(dispatch_get_main_queue(), ^{
self->_contentSize = [self textContentSize:self.attributedTextMessage removeVerticalInset:NO];
});
}
else
{
_contentSize = [self textContentSize:self.attributedTextMessage removeVerticalInset:NO];
}
}
else if (self.isAttachmentWithThumbnail)
{
CGFloat width, height;
// Set default content size
width = height = MXKROOMBUBBLECELLDATA_MAX_ATTACHMENTVIEW_WIDTH;
if (attachment.thumbnailInfo || attachment.contentInfo)
{
if (attachment.thumbnailInfo && attachment.thumbnailInfo[@"w"] && attachment.thumbnailInfo[@"h"])
{
width = [attachment.thumbnailInfo[@"w"] integerValue];
height = [attachment.thumbnailInfo[@"h"] integerValue];
}
else if (attachment.contentInfo[@"w"] && attachment.contentInfo[@"h"])
{
width = [attachment.contentInfo[@"w"] integerValue];
height = [attachment.contentInfo[@"h"] integerValue];
}
if (width > MXKROOMBUBBLECELLDATA_MAX_ATTACHMENTVIEW_WIDTH || height > MXKROOMBUBBLECELLDATA_MAX_ATTACHMENTVIEW_WIDTH)
{
if (width > height)
{
height = (height * MXKROOMBUBBLECELLDATA_MAX_ATTACHMENTVIEW_WIDTH) / width;
height = floorf(height / 2) * 2;
width = MXKROOMBUBBLECELLDATA_MAX_ATTACHMENTVIEW_WIDTH;
}
else
{
width = (width * MXKROOMBUBBLECELLDATA_MAX_ATTACHMENTVIEW_WIDTH) / height;
width = floorf(width / 2) * 2;
height = MXKROOMBUBBLECELLDATA_MAX_ATTACHMENTVIEW_WIDTH;
}
}
}
// Check here thumbnail orientation
if (attachment.thumbnailOrientation == UIImageOrientationLeft || attachment.thumbnailOrientation == UIImageOrientationRight)
{
_contentSize = CGSizeMake(height, width);
}
else
{
_contentSize = CGSizeMake(width, height);
}
}
else if (attachment.type == MXKAttachmentTypeFile || attachment.type == MXKAttachmentTypeAudio)
{
// Presently we displayed only the file name for attached file (no icon yet)
// Return suitable content size of a text view to display the file name (available in text message).
if ([NSThread currentThread] != [NSThread mainThread])
{
dispatch_sync(dispatch_get_main_queue(), ^{
self->_contentSize = [self textContentSize:self.attributedTextMessage removeVerticalInset:NO];
});
}
else
{
_contentSize = [self textContentSize:self.attributedTextMessage removeVerticalInset:NO];
}
}
else
{
_contentSize = CGSizeMake(40, 40);
}
}
return _contentSize;
}
- (MXKEventFormatter *)eventFormatter
{
MXKRoomBubbleComponent *firstComponent = [bubbleComponents firstObject];
// Retrieve event formatter from the first component
if (firstComponent)
{
return firstComponent.eventFormatter;
}
return nil;
}
- (BOOL)showAntivirusScanStatus
{
MXKRoomBubbleComponent *firstBubbleComponent = self.bubbleComponents.firstObject;
if (self.attachment == nil || firstBubbleComponent == nil)
{
return NO;
}
MXEventScan *eventScan = firstBubbleComponent.eventScan;
return eventScan != nil && eventScan.antivirusScanStatus != MXAntivirusScanStatusTrusted;
}
- (BOOL)containsBubbleComponentWithEncryptionBadge
{
BOOL containsBubbleComponentWithEncryptionBadge = NO;
@synchronized(bubbleComponents)
{
for (MXKRoomBubbleComponent *component in bubbleComponents)
{
if (component.encryptionDecoration != EventEncryptionDecorationNone)
{
containsBubbleComponentWithEncryptionBadge = YES;
break;
}
}
}
return containsBubbleComponentWithEncryptionBadge;
}
#pragma mark - Bubble collapsing
- (BOOL)collapseWith:(id<MXKRoomBubbleCellDataStoring>)cellData
{
// NO by default
return NO;
}
#pragma mark - Internals
- (void)highlightPattern
{
NSMutableAttributedString *customAttributedTextMsg = nil;
NSString *currentTextMessage = self.textMessage;
NSRange range = [currentTextMessage rangeOfString:highlightedPattern options:NSCaseInsensitiveSearch];
if (range.location != NSNotFound)
{
customAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedTextMessage];
while (range.location != NSNotFound)
{
if (highlightedPatternBackgroundColor)
{
// Update background color
[customAttributedTextMsg addAttribute:NSBackgroundColorAttributeName value:highlightedPatternBackgroundColor range:range];
}
if (highlightedPatternForegroundColor)
{
// Update text color
[customAttributedTextMsg addAttribute:NSForegroundColorAttributeName value:highlightedPatternForegroundColor range:range];
}
if (highlightedPatternFont)
{
// Update text font
[customAttributedTextMsg addAttribute:NSFontAttributeName value:highlightedPatternFont range:range];
}
// Look for the next pattern occurrence
range.location += range.length;
if (range.location < currentTextMessage.length)
{
range.length = currentTextMessage.length - range.location;
range = [currentTextMessage rangeOfString:highlightedPattern options:NSCaseInsensitiveSearch range:range];
}
else
{
range.location = NSNotFound;
}
}
}
if (customAttributedTextMsg)
{
// Update resulting message body
attributedTextMessage = customAttributedTextMsg;
}
}
@end