diff --git a/Vector.xcodeproj/project.pbxproj b/Vector.xcodeproj/project.pbxproj index af88c0379..d48e4fbda 100644 --- a/Vector.xcodeproj/project.pbxproj +++ b/Vector.xcodeproj/project.pbxproj @@ -127,6 +127,7 @@ F025290D1C11B6FC00E1FE1B /* voice_call_icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F02528D11C11B6FC00E1FE1B /* voice_call_icon@2x.png */; }; F025290E1C11B6FC00E1FE1B /* voice_call_icon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F02528D21C11B6FC00E1FE1B /* voice_call_icon@3x.png */; }; F02D87C69D1FFCD2C1531F3D /* libPods-Vector.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B179239B79688A61A3F465F /* libPods-Vector.a */; }; + F0418BE41C9067B40030356E /* edit_icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0418BE31C9067B40030356E /* edit_icon@2x.png */; }; F047DBB51C576F2200952DA2 /* AuthenticationViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = F047DBB41C576F2200952DA2 /* AuthenticationViewController.xib */; }; F047DBB91C576F6600952DA2 /* AuthInputsPasswordBasedView.m in Sources */ = {isa = PBXBuildFile; fileRef = F047DBB71C576F6600952DA2 /* AuthInputsPasswordBasedView.m */; }; F047DBBA1C576F6600952DA2 /* AuthInputsPasswordBasedView.xib in Resources */ = {isa = PBXBuildFile; fileRef = F047DBB81C576F6600952DA2 /* AuthInputsPasswordBasedView.xib */; }; @@ -381,6 +382,7 @@ F02528D01C11B6FC00E1FE1B /* voice_call_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voice_call_icon.png; sourceTree = ""; }; F02528D11C11B6FC00E1FE1B /* voice_call_icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "voice_call_icon@2x.png"; sourceTree = ""; }; F02528D21C11B6FC00E1FE1B /* voice_call_icon@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "voice_call_icon@3x.png"; sourceTree = ""; }; + F0418BE31C9067B40030356E /* edit_icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "edit_icon@2x.png"; sourceTree = ""; }; F047DBB41C576F2200952DA2 /* AuthenticationViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AuthenticationViewController.xib; sourceTree = ""; }; F047DBB61C576F6600952DA2 /* AuthInputsPasswordBasedView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AuthInputsPasswordBasedView.h; sourceTree = ""; }; F047DBB71C576F6600952DA2 /* AuthInputsPasswordBasedView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AuthInputsPasswordBasedView.m; sourceTree = ""; }; @@ -964,6 +966,7 @@ F0DD7D1B1B7AA8C900C4BE02 /* Images */ = { isa = PBXGroup; children = ( + F0418BE31C9067B40030356E /* edit_icon@2x.png */, F056418F1C7DF9DE002276ED /* error.png */, F05641901C7DF9DE002276ED /* error@2x.png */, F05641911C7DF9DE002276ED /* error@3x.png */, @@ -1253,6 +1256,7 @@ F0C34B721C15CA2E00C36F09 /* RoomOutgoingTextMsgWithPaginationTitleBubbleCell.xib in Resources */, F022285E1C64E356000AF23C /* RoomViewController.xib in Resources */, F09EE0071C5134BE0078712F /* RoomOutgoingTextMsgWithoutSenderNameBubbleCell.xib in Resources */, + F0418BE41C9067B40030356E /* edit_icon@2x.png in Resources */, F0C34B661C15C28300C36F09 /* RoomOutgoingTextMsgBubbleCell.xib in Resources */, F0D2D9841C197DCB007B8C96 /* RoomIncomingAttachmentBubbleCell.xib in Resources */, F094AA2C1B78E42600B1FBBF /* Vector.strings in Resources */, diff --git a/Vector/Assets/Images/edit_icon@2x.png b/Vector/Assets/Images/edit_icon@2x.png new file mode 100644 index 000000000..e5a23e14f Binary files /dev/null and b/Vector/Assets/Images/edit_icon@2x.png differ diff --git a/Vector/Assets/en.lproj/Vector.strings b/Vector/Assets/en.lproj/Vector.strings index 7156c2ef0..fecbf87f0 100644 --- a/Vector/Assets/en.lproj/Vector.strings +++ b/Vector/Assets/en.lproj/Vector.strings @@ -122,7 +122,6 @@ "room_prompt_resend" = "Resend now?"; "room_resend_unsent_messages" = "Resend unsent messages"; "room_delete_unsent_messages" = "Delete unsent messages"; -"room_event_action_edit" = "Edit"; "room_event_action_copy" = "Copy"; "room_event_action_share" = "Share"; "room_event_action_redact" = "Redact"; diff --git a/Vector/Categories/MXKRoomBubbleTableViewCell+Vector.h b/Vector/Categories/MXKRoomBubbleTableViewCell+Vector.h index a624e3faa..025484d22 100644 --- a/Vector/Categories/MXKRoomBubbleTableViewCell+Vector.h +++ b/Vector/Categories/MXKRoomBubbleTableViewCell+Vector.h @@ -37,11 +37,6 @@ extern NSString *const kMXKRoomBubbleCellVectorEditButtonPressed; */ - (void)addTimestampLabelForComponent:(NSUInteger)componentIndex; -/** - Remove timestamp labels - */ -- (void)removeTimestampLabels; - /** Highlight a component in receiver. @@ -49,11 +44,6 @@ extern NSString *const kMXKRoomBubbleCellVectorEditButtonPressed; */ - (void)selectComponent:(NSUInteger)componentIndex; -/** - Cancel the highlighting of a component in receiver. - */ -- (void)unselectComponent; - /** Blur the view by adding a transparent overlay. Default is NO. */ @@ -64,14 +54,4 @@ extern NSString *const kMXKRoomBubbleCellVectorEditButtonPressed; */ @property(nonatomic) UIButton *editButton; -/** - The read receipts container associated to the selected component (if any). Default is nil. - */ -@property(nonatomic) MXKReceiptSendersContainer* selectedReadReceiptsContainer; - -/** - The trailing constraint of the read receipts container associated to the selected component (if any). Default is nil. - */ -@property(nonatomic) NSLayoutConstraint* selectedReadReceiptsContainerTrailingConstraint; - @end diff --git a/Vector/Categories/MXKRoomBubbleTableViewCell+Vector.m b/Vector/Categories/MXKRoomBubbleTableViewCell+Vector.m index 8adf91b05..ca6b2fbbc 100644 --- a/Vector/Categories/MXKRoomBubbleTableViewCell+Vector.m +++ b/Vector/Categories/MXKRoomBubbleTableViewCell+Vector.m @@ -22,6 +22,8 @@ #import +#define VECTOR_ROOMBUBBLETABLEVIEWCELL_TIMELABEL_WIDTH 39 + NSString *const kMXKRoomBubbleCellVectorEditButtonPressed = @"kMXKRoomBubbleCellVectorEditButtonPressed"; @implementation MXKRoomBubbleTableViewCell (Vector) @@ -38,49 +40,51 @@ NSString *const kMXKRoomBubbleCellVectorEditButtonPressed = @"kMXKRoomBubbleCell if (component && component.date) { - UILabel *dateTimeLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, component.position.y, self.bubbleInfoContainer.frame.size.width , 18)]; + CGFloat timeLabelPosX = self.bubbleInfoContainer.frame.size.width - VECTOR_ROOMBUBBLETABLEVIEWCELL_TIMELABEL_WIDTH; + CGFloat timeLabelPosY = componentIndex ? component.position.y + self.msgTextViewTopConstraint.constant - self.bubbleInfoContainerTopConstraint.constant: 0; + UILabel *timeLabel = [[UILabel alloc] initWithFrame:CGRectMake(timeLabelPosX, timeLabelPosY, VECTOR_ROOMBUBBLETABLEVIEWCELL_TIMELABEL_WIDTH , 18)]; - dateTimeLabel.text = [self.bubbleData.eventFormatter timeStringFromDate:component.date]; - dateTimeLabel.textAlignment = NSTextAlignmentRight; - dateTimeLabel.textColor = kVectorTextColorGray; + timeLabel.text = [self.bubbleData.eventFormatter timeStringFromDate:component.date]; + timeLabel.textAlignment = NSTextAlignmentRight; + timeLabel.textColor = kVectorTextColorGray; if ([UIFont respondsToSelector:@selector(systemFontOfSize:weight:)]) { - dateTimeLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightLight]; + timeLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightLight]; } else { - dateTimeLabel.font = [UIFont systemFontOfSize:12]; + timeLabel.font = [UIFont systemFontOfSize:12]; } - dateTimeLabel.adjustsFontSizeToFitWidth = YES; + timeLabel.adjustsFontSizeToFitWidth = YES; - dateTimeLabel.tag = componentIndex; + timeLabel.tag = componentIndex; - [dateTimeLabel setTranslatesAutoresizingMaskIntoConstraints:NO]; - [self.bubbleInfoContainer addSubview:dateTimeLabel]; + [timeLabel setTranslatesAutoresizingMaskIntoConstraints:NO]; + [self.bubbleInfoContainer addSubview:timeLabel]; - // Force dateTimeLabel in full width (to handle auto-layout in case of screen rotation) - NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:dateTimeLabel - attribute:NSLayoutAttributeLeading - relatedBy:NSLayoutRelationEqual - toItem:self.bubbleInfoContainer - attribute:NSLayoutAttributeLeading - multiplier:1.0 - constant:0]; - NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:dateTimeLabel + // 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:dateTimeLabel + NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:timeLabel attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.bubbleInfoContainer attribute:NSLayoutAttributeTop multiplier:1.0 - constant:component.position.y]; - NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:dateTimeLabel + constant:timeLabelPosY]; + NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:timeLabel + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1.0 + constant:VECTOR_ROOMBUBBLETABLEVIEWCELL_TIMELABEL_WIDTH]; + NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:timeLabel attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil @@ -89,35 +93,17 @@ NSString *const kMXKRoomBubbleCellVectorEditButtonPressed = @"kMXKRoomBubbleCell constant:18]; // Available on iOS 8 and later - [NSLayoutConstraint activateConstraints:@[leftConstraint, rightConstraint, topConstraint, heightConstraint]]; + [NSLayoutConstraint activateConstraints:@[rightConstraint, topConstraint, widthConstraint, heightConstraint]]; } } -- (void)removeTimestampLabels -{ - // In Vector, only time labels are displayed in bubbleInfoContainer - // So we may remove all bubbleInfoContainer subviews here. - NSArray* views = [self.bubbleInfoContainer subviews]; - for (UIView* view in views) - { - [view removeFromSuperview]; - } - - self.bubbleInfoContainer.hidden = YES; -} - - (void)selectComponent:(NSUInteger)componentIndex { - MXKRoomBubbleComponent *component; if (componentIndex < self.bubbleData.bubbleComponents.count) { // Add time label [self addTimestampLabelForComponent:componentIndex]; - // Hightlight selection by blurring other components - component = self.bubbleData.bubbleComponents[componentIndex]; - [self highlightTextMessageForEvent:component.event.eventId]; - // Blur timestamp labels which are not related to the selected component (if any) for (UIView* view in self.bubbleInfoContainer.subviews) { @@ -137,55 +123,13 @@ NSString *const kMXKRoomBubbleCellVectorEditButtonPressed = @"kMXKRoomBubbleCell { view.alpha = 0.2; } - else if ([view isKindOfClass:MXKReceiptSendersContainer.class]) - { - self.selectedReadReceiptsContainer = (MXKReceiptSendersContainer*)view; - } } - // Add the edit button (shift left the receipts container if any). + // Add the edit button [self addEditButtonForComponent:componentIndex completion:nil]; } } -- (void)unselectComponent -{ - // Remove edit button (Restore receipts container position if any) - [self removeEditButton:nil]; - - // Remove all timestamps by default - [self removeTimestampLabels]; - - // Restore timestamp for the last message if the current bubble is the last one - if ([self.bubbleData isKindOfClass:RoomBubbleCellData.class]) - { - RoomBubbleCellData *cellData = (RoomBubbleCellData*)self.bubbleData; - if (cellData.containsLastMessage) - { - NSArray *components = cellData.bubbleComponents; - NSInteger index = components.count; - while (index--) - { - MXKRoomBubbleComponent *component = components[index]; - if (component.date) - { - [self addTimestampLabelForComponent:index]; - break; - } - } - } - } - - // Restore original string - [self highlightTextMessageForEvent:nil]; - - // Restore read receipts display - for (UIView* view in self.bubbleOverlayContainer.subviews) - { - view.alpha = 1; - } -} - - (void)setBlurred:(BOOL)blurred { objc_setAssociatedObject(self, @selector(blurred), [NSNumber numberWithBool:blurred], OBJC_ASSOCIATION_RETAIN_NONATOMIC); @@ -245,26 +189,6 @@ NSString *const kMXKRoomBubbleCellVectorEditButtonPressed = @"kMXKRoomBubbleCell return objc_getAssociatedObject(self, @selector(editButton)); } -- (void)setSelectedReadReceiptsContainer:(MXKReceiptSendersContainer *)readReceiptsContainer -{ - objc_setAssociatedObject(self, @selector(selectedReadReceiptsContainer), readReceiptsContainer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -- (UIButton*)selectedReadReceiptsContainer -{ - return objc_getAssociatedObject(self, @selector(selectedReadReceiptsContainer)); -} - -- (void)setSelectedReadReceiptsContainerTrailingConstraint:(NSLayoutConstraint *)readReceiptsContainerTrailingConstraint -{ - objc_setAssociatedObject(self, @selector(selectedReadReceiptsContainerTrailingConstraint), readReceiptsContainerTrailingConstraint, OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -- (UIButton*)selectedReadReceiptsContainerTrailingConstraint -{ - return objc_getAssociatedObject(self, @selector(selectedReadReceiptsContainerTrailingConstraint)); -} - #pragma mark - User actions - (IBAction)onEditButtonPressed:(id)sender @@ -294,32 +218,15 @@ NSString *const kMXKRoomBubbleCellVectorEditButtonPressed = @"kMXKRoomBubbleCell { MXKRoomBubbleComponent *component = self.bubbleData.bubbleComponents[componentIndex]; - // Define 'Edit' button frame by overlapping slightly the time label - // (vertical pos = (component.position.y + 4) instead of (component.position.y + 18)) - UIButton *editButton = [[UIButton alloc] initWithFrame:CGRectMake(0, component.position.y + 4, self.bubbleInfoContainer.frame.size.width + 15 , 44)]; + // Define 'Edit' button frame + UIImage *editIcon = [UIImage imageNamed:@"edit_icon"]; + CGFloat editBtnPosX = self.bubbleInfoContainer.frame.size.width - VECTOR_ROOMBUBBLETABLEVIEWCELL_TIMELABEL_WIDTH - 22 - editIcon.size.width / 2; + CGFloat editBtnPosY = componentIndex ? component.position.y + self.msgTextViewTopConstraint.constant - self.bubbleInfoContainerTopConstraint.constant - 13 : -13; + UIButton *editButton = [[UIButton alloc] initWithFrame:CGRectMake(editBtnPosX, editBtnPosY, 44, 44)]; - [editButton setTitle:NSLocalizedStringFromTable(@"room_event_action_edit", @"Vector", nil) forState:UIControlStateNormal]; - [editButton setTitle:NSLocalizedStringFromTable(@"room_event_action_edit", @"Vector", nil) forState:UIControlStateSelected]; - [editButton setTitleColor:kVectorColorGreen forState:UIControlStateNormal]; - [editButton setTitleColor:kVectorColorGreen forState:UIControlStateSelected]; + [editButton setImage:editIcon forState:UIControlStateNormal]; + [editButton setImage:editIcon forState:UIControlStateSelected]; - // Align button label on the right border of the bubbleInfoContainer - editButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentRight; - UIEdgeInsets edgeInset = editButton.titleEdgeInsets; - edgeInset.right = 15; - editButton.titleEdgeInsets = edgeInset; - - editButton.backgroundColor = [UIColor clearColor]; - if ([UIFont respondsToSelector:@selector(systemFontOfSize:weight:)]) - { - editButton.titleLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightMedium]; - } - else - { - editButton.titleLabel.font = [UIFont systemFontOfSize:15]; - } - - editButton.hidden = YES; editButton.tag = componentIndex; [editButton addTarget:self action:@selector(onEditButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; @@ -327,28 +234,28 @@ NSString *const kMXKRoomBubbleCellVectorEditButtonPressed = @"kMXKRoomBubbleCell [self.bubbleInfoContainer addSubview:editButton]; self.bubbleInfoContainer.userInteractionEnabled = YES; - // Force edit button in full width (to handle auto-layout in case of screen rotation) + // Define edit button constraints NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:editButton attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.bubbleInfoContainer attribute:NSLayoutAttributeLeading multiplier:1.0 - constant:0]; - NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:editButton - attribute:NSLayoutAttributeTrailing - relatedBy:NSLayoutRelationEqual - toItem:self.bubbleInfoContainer - attribute:NSLayoutAttributeTrailing - multiplier:1.0 - constant:15]; + constant:editBtnPosX]; NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:editButton attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.bubbleInfoContainer attribute:NSLayoutAttributeTop multiplier:1.0 - constant:component.position.y + 4]; + 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 @@ -357,106 +264,10 @@ NSString *const kMXKRoomBubbleCellVectorEditButtonPressed = @"kMXKRoomBubbleCell multiplier:1.0 constant:44]; // Available on iOS 8 and later - [NSLayoutConstraint activateConstraints:@[leftConstraint, rightConstraint, topConstraint, heightConstraint]]; + [NSLayoutConstraint activateConstraints:@[leftConstraint, topConstraint, widthConstraint, heightConstraint]]; // Store the created button self.editButton = editButton; - - // Check whether this edit button overlaps a potential receipts container displayed for this component - if (self.selectedReadReceiptsContainer) - { - // Adjust edit button frame to be able to compare it to bubble overlay container (superview of receipts container). - CGRect frame = CGRectOffset (editButton.frame, self.bubbleInfoContainer.frame.origin.x, self.bubbleInfoContainer.frame.origin.y); - if (CGRectIntersectsRect(frame, self.selectedReadReceiptsContainer.frame)) - { - // Retrieve the trailing constraint of the receipts container - NSArray *constraints = self.bubbleOverlayContainer.constraints; - for (NSLayoutConstraint *constraint in constraints) - { - if (constraint.firstAttribute == NSLayoutAttributeTrailing && constraint.firstItem == self.selectedReadReceiptsContainer) - { - self.selectedReadReceiptsContainerTrailingConstraint = constraint; - break; - } - } - } - } - - if (self.selectedReadReceiptsContainerTrailingConstraint) - { - // Update layout with animation - [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn - animations:^{ - - // Shift the container to left - self.selectedReadReceiptsContainerTrailingConstraint.constant -= (self.bubbleInfoContainer.frame.size.width + 6); - - // Force to render the view - [self layoutIfNeeded]; - - editButton.hidden = NO; - - } - completion:^(BOOL finished){ - - if (completion) - { - completion(finished); - } - - }]; - } - else - { - editButton.hidden = NO; - - if (completion) - { - completion(YES); - } - } -} - -- (void)removeEditButton:(void (^ __nullable)(BOOL finished))completion -{ - if (self.selectedReadReceiptsContainerTrailingConstraint) - { - // Update layout with animation - [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn - animations:^{ - - [self.editButton removeFromSuperview]; - - self.selectedReadReceiptsContainerTrailingConstraint.constant = -6; - - // Force to render the view - [self layoutIfNeeded]; - } - completion:^(BOOL finished){ - - self.editButton = nil; - self.selectedReadReceiptsContainer = nil; - self.selectedReadReceiptsContainerTrailingConstraint = nil; - - if (completion) - { - completion(finished); - } - - }]; - } - else - { - [self.editButton removeFromSuperview]; - self.editButton = nil; - self.selectedReadReceiptsContainer = nil; - self.selectedReadReceiptsContainerTrailingConstraint = nil; - - if (completion) - { - completion(YES); - } - } } @end diff --git a/Vector/Model/Room/RoomBubbleCellData.h b/Vector/Model/Room/RoomBubbleCellData.h index 07e59e358..48dfa5e6b 100644 --- a/Vector/Model/Room/RoomBubbleCellData.h +++ b/Vector/Model/Room/RoomBubbleCellData.h @@ -24,8 +24,6 @@ /** A Boolean value that determines whether this bubble contains the current last message. Used to keep displaying the timestamp of the last message. - - CAUTION: This property is presently set during bubble rendering in order to be used during bubble cell life. */ @property(nonatomic) BOOL containsLastMessage; @@ -34,4 +32,19 @@ */ @property(nonatomic) BOOL hasReadReceipts; +/** + The event id of the current selected event inside the bubble. Default is nil. + */ +@property(nonatomic) NSString *selectedEventId; + +/** + The index of the most recent component (component with timestamp). NSNotFound by default. + */ +@property(nonatomic, readonly) NSInteger mostRecentComponentIndex; + +/** + The index of the current selected component. NSNotFound by default. + */ +@property(nonatomic, readonly) NSInteger selectedComponentIndex; + @end diff --git a/Vector/Model/Room/RoomBubbleCellData.m b/Vector/Model/Room/RoomBubbleCellData.m index 467d88103..b00ef6578 100644 --- a/Vector/Model/Room/RoomBubbleCellData.m +++ b/Vector/Model/Room/RoomBubbleCellData.m @@ -20,6 +20,7 @@ #import "AvatarGenerator.h" +static NSAttributedString *timestampVerticalWhitespace = nil; static NSAttributedString *readReceiptVerticalWhitespace = nil; @implementation RoomBubbleCellData @@ -53,57 +54,11 @@ static NSAttributedString *readReceiptVerticalWhitespace = nil; return self; } -- (NSAttributedString*)attributedTextMessageWithHighlightedEvent:(NSString*)eventId tintColor:(UIColor*)tintColor -{ - // Use this method to highlight a component in text message: - // The selected component is unchanged, while an alpha is applied on other components. - NSMutableAttributedString *customAttributedTextMsg; - NSAttributedString *componentString; - - @synchronized(bubbleComponents) - { - for (MXKRoomBubbleComponent* component in bubbleComponents) - { - componentString = component.attributedTextMessage; - - if ([component.event.eventId isEqualToString:eventId] == NO) - { - // Apply alpha to blur this component - NSMutableAttributedString *customComponentString = [[NSMutableAttributedString alloc] initWithAttributedString:componentString]; - UIColor *color = [componentString attribute:NSForegroundColorAttributeName atIndex:0 effectiveRange:nil]; - color = [color colorWithAlphaComponent:0.2]; - - [customComponentString addAttribute:NSForegroundColorAttributeName 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]; - } - - // Add vertical whitespace in case of read receipts - if ([roomDataSource.room getEventReceipts:component.event.eventId sorted:NO]) - { - _hasReadReceipts = YES; - [customAttributedTextMsg appendAttributedString:[RoomBubbleCellData readReceiptVerticalWhitespace]]; - } - } - } - - return customAttributedTextMsg; -} - - (void)prepareBubbleComponentsPosition { if (shouldUpdateComponentsPosition) { + // Refresh the receipt flag during this process. _hasReadReceipts = NO; @synchronized(bubbleComponents) @@ -112,20 +67,33 @@ static NSAttributedString *readReceiptVerticalWhitespace = nil; if (bubbleComponents.count) { // Set position of the first component - MXKRoomBubbleComponent *firstComponent = [bubbleComponents firstObject]; + MXKRoomBubbleComponent *component = [bubbleComponents firstObject]; CGFloat positionY = (self.attachment == nil || self.attachment.type == MXKAttachmentTypeFile) ? MXKROOMBUBBLECELLDATA_TEXTVIEW_DEFAULT_VERTICAL_INSET : 0; - firstComponent.position = CGPointMake(0, positionY); + component.position = CGPointMake(0, positionY); - _hasReadReceipts = ([roomDataSource.room getEventReceipts:firstComponent.event.eventId sorted:NO] != nil); + _hasReadReceipts = ([roomDataSource.room getEventReceipts:component.event.eventId sorted:NO] != nil); // Check whether the position of other components need to be refreshed if (!self.attachment && bubbleComponents.count > 1) { - // Init attributed string with the first text component - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithAttributedString:firstComponent.attributedTextMessage]; + NSMutableAttributedString *attributedString; + NSInteger selectedComponentIndex = self.selectedComponentIndex; + NSInteger lastMessageIndex = self.containsLastMessage ? self.mostRecentComponentIndex : NSNotFound; - // Vertical whitescape is added in case of read receipts + // Check whether the timestamp is displayed for this first component, and check whether a vertical whitespace is required + if ((selectedComponentIndex == 0 || lastMessageIndex == 0) && (self.shouldHideSenderInformation || self.shouldHideSenderName)) + { + attributedString = [[NSMutableAttributedString alloc] initWithAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; + [attributedString appendAttributedString:component.attributedTextMessage]; + } + else + { + // Init attributed string with the first text component + attributedString = [[NSMutableAttributedString alloc] initWithAttributedString:component.attributedTextMessage]; + } + + // Vertical whitespace is added in case of read receipts if (_hasReadReceipts) { [attributedString appendAttributedString:[RoomBubbleCellData readReceiptVerticalWhitespace]]; @@ -135,19 +103,35 @@ static NSAttributedString *readReceiptVerticalWhitespace = nil; for (NSUInteger index = 1; index < bubbleComponents.count; index++) { - // Append the next text component - MXKRoomBubbleComponent *component = [bubbleComponents objectAtIndex:index]; - [attributedString appendAttributedString:component.attributedTextMessage]; + // Compute the vertical position for next component + component = [bubbleComponents objectAtIndex:index]; - // Compute the height of the resulting string + // Prepare its attributed string by considering potential vertical margin required to display timestamp. + NSAttributedString *componentString; + if (selectedComponentIndex == index || lastMessageIndex == index) + { + NSMutableAttributedString *componentAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; + [componentAttributedString appendAttributedString:component.attributedTextMessage]; + + componentString = componentAttributedString; + } + else + { + componentString = component.attributedTextMessage; + } + + // Append this attributed string. + [attributedString appendAttributedString:componentString]; + + // 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]); + // Deduce the position of the beginning of this component. + CGFloat positionY = MXKROOMBUBBLECELLDATA_TEXTVIEW_DEFAULT_VERTICAL_INSET + (cumulatedHeight - [self rawTextHeight:componentString]); component.position = CGPointMake(0, positionY); - // Add vertical whitespace in case of read receipts + // Add vertical whitespace in case of read receipts. if ([roomDataSource.room getEventReceipts:component.event.eventId sorted:NO]) { _hasReadReceipts = YES; @@ -166,28 +150,82 @@ static NSAttributedString *readReceiptVerticalWhitespace = nil; - (NSAttributedString*)attributedTextMessage { + // Note: When a component is selected, it is highlighted by applying an alpha on other components. + @synchronized(bubbleComponents) { if (!attributedTextMessage.length && bubbleComponents.count) { + // Refresh the receipt flag during this process _hasReadReceipts = NO; // Create attributed string NSMutableAttributedString *currentAttributedTextMsg; - for (MXKRoomBubbleComponent* component in bubbleComponents) + MXKRoomBubbleComponent *component = [bubbleComponents firstObject]; + NSAttributedString *componentString = component.attributedTextMessage; + + NSInteger selectedComponentIndex = self.selectedComponentIndex; + NSInteger lastMessageIndex = self.containsLastMessage ? self.mostRecentComponentIndex : NSNotFound; + + // Check whether another component than the first one is selected + if (selectedComponentIndex != NSNotFound && selectedComponentIndex != 0) { - if (!currentAttributedTextMsg) + // Apply alpha to blur this component + NSMutableAttributedString *customComponentString = [[NSMutableAttributedString alloc] initWithAttributedString:componentString]; + UIColor *color = [componentString attribute:NSForegroundColorAttributeName atIndex:0 effectiveRange:nil]; + color = [color colorWithAlphaComponent:0.2]; + + [customComponentString addAttribute:NSForegroundColorAttributeName value:color range:NSMakeRange(0, customComponentString.length)]; + componentString = customComponentString; + } + + // Check whether the timestamp is displayed for this first component, and check whether a vertical whitespace is required + if ((selectedComponentIndex == 0 || lastMessageIndex == 0) && (self.shouldHideSenderInformation || self.shouldHideSenderName)) + { + currentAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; + [currentAttributedTextMsg appendAttributedString:componentString]; + } + else + { + // Init attributed string with the first text component + currentAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:componentString]; + } + + // Vertical whitespace is added in case of read receipts + if ([roomDataSource.room getEventReceipts:component.event.eventId sorted:NO]) + { + [currentAttributedTextMsg appendAttributedString:[RoomBubbleCellData readReceiptVerticalWhitespace]]; + } + + for (NSInteger index = 1; index < bubbleComponents.count; index++) + { + [currentAttributedTextMsg appendAttributedString:[MXKRoomBubbleCellDataWithAppendingMode messageSeparator]]; + + component = bubbleComponents[index]; + componentString = component.attributedTextMessage; + + // Check whether another component than this one is selected + if (selectedComponentIndex != NSNotFound && selectedComponentIndex != index) { - currentAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:component.attributedTextMessage]; + // Apply alpha to blur this component + NSMutableAttributedString *customComponentString = [[NSMutableAttributedString alloc] initWithAttributedString:componentString]; + UIColor *color = [componentString attribute:NSForegroundColorAttributeName atIndex:0 effectiveRange:nil]; + color = [color colorWithAlphaComponent:0.2]; + + [customComponentString addAttribute:NSForegroundColorAttributeName value:color range:NSMakeRange(0, customComponentString.length)]; + componentString = customComponentString; } - else + + // Check whether the timestamp is displayed + if (selectedComponentIndex == index || lastMessageIndex == index) { - // Append attributed text - [currentAttributedTextMsg appendAttributedString:[MXKRoomBubbleCellDataWithAppendingMode messageSeparator]]; - [currentAttributedTextMsg appendAttributedString:component.attributedTextMessage]; + [currentAttributedTextMsg appendAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; } + // Append attributed text + [currentAttributedTextMsg appendAttributedString:componentString]; + // Add vertical whitespace in case of read receipts if ([roomDataSource.room getEventReceipts:component.event.eventId sorted:NO]) { @@ -202,8 +240,106 @@ static NSAttributedString *readReceiptVerticalWhitespace = nil; return attributedTextMessage; } +#pragma mark - + +- (void)setContainsLastMessage:(BOOL)containsLastMessage +{ + // Check whether there is something to do + if (_containsLastMessage || containsLastMessage) + { + // Update flag + _containsLastMessage = containsLastMessage; + + // Recompute the text message layout + self.attributedTextMessage = nil; + } +} + +- (void)setHasReadReceipts:(BOOL)hasReadReceipts +{ + // Check whether there is something to do + if (_hasReadReceipts || hasReadReceipts) + { + // Update flag + _hasReadReceipts = hasReadReceipts; + + // Recompute the text message layout + self.attributedTextMessage = nil; + } +} + +- (void)setSelectedEventId:(NSString *)selectedEventId +{ + // Check whether there is something to do + if (_selectedEventId || selectedEventId.length) + { + // Update flag + _selectedEventId = selectedEventId; + + // Recompute the text message layout + self.attributedTextMessage = nil; + } +} + +- (NSInteger)mostRecentComponentIndex +{ + // Update the related component index + NSInteger mostRecentComponentIndex = NSNotFound; + + NSArray *components = self.bubbleComponents; + NSInteger index = components.count; + while (index--) + { + MXKRoomBubbleComponent *component = components[index]; + if (component.date) + { + mostRecentComponentIndex = index; + break; + } + } + + return mostRecentComponentIndex; +} + +- (NSInteger)selectedComponentIndex +{ + // Update the related component index + NSInteger selectedComponentIndex = NSNotFound; + + if (_selectedEventId) + { + NSArray *components = self.bubbleComponents; + NSInteger index = components.count; + while (index--) + { + MXKRoomBubbleComponent *component = components[index]; + if ([component.event.eventId isEqualToString:_selectedEventId]) + { + selectedComponentIndex = index; + break; + } + } + } + + return selectedComponentIndex; +} + #pragma mark - ++ (NSAttributedString *)timestampVerticalWhitespace +{ + @synchronized(self) + { + if (timestampVerticalWhitespace == nil) + { + timestampVerticalWhitespace = [[NSAttributedString alloc] initWithString:@"\n" attributes:@{NSForegroundColorAttributeName : [UIColor blackColor], + NSFontAttributeName: [UIFont systemFontOfSize:12]}]; + } + } + return timestampVerticalWhitespace; +} + + + (NSAttributedString *)readReceiptVerticalWhitespace { @synchronized(self) @@ -211,7 +347,7 @@ static NSAttributedString *readReceiptVerticalWhitespace = nil; if (readReceiptVerticalWhitespace == nil) { readReceiptVerticalWhitespace = [[NSAttributedString alloc] initWithString:@"\n\n" attributes:@{NSForegroundColorAttributeName : [UIColor blackColor], - NSFontAttributeName: [UIFont systemFontOfSize:4]}]; + NSFontAttributeName: [UIFont systemFontOfSize:4]}]; } } return readReceiptVerticalWhitespace; diff --git a/Vector/Model/Room/RoomDataSource.m b/Vector/Model/Room/RoomDataSource.m index 1807ad9a5..5023db759 100644 --- a/Vector/Model/Room/RoomDataSource.m +++ b/Vector/Model/Room/RoomDataSource.m @@ -22,15 +22,6 @@ #import "MXKRoomBubbleTableViewCell+Vector.h" #import "AvatarGenerator.h" -@interface RoomDataSource () -{ - /** - Store here the cell index of the last message (Updated at each table refresh). - */ - NSInteger lastMessageCellIndex; -} -@end - @implementation RoomDataSource - (instancetype)initWithRoomId:(NSString *)roomId andMatrixSession:(MXSession *)matrixSession @@ -64,20 +55,15 @@ { for (RoomBubbleCellData *cellData in bubbles) { - if (cellData.hasReadReceipts) - { - // Recompute the text message layout - cellData.attributedTextMessage = nil; - } + cellData.hasReadReceipts = NO; } } NSArray *readEventIds = receiptEvent.readReceiptEventIds; for (NSString* eventId in readEventIds) { - id bubbleData = [self cellDataOfEventWithEventId:eventId]; - // Recompute the text message layout - bubbleData.attributedTextMessage = nil; + RoomBubbleCellData *cellData = [self cellDataOfEventWithEventId:eventId]; + cellData.hasReadReceipts = YES; } @@ -85,24 +71,31 @@ [super didReceiveReceiptEvent:receiptEvent roomState:roomState]; } +#pragma mark - + - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { NSInteger count = [super tableView:tableView numberOfRowsInSection:section]; if (count) { - // Refresh the cell index of the last message - lastMessageCellIndex = 0; + // Look for the cell data which contains the last message. + // Reset first the flag in each cell data + @synchronized(bubbles) + { + for (RoomBubbleCellData *cellData in bubbles) + { + cellData.containsLastMessage = NO; + } + } + + // Set the flag in the right cell data MXEvent *lastMessage = self.lastMessage; if (lastMessage.eventId) { - lastMessageCellIndex = [self indexOfCellDataWithEventId:lastMessage.eventId]; - // Sanity check - if (lastMessageCellIndex == NSNotFound) - { - lastMessageCellIndex = 0; - } + RoomBubbleCellData *cellData = [self cellDataOfEventWithEventId:lastMessage.eventId]; + cellData.containsLastMessage = YES; } } @@ -119,23 +112,10 @@ MXKRoomBubbleTableViewCell *bubbleCell = (MXKRoomBubbleTableViewCell*)cell; RoomBubbleCellData *cellData = (RoomBubbleCellData*)bubbleCell.bubbleData; - // Check whether this bubble is the last one - cellData.containsLastMessage = (indexPath.row == lastMessageCellIndex); - - // Display timestamp for the last message + // Display timestamp of the last message if (cellData.containsLastMessage) { - NSArray *components = cellData.bubbleComponents; - NSInteger index = components.count; - while (index--) - { - MXKRoomBubbleComponent *component = components[index]; - if (component.date) - { - [bubbleCell addTimestampLabelForComponent:index]; - break; - } - } + [bubbleCell addTimestampLabelForComponent:cellData.mostRecentComponentIndex]; } // Handle read receipts display. @@ -242,25 +222,15 @@ // Check whether an event is currently selected: the other messages are then blurred if (_selectedEventId) { - NSInteger index = [self indexOfCellDataWithEventId:_selectedEventId]; - - if (indexPath.row != index) + // Check whether the selected event belongs to this bubble + NSInteger selectedComponentIndex = cellData.selectedComponentIndex; + if (selectedComponentIndex != NSNotFound) { - // The cell should be displayed in blur mode - bubbleCell.blurred = YES; + [bubbleCell selectComponent:cellData.selectedComponentIndex]; } else { - // Highlight the selected event in the displayed message - for (NSUInteger index = 0; index < cellData.bubbleComponents.count; index ++) - { - MXKRoomBubbleComponent *component = cellData.bubbleComponents[index]; - if ([component.event.eventId isEqualToString:_selectedEventId]) - { - [bubbleCell selectComponent:index]; - break; - } - } + bubbleCell.blurred = YES; } } } @@ -268,4 +238,24 @@ return cell; } +#pragma mark - + +- (void)setSelectedEventId:(NSString *)selectedEventId +{ + // Cancel the current selection (if any) + if (_selectedEventId) + { + RoomBubbleCellData *cellData = [self cellDataOfEventWithEventId:_selectedEventId]; + cellData.selectedEventId = nil; + } + + if (selectedEventId.length) + { + RoomBubbleCellData *cellData = [self cellDataOfEventWithEventId:selectedEventId]; + cellData.selectedEventId = selectedEventId; + } + + _selectedEventId = selectedEventId; +} + @end diff --git a/Vector/ViewController/RoomViewController.m b/Vector/ViewController/RoomViewController.m index b0c82e68a..7b76bb71e 100644 --- a/Vector/ViewController/RoomViewController.m +++ b/Vector/ViewController/RoomViewController.m @@ -623,36 +623,11 @@ else if (tappedEvent) { // Highlight this event in displayed message - - // Update display of the visible table cell view - NSArray* cellArray = self.bubblesTableView.visibleCells; - - // Blur all table cells, except the tapped one - for (MXKRoomBubbleTableViewCell *tableViewCell in cellArray) - { - tableViewCell.blurred = YES; - } - MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell; - roomBubbleTableViewCell.blurred = NO; - - // Compute the component index if tapped event is provided - for (NSUInteger componentIndex = 0; componentIndex < roomBubbleTableViewCell.bubbleData.bubbleComponents.count; componentIndex++) - { - MXKRoomBubbleComponent *component = roomBubbleTableViewCell.bubbleData.bubbleComponents[componentIndex]; - if ([component.event.eventId isEqualToString:tappedEvent.eventId]) - { - // Report the selected event id in data source to keep this event selected in case of table reload. - if (customizedRoomDataSource) - { - customizedRoomDataSource.selectedEventId = tappedEvent.eventId; - } - - [roomBubbleTableViewCell selectComponent:componentIndex]; - - break; - } - } + customizedRoomDataSource.selectedEventId = tappedEvent.eventId; } + + // Force table refresh + [self dataSource:self.roomDataSource didCellChange:nil]; } else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnOverlayContainer]) { @@ -945,21 +920,10 @@ self.currentAlert = nil; } - // Cancel the current selection - NSArray* cellArray = self.bubblesTableView.visibleCells; - for (MXKRoomBubbleTableViewCell *tableViewCell in cellArray) - { - if (tableViewCell.blurred) - { - tableViewCell.blurred = NO; - } - else - { - [tableViewCell unselectComponent]; - } - } - customizedRoomDataSource.selectedEventId = nil; + + // Force table refresh + [self dataSource:self.roomDataSource didCellChange:nil]; } #pragma mark - Segues diff --git a/Vector/Views/RoomBubbleList/RoomIncomingAttachmentBubbleCell.xib b/Vector/Views/RoomBubbleList/RoomIncomingAttachmentBubbleCell.xib index 962e2053a..3d91dfb0e 100644 --- a/Vector/Views/RoomBubbleList/RoomIncomingAttachmentBubbleCell.xib +++ b/Vector/Views/RoomBubbleList/RoomIncomingAttachmentBubbleCell.xib @@ -24,7 +24,7 @@