diff --git a/Riot.xcodeproj/project.pbxproj b/Riot.xcodeproj/project.pbxproj index ad472c832..dc22970ad 100644 --- a/Riot.xcodeproj/project.pbxproj +++ b/Riot.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 24B5103E1EFA7083004C6AD2 /* ReadReceiptsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 24B5103D1EFA7083004C6AD2 /* ReadReceiptsViewController.m */; }; + 24B510401EFA88CC004C6AD2 /* ReadReceiptsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 24B5103F1EFA88CC004C6AD2 /* ReadReceiptsViewController.xib */; }; 3205ED7D1E976C8A003D65FA /* DirectoryServerPickerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3205ED7C1E976C8A003D65FA /* DirectoryServerPickerViewController.m */; }; 3205ED841E97725E003D65FA /* DirectoryServerTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 3205ED821E97725E003D65FA /* DirectoryServerTableViewCell.m */; }; 3205ED851E97725E003D65FA /* DirectoryServerTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3205ED831E97725E003D65FA /* DirectoryServerTableViewCell.xib */; }; @@ -471,6 +473,9 @@ /* Begin PBXFileReference section */ 1129C74A281B080432B1A1A1 /* Pods-Riot.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Riot.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Riot/Pods-Riot.debug.xcconfig"; sourceTree = ""; }; + 24B5103C1EFA7083004C6AD2 /* ReadReceiptsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ReadReceiptsViewController.h; sourceTree = ""; }; + 24B5103D1EFA7083004C6AD2 /* ReadReceiptsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ReadReceiptsViewController.m; sourceTree = ""; }; + 24B5103F1EFA88CC004C6AD2 /* ReadReceiptsViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ReadReceiptsViewController.xib; sourceTree = ""; }; 3205ED7B1E976C8A003D65FA /* DirectoryServerPickerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DirectoryServerPickerViewController.h; sourceTree = ""; }; 3205ED7C1E976C8A003D65FA /* DirectoryServerPickerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DirectoryServerPickerViewController.m; sourceTree = ""; }; 3205ED811E97725E003D65FA /* DirectoryServerTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DirectoryServerTableViewCell.h; sourceTree = ""; }; @@ -1585,6 +1590,9 @@ F083BC541E7009EC00A9B29C /* StartChatViewController.xib */, F083BC551E7009EC00A9B29C /* UsersDevicesViewController.h */, F083BC561E7009EC00A9B29C /* UsersDevicesViewController.m */, + 24B5103C1EFA7083004C6AD2 /* ReadReceiptsViewController.h */, + 24B5103D1EFA7083004C6AD2 /* ReadReceiptsViewController.m */, + 24B5103F1EFA88CC004C6AD2 /* ReadReceiptsViewController.xib */, ); path = ViewController; sourceTree = ""; @@ -2041,6 +2049,7 @@ F083BD881E7009ED00A9B29C /* file_doc_icon@3x.png in Resources */, F083BDAB1E7009ED00A9B29C /* placeholder@2x.png in Resources */, F083BE4D1E7009ED00A9B29C /* RoomOutgoingEncryptedAttachmentWithoutSenderInfoBubbleCell.xib in Resources */, + 24B510401EFA88CC004C6AD2 /* ReadReceiptsViewController.xib in Resources */, F083BDC81E7009ED00A9B29C /* search_icon.png in Resources */, F083BDE91E7009ED00A9B29C /* ring.mp3 in Resources */, F083BE431E7009ED00A9B29C /* RoomIncomingEncryptedTextMsgWithoutSenderInfoBubbleCell.xib in Resources */, @@ -2405,7 +2414,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -2457,6 +2466,7 @@ F083BE761E7009ED00A9B29C /* RoomOutgoingTextMsgWithPaginationTitleBubbleCell.m in Sources */, F083BE5A1E7009ED00A9B29C /* RoomIncomingAttachmentBubbleCell.m in Sources */, F083BE541E7009ED00A9B29C /* RoomOutgoingEncryptedTextMsgWithoutSenderNameBubbleCell.m in Sources */, + 24B5103E1EFA7083004C6AD2 /* ReadReceiptsViewController.m in Sources */, F083BE621E7009ED00A9B29C /* RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m in Sources */, F083BE881E7009ED00A9B29C /* RoomMemberTitleView.m in Sources */, F083BE701E7009ED00A9B29C /* RoomOutgoingTextMsgBubbleCell.m in Sources */, diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index a0877f3e9..874dab02e 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -414,6 +414,10 @@ "room_details_copy_room_address" = "Copy Room Address"; "room_details_copy_room_url" = "Copy Room URL"; +// Read Receipts +"read_receipts_list" = "Read Receipts List"; +"receipt_status_read" = "Read: "; + // Media picker "media_picker_library" = "Library"; "media_picker_select" = "Select"; diff --git a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.h b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.h index 9f5dc357d..7967ba35e 100644 --- a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.h +++ b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.h @@ -23,6 +23,13 @@ */ extern NSString *const kMXKRoomBubbleCellRiotEditButtonPressed; +/** + Action identifier used when the user tapped on receipts area. + + The 'userInfo' disctionary contains an 'MXKReceiptSendersContainer' object under the 'kMXKRoomBubbleCellReceiptsContainerKey' key, representing the receipts container which was tapped on. + */ +extern NSString *const kMXKRoomBubbleCellTapOnReceiptsContainer; + /** Define a `MXKRoomBubbleTableViewCell` category at Riot level to handle bubble customisation. */ @@ -56,6 +63,11 @@ extern NSString *const kMXKRoomBubbleCellRiotEditButtonPressed; */ - (void)addDateLabel; +/** + Called when the user taps on the Receipt Container. + */ +- (IBAction)onReceiptContainerTap:(UITapGestureRecognizer *)sender; + /** Blur the view by adding a transparent overlay. Default is NO. */ diff --git a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m index 02cb801b6..f4c7562b0 100644 --- a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m +++ b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m @@ -29,6 +29,7 @@ #define VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_WIDTH 4 NSString *const kMXKRoomBubbleCellRiotEditButtonPressed = @"kMXKRoomBubbleCellRiotEditButtonPressed"; +NSString *const kMXKRoomBubbleCellTapOnReceiptsContainer = @"kMXKRoomBubbleCellTapOnReceiptsContainer"; @implementation MXKRoomBubbleTableViewCell (Riot) @@ -153,7 +154,7 @@ NSString *const kMXKRoomBubbleCellRiotEditButtonPressed = @"kMXKRoomBubbleCellRi // Retrieve the read receipts container related to the selected component (if any) // Blur the others - for (UIView* view in self.bubbleOverlayContainer.subviews) + for (UIView* view in self.tmpSubviews) { // Note read receipt container tag is equal to the index of the related component. if (view.tag != componentIndex) @@ -323,11 +324,14 @@ NSString *const kMXKRoomBubbleCellRiotEditButtonPressed = @"kMXKRoomBubbleCellRi self.bubbleOverlayContainer.alpha = 0.8; self.bubbleOverlayContainer.userInteractionEnabled = YES; - // Blur read receipts if any + // Blur subviews if any for (UIView* view in self.bubbleOverlayContainer.subviews) { view.alpha = 0.2; } + + // Move this view in front + [self.contentView bringSubviewToFront:self.bubbleOverlayContainer]; } else { @@ -338,7 +342,7 @@ NSString *const kMXKRoomBubbleCellRiotEditButtonPressed = @"kMXKRoomBubbleCellRi self.bubbleOverlayContainer.alpha = 1; self.bubbleOverlayContainer.userInteractionEnabled = NO; - // Restore read receipts display + // Restore subviews display for (UIView* view in self.bubbleOverlayContainer.subviews) { view.alpha = 1; @@ -406,6 +410,14 @@ NSString *const kMXKRoomBubbleCellRiotEditButtonPressed = @"kMXKRoomBubbleCellRi } } +- (IBAction)onReceiptContainerTap:(UITapGestureRecognizer *)sender +{ + if (self.delegate) + { + [self.delegate cell:self didRecognizeAction:kMXKRoomBubbleCellTapOnReceiptsContainer userInfo:@{kMXKRoomBubbleCellReceiptsContainerKey : sender.view}]; + } +} + #pragma mark - Internals - (void)addEditButtonForComponent:(NSUInteger)componentIndex completion:(void (^ __nullable)(BOOL finished))completion diff --git a/Riot/Model/Room/RoomDataSource.m b/Riot/Model/Room/RoomDataSource.m index 4a1a71d77..8a3f8ff6d 100644 --- a/Riot/Model/Room/RoomDataSource.m +++ b/Riot/Model/Room/RoomDataSource.m @@ -144,13 +144,8 @@ // Ignore the read receipts on the bubble without actual display. if ((self.showBubbleReceipts && cellData.hasReadReceipts) || self.showReadMarker) { - // Read receipts container are inserted here on the right side into the overlay container. + // Read receipts container are inserted here on the right side into the content view. // 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 = bubbleComponents.count; CGFloat bottomPositionY = bubbleCell.frame.size.height; while (index--) @@ -199,10 +194,26 @@ avatarsContainer.tag = index; [avatarsContainer refreshReceiptSenders:roomMembers withPlaceHolders:placeholders andAlignment:ReadReceiptAlignmentRight]; + avatarsContainer.readReceipts = receipts; + UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:cell action:@selector(onReceiptContainerTap:)]; + [tapRecognizer setNumberOfTapsRequired:1]; + [tapRecognizer setNumberOfTouchesRequired:1]; + [avatarsContainer addGestureRecognizer:tapRecognizer]; + avatarsContainer.userInteractionEnabled = YES; avatarsContainer.translatesAutoresizingMaskIntoConstraints = NO; avatarsContainer.accessibilityIdentifier = @"readReceiptsContainer"; - [bubbleCell.bubbleOverlayContainer addSubview:avatarsContainer]; + + // Add this read receipts container in the content view + if (!bubbleCell.tmpSubviews) + { + bubbleCell.tmpSubviews = [NSMutableArray arrayWithArray:@[avatarsContainer]]; + } + else + { + [bubbleCell.tmpSubviews addObject:avatarsContainer]; + } + [bubbleCell.contentView addSubview:avatarsContainer]; // Force receipts container size NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:avatarsContainer @@ -224,14 +235,14 @@ NSLayoutConstraint *trailingConstraint = [NSLayoutConstraint constraintWithItem:avatarsContainer attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual - toItem:bubbleCell.bubbleOverlayContainer + toItem:avatarsContainer.superview attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:-6]; NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:avatarsContainer attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual - toItem:bubbleCell.bubbleOverlayContainer + toItem:avatarsContainer.superview attribute:NSLayoutAttributeTop multiplier:1.0 constant:bottomPositionY - 13]; @@ -244,6 +255,13 @@ // Check whether the read marker must be displayed here. if (self.showReadMarker) { + // The read marker is added into the overlay container. + // CAUTION: Keep disabled the user interaction on this container to not disturb tap gesture handling. + bubbleCell.bubbleOverlayContainer.backgroundColor = [UIColor clearColor]; + bubbleCell.bubbleOverlayContainer.alpha = 1; + bubbleCell.bubbleOverlayContainer.userInteractionEnabled = NO; + bubbleCell.bubbleOverlayContainer.hidden = NO; + if ([component.event.eventId isEqualToString:self.room.accountData.readMarkerEventId]) { bubbleCell.readMarkerView = [[UIView alloc] initWithFrame:CGRectMake(0, bottomPositionY - 2, bubbleCell.bubbleOverlayContainer.frame.size.width, 2)]; diff --git a/Riot/ViewController/ReadReceiptsViewController.h b/Riot/ViewController/ReadReceiptsViewController.h new file mode 100644 index 000000000..eca67c3ce --- /dev/null +++ b/Riot/ViewController/ReadReceiptsViewController.h @@ -0,0 +1,24 @@ +/* + Copyright 2017 Aram Sargsyan + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import +#import + +@interface ReadReceiptsViewController : MXKViewController + ++ (void)openInViewController:(UIViewController *)viewController fromContainer:(MXKReceiptSendersContainer *)receiptSendersContainer withSession:(MXSession *)session; + +@end diff --git a/Riot/ViewController/ReadReceiptsViewController.m b/Riot/ViewController/ReadReceiptsViewController.m new file mode 100644 index 000000000..b2c29ed75 --- /dev/null +++ b/Riot/ViewController/ReadReceiptsViewController.m @@ -0,0 +1,168 @@ +/* + Copyright 2017 Aram Sargsyan + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "ReadReceiptsViewController.h" +#import +#import "RiotDesignValues.h" + +@interface ReadReceiptsViewController () + +@property (nonatomic) MXRestClient* restClient; +@property (nonatomic) MXSession *session; + +@property (nonatomic) NSArray *roomMembers; +@property (nonatomic) NSArray *placeholders; +@property (nonatomic) NSArray *receipts; + +@property (weak, nonatomic) IBOutlet UIView *overlayView; +@property (weak, nonatomic) IBOutlet UILabel *titleLabel; +@property (weak, nonatomic) IBOutlet UIView *containerView; +@property (weak, nonatomic) IBOutlet UITableView *receiptsTableView; + +@end + +@implementation ReadReceiptsViewController + +#pragma mark - Public + ++ (void)openInViewController:(UIViewController *)viewController fromContainer:(MXKReceiptSendersContainer *)receiptSendersContainer withSession:(MXSession *)session +{ + ReadReceiptsViewController *receiptsController = [[[self class] alloc] initWithNibName:NSStringFromClass([self class]) bundle:nil]; + receiptsController.restClient = receiptSendersContainer.restClient; + receiptsController.session = session; + + receiptsController.roomMembers = receiptSendersContainer.roomMembers; + receiptsController.placeholders = receiptSendersContainer.placeholders; + receiptsController.receipts = receiptSendersContainer.readReceipts; + + receiptsController.providesPresentationContextTransitionStyle = YES; + receiptsController.definesPresentationContext = YES; + receiptsController.modalPresentationStyle = UIModalPresentationOverFullScreen; + receiptsController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; + + [viewController presentViewController:receiptsController animated:YES completion:nil]; +} + +#pragma mark - Lifecycle + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + [self configureViews]; + [self configureReceiptsTableView]; + [self addOverlayViewGesture]; +} + +- (void)didReceiveMemoryWarning +{ + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +#pragma mark - Views + +- (void)configureViews +{ + self.containerView.layer.cornerRadius = 20; + self.titleLabel.text = NSLocalizedStringFromTable(@"read_receipts_list", @"Vector", nil); +} + +- (void)configureReceiptsTableView +{ + self.receiptsTableView.dataSource = self; + self.receiptsTableView.delegate = self; + self.receiptsTableView.showsVerticalScrollIndicator = NO; + self.receiptsTableView.separatorStyle = UITableViewCellSeparatorStyleNone; + + [self.receiptsTableView registerNib:[MXKReadReceiptTableViewCell nib] forCellReuseIdentifier:[MXKReadReceiptTableViewCell defaultReuseIdentifier]]; +} + +- (void)addOverlayViewGesture +{ + UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(overlayTap)]; + [tapRecognizer setNumberOfTapsRequired:1]; + [tapRecognizer setNumberOfTouchesRequired:1]; + [self.overlayView addGestureRecognizer:tapRecognizer]; +} + +#pragma mark - Actions + +- (void)overlayTap +{ + [self dismissViewControllerAnimated:YES completion:nil]; +} + +#pragma mark - UITableViewDataSource + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return self.roomMembers.count; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return 1; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + MXKReadReceiptTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[MXKReadReceiptTableViewCell defaultReuseIdentifier] forIndexPath:indexPath]; + + if (indexPath.row < self.roomMembers.count) + { + NSString *name = self.roomMembers[indexPath.row].displayname; + if (name.length == 0) { + name = self.roomMembers[indexPath.row].userId; + } + cell.displayNameLabel.text = name; + } + if (indexPath.row < self.placeholders.count) + { + NSString *avatarUrl = self.roomMembers[indexPath.row].avatarUrl; + if (self.restClient && avatarUrl) + { + CGFloat side = CGRectGetWidth(cell.avatarImageView.frame); + avatarUrl = [self.restClient urlOfContentThumbnail:avatarUrl toFitViewSize:CGSizeMake(side, side) withMethod:MXThumbnailingMethodCrop]; + } + [cell.avatarImageView setImageURL:avatarUrl withType:nil andImageOrientation:UIImageOrientationUp previewImage:self.placeholders[indexPath.row]]; + } + if (indexPath.row < self.receipts.count) + { + NSString *receiptReadText = NSLocalizedStringFromTable(@"receipt_status_read", @"Vector", nil); + NSString *receiptTimeText = [(MXKEventFormatter*)self.session.roomSummaryUpdateDelegate dateStringFromTimestamp:self.receipts[indexPath.row].ts withTime:YES]; + + NSMutableAttributedString *receiptDescription = [[NSMutableAttributedString alloc] initWithString:receiptReadText attributes:@{NSForegroundColorAttributeName : kRiotTextColorGray, NSFontAttributeName : [UIFont boldSystemFontOfSize:15]}]; + + [receiptDescription appendAttributedString:[[NSAttributedString alloc] initWithString:receiptTimeText attributes:@{NSForegroundColorAttributeName : kRiotTextColorGray, NSFontAttributeName : [UIFont systemFontOfSize:15]}]]; + + cell.receiptDescriptionLabel.attributedText = receiptDescription; + } + + cell.selectionStyle = UITableViewCellSelectionStyleNone; + + return cell; +} + +#pragma mark - UITableViewDelegate + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + return 70; +} + + +@end diff --git a/Riot/ViewController/ReadReceiptsViewController.xib b/Riot/ViewController/ReadReceiptsViewController.xib new file mode 100644 index 000000000..26b0926e7 --- /dev/null +++ b/Riot/ViewController/ReadReceiptsViewController.xib @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/ViewController/RoomViewController.m b/Riot/ViewController/RoomViewController.m index e38ac65c3..3cff2c451 100644 --- a/Riot/ViewController/RoomViewController.m +++ b/Riot/ViewController/RoomViewController.m @@ -45,6 +45,8 @@ #import "UsersDevicesViewController.h" +#import "ReadReceiptsViewController.h" + #import "RoomEmptyBubbleCell.h" #import "RoomIncomingTextMsgBubbleCell.h" @@ -1709,6 +1711,11 @@ [self showEncryptionInformation:tappedEvent]; } } + else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnReceiptsContainer]) + { + MXKReceiptSendersContainer *container = userInfo[kMXKRoomBubbleCellReceiptsContainerKey]; + [ReadReceiptsViewController openInViewController:self fromContainer:container withSession:self.mainSession]; + } else { // Keep default implementation for other actions