diff --git a/Vector/Assets/Images/camera_capture.png b/Vector/Assets/Images/camera_capture.png index 9e7ee3409..d117d96b5 100644 Binary files a/Vector/Assets/Images/camera_capture.png and b/Vector/Assets/Images/camera_capture.png differ diff --git a/Vector/Assets/Images/camera_capture@2x.png b/Vector/Assets/Images/camera_capture@2x.png index d117d96b5..3b79d259d 100644 Binary files a/Vector/Assets/Images/camera_capture@2x.png and b/Vector/Assets/Images/camera_capture@2x.png differ diff --git a/Vector/Assets/Images/camera_capture@3x.png b/Vector/Assets/Images/camera_capture@3x.png index 0889db4d8..53ca45637 100644 Binary files a/Vector/Assets/Images/camera_capture@3x.png and b/Vector/Assets/Images/camera_capture@3x.png differ diff --git a/Vector/Assets/Images/camera_switch.png b/Vector/Assets/Images/camera_switch.png index be23b2306..61381bd7b 100644 Binary files a/Vector/Assets/Images/camera_switch.png and b/Vector/Assets/Images/camera_switch.png differ diff --git a/Vector/Assets/Images/camera_switch@2x.png b/Vector/Assets/Images/camera_switch@2x.png index 61381bd7b..9b21264dd 100644 Binary files a/Vector/Assets/Images/camera_switch@2x.png and b/Vector/Assets/Images/camera_switch@2x.png differ diff --git a/Vector/Assets/Images/camera_switch@3x.png b/Vector/Assets/Images/camera_switch@3x.png index 12a95682e..10eba70c6 100644 Binary files a/Vector/Assets/Images/camera_switch@3x.png and b/Vector/Assets/Images/camera_switch@3x.png differ diff --git a/Vector/Assets/Images/create_room.png b/Vector/Assets/Images/create_room.png index 57d3d1633..54f1920c8 100644 Binary files a/Vector/Assets/Images/create_room.png and b/Vector/Assets/Images/create_room.png differ diff --git a/Vector/Assets/Images/create_room@2x.png b/Vector/Assets/Images/create_room@2x.png index 54f1920c8..6ec374de1 100644 Binary files a/Vector/Assets/Images/create_room@2x.png and b/Vector/Assets/Images/create_room@2x.png differ diff --git a/Vector/Assets/Images/create_room@3x.png b/Vector/Assets/Images/create_room@3x.png index be77197e9..b085c20e5 100644 Binary files a/Vector/Assets/Images/create_room@3x.png and b/Vector/Assets/Images/create_room@3x.png differ diff --git a/Vector/Assets/Images/favorite_icon.png b/Vector/Assets/Images/favorite_icon.png index e5ab208cf..fd86162d3 100644 Binary files a/Vector/Assets/Images/favorite_icon.png and b/Vector/Assets/Images/favorite_icon.png differ diff --git a/Vector/Assets/Images/favorite_icon@2x.png b/Vector/Assets/Images/favorite_icon@2x.png index fd86162d3..f1b35c28e 100644 Binary files a/Vector/Assets/Images/favorite_icon@2x.png and b/Vector/Assets/Images/favorite_icon@2x.png differ diff --git a/Vector/Assets/Images/favorite_icon@3x.png b/Vector/Assets/Images/favorite_icon@3x.png index ea3a8d689..f2113cef0 100644 Binary files a/Vector/Assets/Images/favorite_icon@3x.png and b/Vector/Assets/Images/favorite_icon@3x.png differ diff --git a/Vector/Assets/Images/low_priority_icon.png b/Vector/Assets/Images/low_priority_icon.png index 7cbb69e09..0b9688255 100644 Binary files a/Vector/Assets/Images/low_priority_icon.png and b/Vector/Assets/Images/low_priority_icon.png differ diff --git a/Vector/Assets/Images/low_priority_icon@2x.png b/Vector/Assets/Images/low_priority_icon@2x.png index 0f702a1c1..e6a0e90be 100644 Binary files a/Vector/Assets/Images/low_priority_icon@2x.png and b/Vector/Assets/Images/low_priority_icon@2x.png differ diff --git a/Vector/Assets/Images/low_priority_icon@3x.png b/Vector/Assets/Images/low_priority_icon@3x.png index 06573abcc..58a00aafc 100644 Binary files a/Vector/Assets/Images/low_priority_icon@3x.png and b/Vector/Assets/Images/low_priority_icon@3x.png differ diff --git a/Vector/Assets/Images/mute_icon.png b/Vector/Assets/Images/mute_icon.png index 93d08929e..271d16ddd 100644 Binary files a/Vector/Assets/Images/mute_icon.png and b/Vector/Assets/Images/mute_icon.png differ diff --git a/Vector/Assets/Images/mute_icon@2x.png b/Vector/Assets/Images/mute_icon@2x.png index 271d16ddd..ae12bb97e 100644 Binary files a/Vector/Assets/Images/mute_icon@2x.png and b/Vector/Assets/Images/mute_icon@2x.png differ diff --git a/Vector/Assets/Images/mute_icon@3x.png b/Vector/Assets/Images/mute_icon@3x.png index 56921c6b4..a254e594b 100644 Binary files a/Vector/Assets/Images/mute_icon@3x.png and b/Vector/Assets/Images/mute_icon@3x.png differ diff --git a/Vector/Assets/Images/placeholder.png b/Vector/Assets/Images/placeholder.png index cbafc59e6..9630bb3c8 100644 Binary files a/Vector/Assets/Images/placeholder.png and b/Vector/Assets/Images/placeholder.png differ diff --git a/Vector/Assets/Images/placeholder@2x.png b/Vector/Assets/Images/placeholder@2x.png index 9630bb3c8..8e5ef48ae 100644 Binary files a/Vector/Assets/Images/placeholder@2x.png and b/Vector/Assets/Images/placeholder@2x.png differ diff --git a/Vector/Assets/Images/placeholder@3x.png b/Vector/Assets/Images/placeholder@3x.png index 18925c634..fba0bea39 100644 Binary files a/Vector/Assets/Images/placeholder@3x.png and b/Vector/Assets/Images/placeholder@3x.png differ diff --git a/Vector/Assets/Images/remove_icon.png b/Vector/Assets/Images/remove_icon.png index a76c88731..66e45060b 100644 Binary files a/Vector/Assets/Images/remove_icon.png and b/Vector/Assets/Images/remove_icon.png differ diff --git a/Vector/Assets/Images/remove_icon@2x.png b/Vector/Assets/Images/remove_icon@2x.png index 66e45060b..9461aaebf 100644 Binary files a/Vector/Assets/Images/remove_icon@2x.png and b/Vector/Assets/Images/remove_icon@2x.png differ diff --git a/Vector/Assets/Images/remove_icon@3x.png b/Vector/Assets/Images/remove_icon@3x.png index 48f6c20a8..90d97382f 100644 Binary files a/Vector/Assets/Images/remove_icon@3x.png and b/Vector/Assets/Images/remove_icon@3x.png differ diff --git a/Vector/Assets/Images/search_bg.png b/Vector/Assets/Images/search_bg.png index 2531f48c9..b78748b04 100644 Binary files a/Vector/Assets/Images/search_bg.png and b/Vector/Assets/Images/search_bg.png differ diff --git a/Vector/Assets/Images/search_bg@2x.png b/Vector/Assets/Images/search_bg@2x.png index b78748b04..284358c5e 100644 Binary files a/Vector/Assets/Images/search_bg@2x.png and b/Vector/Assets/Images/search_bg@2x.png differ diff --git a/Vector/Assets/Images/search_bg@3x.png b/Vector/Assets/Images/search_bg@3x.png index b42555e83..4525d4f5a 100644 Binary files a/Vector/Assets/Images/search_bg@3x.png and b/Vector/Assets/Images/search_bg@3x.png differ diff --git a/Vector/Assets/Images/search_icon.png b/Vector/Assets/Images/search_icon.png index 7c9202603..1398efe9e 100644 Binary files a/Vector/Assets/Images/search_icon.png and b/Vector/Assets/Images/search_icon.png differ diff --git a/Vector/Assets/Images/search_icon@2x.png b/Vector/Assets/Images/search_icon@2x.png index d444de1b6..f1c62702e 100644 Binary files a/Vector/Assets/Images/search_icon@2x.png and b/Vector/Assets/Images/search_icon@2x.png differ diff --git a/Vector/Assets/Images/search_icon@3x.png b/Vector/Assets/Images/search_icon@3x.png index 93b02b847..f4c756d53 100644 Binary files a/Vector/Assets/Images/search_icon@3x.png and b/Vector/Assets/Images/search_icon@3x.png differ diff --git a/Vector/Assets/Images/selection_tick.png b/Vector/Assets/Images/selection_tick.png index 470dd3e1a..634a459bc 100644 Binary files a/Vector/Assets/Images/selection_tick.png and b/Vector/Assets/Images/selection_tick.png differ diff --git a/Vector/Assets/Images/selection_tick@2x.png b/Vector/Assets/Images/selection_tick@2x.png index 634a459bc..8855aa986 100644 Binary files a/Vector/Assets/Images/selection_tick@2x.png and b/Vector/Assets/Images/selection_tick@2x.png differ diff --git a/Vector/Assets/Images/selection_tick@3x.png b/Vector/Assets/Images/selection_tick@3x.png index 838e26c92..540ad7380 100644 Binary files a/Vector/Assets/Images/selection_tick@3x.png and b/Vector/Assets/Images/selection_tick@3x.png differ diff --git a/Vector/Assets/Images/selection_untick.png b/Vector/Assets/Images/selection_untick.png index 5ee3ab7c6..90934970c 100644 Binary files a/Vector/Assets/Images/selection_untick.png and b/Vector/Assets/Images/selection_untick.png differ diff --git a/Vector/Assets/Images/selection_untick@2x.png b/Vector/Assets/Images/selection_untick@2x.png index 90934970c..15dae8d0d 100644 Binary files a/Vector/Assets/Images/selection_untick@2x.png and b/Vector/Assets/Images/selection_untick@2x.png differ diff --git a/Vector/Assets/Images/selection_untick@3x.png b/Vector/Assets/Images/selection_untick@3x.png index d48835fbc..871b99933 100644 Binary files a/Vector/Assets/Images/selection_untick@3x.png and b/Vector/Assets/Images/selection_untick@3x.png differ diff --git a/Vector/Assets/Images/settings_icon.png b/Vector/Assets/Images/settings_icon.png index 06f2fa883..90274882d 100644 Binary files a/Vector/Assets/Images/settings_icon.png and b/Vector/Assets/Images/settings_icon.png differ diff --git a/Vector/Assets/Images/settings_icon@2x.png b/Vector/Assets/Images/settings_icon@2x.png index 75c6771a5..3d06a4a7d 100644 Binary files a/Vector/Assets/Images/settings_icon@2x.png and b/Vector/Assets/Images/settings_icon@2x.png differ diff --git a/Vector/Assets/Images/settings_icon@3x.png b/Vector/Assets/Images/settings_icon@3x.png new file mode 100644 index 000000000..8178e63c7 Binary files /dev/null and b/Vector/Assets/Images/settings_icon@3x.png differ diff --git a/Vector/Assets/Images/typing.png b/Vector/Assets/Images/typing.png index 97a030888..ead80a106 100644 Binary files a/Vector/Assets/Images/typing.png and b/Vector/Assets/Images/typing.png differ diff --git a/Vector/Assets/Images/typing@2x.png b/Vector/Assets/Images/typing@2x.png index ead80a106..8ba44e832 100644 Binary files a/Vector/Assets/Images/typing@2x.png and b/Vector/Assets/Images/typing@2x.png differ diff --git a/Vector/Assets/Images/typing@3x.png b/Vector/Assets/Images/typing@3x.png index 9174382b7..e4aa3ad98 100644 Binary files a/Vector/Assets/Images/typing@3x.png and b/Vector/Assets/Images/typing@3x.png differ diff --git a/Vector/Assets/Images/upload_icon.png b/Vector/Assets/Images/upload_icon.png index 82767b662..db35287b8 100644 Binary files a/Vector/Assets/Images/upload_icon.png and b/Vector/Assets/Images/upload_icon.png differ diff --git a/Vector/Assets/Images/upload_icon@2x.png b/Vector/Assets/Images/upload_icon@2x.png index db35287b8..4504684ba 100644 Binary files a/Vector/Assets/Images/upload_icon@2x.png and b/Vector/Assets/Images/upload_icon@2x.png differ diff --git a/Vector/Assets/Images/upload_icon@3x.png b/Vector/Assets/Images/upload_icon@3x.png index 809897402..c4af7fbdf 100644 Binary files a/Vector/Assets/Images/upload_icon@3x.png and b/Vector/Assets/Images/upload_icon@3x.png differ diff --git a/Vector/Assets/Images/voice_call_icon.png b/Vector/Assets/Images/voice_call_icon.png index b0ca484b6..382d09080 100644 Binary files a/Vector/Assets/Images/voice_call_icon.png and b/Vector/Assets/Images/voice_call_icon.png differ diff --git a/Vector/Assets/Images/voice_call_icon@2x.png b/Vector/Assets/Images/voice_call_icon@2x.png index 382d09080..324c45e3e 100644 Binary files a/Vector/Assets/Images/voice_call_icon@2x.png and b/Vector/Assets/Images/voice_call_icon@2x.png differ diff --git a/Vector/Assets/Images/voice_call_icon@3x.png b/Vector/Assets/Images/voice_call_icon@3x.png index 4eb533755..01fb0cdfc 100644 Binary files a/Vector/Assets/Images/voice_call_icon@3x.png and b/Vector/Assets/Images/voice_call_icon@3x.png differ diff --git a/Vector/Categories/MXKRoomBubbleTableViewCell+Vector.m b/Vector/Categories/MXKRoomBubbleTableViewCell+Vector.m index b4ab4fa78..7b9d3a879 100644 --- a/Vector/Categories/MXKRoomBubbleTableViewCell+Vector.m +++ b/Vector/Categories/MXKRoomBubbleTableViewCell+Vector.m @@ -16,22 +16,16 @@ #import "MXKRoomBubbleTableViewCell+Vector.h" +#import "RoomBubbleCellData.h" + +#import "VectorDesignValues.h" + #import @implementation MXKRoomBubbleTableViewCell (Vector) - (void)addTimestampLabelForComponent:(NSUInteger)componentIndex { - // FIXME GFO uncomment the following block -// // Ensure that older subviews are removed -// // They should be (they are removed when the is not anymore used). -// // But, it seems that is not always true. -// NSArray* views = [self.bubbleInfoContainer subviews]; -// for (UIView* view in views) -// { -// [view removeFromSuperview]; -// } - self.bubbleInfoContainer.hidden = NO; MXKRoomBubbleComponent *component; @@ -42,13 +36,21 @@ if (component && component.date) { - UILabel *dateTimeLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, component.position.y, self.bubbleInfoContainer.frame.size.width , 15)]; + UILabel *dateTimeLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, component.position.y, self.bubbleInfoContainer.frame.size.width , 18)]; dateTimeLabel.text = [self.bubbleData.eventFormatter timeStringFromDate:component.date]; dateTimeLabel.textAlignment = NSTextAlignmentRight; - dateTimeLabel.textColor = [UIColor lightGrayColor]; - dateTimeLabel.font = [UIFont systemFontOfSize:11]; - dateTimeLabel.adjustsFontSizeToFitWidth = NO; + dateTimeLabel.textColor = VECTOR_TEXT_GRAY_COLOR; + if ([UIFont respondsToSelector:@selector(systemFontOfSize:weight:)]) + { + dateTimeLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightMedium]; + } + else + { + dateTimeLabel.font = [UIFont systemFontOfSize:15]; + } + + dateTimeLabel.tag = componentIndex; [dateTimeLabel setTranslatesAutoresizingMaskIntoConstraints:NO]; [self.bubbleInfoContainer addSubview:dateTimeLabel]; @@ -120,16 +122,52 @@ 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) + { + // Note dateTime label tag is equal to the index of the related component. + if (view.tag != componentIndex) + { + view.alpha = 0.2; + } + } + + // Blur read receipts which are not related to the selected component (if any) + for (UIView* view in self.bubbleOverlayContainer.subviews) + { + // Note read receipt container tag is equal to the index of the related component. + if (view.tag != componentIndex) + { + view.alpha = 0.2; + } + } } } - (void)unselectComponent { - // FIXME GFO: handle the case of the last message thanks to showDateTime flag + // 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.isLastBubble && cellData.bubbleComponents.count) + { + [self addTimestampLabelForComponent:cellData.bubbleComponents.count - 1]; + } + } + // Restore original string [self highlightTextMessageForEvent:nil]; + + // Restore read receipts display + for (UIView* view in self.bubbleOverlayContainer.subviews) + { + view.alpha = 1; + } } - (void)setBlurred:(BOOL)blurred @@ -138,13 +176,36 @@ if (blurred) { + self.bubbleOverlayContainer.hidden = NO; self.bubbleOverlayContainer.backgroundColor = [UIColor whiteColor]; self.bubbleOverlayContainer.alpha = 0.8; - self.bubbleOverlayContainer.hidden = NO; + self.bubbleOverlayContainer.userInteractionEnabled = YES; + + // Blur read receipts if any + for (UIView* view in self.bubbleOverlayContainer.subviews) + { + view.alpha = 0.2; + } } else { - self.bubbleOverlayContainer.hidden = YES; + 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 read receipts display + for (UIView* view in self.bubbleOverlayContainer.subviews) + { + view.alpha = 1; + } + } + else + { + self.bubbleOverlayContainer.hidden = YES; + } } } diff --git a/Vector/Model/Room/RoomBubbleCellData.h b/Vector/Model/Room/RoomBubbleCellData.h index e8d9bfe58..fbf77f516 100644 --- a/Vector/Model/Room/RoomBubbleCellData.h +++ b/Vector/Model/Room/RoomBubbleCellData.h @@ -21,4 +21,17 @@ */ @interface RoomBubbleCellData : MXKRoomBubbleCellDataWithAppendingMode +/** + A Boolean value that determines whether this bubble is the current last one. + 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 isLastBubble; + +/** + A Boolean value that determines whether some read receipts are currently displayed in this bubble. + */ +@property(nonatomic) BOOL hasReadReceipts; + @end diff --git a/Vector/Model/Room/RoomBubbleCellData.m b/Vector/Model/Room/RoomBubbleCellData.m index ff3815101..58a513747 100644 --- a/Vector/Model/Room/RoomBubbleCellData.m +++ b/Vector/Model/Room/RoomBubbleCellData.m @@ -20,9 +20,13 @@ #import "AvatarGenerator.h" +#define VECTOR_ROOM_BUBBLE_CELL_DATA_TEXTVIEW_MARGIN 10 + +static NSAttributedString *readReceiptVerticalWhitespace = nil; + @implementation RoomBubbleCellData -#pragma mark - +#pragma mark - Override MXKRoomBubbleCellData - (instancetype)initWithEvent:(MXEvent *)event andRoomState:(MXRoomState *)roomState andRoomDataSource:(MXKRoomDataSource *)roomDataSource2 { @@ -30,8 +34,22 @@ if (self) { - // use the vector style placeholder + // Use the vector style placeholder self.senderAvatarPlaceholder = [AvatarGenerator generateRoomMemberAvatar:self.senderId displayName:self.senderDisplayName]; + + // Check whether some read receipts are linked to this event + _hasReadReceipts = NO; + if ([roomDataSource.room getEventReceipts:event.eventId sorted:NO]) + { + _hasReadReceipts = YES; + + // Update attributed string by inserting vertical whitespace at the end to display read receipts + NSMutableAttributedString *updatedAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:attributedTextMessage]; + [updatedAttributedTextMsg appendAttributedString:[RoomBubbleCellData readReceiptVerticalWhitespace]]; + + // Update the current text message by reseting content size + self.attributedTextMessage = updatedAttributedTextMsg; + } } return self; @@ -71,10 +89,137 @@ [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) + { + _hasReadReceipts = NO; + + @synchronized(bubbleComponents) + { + // Check whether there is at least one component. + if (bubbleComponents.count) + { + // Set position of the first component + MXKRoomBubbleComponent *firstComponent = [bubbleComponents firstObject]; + + CGFloat positionY = (self.attachment == nil || self.attachment.type == MXKAttachmentTypeFile) ? VECTOR_ROOM_BUBBLE_CELL_DATA_TEXTVIEW_MARGIN : 0; + firstComponent.position = CGPointMake(0, positionY); + + _hasReadReceipts = ([roomDataSource.room getEventReceipts:firstComponent.event.eventId sorted:NO] != nil); + + // Check whether the position of other components need to be refreshed + if (!self.attachment && bubbleComponents.count > 1) + { + // Compute height of the first text component + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithAttributedString:firstComponent.attributedTextMessage]; + + // Vertical whitescape is added in case of read receipts + if (_hasReadReceipts) + { + [attributedString appendAttributedString:[RoomBubbleCellData readReceiptVerticalWhitespace]]; + } + + + CGFloat componentHeight = [self rawTextHeight:attributedString]; + + // Set position for each other component + CGFloat positionY = firstComponent.position.y; + CGFloat cumulatedHeight = 0; + + for (NSUInteger index = 1; index < bubbleComponents.count; index++) + { + cumulatedHeight += componentHeight; + positionY += componentHeight; + + MXKRoomBubbleComponent *component = [bubbleComponents objectAtIndex:index]; + component.position = CGPointMake(0, positionY); + + // Compute height of the current component + [attributedString appendAttributedString:[MXKRoomBubbleCellDataWithAppendingMode messageSeparator]]; + [attributedString appendAttributedString:component.attributedTextMessage]; + + // Add vertical whitespace in case of read receipts + if ([roomDataSource.room getEventReceipts:component.event.eventId sorted:NO]) + { + _hasReadReceipts = YES; + [attributedString appendAttributedString:[RoomBubbleCellData readReceiptVerticalWhitespace]]; + } + + componentHeight = [self rawTextHeight:attributedString] - cumulatedHeight; + } + } + } + } + + shouldUpdateComponentsPosition = NO; + } +} + +- (NSAttributedString*)attributedTextMessage +{ + @synchronized(bubbleComponents) + { + if (!attributedTextMessage.length && bubbleComponents.count) + { + _hasReadReceipts = NO; + + // Create attributed string + NSMutableAttributedString *currentAttributedTextMsg; + + for (MXKRoomBubbleComponent* component in bubbleComponents) + { + if (!currentAttributedTextMsg) + { + currentAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:component.attributedTextMessage]; + } + else + { + // Append attributed text + [currentAttributedTextMsg appendAttributedString:[MXKRoomBubbleCellDataWithAppendingMode messageSeparator]]; + [currentAttributedTextMsg appendAttributedString:component.attributedTextMessage]; + } + + // Add vertical whitespace in case of read receipts + if ([roomDataSource.room getEventReceipts:component.event.eventId sorted:NO]) + { + _hasReadReceipts = YES; + [currentAttributedTextMsg appendAttributedString:[RoomBubbleCellData readReceiptVerticalWhitespace]]; + } + } + attributedTextMessage = currentAttributedTextMsg; + } + } + + return attributedTextMessage; +} + +#pragma mark - + ++ (NSAttributedString *)readReceiptVerticalWhitespace +{ + @synchronized(self) + { + if (readReceiptVerticalWhitespace == nil) + { + readReceiptVerticalWhitespace = [[NSAttributedString alloc] initWithString:@"\n\n" attributes:@{NSForegroundColorAttributeName : [UIColor blackColor], + NSFontAttributeName: [UIFont systemFontOfSize:4]}]; + } + } + return readReceiptVerticalWhitespace; +} + @end diff --git a/Vector/Model/Room/RoomDataSource.m b/Vector/Model/Room/RoomDataSource.m index a80a03bef..2a9d4173b 100644 --- a/Vector/Model/Room/RoomDataSource.m +++ b/Vector/Model/Room/RoomDataSource.m @@ -20,6 +20,7 @@ #import "RoomBubbleCellData.h" #import "MXKRoomBubbleTableViewCell+Vector.h" +#import "AvatarGenerator.h" @implementation RoomDataSource @@ -36,8 +37,7 @@ // Handle timestamp and read receips display at Vector app level (see [tableView: cellForRowAtIndexPath:]) self.useCustomDateTimeLabel = YES; - //FIXME GFO: disable default receipts display - //self.useCustomReceipts = YES; + self.useCustomReceipts = YES; // TODO custom here self.eventsFilterForMessages according to Vector requirements @@ -47,6 +47,34 @@ return self; } +- (void)didReceiveReceiptEvent:(MXEvent *)receiptEvent roomState:(MXRoomState *)roomState +{ + // Override this callback to force rendering of each cell with read receipts information. + @synchronized(bubbles) + { + for (RoomBubbleCellData *cellData in bubbles) + { + if (cellData.hasReadReceipts) + { + // Recompute the text message layout + cellData.attributedTextMessage = nil; + } + } + } + + NSArray *readEventIds = receiptEvent.readReceiptEventIds; + for (NSString* eventId in readEventIds) + { + id bubbleData = [self cellDataOfEventWithEventId:eventId]; + // Recompute the text message layout + bubbleData.attributedTextMessage = nil; + } + + + // Let super handle this receipt + [super didReceiveReceiptEvent:receiptEvent roomState:roomState]; +} + - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath]; @@ -55,13 +83,127 @@ if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) { MXKRoomBubbleTableViewCell *bubbleCell = (MXKRoomBubbleTableViewCell*)cell; + RoomBubbleCellData *cellData = (RoomBubbleCellData*)bubbleCell.bubbleData; + + // Check whether this bubble is the last one + cellData.isLastBubble = (indexPath.row == [tableView numberOfRowsInSection:0] - 1); // Display timestamp for the last message. - if (indexPath.row == [tableView numberOfRowsInSection:0] - 1) + if (cellData.isLastBubble) { - if (bubbleCell.bubbleData.bubbleComponents.count) + if (cellData.bubbleComponents.count) { - [bubbleCell addTimestampLabelForComponent:bubbleCell.bubbleData.bubbleComponents.count - 1]; + [bubbleCell addTimestampLabelForComponent:cellData.bubbleComponents.count - 1]; + } + } + + // Handle read receipts display. + if (cellData.hasReadReceipts && self.showBubbleReceipts) + { + // Read receipts container are inserted here on the right side into the overlay container. + // Some vertical whitespaces are added in message text view (see RoomBubbleCellData class) to insert correctly multiple receipts. + bubbleCell.bubbleOverlayContainer.backgroundColor = [UIColor clearColor]; + bubbleCell.bubbleOverlayContainer.alpha = 1; + bubbleCell.bubbleOverlayContainer.userInteractionEnabled = NO; + bubbleCell.bubbleOverlayContainer.hidden = NO; + + NSInteger index = cellData.bubbleComponents.count; + CGFloat bottomPositionY = bubbleCell.frame.size.height; + while (index--) + { + MXKRoomBubbleComponent *component = cellData.bubbleComponents[index]; + + if (component.event.mxkState != MXKEventStateSendingFailed) + { + // Get the events receipts by ignoring the current user receipt. + NSArray* receipts = [self.room getEventReceipts:component.event.eventId sorted:YES]; + NSMutableArray *roomMembers; + NSMutableArray *placeholders; + + // Check whether some receipts are found + if (receipts.count) + { + // Retrieve the corresponding room members + roomMembers = [[NSMutableArray alloc] initWithCapacity:receipts.count]; + placeholders = [[NSMutableArray alloc] initWithCapacity:receipts.count]; + + for (MXReceiptData* data in receipts) + { + MXRoomMember * roomMember = [self.room.state memberWithUserId:data.userId]; + if (roomMember) + { + [roomMembers addObject:roomMember]; + [placeholders addObject:[AvatarGenerator generateRoomMemberAvatar:roomMember.userId displayName:roomMember.displayname]]; + } + } + } + + // Check whether some receipts are found + if (roomMembers.count) + { + // Define the read receipts container, positioned on the right border of the bubble cell (Note the right margin 6 pts). + MXKReceiptSendersContainer* avatarsContainer = [[MXKReceiptSendersContainer alloc] initWithFrame:CGRectMake(bubbleCell.frame.size.width - 156, bottomPositionY - 12, 150, 12) andRestClient:self.mxSession.matrixRestClient]; + + // Custom avatar display + avatarsContainer.maxDisplayedAvatars = 5; + avatarsContainer.avatarMargin = 6; + + // Set the container tag to be able to retrieve read receipts container from component index (see component selection in MXKRoomBubbleTableViewCell (Vector) category). + avatarsContainer.tag = index; + + [avatarsContainer refreshReceiptSenders:roomMembers withPlaceHolders:placeholders andAlignment:ReadReceiptAlignmentRight]; + + avatarsContainer.translatesAutoresizingMaskIntoConstraints = NO; + [bubbleCell.bubbleOverlayContainer addSubview:avatarsContainer]; + + // Force receipts container size + NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:avatarsContainer + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1.0 + constant:150]; + NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:avatarsContainer + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1.0 + constant:12]; + + // Force receipts container position + NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:avatarsContainer + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:bubbleCell.bubbleOverlayContainer + attribute:NSLayoutAttributeTrailing + multiplier:1.0 + constant:-6]; + NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:avatarsContainer + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:bubbleCell.bubbleOverlayContainer + attribute:NSLayoutAttributeTop + multiplier:1.0 + constant:bottomPositionY - 12]; + + if ([NSLayoutConstraint respondsToSelector:@selector(activateConstraints:)]) + { + [NSLayoutConstraint activateConstraints:@[widthConstraint, heightConstraint, topConstraint, rightConstraint]]; + } + else + { + [avatarsContainer addConstraint:heightConstraint]; + [avatarsContainer addConstraint:widthConstraint]; + [bubbleCell.bubbleOverlayContainer addConstraint:topConstraint]; + [bubbleCell.bubbleOverlayContainer addConstraint:rightConstraint]; + } + } + } + + // Prepare the bottom position for the next read receipt container (if any) + bottomPositionY = bubbleCell.msgTextViewTopConstraint.constant + component.position.y; } } @@ -78,8 +220,6 @@ else { // Highlight the selected event in the displayed message - MXKRoomBubbleCellData *cellData = (MXKRoomBubbleCellData*)bubbleCell.bubbleData; - for (NSUInteger index = 0; index < cellData.bubbleComponents.count; index ++) { MXKRoomBubbleComponent *component = cellData.bubbleComponents[index]; diff --git a/Vector/Model/RoomList/RecentsDataSource.h b/Vector/Model/RoomList/RecentsDataSource.h index b03bb3ccd..6f63aa49d 100644 --- a/Vector/Model/RoomList/RecentsDataSource.h +++ b/Vector/Model/RoomList/RecentsDataSource.h @@ -31,9 +31,42 @@ */ @property (nonatomic, copy) void (^onRoomInvitationAccept)(MXRoom*); +/** + There is a pending drag and drop cell. + It defines its path of the source cell. + */ +@property (nonatomic, copy) NSIndexPath* hiddenCellIndexPath; + +/** + There is a pending drag and drop cell. + It defines its path of the destination cell. + */ +@property (nonatomic, copy) NSIndexPath* droppingCellIndexPath; + +/** + The movingCellBackgroundImage; + */ +@property (nonatomic) UIImageView* droppingCellBackGroundView; + /** Return the header height from the section. */ - (CGFloat)heightForHeaderInSection:(NSInteger)section; +/** + Return true of the cell can be moved from a section to another one. + */ +- (BOOL)isDraggableCellAt:(NSIndexPath*)path; + +/** + Return true of the cell can be moved from a section to another one. + */ +- (BOOL)canCellMoveFrom:(NSIndexPath*)oldPath to:(NSIndexPath*)newPath; + +/** + Move a cell from a path to another one. + It is based on room Tag. + */ +- (void)moveRoomCell:(MXRoom*)room from:(NSIndexPath*)oldPath to:(NSIndexPath*)newPath success:(void (^)())moveSuccess failure:(void (^)(NSError *error))moveFailure; + @end diff --git a/Vector/Model/RoomList/RecentsDataSource.m b/Vector/Model/RoomList/RecentsDataSource.m index d598ba2f0..7c94d419a 100644 --- a/Vector/Model/RoomList/RecentsDataSource.m +++ b/Vector/Model/RoomList/RecentsDataSource.m @@ -45,6 +45,7 @@ @implementation RecentsDataSource @synthesize onRoomInvitationReject, onRoomInvitationAccept; +@synthesize hiddenCellIndexPath, droppingCellIndexPath, droppingCellBackGroundView; - (instancetype)init { @@ -106,11 +107,8 @@ { dispatch_async(dispatch_get_main_queue(), ^{ - // refresh the sections - [self refreshRoomsSections]; - - // And inform the delegate about the update - [self.delegate dataSource:self didCellChange:nil]; + [self refreshRoomsSectionsAndReload]; + }); } @@ -143,16 +141,24 @@ } } +- (void)refreshRoomsSectionsAndReload +{ + // Refresh is disabled during drag&drop animation" + if (!self.droppingCellIndexPath) + { + [self refreshRoomsSections]; + + // And inform the delegate about the update + [self.delegate dataSource:self didCellChange:nil]; + } +} + - (void)didMXSessionInviteRoomUpdate:(NSNotification *)notif { MXSession *mxSession = notif.object; if (mxSession == self.mxSession) { - // refresh the sections - [self refreshRoomsSections]; - - // And inform the delegate about the update - [self.delegate dataSource:self didCellChange:nil]; + [self refreshRoomsSectionsAndReload]; } } @@ -182,6 +188,16 @@ return 0; } +- (BOOL)isMovingCellSection:(NSInteger)section +{ + return self.droppingCellIndexPath && (self.droppingCellIndexPath.section == section); +} + +- (BOOL)isHiddenCellSection:(NSInteger)section +{ + return self.hiddenCellIndexPath && (self.hiddenCellIndexPath.section == section); +} + - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { NSUInteger count = 0; @@ -203,7 +219,16 @@ count = invitesCellDataArray.count; } - + if ([self isMovingCellSection:section]) + { + count++; + } + + if ([self isHiddenCellSection:section]) + { + count--; + } + return count; } @@ -244,10 +269,52 @@ return [super viewForHeaderInSection:section withFrame:frame]; } -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)anIndexPath { - UITableViewCell* cell = [super tableView:tableView cellForRowAtIndexPath:indexPath]; + NSIndexPath* indexPath = anIndexPath; + if (self.droppingCellIndexPath && (self.droppingCellIndexPath.section == indexPath.section)) + { + if ([anIndexPath isEqual:self.droppingCellIndexPath]) + { + static NSString* cellIdentifier = @"VectorRecentsMovingCell"; + + UITableViewCell* cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"VectorRecentsMovingCell"]; + + // add an imageview of the cell. + // The image is a shot of the genuine cell. + // Thus, this cell has the same look as the genuine cell withourt computing it. + UIImageView* imageView = [cell viewWithTag:[cellIdentifier hash]]; + + if (!imageView || (imageView != self.droppingCellBackGroundView)) + { + if (imageView) + { + [imageView removeFromSuperview]; + } + self.droppingCellBackGroundView.tag = [cellIdentifier hash]; + [cell.contentView addSubview:self.droppingCellBackGroundView]; + } + + self.droppingCellBackGroundView.frame = self.droppingCellBackGroundView.frame; + cell.contentView.backgroundColor = [UIColor clearColor]; + cell.backgroundColor = [UIColor clearColor]; + + return cell; + } + + if (anIndexPath.row > self.droppingCellIndexPath.row) + { + indexPath = [NSIndexPath indexPathForRow:anIndexPath.row-1 inSection:anIndexPath.section]; + } + } + + if (self.hiddenCellIndexPath && [anIndexPath isEqual:self.hiddenCellIndexPath]) + { + indexPath = [NSIndexPath indexPathForRow:anIndexPath.row-1 inSection:anIndexPath.section]; + } + + UITableViewCell* cell = [super tableView:tableView cellForRowAtIndexPath:indexPath]; // on invite cell, add listeners on accept / reject buttons if (cell && [cell isKindOfClass:[InviteRecentTableViewCell class]]) @@ -273,25 +340,35 @@ return cell; } -- (id)cellDataAtIndexPath:(NSIndexPath *)indexPath +- (id)cellDataAtIndexPath:(NSIndexPath *)anIndexPath { id cellData = nil; + NSInteger row = anIndexPath.row; + NSInteger section = anIndexPath.section; - if (indexPath.section == favoritesSection) + if (self.droppingCellIndexPath && (self.droppingCellIndexPath.section == section)) { - cellData = [favoriteCellDataArray objectAtIndex:indexPath.row]; + if (anIndexPath.row > self.droppingCellIndexPath.row) + { + row = anIndexPath.row - 1; + } } - else if (indexPath.section == conversationSection) + + if (section == favoritesSection) { - cellData = [conversationCellDataArray objectAtIndex:indexPath.row]; + cellData = [favoriteCellDataArray objectAtIndex:row]; } - else if (indexPath.section == lowPrioritySection) + else if (section== conversationSection) { - cellData = [lowPriorityCellDataArray objectAtIndex:indexPath.row]; + cellData = [conversationCellDataArray objectAtIndex:row]; } - else if (indexPath.section == invitesSection) + else if (section == lowPrioritySection) { - cellData = [invitesCellDataArray objectAtIndex:indexPath.row]; + cellData = [lowPriorityCellDataArray objectAtIndex:row]; + } + else if (section == invitesSection) + { + cellData = [invitesCellDataArray objectAtIndex:row]; } return cellData; @@ -299,6 +376,11 @@ - (CGFloat)cellHeightAtIndexPath:(NSIndexPath *)indexPath { + if (self.droppingCellIndexPath && [indexPath isEqual:self.droppingCellIndexPath]) + { + return self.droppingCellBackGroundView.frame.size.height; + } + // Override this method here to use our own cellDataAtIndexPath id cellData = [self cellDataAtIndexPath:indexPath]; @@ -494,6 +576,12 @@ - (void)dataSource:(MXKDataSource*)dataSource didCellChange:(id)changes { + // Refresh is disabled during drag&drop animation + if (self.droppingCellIndexPath) + { + return; + } + // FIXME : manage multi accounts // to manage multi accounts // this method in MXKInterleavedRecentsDataSource must be split in two parts @@ -519,4 +607,98 @@ [super destroy]; } + +#pragma mark - drag and drop managemenent + +- (BOOL)isDraggableCellAt:(NSIndexPath*)path +{ + return (path && ((path.section == favoritesSection) || (path.section == lowPrioritySection) || (path.section == conversationSection))); +} + +- (BOOL)canCellMoveFrom:(NSIndexPath*)oldPath to:(NSIndexPath*)newPath +{ + BOOL res = [self isDraggableCellAt:oldPath] && [self isDraggableCellAt:newPath]; + + // the both index pathes are movable + if (res) + { + // only the favorites cell can be moved within the same section + res &= (oldPath.section == favoritesSection) || (newPath.section != oldPath.section); + + // other cases ? + } + + return res; +} + +- (NSString*)roomTagAt:(NSIndexPath*)path +{ + if (path.section == favoritesSection) + { + return kMXRoomTagFavourite; + } + else if (path.section == lowPrioritySection) + { + return kMXRoomTagLowPriority; + } + + return nil; +} + +- (void)moveRoomCell:(MXRoom*)room from:(NSIndexPath*)oldPath to:(NSIndexPath*)newPath success:(void (^)())moveSuccess failure:(void (^)(NSError *error))moveFailure; +{ + NSLog(@"[RecentsDataSource] moveCellFrom (%d, %d) to (%d, %d)", oldPath.section, oldPath.row, newPath.section, newPath.row); + + if ([self canCellMoveFrom:oldPath to:newPath] && ![newPath isEqual:oldPath]) + { + NSString* oldRoomTag = [self roomTagAt:oldPath]; + NSString* dstRoomTag = [self roomTagAt:newPath]; + NSUInteger oldPos = (oldPath.section == newPath.section) ? oldPath.row : NSNotFound; + + NSString* tagOrder = [room.mxSession tagOrderToBeAtIndex:newPath.row from:oldPos withTag:dstRoomTag]; + + NSLog(@"[RecentsDataSource] Update the room %@ [%@] tag from %@ to %@ with tag order %@", room.state.roomId, room.state.displayname, oldRoomTag, dstRoomTag, tagOrder); + + [room replaceTag:oldRoomTag + byTag:dstRoomTag + withOrder:tagOrder + success: ^{ + + NSLog(@"[RecentsDataSource] move is done"); + + if (moveSuccess) + { + moveSuccess(); + } + + // wait the server echo to reload the tableview. + + } failure:^(NSError *error) { + + NSLog(@"[RecentsDataSource] Failed to update the tag %@ of room (%@) failed: %@", dstRoomTag, room.state.roomId, error); + + if (moveFailure) + { + moveFailure(error); + } + + [self refreshRoomsSectionsAndReload]; + + // Notify MatrixKit user + [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error]; + }]; + } + else + { + NSLog(@"[RecentsDataSource] cannot move this cell"); + + if (moveFailure) + { + moveFailure(nil); + } + + [self refreshRoomsSectionsAndReload]; + } +} + @end diff --git a/Vector/ViewController/HomeViewController.m b/Vector/ViewController/HomeViewController.m index 80f04b6aa..510c474b4 100644 --- a/Vector/ViewController/HomeViewController.m +++ b/Vector/ViewController/HomeViewController.m @@ -184,9 +184,6 @@ [tap setDelegate:self]; [createNewRoomImageView addGestureRecognizer:tap]; } - - // Forward the event to the child - [self.displayedViewController viewWillAppear:animated]; } - (void)viewDidAppear:(BOOL)animated @@ -434,27 +431,41 @@ self.navigationItem.rightBarButtonItem = backupRightBarButtonItem; } + [recentsDataSource searchWithPatterns:nil]; + // Hide the tabs header if (animated) { + // If the currently selected tab is the recents, force to show it right now + // The transition looks smoother + if (self.selectedViewController.view.hidden == YES && self.selectedViewController == recentsViewController) + { + self.selectedViewController.view.hidden = NO; + } + [UIView animateWithDuration:.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn animations:^{ self.selectionContainerHeightConstraint.constant = 0; [self.view layoutIfNeeded]; } - completion:nil]; + completion:^(BOOL finished) { + // Go back to the recents tab + // Do it at the end of the animation when the tabs header of the SegmentedVC is hidden + // so that the user cannot see the selection bar of this header moving + self.selectedIndex = 0; + self.selectedViewController.view.hidden = NO; + }]; } else { self.selectionContainerHeightConstraint.constant = 0; [self.view layoutIfNeeded]; - } - // Go back under the recents tab - // TODO: Open the feature in SegmentedVC - [recentsDataSource searchWithPatterns:nil]; - self.displayedViewController.view.hidden = NO; + // Go back to the recents tab + self.selectedIndex = 0; + self.selectedViewController.view.hidden = NO; + } } // Update search results under the currently selected tab @@ -462,10 +473,10 @@ { if (searchBar.text.length) { - self.displayedViewController.view.hidden = NO; + self.selectedViewController.view.hidden = NO; // Forward the search request to the data source - if (self.displayedViewController == recentsViewController) + if (self.selectedViewController == recentsViewController) { [recentsDataSource searchWithPatterns:@[searchBar.text]]; } @@ -473,7 +484,7 @@ else { // Nothing to search = Show nothing - self.displayedViewController.view.hidden = YES; + self.selectedViewController.view.hidden = YES; } } @@ -481,7 +492,7 @@ - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { - if (self.displayedViewController == recentsViewController) + if (self.selectedViewController == recentsViewController) { // As the search is local, it can be updated on each text change [self updateSearch]; diff --git a/Vector/ViewController/RecentsViewController.m b/Vector/ViewController/RecentsViewController.m index bfd650c99..012949138 100644 --- a/Vector/ViewController/RecentsViewController.m +++ b/Vector/ViewController/RecentsViewController.m @@ -36,6 +36,13 @@ // The "parent" segmented view controller HomeViewController *homeViewController; + + // recents drag and drop management + UIImageView *cellSnapshot; + NSIndexPath* movingCellPath; + MXRoom* movingRoom; + + NSIndexPath* lastPotentialCellPath; } @end @@ -65,6 +72,9 @@ // Register here the customized cell view class used to render recents [self.recentsTableView registerNib:RecentTableViewCell.nib forCellReuseIdentifier:RecentTableViewCell.defaultReuseIdentifier]; [self.recentsTableView registerNib:InviteRecentTableViewCell.nib forCellReuseIdentifier:InviteRecentTableViewCell.defaultReuseIdentifier]; + + UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onRecentsLongPress:)]; + [self.recentsTableView addGestureRecognizer:longPress]; } - (void)destroy @@ -214,6 +224,12 @@ - (void)dataSource:(MXKDataSource *)dataSource didCellChange:(id)changes { + // do not refresh if there is a pending recent drag and drop + if (movingCellPath) + { + return; + } + if ([dataSource isKindOfClass:[RecentsDataSource class]]) { RecentsDataSource* recentsDataSource = (RecentsDataSource*)dataSource; @@ -234,7 +250,6 @@ [self.delegate recentListViewController:self didSelectRoom:room.state.roomId inMatrixSession:room.mxSession]; }; } - [self.recentsTableView reloadData]; @@ -317,7 +332,6 @@ static NSMutableDictionary* backgroundByImageNameDict; { NSString* title = @" "; - // pushes settings BOOL isMuted = ![self.dataSource isRoomNotifiedAtIndexPath:indexPath]; @@ -457,4 +471,217 @@ static NSMutableDictionary* backgroundByImageNameDict; [self performSegueWithIdentifier:@"presentSearch" sender:self]; } +#pragma mark - recents drag & drop management + +- (void)onRecentsDragEnd +{ + [cellSnapshot removeFromSuperview]; + cellSnapshot = nil; + movingCellPath = nil; + movingRoom = nil; + + lastPotentialCellPath = nil; + ((RecentsDataSource*)self.dataSource).droppingCellIndexPath = nil; + ((RecentsDataSource*)self.dataSource).hiddenCellIndexPath = nil; + + [self.activityIndicator stopAnimating]; +} + +- (IBAction) onRecentsLongPress:(id)sender +{ + RecentsDataSource* recentsDataSource = nil; + + if ([self.dataSource isKindOfClass:[RecentsDataSource class]]) + { + recentsDataSource = (RecentsDataSource*)self.dataSource; + } + + // only support RecentsDataSource + if (!recentsDataSource) + { + return; + } + + UILongPressGestureRecognizer *longPress = (UILongPressGestureRecognizer *)sender; + UIGestureRecognizerState state = longPress.state; + + // check if there is a moving cell during the long press managemnt + if ((state != UIGestureRecognizerStateBegan) && !movingCellPath) + { + return; + } + + CGPoint location = [longPress locationInView:self.recentsTableView]; + + switch (state) + { + // step 1 : display the selected cell + case UIGestureRecognizerStateBegan: + { + NSIndexPath *indexPath = [self.recentsTableView indexPathForRowAtPoint:location]; + + // check if the cell can be moved + if (indexPath && [recentsDataSource isDraggableCellAt:indexPath]) + { + UITableViewCell *cell = [self.recentsTableView cellForRowAtIndexPath:indexPath]; + + // snapshot the cell + UIGraphicsBeginImageContextWithOptions(cell.bounds.size, NO, 0); + [cell.layer renderInContext:UIGraphicsGetCurrentContext()]; + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + cellSnapshot = [[UIImageView alloc] initWithImage:image]; + recentsDataSource.droppingCellBackGroundView = [[UIImageView alloc] initWithImage:image]; + + // display the selected cell over the tableview + CGPoint center = cell.center; + center.y = location.y; + cellSnapshot.center = center; + cellSnapshot.alpha = 0.5f; + [self.recentsTableView addSubview:cellSnapshot]; + + cell = [[UITableViewCell alloc] init]; + cell.frame = CGRectMake(0, 0, 100, 80); + cell.backgroundColor = [UIColor redColor]; + + lastPotentialCellPath = indexPath; + recentsDataSource.droppingCellIndexPath = indexPath; + + movingCellPath = indexPath; + recentsDataSource.hiddenCellIndexPath = movingCellPath; + movingRoom = [recentsDataSource getRoomAtIndexPath:movingCellPath]; + } + break; + } + + // step 2 : the cell must follow the finger + case UIGestureRecognizerStateChanged: + { + CGPoint center = cellSnapshot.center; + CGFloat halfHeight = cellSnapshot.frame.size.height / 2.0f; + CGFloat cellTop = location.y - halfHeight; + CGFloat cellBottom = location.y + halfHeight; + + CGPoint contentOffset = self.recentsTableView.contentOffset; + CGFloat height = MIN(self.recentsTableView.frame.size.height, self.recentsTableView.contentSize.height); + CGFloat bottomOffset = contentOffset.y + height; + + // check if the moving cell is trying to move under the tableview + if (cellBottom > self.recentsTableView.contentSize.height) + { + // force the cell to stay at the tableview bottom + location.y = self.recentsTableView.contentSize.height - halfHeight; + } + // check if the cell is moving over the displayed tableview bottom + else if (cellBottom > bottomOffset) + { + CGFloat diff = cellBottom - bottomOffset; + + // moving down the cell + location.y -= diff; + // scroll up the tableview + contentOffset.y += diff; + } + // the moving is tryin to move over the tableview topmost + else if (cellTop < 0) + { + // force to stay in the topmost + contentOffset.y = 0; + location.y = contentOffset.y + halfHeight; + } + // the moving cell is displayed over the current scroll top + else if (cellTop < contentOffset.y) + { + CGFloat diff = contentOffset.y - cellTop; + + // move up the cell and the table up + location.y -= diff; + contentOffset.y -= diff; + } + + // move the cell to follow the user finger + center.y = location.y; + cellSnapshot.center = center; + + // scroll the tableview if it is required + if (contentOffset.y != self.recentsTableView.contentOffset.y) + { + [self.recentsTableView setContentOffset:contentOffset animated:NO]; + } + + NSIndexPath *indexPath = [self.recentsTableView indexPathForRowAtPoint:location]; + + if (![indexPath isEqual:lastPotentialCellPath]) + { + if ([recentsDataSource canCellMoveFrom:movingCellPath to:indexPath]) + { + [self.recentsTableView beginUpdates]; + if (recentsDataSource.droppingCellIndexPath && recentsDataSource.hiddenCellIndexPath) + { + [self.recentsTableView moveRowAtIndexPath:lastPotentialCellPath toIndexPath:indexPath]; + } + else if (indexPath) + { + [self.recentsTableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; + [self.recentsTableView deleteRowsAtIndexPaths:@[movingCellPath] withRowAnimation:UITableViewRowAnimationNone]; + } + recentsDataSource.hiddenCellIndexPath = movingCellPath; + recentsDataSource.droppingCellIndexPath = indexPath; + [self.recentsTableView endUpdates]; + } + // the cell cannot be moved + else if (recentsDataSource.droppingCellIndexPath) + { + NSIndexPath* pathToDelete = recentsDataSource.droppingCellIndexPath; + NSIndexPath* pathToAdd = recentsDataSource.hiddenCellIndexPath; + + // remove it + [self.recentsTableView beginUpdates]; + [self.recentsTableView deleteRowsAtIndexPaths:@[pathToDelete] withRowAnimation:UITableViewRowAnimationNone]; + [self.recentsTableView insertRowsAtIndexPaths:@[pathToAdd] withRowAnimation:UITableViewRowAnimationNone]; + recentsDataSource.droppingCellIndexPath = nil; + recentsDataSource.hiddenCellIndexPath = nil; + [self.recentsTableView endUpdates]; + } + + lastPotentialCellPath = indexPath; + } + + break; + } + + // step 3 : remove the view + // and insert when it is possible. + case UIGestureRecognizerStateEnded: + { + [cellSnapshot removeFromSuperview]; + cellSnapshot = nil; + + [self.activityIndicator startAnimating]; + + [recentsDataSource moveRoomCell:movingRoom from:movingCellPath to:lastPotentialCellPath success:^{ + + [self onRecentsDragEnd]; + + } failure:^(NSError *error) { + + [self onRecentsDragEnd]; + + }]; + + break; + } + + // default behaviour + // remove the cell and cancel the insertion + default: + { + [self onRecentsDragEnd]; + break; + } + } +} + + @end diff --git a/Vector/ViewController/SegmentedViewController.h b/Vector/ViewController/SegmentedViewController.h index c33861acf..93c6b36f2 100644 --- a/Vector/ViewController/SegmentedViewController.h +++ b/Vector/ViewController/SegmentedViewController.h @@ -32,10 +32,15 @@ limitations under the License. @property (weak, nonatomic) IBOutlet UIView *viewControllerContainer; @property (weak, nonatomic) IBOutlet UIImageView *backgroundImageView; +/** + The index of the view controller that currently has the focus. + */ +@property (nonatomic) NSUInteger selectedIndex; + /** The view controller that currently has the focus. */ -@property (nonatomic, readonly) UIViewController *displayedViewController; +@property (nonatomic, readonly) UIViewController *selectedViewController; /** Returns the `UINib` object initialized for a `SegmentedViewController`. diff --git a/Vector/ViewController/SegmentedViewController.m b/Vector/ViewController/SegmentedViewController.m index a85d31928..7662791aa 100644 --- a/Vector/ViewController/SegmentedViewController.m +++ b/Vector/ViewController/SegmentedViewController.m @@ -38,15 +38,11 @@ // the selected marker view UIView* selectedMarkerView; NSLayoutConstraint *leftMarkerViewConstraint; - - // the index of the viewcontroller displayed at first load - NSUInteger selectedIndex; } @end @implementation SegmentedViewController -@synthesize displayedViewController; #pragma mark - Class methods @@ -72,7 +68,16 @@ { viewControllers = someViewControllers; sectionTitles = titles; - selectedIndex = index; + _selectedIndex = index; +} + +- (void)setSelectedIndex:(NSUInteger)selectedIndex +{ + if (_selectedIndex != selectedIndex) + { + _selectedIndex = selectedIndex; + [self displaySelectedViewController]; + } } #pragma mark - @@ -251,7 +256,7 @@ leftMarkerViewConstraint = [NSLayoutConstraint constraintWithItem:selectedMarkerView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual - toItem:[sectionLabels objectAtIndex:selectedIndex] + toItem:[sectionLabels objectAtIndex:_selectedIndex] attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0]; @@ -299,9 +304,9 @@ - (void)displaySelectedViewController { - if (displayedViewController) + if (_selectedViewController) { - NSUInteger index = [viewControllers indexOfObject:displayedViewController]; + NSUInteger index = [viewControllers indexOfObject:_selectedViewController]; if (index != NSNotFound) { @@ -309,27 +314,44 @@ label.font = [UIFont systemFontOfSize:17]; } - [displayedViewController.view removeFromSuperview]; - [displayedViewController removeFromParentViewController]; + [_selectedViewController.view removeFromSuperview]; + [_selectedViewController removeFromParentViewController]; - [self removeConstraint:displayedViewController.view constraint:displayedVCWidthConstraint]; - [self removeConstraint:displayedViewController.view constraint:displayedVCHeightConstraint]; + [self removeConstraint:_selectedViewController.view constraint:displayedVCWidthConstraint]; + [self removeConstraint:_selectedViewController.view constraint:displayedVCHeightConstraint]; [self removeConstraint:self.viewControllerContainer constraint:displayedVCTopConstraint]; [self removeConstraint:self.viewControllerContainer constraint:displayedVCLeftConstraint]; } - UILabel* label = [sectionLabels objectAtIndex:selectedIndex]; + UILabel* label = [sectionLabels objectAtIndex:_selectedIndex]; label.font = [UIFont boldSystemFontOfSize:17]; + + // update the marker view position + [self removeConstraint:selectedMarkerView constraint:leftMarkerViewConstraint]; + + leftMarkerViewConstraint = [NSLayoutConstraint constraintWithItem:selectedMarkerView + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:[sectionLabels objectAtIndex:_selectedIndex] + attribute:NSLayoutAttributeLeading + multiplier:1.0 + constant:0]; + + [self addConstraint:selectedMarkerView constraint:leftMarkerViewConstraint]; + + // Set the new selected view controller + _selectedViewController = [viewControllers objectAtIndex:_selectedIndex]; + + // Make iOS invoke child viewWillAppear + [_selectedViewController beginAppearanceTransition:YES animated:YES]; + + [self addChildViewController:_selectedViewController]; - displayedViewController = [viewControllers objectAtIndex:selectedIndex]; - - [self addChildViewController:displayedViewController]; - - [displayedViewController.view setTranslatesAutoresizingMaskIntoConstraints:NO]; - [self.viewControllerContainer addSubview:displayedViewController.view]; + [_selectedViewController.view setTranslatesAutoresizingMaskIntoConstraints:NO]; + [self.viewControllerContainer addSubview:_selectedViewController.view]; - displayedVCTopConstraint = [NSLayoutConstraint constraintWithItem:displayedViewController.view + displayedVCTopConstraint = [NSLayoutConstraint constraintWithItem:_selectedViewController.view attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.viewControllerContainer @@ -338,7 +360,7 @@ constant:0.0f]; [self addConstraint:self.viewControllerContainer constraint:displayedVCTopConstraint]; - displayedVCLeftConstraint = [NSLayoutConstraint constraintWithItem:displayedViewController.view + displayedVCLeftConstraint = [NSLayoutConstraint constraintWithItem:_selectedViewController.view attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.viewControllerContainer @@ -348,25 +370,26 @@ [self addConstraint:self.viewControllerContainer constraint:displayedVCLeftConstraint]; - displayedVCWidthConstraint = [NSLayoutConstraint constraintWithItem:displayedViewController.view + displayedVCWidthConstraint = [NSLayoutConstraint constraintWithItem:_selectedViewController.view attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.viewControllerContainer attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0]; - [self addConstraint:displayedViewController.view constraint:displayedVCWidthConstraint]; + [self addConstraint:_selectedViewController.view constraint:displayedVCWidthConstraint]; - displayedVCHeightConstraint = [NSLayoutConstraint constraintWithItem:displayedViewController.view + displayedVCHeightConstraint = [NSLayoutConstraint constraintWithItem:_selectedViewController.view attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.viewControllerContainer attribute:NSLayoutAttributeHeight multiplier:1.0 constant:0]; - [self addConstraint:displayedViewController.view constraint:displayedVCHeightConstraint]; + [self addConstraint:_selectedViewController.view constraint:displayedVCHeightConstraint]; - [displayedViewController didMoveToParentViewController:self]; + [_selectedViewController didMoveToParentViewController:self]; + [_selectedViewController endAppearanceTransition]; // refresh the navbar background color // to display if the homeserver is reachable. @@ -380,23 +403,10 @@ NSUInteger pos = [sectionLabels indexOfObject:gestureRecognizer.view]; // check if there is an update before triggering anything - if ((pos != NSNotFound) && (selectedIndex != pos)) + if ((pos != NSNotFound) && (_selectedIndex != pos)) { // update the selected index - selectedIndex = pos; - - // update the marker view position - [self removeConstraint:selectedMarkerView constraint:leftMarkerViewConstraint]; - - leftMarkerViewConstraint = [NSLayoutConstraint constraintWithItem:selectedMarkerView - attribute:NSLayoutAttributeLeading - relatedBy:NSLayoutRelationEqual - toItem:[sectionLabels objectAtIndex:selectedIndex] - attribute:NSLayoutAttributeLeading - multiplier:1.0 - constant:0]; - - [self addConstraint:selectedMarkerView constraint:leftMarkerViewConstraint]; + _selectedIndex = pos; [self displaySelectedViewController]; } diff --git a/Vector/Views/RoomBubbleList/RoomIncomingAttachmentBubbleCell.m b/Vector/Views/RoomBubbleList/RoomIncomingAttachmentBubbleCell.m index 89e68b84a..1acfff6c9 100644 --- a/Vector/Views/RoomBubbleList/RoomIncomingAttachmentBubbleCell.m +++ b/Vector/Views/RoomBubbleList/RoomIncomingAttachmentBubbleCell.m @@ -16,6 +16,15 @@ #import "RoomIncomingAttachmentBubbleCell.h" +#import "VectorDesignValues.h" + @implementation RoomIncomingAttachmentBubbleCell +- (void)awakeFromNib +{ + [super awakeFromNib]; + + self.userNameLabel.textColor = VECTOR_TEXT_BLACK_COLOR; +} + @end diff --git a/Vector/Views/RoomBubbleList/RoomIncomingAttachmentBubbleCell.xib b/Vector/Views/RoomBubbleList/RoomIncomingAttachmentBubbleCell.xib index 517714adf..ae04ec335 100644 --- a/Vector/Views/RoomBubbleList/RoomIncomingAttachmentBubbleCell.xib +++ b/Vector/Views/RoomBubbleList/RoomIncomingAttachmentBubbleCell.xib @@ -9,14 +9,14 @@ - + - + - + @@ -24,36 +24,28 @@ - - + - + @@ -132,26 +124,24 @@ - + - - - + + + - + - + - - - + + - + - - + @@ -175,7 +165,6 @@ - diff --git a/Vector/Views/RoomBubbleList/RoomIncomingAttachmentWithPaginationTitleBubbleCell.h b/Vector/Views/RoomBubbleList/RoomIncomingAttachmentWithPaginationTitleBubbleCell.h index 4844d96cd..50bc9219e 100644 --- a/Vector/Views/RoomBubbleList/RoomIncomingAttachmentWithPaginationTitleBubbleCell.h +++ b/Vector/Views/RoomBubbleList/RoomIncomingAttachmentWithPaginationTitleBubbleCell.h @@ -23,5 +23,6 @@ @property (weak, nonatomic) IBOutlet UIView *paginationTitleView; @property (weak, nonatomic) IBOutlet UILabel *paginationLabel; +@property (weak, nonatomic) IBOutlet UIView *paginationSeparatorView; @end diff --git a/Vector/Views/RoomBubbleList/RoomIncomingAttachmentWithPaginationTitleBubbleCell.m b/Vector/Views/RoomBubbleList/RoomIncomingAttachmentWithPaginationTitleBubbleCell.m index 479b542d5..db2f6defc 100644 --- a/Vector/Views/RoomBubbleList/RoomIncomingAttachmentWithPaginationTitleBubbleCell.m +++ b/Vector/Views/RoomBubbleList/RoomIncomingAttachmentWithPaginationTitleBubbleCell.m @@ -16,6 +16,25 @@ #import "RoomIncomingAttachmentWithPaginationTitleBubbleCell.h" +#import "VectorDesignValues.h" + @implementation RoomIncomingAttachmentWithPaginationTitleBubbleCell +- (void)awakeFromNib +{ + [super awakeFromNib]; + + self.userNameLabel.textColor = VECTOR_TEXT_BLACK_COLOR; +} + +- (void)render:(MXKCellData *)cellData +{ + [super render:cellData]; + + if (self.bubbleData) + { + self.paginationLabel.text = [self.bubbleData.eventFormatter dateStringFromDate:self.bubbleData.date withTime:NO]; + } +} + @end diff --git a/Vector/Views/RoomBubbleList/RoomIncomingAttachmentWithPaginationTitleBubbleCell.xib b/Vector/Views/RoomBubbleList/RoomIncomingAttachmentWithPaginationTitleBubbleCell.xib index da644e81f..dc4c17203 100644 --- a/Vector/Views/RoomBubbleList/RoomIncomingAttachmentWithPaginationTitleBubbleCell.xib +++ b/Vector/Views/RoomBubbleList/RoomIncomingAttachmentWithPaginationTitleBubbleCell.xib @@ -9,37 +9,42 @@ - + - + - + + + + + + - - + - - - + - + @@ -47,36 +52,28 @@ - - + - + @@ -154,31 +151,29 @@ - - + - - - + + + - - + + - - + - - + + - - + + - - + + @@ -197,13 +192,13 @@ + - diff --git a/Vector/Views/RoomBubbleList/RoomIncomingAttachmentWithoutSenderInfoBubbleCell.xib b/Vector/Views/RoomBubbleList/RoomIncomingAttachmentWithoutSenderInfoBubbleCell.xib index 84a26bd3c..0c88024e0 100644 --- a/Vector/Views/RoomBubbleList/RoomIncomingAttachmentWithoutSenderInfoBubbleCell.xib +++ b/Vector/Views/RoomBubbleList/RoomIncomingAttachmentWithoutSenderInfoBubbleCell.xib @@ -9,14 +9,14 @@ - + - + - + @@ -25,7 +25,7 @@ diff --git a/Vector/Views/RoomBubbleList/RoomOutgoingAttachmentBubbleCell.m b/Vector/Views/RoomBubbleList/RoomOutgoingAttachmentBubbleCell.m index e817473a4..49a47576f 100644 --- a/Vector/Views/RoomBubbleList/RoomOutgoingAttachmentBubbleCell.m +++ b/Vector/Views/RoomBubbleList/RoomOutgoingAttachmentBubbleCell.m @@ -16,12 +16,16 @@ #import "RoomOutgoingAttachmentBubbleCell.h" +#import "VectorDesignValues.h" + @implementation RoomOutgoingAttachmentBubbleCell - (void)awakeFromNib { [super awakeFromNib]; self.readReceiptsAlignment = ReadReceiptAlignmentRight; + + self.userNameLabel.textColor = VECTOR_TEXT_BLACK_COLOR; } - (void)render:(MXKCellData *)cellData diff --git a/Vector/Views/RoomBubbleList/RoomOutgoingAttachmentBubbleCell.xib b/Vector/Views/RoomBubbleList/RoomOutgoingAttachmentBubbleCell.xib index eba62bada..9b196dfda 100644 --- a/Vector/Views/RoomBubbleList/RoomOutgoingAttachmentBubbleCell.xib +++ b/Vector/Views/RoomBubbleList/RoomOutgoingAttachmentBubbleCell.xib @@ -9,14 +9,14 @@ - + - + - + @@ -25,27 +25,27 @@ - + - + -