diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 595a93577..7e8623061 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 34330A5E1E787BD800DF2FB9 /* ElegantIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A5D1E787BD800DF2FB9 /* ElegantIcons.ttf */; }; 34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34330AA21E79686200DF2FB9 /* OWSProgressView.m */; }; 343A65951FC47D5E000477A1 /* DebugUISyncMessages.m in Sources */ = {isa = PBXBuildFile; fileRef = 343A65941FC47D5E000477A1 /* DebugUISyncMessages.m */; }; + 343A65981FC4CFE7000477A1 /* ConversationScrollButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 343A65961FC4CFE6000477A1 /* ConversationScrollButton.m */; }; 343D3D9B1E9283F100165CA4 /* BlockListUIUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 343D3D9A1E9283F100165CA4 /* BlockListUIUtils.m */; }; 344F2F671E57A932000D9322 /* UIViewController+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 344F2F661E57A932000D9322 /* UIViewController+OWS.m */; }; 34533F181EA8D2070006114F /* OWSAudioAttachmentPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 34533F171EA8D2070006114F /* OWSAudioAttachmentPlayer.m */; }; @@ -424,6 +425,8 @@ 34330AA21E79686200DF2FB9 /* OWSProgressView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSProgressView.m; sourceTree = ""; }; 343A65931FC47D5D000477A1 /* DebugUISyncMessages.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUISyncMessages.h; sourceTree = ""; }; 343A65941FC47D5E000477A1 /* DebugUISyncMessages.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUISyncMessages.m; sourceTree = ""; }; + 343A65961FC4CFE6000477A1 /* ConversationScrollButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationScrollButton.m; sourceTree = ""; }; + 343A65971FC4CFE7000477A1 /* ConversationScrollButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConversationScrollButton.h; sourceTree = ""; }; 343D3D991E9283F100165CA4 /* BlockListUIUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BlockListUIUtils.h; sourceTree = ""; }; 343D3D9A1E9283F100165CA4 /* BlockListUIUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BlockListUIUtils.m; sourceTree = ""; }; 34491FC11FB0F78500B3E5A3 /* my */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = my; path = translations/my.lproj/Localizable.strings; sourceTree = ""; }; @@ -972,6 +975,8 @@ 34D1F0681F8678AA0066283D /* ConversationInputTextView.m */, 34D1F0691F8678AA0066283D /* ConversationInputToolbar.h */, 34D1F06A1F8678AA0066283D /* ConversationInputToolbar.m */, + 343A65971FC4CFE7000477A1 /* ConversationScrollButton.h */, + 343A65961FC4CFE6000477A1 /* ConversationScrollButton.m */, 34D1F06D1F8678AA0066283D /* ConversationViewController.h */, 34D1F06E1F8678AA0066283D /* ConversationViewController.m */, 34D1F06F1F8678AA0066283D /* ConversationViewItem.h */, @@ -2210,6 +2215,7 @@ 45CD81F21DC03A22004C9430 /* OWSLogger.m in Sources */, 4542F0941EB9372700C7EE92 /* SystemContactsFetcher.swift in Sources */, B60C16651988999D00E97A6C /* VersionMigrations.m in Sources */, + 343A65981FC4CFE7000477A1 /* ConversationScrollButton.m in Sources */, B97940271832BD2400BD66CB /* UIUtil.m in Sources */, 34CE88ED1F3237260098030F /* ProfileFetcherJob.swift in Sources */, 34B3F8791E8DF1700035BE1A /* CountryCodeViewController.m in Sources */, diff --git a/Signal/src/ViewControllers/ConversationView/ConversationScrollButton.h b/Signal/src/ViewControllers/ConversationView/ConversationScrollButton.h new file mode 100644 index 000000000..dea8f43de --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/ConversationScrollButton.h @@ -0,0 +1,15 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +@interface ConversationScrollButton : UIButton + +@property (nonatomic) BOOL hasUnreadMessages; + +- (nullable instancetype)initWithIconText:(NSString *)iconText; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/ConversationScrollButton.m b/Signal/src/ViewControllers/ConversationView/ConversationScrollButton.m new file mode 100644 index 000000000..3a196d7e1 --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/ConversationScrollButton.m @@ -0,0 +1,97 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "ConversationScrollButton.h" +#import "UIColor+OWS.h" +#import "UIFont+OWS.h" +#import "UIView+OWS.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface ConversationScrollButton () + +@property (nonatomic) NSString *iconText; +@property (nonatomic) UILabel *iconLabel; +@property (nonatomic) UIView *circleView; + +@end + +#pragma mark - + +@implementation ConversationScrollButton + +- (nullable instancetype)initWithIconText:(NSString *)iconText +{ + self = [super initWithFrame:CGRectMake(0, 0, self.buttonSize, self.buttonSize)]; + if (!self) { + return self; + } + + self.iconText = iconText; + + [self createContents]; + + return self; +} + +- (CGFloat)circleSize +{ + return ScaleFromIPhone5To7Plus(35.f, 40.f); +} + +- (CGFloat)buttonSize +{ + return self.circleSize + 2 * 15.f; +} + +- (void)createContents +{ + UILabel *iconLabel = [UILabel new]; + self.iconLabel = iconLabel; + iconLabel.userInteractionEnabled = NO; + + UIView *circleView = [UIView new]; + self.circleView = circleView; + circleView.backgroundColor = [UIColor colorWithWhite:0.95f alpha:1.f]; + circleView.userInteractionEnabled = NO; + circleView.layer.cornerRadius = self.circleSize * 0.5f; + circleView.layer.shadowColor = [UIColor colorWithWhite:0.5f alpha:1.f].CGColor; + circleView.layer.shadowOffset = CGSizeMake(+1.f, +2.f); + circleView.layer.shadowRadius = 1.5f; + circleView.layer.shadowOpacity = 0.35f; + [circleView autoSetDimension:ALDimensionWidth toSize:self.circleSize]; + [circleView autoSetDimension:ALDimensionHeight toSize:self.circleSize]; + + [self addSubview:circleView]; + [self addSubview:iconLabel]; + [circleView autoCenterInSuperview]; + [iconLabel autoCenterInSuperview]; + + [self updateColors]; +} + +- (void)setHasUnreadMessages:(BOOL)hasUnreadMessages +{ + _hasUnreadMessages = hasUnreadMessages; + + [self updateColors]; +} + +- (void)updateColors +{ + self.circleView.backgroundColor + = (self.hasUnreadMessages ? [UIColor ows_materialBlueColor] : [UIColor colorWithWhite:0.95f alpha:1.f]); + self.iconLabel.attributedText = [[NSAttributedString alloc] + initWithString:self.iconText + attributes:@{ + NSFontAttributeName : [UIFont ows_fontAwesomeFont:self.circleSize * 0.8f], + NSForegroundColorAttributeName : + (self.hasUnreadMessages ? [UIColor whiteColor] : [UIColor ows_materialBlueColor]), + NSBaselineOffsetAttributeName : @(-0.5f), + }]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index d3c9f231e..6c7a37bc5 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -11,6 +11,7 @@ #import "ConversationHeaderView.h" #import "ConversationInputTextView.h" #import "ConversationInputToolbar.h" +#import "ConversationScrollButton.h" #import "ConversationViewCell.h" #import "ConversationViewItem.h" #import "ConversationViewLayout.h" @@ -214,9 +215,9 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { @property (nonatomic, readonly) BOOL isGroupConversation; @property (nonatomic) BOOL isUserScrolling; -@property (nonatomic) UIView *scrollDownButton; +@property (nonatomic) ConversationScrollButton *scrollDownButton; #ifdef DEBUG -@property (nonatomic) UIView *scrollUpButton; +@property (nonatomic) ConversationScrollButton *scrollUpButton; #endif @property (nonatomic) BOOL isViewVisible; @@ -225,6 +226,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { @property (nonatomic) BOOL viewHasEverAppeared; @property (nonatomic) BOOL wasScrolledToBottomBeforeKeyboardShow; @property (nonatomic) BOOL wasScrolledToBottomBeforeLayoutChange; +@property (nonatomic) BOOL hasUnreadMessages; @end @@ -472,7 +474,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { [self registerCellClasses]; - [self createScrollButtons]; + [self createConversationScrollButtons]; [self createHeaderViews]; [self createBackButton]; [self addNotificationListeners]; @@ -596,12 +598,13 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { [self updateBarButtonItems]; [self setNavigationTitle]; - [self updateLastVisibleTimestamp]; // We want to set the initial scroll state the first time we enter the view. if (!self.viewHasEverAppeared) { [self scrollToDefaultPosition]; } + + [self updateLastVisibleTimestamp]; } - (NSIndexPath *_Nullable)indexPathOfUnreadMessagesIndicator @@ -2222,51 +2225,41 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { } } -- (void)createScrollButtons +- (void)createConversationScrollButtons { - self.scrollDownButton = [self createScrollButton:@"\uf103" selector:@selector(scrollDownButtonTapped)]; + self.scrollDownButton = [[ConversationScrollButton alloc] initWithIconText:@"\uf103"]; + [self.scrollDownButton addTarget:self + action:@selector(scrollDownButtonTapped) + forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.scrollDownButton]; + [self.scrollDownButton autoSetDimension:ALDimensionWidth toSize:self.scrollDownButton.width]; + [self.scrollDownButton autoSetDimension:ALDimensionHeight toSize:self.scrollDownButton.width]; + [self.scrollDownButton autoPinEdge:ALEdgeBottom toEdge:ALEdgeTop ofView:self.inputToolbar]; + [self.scrollDownButton autoPinEdgeToSuperviewEdge:ALEdgeTrailing]; + #ifdef DEBUG - self.scrollUpButton = [self createScrollButton:@"\uf102" selector:@selector(scrollUpButtonTapped)]; + self.scrollUpButton = [[ConversationScrollButton alloc] initWithIconText:@"\uf102"]; + [self.scrollUpButton addTarget:self + action:@selector(scrollUpButtonTapped) + forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.scrollUpButton]; + [self.scrollUpButton autoSetDimension:ALDimensionWidth toSize:self.scrollUpButton.width]; + [self.scrollUpButton autoSetDimension:ALDimensionHeight toSize:self.scrollUpButton.width]; + [self.scrollUpButton autoPinToTopLayoutGuideOfViewController:self withInset:0]; + [self.scrollUpButton autoPinEdgeToSuperviewEdge:ALEdgeTrailing]; #endif } -- (UIView *)createScrollButton:(NSString *)label selector:(SEL)selector +- (void)setHasUnreadMessages:(BOOL)hasUnreadMessages { - const CGFloat kCircleSize = ScaleFromIPhone5To7Plus(35.f, 40.f); - - UILabel *iconLabel = [UILabel new]; - iconLabel.attributedText = - [[NSAttributedString alloc] initWithString:label - attributes:@{ - NSFontAttributeName : [UIFont ows_fontAwesomeFont:kCircleSize * 0.8f], - NSForegroundColorAttributeName : [UIColor ows_materialBlueColor], - NSBaselineOffsetAttributeName : @(-0.5f), - }]; - iconLabel.userInteractionEnabled = NO; - - UIView *circleView = [UIView new]; - circleView.backgroundColor = [UIColor colorWithWhite:0.95f alpha:1.f]; - circleView.userInteractionEnabled = NO; - circleView.layer.cornerRadius = kCircleSize * 0.5f; - circleView.layer.shadowColor = [UIColor colorWithWhite:0.5f alpha:1.f].CGColor; - circleView.layer.shadowOffset = CGSizeMake(+1.f, +2.f); - circleView.layer.shadowRadius = 1.5f; - circleView.layer.shadowOpacity = 0.35f; - [circleView autoSetDimension:ALDimensionWidth toSize:kCircleSize]; - [circleView autoSetDimension:ALDimensionHeight toSize:kCircleSize]; - - const CGFloat kButtonSize = kCircleSize + 2 * 15.f; - UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; - [button addTarget:self action:selector forControlEvents:UIControlEventTouchUpInside]; - button.frame = CGRectMake(0, 0, kButtonSize, kButtonSize); - [self.view addSubview:button]; + if (_hasUnreadMessages == hasUnreadMessages) { + return; + } - [button addSubview:circleView]; - [button addSubview:iconLabel]; - [circleView autoCenterInSuperview]; - [iconLabel autoCenterInSuperview]; + _hasUnreadMessages = hasUnreadMessages; - return button; + self.scrollDownButton.hasUnreadMessages = hasUnreadMessages; + [self ensureDynamicInteractions]; } - (void)scrollDownButtonTapped @@ -2330,11 +2323,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { if (shouldShowScrollDownButton) { self.scrollDownButton.hidden = NO; - self.scrollDownButton.frame - = CGRectMake((self.view.isRTL ? 0.f : self.scrollDownButton.superview.width - self.scrollDownButton.width), - self.inputToolbar.top - self.scrollDownButton.height, - self.scrollDownButton.width, - self.scrollDownButton.height); } else { self.scrollDownButton.hidden = YES; } @@ -2343,11 +2331,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { BOOL shouldShowScrollUpButton = self.collectionView.contentOffset.y > 0; if (shouldShowScrollUpButton) { self.scrollUpButton.hidden = NO; - self.scrollUpButton.frame - = CGRectMake((self.view.isRTL ? 0.f : self.scrollUpButton.superview.width - self.scrollUpButton.width), - 0, - self.scrollUpButton.width, - self.scrollUpButton.height); } else { self.scrollUpButton.hidden = YES; } @@ -3407,6 +3390,23 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { return [self viewItemForIndex:lastVisibleIndexPath.row]; } +// In the case where we explicitly scroll to bottom, we want to synchronously +// update the UI to reflect that, since the "mark as read" logic is asynchronous +// and won't update the UI state immediately. +- (void)didScrollToBottom +{ + + ConversationViewItem *_Nullable lastVisibleViewItem = [self.viewItems lastObject]; + if (lastVisibleViewItem) { + uint64_t lastVisibleTimestamp = lastVisibleViewItem.interaction.timestampForSorting; + self.lastVisibleTimestamp = MAX(self.lastVisibleTimestamp, lastVisibleTimestamp); + } + + self.scrollDownButton.hidden = YES; + + self.hasUnreadMessages = NO; +} + - (void)updateLastVisibleTimestamp { ConversationViewItem *_Nullable lastVisibleViewItem = [self lastVisibleViewItem]; @@ -3416,6 +3416,13 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { } [self ensureScrollDownButton]; + + __block NSUInteger numberOfUnreadMessages; + [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + numberOfUnreadMessages = + [[transaction ext:TSUnreadDatabaseViewExtensionName] numberOfItemsInGroup:self.thread.uniqueId]; + }]; + self.hasUnreadMessages = numberOfUnreadMessages > 0; } - (void)updateLastVisibleTimestamp:(uint64_t)timestamp @@ -3697,6 +3704,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { CGFloat contentHeight = self.safeContentHeight; CGFloat dstY = MAX(0, contentHeight - self.collectionView.height); [self.collectionView setContentOffset:CGPointMake(0, dstY) animated:animated]; + + [self didScrollToBottom]; } #pragma mark - UIScrollViewDelegate