Chat screen: Timestamp and message edition display.

We unify here the design across iOS and Android:
- The messages is over the full width.
- The time stamp is at the user name level.
- If several messages are sent in a row, clicking on a message (not the first one) will make it move slightly down to display the timestamp just above.
- On the right side of the timestamp we would have an "Edit" icon.
This commit is contained in:
giomfo
2016-03-09 18:29:39 +01:00
parent d75f564a6f
commit 507abb294d
25 changed files with 387 additions and 490 deletions
+15 -2
View File
@@ -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
+204 -68
View File
@@ -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;
+45 -55
View File
@@ -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<MXKRoomBubbleCellDataStoring> 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