/* 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 #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 = 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 *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