element-ios/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellDataWithAp...

359 lines
13 KiB
Objective-C

/*
Copyright 2024 New Vector Ltd.
Copyright 2015 OpenMarket Ltd
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
#import "MXKRoomBubbleCellDataWithAppendingMode.h"
#import "GeneratedInterface-Swift.h"
static NSAttributedString *messageSeparator = nil;
@implementation MXKRoomBubbleCellDataWithAppendingMode
#pragma mark - MXKRoomBubbleCellDataStoring
- (instancetype)initWithEvent:(MXEvent *)event andRoomState:(MXRoomState *)roomState andRoomDataSource:(MXKRoomDataSource *)roomDataSource
{
self = [super initWithEvent:event andRoomState:roomState andRoomDataSource:roomDataSource];
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
// 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;
NSString *eventSenderName = [roomDataSource.eventFormatter senderDisplayNameForEvent:event withRoomState:displayRoomState];
NSString *eventSenderAvatar = [roomDataSource.eventFormatter senderAvatarUrlForEvent:event withRoomState:displayRoomState];
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
andLatestRoomState:roomDataSource.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<MXKRoomBubbleCellDataStoring>)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