element-ios/Riot/Categories/MXKRoomBubbleTableViewCell+...

864 lines
39 KiB
Objective-C

/*
Copyright 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.
*/
#import "MXKRoomBubbleTableViewCell+Riot.h"
#import <objc/runtime.h>
#import "RoomBubbleCellData.h"
#import "ThemeService.h"
#import "GeneratedInterface-Swift.h"
#define VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_X 48
#define VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_WIDTH 4
NSString *const kMXKRoomBubbleCellRiotEditButtonPressed = @"kMXKRoomBubbleCellRiotEditButtonPressed";
NSString *const kMXKRoomBubbleCellTapOnReceiptsContainer = @"kMXKRoomBubbleCellTapOnReceiptsContainer";
NSString *const kMXKRoomBubbleCellTapOnAddReaction = @"kMXKRoomBubbleCellTapOnAddReaction";
NSString *const kMXKRoomBubbleCellLongPressOnReactionView = @"kMXKRoomBubbleCellLongPressOnReactionView";
NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestAcceptPressed = @"kMXKRoomBubbleCellKeyVerificationAcceptPressed";
NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed = @"kMXKRoomBubbleCellKeyVerificationDeclinePressed";
@implementation MXKRoomBubbleTableViewCell (Riot)
- (void)addTimestampLabelForComponent:(NSUInteger)componentIndex
{
BOOL isFirstDisplayedComponent = (componentIndex == 0);
BOOL isLastMessageMostRecentComponent = NO;
RoomBubbleCellData *roomBubbleCellData;
if ([bubbleData isKindOfClass:RoomBubbleCellData.class])
{
roomBubbleCellData = (RoomBubbleCellData*)bubbleData;
isFirstDisplayedComponent = (componentIndex == roomBubbleCellData.oldestComponentIndex);
isLastMessageMostRecentComponent = roomBubbleCellData.containsLastMessage && (componentIndex == roomBubbleCellData.mostRecentComponentIndex);
}
// Display timestamp on the left for selected component when it cannot overlap other UI elements like user's avatar
BOOL displayLabelOnLeft = roomBubbleCellData.displayTimestampForSelectedComponentOnLeftWhenPossible
&& !isLastMessageMostRecentComponent
&& (!isFirstDisplayedComponent || roomBubbleCellData.shouldHideSenderInformation);
[self addTimestampLabelForComponent:componentIndex displayOnLeft:displayLabelOnLeft];
}
- (void)addTimestampLabelForComponent:(NSUInteger)componentIndex
displayOnLeft:(BOOL)displayLabelOnLeft
{
MXKRoomBubbleComponent *component;
NSArray *bubbleComponents = bubbleData.bubbleComponents;
if (componentIndex < bubbleComponents.count)
{
component = bubbleComponents[componentIndex];
}
if (component && component.date)
{
BOOL isFirstDisplayedComponent = (componentIndex == 0);
RoomBubbleCellData *roomBubbleCellData;
if ([bubbleData isKindOfClass:RoomBubbleCellData.class])
{
roomBubbleCellData = (RoomBubbleCellData*)bubbleData;
isFirstDisplayedComponent = (componentIndex == roomBubbleCellData.oldestComponentIndex);
}
[self addTimestampLabelForComponentIndex:componentIndex
isFirstDisplayedComponent:isFirstDisplayedComponent
viewTag:componentIndex
displayOnLeft:displayLabelOnLeft];
}
}
- (void)addTimestampLabelForComponentIndex:(NSInteger)componentIndex
isFirstDisplayedComponent:(BOOL)isFirstDisplayedComponent
viewTag:(NSInteger)viewTag
displayOnLeft:(BOOL)displayOnLeft
{
if (!self.bubbleInfoContainer)
{
MXLogDebug(@"[MXKRoomBubbleTableViewCell+Riot] bubbleInfoContainer property is missing for cell class: %@", NSStringFromClass(self.class));
return;
}
NSArray *bubbleComponents = bubbleData.bubbleComponents;
MXKRoomBubbleComponent *component = bubbleComponents[componentIndex];
self.bubbleInfoContainer.hidden = NO;
CGFloat timeLabelPosX;
CGFloat timeLabelPosY;
CGFloat timeLabelHeight = PlainRoomCellLayoutConstants.timestampLabelHeight;
CGFloat timeLabelWidth;
NSTextAlignment timeLabelTextAlignment;
CGRect componentFrame = [self componentFrameInContentViewForIndex:componentIndex];
if (displayOnLeft)
{
CGFloat leftMargin = 10.0;
CGFloat rightMargin = (self.contentView.frame.size.width - (self.bubbleInfoContainer.frame.origin.x + self.bubbleInfoContainer.frame.size.width));
timeLabelPosX = 0;
if (CGRectEqualToRect(componentFrame, CGRectNull) == false)
{
timeLabelPosY = componentFrame.origin.y - self.bubbleInfoContainerTopConstraint.constant;
}
else
{
timeLabelPosY = component.position.y + self.msgTextViewTopConstraint.constant - self.bubbleInfoContainerTopConstraint.constant;
}
timeLabelWidth = self.contentView.frame.size.width - leftMargin - rightMargin;
timeLabelTextAlignment = NSTextAlignmentLeft;
}
else
{
timeLabelPosX = self.bubbleInfoContainer.frame.size.width - PlainRoomCellLayoutConstants.timestampLabelWidth;
if (isFirstDisplayedComponent)
{
timeLabelPosY = 0;
}
else if (CGRectEqualToRect(componentFrame, CGRectNull) == false)
{
timeLabelPosY = componentFrame.origin.y - self.bubbleInfoContainerTopConstraint.constant - timeLabelHeight;
}
else
{
timeLabelPosY = component.position.y + self.msgTextViewTopConstraint.constant - timeLabelHeight - self.bubbleInfoContainerTopConstraint.constant;
}
timeLabelWidth = PlainRoomCellLayoutConstants.timestampLabelWidth;
timeLabelTextAlignment = NSTextAlignmentRight;
}
timeLabelPosY = MAX(0.0, timeLabelPosY);
UILabel *timeLabel = [[UILabel alloc] initWithFrame:CGRectMake(timeLabelPosX, timeLabelPosY, timeLabelWidth, timeLabelHeight)];
timeLabel.text = [bubbleData.eventFormatter timeStringFromDate:component.date];
timeLabel.textAlignment = timeLabelTextAlignment;
timeLabel.textColor = ThemeService.shared.theme.textSecondaryColor;
timeLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightLight];
timeLabel.adjustsFontSizeToFitWidth = YES;
timeLabel.tag = viewTag;
[timeLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
timeLabel.accessibilityIdentifier = @"timestampLabel";
[self.bubbleInfoContainer addSubview:timeLabel];
// Define timeLabel constraints (to handle auto-layout in case of screen rotation)
NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:timeLabel
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:self.bubbleInfoContainer
attribute:NSLayoutAttributeTrailing
multiplier:1.0
constant:0];
NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:timeLabel
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.bubbleInfoContainer
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:timeLabelPosY];
NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:timeLabel
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:timeLabelWidth];
NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:timeLabel
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:timeLabelHeight];
// Available on iOS 8 and later
[NSLayoutConstraint activateConstraints:@[rightConstraint, topConstraint, widthConstraint, heightConstraint]];
}
- (void)selectComponent:(NSUInteger)componentIndex
{
[self selectComponent:componentIndex showEditButton:NO showTimestamp:YES];
}
- (void)selectComponent:(NSUInteger)componentIndex showEditButton:(BOOL)showEditButton showTimestamp:(BOOL)showTimestamp
{
if (componentIndex < bubbleData.bubbleComponents.count)
{
if (showTimestamp)
{
// Add time label
[self addTimestampLabelForComponent:componentIndex];
}
// Blur timestamp labels which are not related to the selected component (if any)
for (UIView* view in self.bubbleInfoContainer.subviews)
{
// Note dateTime label tag is equal to the index of the related component.
if (view.tag != componentIndex)
{
view.alpha = 0.2;
}
}
// Retrieve the read receipts container related to the selected component (if any)
// Blur the others
for (UIView* view in self.tmpSubviews)
{
// Note read receipt container tag is equal to the index of the related component.
if (view.tag != componentIndex)
{
view.alpha = 0.2;
}
}
if (showEditButton)
{
// Add the edit button
[self addEditButtonForComponent:componentIndex completion:nil];
}
}
}
- (void)markComponent:(NSUInteger)componentIndex
{
NSArray *bubbleComponents = bubbleData.bubbleComponents;
if (componentIndex < bubbleComponents.count)
{
CGRect componentFrame = [self componentFrameInContentViewForIndex:componentIndex];
if (CGRectIsEmpty(componentFrame))
{
return;
}
CGRect markerFrame = CGRectMake(VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_X,
CGRectGetMinY(componentFrame),
VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_WIDTH,
CGRectGetHeight(componentFrame));
UIView *markerView = [[UIView alloc] initWithFrame:markerFrame];
markerView.backgroundColor = ThemeService.shared.theme.tintColor;
[markerView setTranslatesAutoresizingMaskIntoConstraints:NO];
markerView.accessibilityIdentifier = @"markerView";
[self.contentView addSubview:markerView];
// Define the marker constraints
NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:markerView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.contentView
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:CGRectGetMinX(markerFrame)];
NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:markerView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.contentView
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:CGRectGetMinY(markerFrame)];
NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:markerView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:CGRectGetWidth(markerFrame)];
NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:markerView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:CGRectGetHeight(markerFrame)];
// Available on iOS 8 and later
[NSLayoutConstraint activateConstraints:@[leftConstraint, topConstraint, widthConstraint, heightConstraint]];
// Store the created button
self.markerView = markerView;
}
}
- (void)addDateLabel
{
self.bubbleInfoContainer.hidden = NO;
NSDate *date = bubbleData.date;
if (date)
{
UILabel *timeLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.bubbleInfoContainer.frame.size.width, PlainRoomCellLayoutConstants.timestampLabelHeight)];
timeLabel.text = [bubbleData.eventFormatter dateStringFromDate:date withTime:NO];
timeLabel.textAlignment = NSTextAlignmentRight;
timeLabel.textColor = ThemeService.shared.theme.textSecondaryColor;
if ([UIFont respondsToSelector:@selector(systemFontOfSize:weight:)])
{
timeLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightLight];
}
else
{
timeLabel.font = [UIFont systemFontOfSize:12];
}
timeLabel.adjustsFontSizeToFitWidth = YES;
[timeLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
timeLabel.accessibilityIdentifier = @"dateLabel";
[self.bubbleInfoContainer addSubview:timeLabel];
// Define timeLabel constraints (to handle auto-layout in case of screen rotation)
NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:timeLabel
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:self.bubbleInfoContainer
attribute:NSLayoutAttributeTrailing
multiplier:1.0
constant:0];
NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:timeLabel
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.bubbleInfoContainer
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0];
NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:timeLabel
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self.bubbleInfoContainer
attribute:NSLayoutAttributeWidth
multiplier:1.0
constant:0];
NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:timeLabel
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:PlainRoomCellLayoutConstants.timestampLabelHeight];
// Available on iOS 8 and later
[NSLayoutConstraint activateConstraints:@[rightConstraint, topConstraint, widthConstraint, heightConstraint]];
}
}
- (void)setBlurred:(BOOL)blurred
{
objc_setAssociatedObject(self, @selector(blurred), @(blurred), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (blurred)
{
self.bubbleOverlayContainer.hidden = NO;
self.bubbleOverlayContainer.backgroundColor = ThemeService.shared.theme.backgroundColor;
self.bubbleOverlayContainer.alpha = 0.8;
self.bubbleOverlayContainer.userInteractionEnabled = YES;
// Blur subviews if any
for (UIView* view in self.bubbleOverlayContainer.subviews)
{
view.alpha = 0.2;
}
// Move this view in front
[self.bubbleOverlayContainer.superview bringSubviewToFront:self.bubbleOverlayContainer];
}
else
{
if (self.bubbleOverlayContainer.subviews.count)
{
// Keep this overlay visible, adjust background color
self.bubbleOverlayContainer.backgroundColor = [UIColor clearColor];
self.bubbleOverlayContainer.alpha = 1;
self.bubbleOverlayContainer.userInteractionEnabled = NO;
// Restore subviews display
for (UIView* view in self.bubbleOverlayContainer.subviews)
{
view.alpha = 1;
}
}
else
{
self.bubbleOverlayContainer.hidden = YES;
}
}
}
- (BOOL)blurred
{
NSNumber *associatedBlurred = objc_getAssociatedObject(self, @selector(blurred));
if (associatedBlurred)
{
return [associatedBlurred boolValue];
}
return NO;
}
- (void)setEditButton:(UIButton *)editButton
{
objc_setAssociatedObject(self, @selector(editButton), editButton, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIButton*)editButton
{
return objc_getAssociatedObject(self, @selector(editButton));
}
- (void)setMarkerView:(UIView *)markerView
{
objc_setAssociatedObject(self, @selector(markerView), markerView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(UIView *)markerView
{
return objc_getAssociatedObject(self, @selector(markerView));
}
- (void)setMessageStatusViews:(NSArray *)arrayOfViews
{
objc_setAssociatedObject(self, @selector(messageStatusViews), arrayOfViews, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSArray *)messageStatusViews
{
return objc_getAssociatedObject(self, @selector(messageStatusViews));
}
- (void)updateUserNameColor
{
static UserNameColorGenerator *userNameColorGenerator;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
userNameColorGenerator = [UserNameColorGenerator new];
});
id<Theme> theme = ThemeService.shared.theme;
userNameColorGenerator.defaultColor = theme.textPrimaryColor;
userNameColorGenerator.userNameColors = theme.userNameColors;
NSString *senderId = self.bubbleData.senderId;
if (senderId)
{
self.userNameLabel.textColor = [userNameColorGenerator colorFrom:senderId];
}
else
{
self.userNameLabel.textColor = userNameColorGenerator.defaultColor;
}
}
- (CGRect)componentFrameInTableViewForIndex:(NSInteger)componentIndex
{
CGRect componentFrameInContentView = [self componentFrameInContentViewForIndex:componentIndex];
return [self.contentView convertRect:componentFrameInContentView toView:self.superview];
}
- (CGRect)surroundingFrameInTableViewForComponentIndex:(NSInteger)componentIndex
{
CGRect surroundingFrame;
CGRect componentFrameInContentView = [self componentFrameInContentViewForIndex:componentIndex];
MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = self;
MXKRoomBubbleCellData *bubbleCellData = roomBubbleTableViewCell.bubbleData;
NSInteger firstVisibleComponentIndex = NSNotFound;
NSInteger lastMostRecentComponentIndex = NSNotFound;
if ([bubbleCellData isKindOfClass:[RoomBubbleCellData class]])
{
RoomBubbleCellData *roomBubbleCellData = (RoomBubbleCellData*)bubbleCellData;
firstVisibleComponentIndex = [roomBubbleCellData firstVisibleComponentIndex];
if (roomBubbleCellData.containsLastMessage
&& roomBubbleCellData.mostRecentComponentIndex != NSNotFound
&& roomBubbleCellData.firstVisibleComponentIndex != roomBubbleCellData.mostRecentComponentIndex
&& componentIndex == roomBubbleCellData.mostRecentComponentIndex)
{
lastMostRecentComponentIndex = roomBubbleCellData.mostRecentComponentIndex;
}
}
// Do not overlap timestamp for last message
if (lastMostRecentComponentIndex != NSNotFound)
{
CGFloat componentBottomY = componentFrameInContentView.origin.y + componentFrameInContentView.size.height;
CGFloat x = 0;
CGFloat y = componentFrameInContentView.origin.y - PlainRoomCellLayoutConstants.timestampLabelHeight;
CGFloat width = roomBubbleTableViewCell.contentView.frame.size.width;
CGFloat height = componentBottomY - y;
surroundingFrame = CGRectMake(x, y, width, height);
} // Do not overlap user name label for first visible component
else if (!CGRectEqualToRect(componentFrameInContentView, CGRectNull)
&& firstVisibleComponentIndex != NSNotFound
&& componentIndex <= firstVisibleComponentIndex
&& roomBubbleTableViewCell.userNameLabel
&& roomBubbleTableViewCell.userNameLabel.isHidden == NO)
{
CGFloat componentBottomY = componentFrameInContentView.origin.y + componentFrameInContentView.size.height;
CGFloat x = 0;
CGFloat y = roomBubbleTableViewCell.userNameLabel.frame.origin.y;
CGFloat width = roomBubbleTableViewCell.contentView.frame.size.width;
CGFloat height = componentBottomY - y;
surroundingFrame = CGRectMake(x, y, width, height);
}
else
{
surroundingFrame = componentFrameInContentView;
}
return [self.contentView convertRect:surroundingFrame toView:self.superview];
}
- (CGRect)componentFrameInContentViewForIndex:(NSInteger)componentIndex
{
MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = self;
MXKRoomBubbleCellData *bubbleCellData = roomBubbleTableViewCell.bubbleData;
MXKRoomBubbleComponent *selectedComponent;
if (bubbleCellData.bubbleComponents.count > componentIndex)
{
selectedComponent = bubbleCellData.bubbleComponents[componentIndex];
}
if (!selectedComponent)
{
return CGRectNull;
}
CGFloat selectedComponenContentViewYOffset = 0;
CGFloat selectedComponentPositionY = 0;
CGFloat selectedComponentHeight = 0;
CGRect componentFrame = CGRectNull;
if (roomBubbleTableViewCell.attachmentView)
{
CGRect attachamentViewFrame = roomBubbleTableViewCell.attachmentView.frame;
selectedComponenContentViewYOffset = attachamentViewFrame.origin.y;
selectedComponentHeight = attachamentViewFrame.size.height;
}
else if (roomBubbleTableViewCell.messageTextView)
{
// Force the textView used underneath to layout its frame properly
[roomBubbleTableViewCell setNeedsLayout];
[roomBubbleTableViewCell layoutIfNeeded];
// Compute the height
CGFloat textMessageHeight = 0;
if ([bubbleCellData isKindOfClass:[RoomBubbleCellData class]])
{
RoomBubbleCellData *roomBubbleCellData = (RoomBubbleCellData*)bubbleCellData;
if (!roomBubbleCellData.attachment && selectedComponent.attributedTextMessage)
{
// Get the width of messageTextView to compute the needed height
CGFloat maxTextWidth = CGRectGetWidth(roomBubbleTableViewCell.messageTextView.bounds);
// Compute text message height
textMessageHeight = [roomBubbleCellData rawTextHeight:selectedComponent.attributedTextMessage withMaxWidth:maxTextWidth];
}
}
// Get the messageText frame in the cell content view (as the messageTextView may be inside a stackView and not directly a child of the tableViewCell)
UITextView *messageTextView = roomBubbleTableViewCell.messageTextView;
CGRect messageTextViewFrame = [messageTextView convertRect:messageTextView.bounds toView:roomBubbleTableViewCell.contentView];
if (textMessageHeight > 0)
{
selectedComponentHeight = textMessageHeight;
}
else
{
// if we don't have a height, use the messageTextView height without the text container vertical insets to stay aligned with the text.
selectedComponentHeight = CGRectGetHeight(messageTextViewFrame) - messageTextView.textContainerInset.top - messageTextView.textContainerInset.bottom;
}
// Get the vertical position of the messageTextView relative to the contentView
selectedComponenContentViewYOffset = CGRectGetMinY(messageTextViewFrame);
// Get the position of the component inside the messageTextView
selectedComponentPositionY = selectedComponent.position.y;
}
if (roomBubbleTableViewCell.attachmentView || roomBubbleTableViewCell.messageTextView)
{
CGFloat x = 0;
CGFloat y = selectedComponenContentViewYOffset + selectedComponentPositionY;
CGFloat width = roomBubbleTableViewCell.contentView.frame.size.width;
componentFrame = CGRectMake(x, y, width, selectedComponentHeight);
}
else
{
componentFrame = roomBubbleTableViewCell.bounds;
}
return componentFrame;
}
+ (CGFloat)attachmentBubbleCellHeightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth
{
MXKRoomBubbleTableViewCell* cell = [self cellWithOriginalXib];
CGFloat rowHeight = 0;
RoomBubbleCellData *bubbleData;
if ([cellData isKindOfClass:[RoomBubbleCellData class]])
{
bubbleData = (RoomBubbleCellData*)cellData;
}
if (bubbleData && cell.attachmentView && bubbleData.isAttachmentWithThumbnail)
{
// retrieve the suggested image view height
rowHeight = bubbleData.contentSize.height;
// Check here the minimum height defined in cell view for text message
if (cell.attachViewMinHeightConstraint && rowHeight < cell.attachViewMinHeightConstraint.constant)
{
rowHeight = cell.attachViewMinHeightConstraint.constant;
}
// Finalize the row height by adding the vertical constraints.
rowHeight += cell.attachViewTopConstraint.constant;
CGFloat additionalHeight = bubbleData.additionalContentHeight;
if (additionalHeight)
{
rowHeight += additionalHeight;
}
else
{
rowHeight += cell.attachViewBottomConstraint.constant;
}
}
return rowHeight;
}
- (void)updateTickViewWithFailedEventIds:(NSSet *)failedEventIds
{
for (UIView *tickView in self.messageStatusViews)
{
[tickView removeFromSuperview];
}
self.messageStatusViews = nil;
NSMutableArray *statusViews = [NSMutableArray new];
UIView *tickView = nil;
if ([bubbleData isKindOfClass:RoomBubbleCellData.class]
&& ((RoomBubbleCellData*)bubbleData).componentIndexOfSentMessageTick >= 0)
{
UIImage *image = AssetImages.sentMessageTick.image;
image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
tickView = [[UIImageView alloc] initWithImage:image];
tickView.tintColor = ThemeService.shared.theme.textTertiaryColor;
[statusViews addObject:tickView];
[self addTickView:tickView atIndex:((RoomBubbleCellData*)bubbleData).componentIndexOfSentMessageTick];
}
NSInteger index = bubbleData.bubbleComponents.count;
while (index--)
{
MXKRoomBubbleComponent *component = bubbleData.bubbleComponents[index];
NSArray<MXReceiptData*> *receipts = bubbleData.readReceipts[component.event.eventId];
if (receipts.count == 0) {
if (component.event.sentState == MXEventSentStateUploading
|| component.event.sentState == MXEventSentStateEncrypting
|| component.event.sentState == MXEventSentStatePreparing
|| component.event.sentState == MXEventSentStateSending)
{
if ([failedEventIds containsObject:component.event.eventId] || (bubbleData.attachment && component.event.sentState != MXEventSentStateSending))
{
UIView *progressContentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 40, 40)];
CircleProgressView *progressView = [[CircleProgressView alloc] initWithFrame:CGRectMake(24, 24, 16, 16)];
progressView.lineColor = ThemeService.shared.theme.textTertiaryColor;
[progressContentView addSubview:progressView];
self.progressChartView = progressView;
tickView = progressContentView;
[progressView startAnimating];
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onProgressLongPressGesture:)];
[tickView addGestureRecognizer:longPress];
}
else
{
UIImage *image = AssetImages.sendingMessageTick.image;
image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
tickView = [[UIImageView alloc] initWithImage:image];
tickView.tintColor = ThemeService.shared.theme.textTertiaryColor;
}
[statusViews addObject:tickView];
[self addTickView:tickView atIndex:index];
}
}
if (component.event.sentState == MXEventSentStateFailed)
{
tickView = [[UIImageView alloc] initWithImage:AssetImages.errorMessageTick.image];
[statusViews addObject:tickView];
[self addTickView:tickView atIndex:index];
}
}
if (statusViews.count)
{
self.messageStatusViews = statusViews;
}
}
#pragma mark - User actions
- (IBAction)onEditButtonPressed:(id)sender
{
if (self.delegate)
{
MXEvent *selectedEvent = nil;
// Note edit button tag is equal to the index of the related component.
NSInteger index = ((UIView*)sender).tag;
NSArray *bubbleComponents = bubbleData.bubbleComponents;
if (index < bubbleComponents.count)
{
MXKRoomBubbleComponent *component = bubbleComponents[index];
selectedEvent = component.event;
}
if (selectedEvent)
{
[self.delegate cell:self didRecognizeAction:kMXKRoomBubbleCellRiotEditButtonPressed userInfo:@{kMXKRoomBubbleCellEventKey:selectedEvent}];
}
}
}
- (IBAction)onReceiptContainerTap:(UITapGestureRecognizer *)sender
{
if (self.delegate)
{
[self.delegate cell:self didRecognizeAction:kMXKRoomBubbleCellTapOnReceiptsContainer userInfo:@{kMXKRoomBubbleCellReceiptsContainerKey : sender.view}];
}
}
#pragma mark - Internals
- (void)addTickView:(UIView *)tickView atIndex:(NSInteger)index
{
CGRect componentFrame = [self componentFrameInContentViewForIndex:index];
tickView.frame = CGRectMake(self.contentView.bounds.size.width - tickView.frame.size.width - 2 * PlainRoomCellLayoutConstants.readReceiptsViewRightMargin, CGRectGetMaxY(componentFrame) - tickView.frame.size.height, tickView.frame.size.width, tickView.frame.size.height);
[self.contentView addSubview:tickView];
}
- (void)addEditButtonForComponent:(NSUInteger)componentIndex completion:(void (^ __nullable)(BOOL finished))completion
{
MXKRoomBubbleComponent *component = bubbleData.bubbleComponents[componentIndex];
// Check whether this is the first displayed component.
BOOL isFirstDisplayedComponent = (componentIndex == 0);
if ([bubbleData isKindOfClass:RoomBubbleCellData.class])
{
isFirstDisplayedComponent = (componentIndex == ((RoomBubbleCellData*)bubbleData).oldestComponentIndex);
}
// Define 'Edit' button frame
UIImage *editIcon = AssetImages.editIcon.image;
CGFloat editBtnPosX = self.bubbleInfoContainer.frame.size.width - PlainRoomCellLayoutConstants.timestampLabelWidth - 22 - editIcon.size.width / 2;
CGFloat editBtnPosY = isFirstDisplayedComponent ? -13 : component.position.y + self.msgTextViewTopConstraint.constant - self.bubbleInfoContainerTopConstraint.constant - 13;
UIButton *editButton = [[UIButton alloc] initWithFrame:CGRectMake(editBtnPosX, editBtnPosY, 44, 44)];
[editButton setImage:editIcon forState:UIControlStateNormal];
[editButton setImage:editIcon forState:UIControlStateSelected];
editButton.tag = componentIndex;
[editButton addTarget:self action:@selector(onEditButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
[editButton setTranslatesAutoresizingMaskIntoConstraints:NO];
editButton.accessibilityIdentifier = @"editButton";
[self.bubbleInfoContainer addSubview:editButton];
self.bubbleInfoContainer.userInteractionEnabled = YES;
// Define edit button constraints
NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:editButton
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.bubbleInfoContainer
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:editBtnPosX];
NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:editButton
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.bubbleInfoContainer
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:editBtnPosY];
NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:editButton
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:44];
NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:editButton
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:44];
// Available on iOS 8 and later
[NSLayoutConstraint activateConstraints:@[leftConstraint, topConstraint, widthConstraint, heightConstraint]];
// Store the created button
self.editButton = editButton;
}
- (IBAction)onProgressLongPressGesture:(UILongPressGestureRecognizer*)recognizer
{
if (recognizer.state == UIGestureRecognizerStateBegan && self.delegate)
{
[self.delegate cell:self didRecognizeAction:kMXKRoomBubbleCellLongPressOnProgressView userInfo:nil];
}
}
@end