Merge remote-tracking branch 'origin/develop' into room_photo_selection

This commit is contained in:
yannick
2015-12-16 15:53:42 +01:00
82 changed files with 1435 additions and 550 deletions
+13
View File
@@ -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
+147 -2
View File
@@ -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
+147 -7
View File
@@ -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<MXKRoomBubbleCellDataStoring> 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];
+33
View File
@@ -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
+204 -22
View File
@@ -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<MXKRecentCellDataStoring>)cellDataAtIndexPath:(NSIndexPath *)indexPath
- (id<MXKRecentCellDataStoring>)cellDataAtIndexPath:(NSIndexPath *)anIndexPath
{
id<MXKRecentCellDataStoring> 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<MXKRecentCellDataStoring> 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