Restore message cell footers.

// FREEBIE
This commit is contained in:
Matthew Chen 2017-10-11 23:03:28 -04:00
parent 227fd5280d
commit c2f07bb3d8
17 changed files with 562 additions and 398 deletions

View File

@ -101,6 +101,7 @@
34D1F0B71F87F8850066283D /* OWSGenericAttachmentView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0B61F87F8850066283D /* OWSGenericAttachmentView.m */; };
34D1F0BA1F8800D90066283D /* OWSAudioMessageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0B91F8800D90066283D /* OWSAudioMessageView.m */; };
34D1F0BD1F8D108C0066283D /* AttachmentUploadView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0BC1F8D108C0066283D /* AttachmentUploadView.m */; };
34D1F0C01F8EC1760066283D /* MessageRecipientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0BF1F8EC1760066283D /* MessageRecipientState.swift */; };
34D5CC961EA6AFAD005515DB /* OWSContactsSyncing.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5CC951EA6AFAD005515DB /* OWSContactsSyncing.m */; };
34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */; };
34D5CCB11EAE7E7F005515DB /* SelectRecipientViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5CCB01EAE7E7F005515DB /* SelectRecipientViewController.m */; };
@ -568,6 +569,7 @@
34D1F0B91F8800D90066283D /* OWSAudioMessageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSAudioMessageView.m; sourceTree = "<group>"; };
34D1F0BB1F8D108C0066283D /* AttachmentUploadView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AttachmentUploadView.h; sourceTree = "<group>"; };
34D1F0BC1F8D108C0066283D /* AttachmentUploadView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AttachmentUploadView.m; sourceTree = "<group>"; };
34D1F0BF1F8EC1760066283D /* MessageRecipientState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRecipientState.swift; sourceTree = "<group>"; };
34D5CC941EA6AFAD005515DB /* OWSContactsSyncing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactsSyncing.h; sourceTree = "<group>"; };
34D5CC951EA6AFAD005515DB /* OWSContactsSyncing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactsSyncing.m; sourceTree = "<group>"; };
34D5CCA71EAE3D30005515DB /* AvatarViewHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AvatarViewHelper.h; sourceTree = "<group>"; };
@ -1094,6 +1096,7 @@
3400C7981EAFB772008A8584 /* ThreadViewHelper.m */,
340CB2251EAC25820001CAA1 /* UpdateGroupViewController.h */,
340CB2261EAC25820001CAA1 /* UpdateGroupViewController.m */,
34D1F0BE1F8EC1760066283D /* Utils */,
34B3F8A01E8EA6040035BE1A /* ViewControllerUtils.h */,
34B3F8A11E8EA6040035BE1A /* ViewControllerUtils.m */,
);
@ -1176,6 +1179,14 @@
path = Cells;
sourceTree = "<group>";
};
34D1F0BE1F8EC1760066283D /* Utils */ = {
isa = PBXGroup;
children = (
34D1F0BF1F8EC1760066283D /* MessageRecipientState.swift */,
);
path = Utils;
sourceTree = "<group>";
};
34D8C0221ED3673300188D7C /* DebugUI */ = {
isa = PBXGroup;
children = (
@ -2335,6 +2346,7 @@
34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */,
34B3F8811E8DF1700035BE1A /* LockInteractionController.m in Sources */,
34CCAF3B1F0C2748004084F4 /* OWSAddToContactViewController.m in Sources */,
34D1F0C01F8EC1760066283D /* MessageRecipientState.swift in Sources */,
45F659731E1BD99C00444429 /* CallKitCallUIAdaptee.swift in Sources */,
45BB93381E688E14001E3939 /* UIDevice+featureSupport.swift in Sources */,
458DE9D61DEE3FD00071BB03 /* PeerConnectionClient.swift in Sources */,

View File

@ -29,8 +29,8 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value);
- (NSArray<NSLayoutConstraint *> *)autoPinToSuperviewEdges;
- (void)autoHCenterInSuperview;
- (void)autoVCenterInSuperview;
- (NSLayoutConstraint *)autoHCenterInSuperview;
- (NSLayoutConstraint *)autoVCenterInSuperview;
- (void)autoPinWidthToWidthOfView:(UIView *)view;
- (void)autoPinHeightToHeightOfView:(UIView *)view;
@ -85,6 +85,8 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value);
- (NSLayoutConstraint *)autoPinTrailingToSuperviewWithMargin:(CGFloat)margin;
- (NSLayoutConstraint *)autoPinLeadingToTrailingOfView:(UIView *)view;
- (NSLayoutConstraint *)autoPinLeadingToTrailingOfView:(UIView *)view margin:(CGFloat)margin;
- (NSLayoutConstraint *)autoPinTrailingToLeadingOfView:(UIView *)view;
- (NSLayoutConstraint *)autoPinTrailingToLeadingOfView:(UIView *)view margin:(CGFloat)margin;
- (NSLayoutConstraint *)autoPinLeadingToView:(UIView *)view;
- (NSLayoutConstraint *)autoPinLeadingToView:(UIView *)view margin:(CGFloat)margin;
- (NSLayoutConstraint *)autoPinTrailingToView:(UIView *)view;

View File

@ -89,14 +89,14 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value)
return result;
}
- (void)autoHCenterInSuperview
- (NSLayoutConstraint *)autoHCenterInSuperview
{
[self autoAlignAxis:ALAxisVertical toSameAxisOfView:self.superview];
return [self autoAlignAxis:ALAxisVertical toSameAxisOfView:self.superview];
}
- (void)autoVCenterInSuperview
- (NSLayoutConstraint *)autoVCenterInSuperview
{
[self autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.superview];
return [self autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.superview];
}
- (void)autoPinWidthToWidthOfView:(UIView *)view
@ -302,17 +302,17 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value)
- (NSLayoutConstraint *)autoPinLeadingToTrailingOfView:(UIView *)view
{
OWSAssert(view);
return [self autoPinLeadingToTrailingOfView:view margin:0];
}
- (NSLayoutConstraint *)autoPinLeadingToTrailingOfView:(UIView *)view margin:(CGFloat)margin
{
OWSAssert(view);
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(9, 0)) {
NSLayoutConstraint *constraint =
[self.leadingAnchor constraintEqualToAnchor:view.trailingAnchor constant:margin];
[self.leadingAnchor constraintEqualToAnchor:view.trailingAnchor constant:margin];
constraint.active = YES;
return constraint;
} else {
@ -320,6 +320,27 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value)
}
}
- (NSLayoutConstraint *)autoPinTrailingToLeadingOfView:(UIView *)view
{
OWSAssert(view);
return [self autoPinTrailingToLeadingOfView:view margin:0];
}
- (NSLayoutConstraint *)autoPinTrailingToLeadingOfView:(UIView *)view margin:(CGFloat)margin
{
OWSAssert(view);
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(9, 0)) {
NSLayoutConstraint *constraint =
[self.trailingAnchor constraintEqualToAnchor:view.leadingAnchor constant:-margin];
constraint.active = YES;
return constraint;
} else {
return [self autoPinEdge:ALEdgeTrailing toEdge:ALEdgeLeading ofView:view withOffset:-margin];
}
}
- (NSLayoutConstraint *)autoPinLeadingToView:(UIView *)view
{
OWSAssert(view);

View File

@ -39,6 +39,10 @@ NS_ASSUME_NONNULL_BEGIN
- (void)tappedAddToContactsOfferMessage:(OWSContactOffersInteraction *)interaction;
- (void)tappedAddToProfileWhitelistOfferMessage:(OWSContactOffersInteraction *)interaction;
#pragma mark - Formatting
- (NSAttributedString *)attributedContactOrProfileNameForPhoneIdentifier:(NSString *)recipientId;
@end
#pragma mark -

View File

@ -6,9 +6,11 @@
NS_ASSUME_NONNULL_BEGIN
extern const CGFloat kExpirationTimerViewSize;
@interface OWSExpirationTimerView : UIView
- (void)startTimerWithExpiresAtSeconds:(double)expiresAtSeconds initialDurationSeconds:(uint32_t)initialDurationSeconds;
- (void)startTimerWithExpiration:(uint64_t)expirationTimestamp initialDurationSeconds:(uint32_t)initialDurationSeconds;
- (void)stopTimer;

View File

@ -4,24 +4,31 @@
#import "OWSExpirationTimerView.h"
#import "ConversationViewController.h"
#import "NSDate+OWS.h"
#import "OWSMath.h"
#import "UIColor+OWS.h"
#import <QuartzCore/CAShapeLayer.h>
#import "UIView+OWS.h"
#import <QuartzCore/QuartzCore.h>
#import <SignalServiceKit/NSTimer+OWS.h>
NS_ASSUME_NONNULL_BEGIN
double const OWSExpirationTimerViewBlinkingSeconds = 2;
const CGFloat kExpirationTimerViewSize = 22.f;
@interface OWSExpirationTimerView ()
@property (nonatomic) uint32_t initialDurationSeconds;
@property (atomic) double expiresAtSeconds;
@property (nonatomic) uint64_t expirationTimestamp;
@property (nonatomic, readonly) UIImageView *emptyHourglassImageView;
@property (nonatomic, readonly) UIImageView *fullHourglassImageView;
@property CGFloat ratioRemaining;
@property (nonatomic, nullable) CAGradientLayer *maskLayer;
@property (nonatomic, nullable) NSTimer *animationTimer;
@end
#pragma mark -
@implementation OWSExpirationTimerView
- (void)dealloc
@ -36,17 +43,7 @@ double const OWSExpirationTimerViewBlinkingSeconds = 2;
return self;
}
self.clipsToBounds = YES;
_emptyHourglassImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ic_hourglass_empty"]];
_emptyHourglassImageView.tintColor = [UIColor ows_blackColor];
[self insertSubview:_emptyHourglassImageView atIndex:0];
_fullHourglassImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ic_hourglass_full"]];
_fullHourglassImageView.tintColor = [UIColor ows_darkGrayColor];
[self insertSubview:_fullHourglassImageView atIndex:1];
_ratioRemaining = 1.0f;
[self commonInit];
return self;
}
@ -58,151 +55,151 @@ double const OWSExpirationTimerViewBlinkingSeconds = 2;
return self;
}
self.clipsToBounds = YES;
_emptyHourglassImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ic_hourglass_empty"]];
_emptyHourglassImageView.tintColor = [UIColor lightGrayColor];
[self insertSubview:_emptyHourglassImageView atIndex:1];
_fullHourglassImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ic_hourglass_full"]];
_fullHourglassImageView.tintColor = [UIColor lightGrayColor];
[self insertSubview:_fullHourglassImageView atIndex:0];
_ratioRemaining = 1.0f;
[self commonInit];
return self;
}
- (void)layoutSubviews
- (void)commonInit
{
CGFloat leftMargin = 0.0f;
CGFloat padding = 6.0f;
CGRect hourglassFrame
= CGRectMake(leftMargin, padding / 2, self.frame.size.height - padding, self.frame.size.height - padding);
self.emptyHourglassImageView.frame = hourglassFrame;
self.emptyHourglassImageView.bounds = hourglassFrame;
self.fullHourglassImageView.frame = hourglassFrame;
self.fullHourglassImageView.bounds = hourglassFrame;
self.clipsToBounds = YES;
UIImage *hourglassEmptyImage = [[UIImage imageNamed:@"ic_hourglass_empty"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
UIImage *hourglassFullImage = [[UIImage imageNamed:@"ic_hourglass_full"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
_emptyHourglassImageView = [[UIImageView alloc] initWithImage:hourglassEmptyImage];
self.emptyHourglassImageView.tintColor = [UIColor lightGrayColor];
[self addSubview:self.emptyHourglassImageView];
_fullHourglassImageView = [[UIImageView alloc] initWithImage:hourglassFullImage];
self.fullHourglassImageView.tintColor = [UIColor lightGrayColor];
[self addSubview:self.fullHourglassImageView];
[self.emptyHourglassImageView autoPinHeightToSuperviewWithMargin:3.f];
[self.emptyHourglassImageView autoHCenterInSuperview];
[self.emptyHourglassImageView autoPinToSquareAspectRatio];
[self.fullHourglassImageView autoPinHeightToSuperviewWithMargin:3.f];
[self.fullHourglassImageView autoHCenterInSuperview];
[self.fullHourglassImageView autoPinToSquareAspectRatio];
[self autoSetDimension:ALDimensionWidth toSize:kExpirationTimerViewSize];
[self autoSetDimension:ALDimensionHeight toSize:kExpirationTimerViewSize];
}
- (void)handleReappearNotification:(NSNotification *)notification
- (void)startTimerWithExpiration:(uint64_t)expirationTimestamp initialDurationSeconds:(uint32_t)initialDurationSeconds
{
DDLogVerbose(@"%@ handleReappearNotification", self.logTag);
[self startAnimation];
}
OWSAssert([NSThread isMainThread]);
- (void)startTimerWithExpiresAtSeconds:(double)expiresAtSeconds initialDurationSeconds:(uint32_t)initialDurationSeconds
{
if (expiresAtSeconds == 0) {
DDLogWarn(
@"%@ Asked to animate expiration for message which hasn't started expiring. intitialDurationSeconds:%u",
self.logTag,
initialDurationSeconds);
}
DDLogVerbose(@"%@ Starting timer with expiresAtSeconds: %f initialDurationSeconds: %d",
self.logTag,
expiresAtSeconds,
initialDurationSeconds);
self.expiresAtSeconds = expiresAtSeconds;
self.expirationTimestamp = expirationTimestamp;
self.initialDurationSeconds = initialDurationSeconds;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleReappearNotification:)
name:ConversationViewControllerDidAppearNotification
object:nil];
[self startAnimation];
[self ensureAnimations];
}
- (void)startAnimation
- (void)clearAnimations
{
DDLogVerbose(@"%@ Starting animation with expiresAtSeconds: %f initialDurationSeconds: %d",
self.logTag,
self.expiresAtSeconds,
self.initialDurationSeconds);
[self.maskLayer removeAllAnimations];
[self.maskLayer removeFromSuperlayer];
self.maskLayer = nil;
[self.fullHourglassImageView.layer.mask removeFromSuperlayer];
self.fullHourglassImageView.layer.mask = nil;
[self.layer removeAllAnimations];
self.layer.opacity = 1.f;
self.emptyHourglassImageView.hidden = YES;
self.fullHourglassImageView.hidden = YES;
[self.animationTimer invalidate];
self.animationTimer = nil;
}
double secondsLeft = self.expiresAtSeconds - [NSDate new].timeIntervalSince1970;
if (secondsLeft < 0) {
secondsLeft = 0;
- (void)setFrame:(CGRect)frame {
BOOL sizeDidChange = CGSizeEqualToSize(self.frame.size, frame.size);
[super setFrame:frame];
if (sizeDidChange) {
[self ensureAnimations];
}
}
// Get hourglass frames to the proper size.
[self setNeedsLayout];
[self layoutIfNeeded];
- (void)setBounds:(CGRect)bounds {
BOOL sizeDidChange = CGSizeEqualToSize(self.bounds.size, bounds.size);
[super setBounds:bounds];
if (sizeDidChange) {
[self ensureAnimations];
}
}
- (void)ensureAnimations
{
OWSAssert([NSThread isMainThread]);
CGFloat secondsLeft = MAX(0, (self.expirationTimestamp - [NSDate ows_millisecondTimeStamp]) / 1000.f);
[self clearAnimations];
const NSTimeInterval kBlinkAnimationDurationSeconds = 2;
if (self.expirationTimestamp == 0) {
// If message hasn't started expiring yet, just show the full hourglass.
self.fullHourglassImageView.hidden = NO;
return;
} else if (secondsLeft <= kBlinkAnimationDurationSeconds + 0.1f) {
// If message has expired, just show the blinking empty hourglass.
self.emptyHourglassImageView.hidden = NO;
// Flashing animation.
[UIView animateWithDuration:0.5f
delay:0.f
options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAutoreverse | UIViewAnimationOptionRepeat
animations:^{
self.layer.opacity = 0.f;
} completion:nil];
return;
}
self.emptyHourglassImageView.hidden = NO;
self.fullHourglassImageView.hidden = NO;
CAGradientLayer *maskLayer = [CAGradientLayer new];
maskLayer.frame = self.fullHourglassImageView.bounds;
self.maskLayer = maskLayer;
self.fullHourglassImageView.layer.mask = maskLayer;
// Without this the hourglass appears empty too soon.
CGFloat borderOffset = 2.0;
maskLayer.frame = CGRectInset(self.fullHourglassImageView.frame, 0, -borderOffset);
// Blur the top of the mask a bit with gradient
maskLayer.colors = @[ (id)[UIColor clearColor].CGColor, (id)[UIColor blackColor].CGColor ];
maskLayer.startPoint = CGPointMake(0.5f, 0);
maskLayer.endPoint = CGPointMake(0.5f, 0.2f);
maskLayer.startPoint = CGPointMake(0.5f, 0.f);
// Use a mask that is 20% tall to soften the edge of the animation.
const CGFloat kMaskEdgeFraction = 0.2f;
maskLayer.endPoint = CGPointMake(0.5f, kMaskEdgeFraction);
NSTimeInterval timeUntilFlashing = MAX(0, secondsLeft - kBlinkAnimationDurationSeconds);
CGFloat ratioRemaining = MAX(0.f, (timeUntilFlashing / (CGFloat)self.initialDurationSeconds));
CGFloat alpha = 1.f - ratioRemaining;
CGFloat maskRange = self.fullHourglassImageView.height;
CGPoint startPosition = maskLayer.position;
startPosition.y += CGFloatLerp(maskRange * -kMaskEdgeFraction, maskRange, alpha);
CGPoint endPosition = maskLayer.position;
endPosition.y += maskRange;
CGFloat ratioRemaining = ((CGFloat)secondsLeft / (CGFloat)self.initialDurationSeconds);
if (ratioRemaining < 0) {
ratioRemaining = 0.0;
}
CGPoint defaultPosition = maskLayer.position;
CGPoint finalPosition
= CGPointMake(defaultPosition.x, defaultPosition.y + maskLayer.bounds.size.height - 2 * borderOffset);
CGPoint startingPosition = CGPointMake(
defaultPosition.x, finalPosition.y - maskLayer.bounds.size.height * ratioRemaining + borderOffset);
maskLayer.position = startingPosition;
CABasicAnimation *revealAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
revealAnimation.duration = secondsLeft;
revealAnimation.fromValue = [NSValue valueWithCGPoint:startingPosition];
revealAnimation.toValue = [NSValue valueWithCGPoint:finalPosition];
[maskLayer addAnimation:revealAnimation forKey:@"revealAnimation"];
maskLayer.position = finalPosition; // don't snap back
__weak typeof(self) wself = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
(long long)((secondsLeft - OWSExpirationTimerViewBlinkingSeconds) * NSEC_PER_SEC)),
dispatch_get_main_queue(),
^{
[wself startBlinking];
});
maskLayer.position = startPosition;
[CATransaction begin];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
animation.duration = timeUntilFlashing;
animation.fromValue = [NSValue valueWithCGPoint:startPosition];
animation.toValue = [NSValue valueWithCGPoint:endPosition];
[maskLayer addAnimation:animation forKey:@"slideAnimation"];
maskLayer.position = endPosition; // don't snap back
[CATransaction commit];
self.animationTimer = [NSTimer weakScheduledTimerWithTimeInterval:timeUntilFlashing
target:self
selector:@selector(ensureAnimations)
userInfo:nil
repeats:NO];
}
- (void)stopTimer
{
[[NSNotificationCenter defaultCenter] removeObserver:self
name:ConversationViewControllerDidAppearNotification
object:nil];
OWSAssert([NSThread isMainThread]);
[self.layer removeAnimationForKey:@"alphaBlink"];
self.layer.opacity = 1;
}
- (BOOL)itIsTimeToBlink
{
double secondsLeft = self.expiresAtSeconds - [NSDate new].timeIntervalSince1970;
return secondsLeft <= OWSExpirationTimerViewBlinkingSeconds;
}
- (void)startBlinking
{
if (![self itIsTimeToBlink]) {
DDLogVerbose(@"Refusing to start blinking too early. Reused cell?");
return;
}
CABasicAnimation *blinkAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
blinkAnimation.duration = 0.5;
blinkAnimation.fromValue = @(1.0);
blinkAnimation.toValue = @(0.0);
blinkAnimation.repeatCount = 4;
blinkAnimation.autoreverses = YES;
blinkAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[self.layer addAnimation:blinkAnimation forKey:@"alphaBlink"];
[self clearAnimations];
}
#pragma mark - Logging

View File

@ -4,8 +4,6 @@
#import "OWSMessageCell.h"
//#import "OWSExpirableMessageView.h"
NS_ASSUME_NONNULL_BEGIN
// TODO: Remove this class.

View File

@ -8,13 +8,6 @@
NS_ASSUME_NONNULL_BEGIN
@interface OWSIncomingMessageCell ()
//@property (strong, nonatomic) IBOutlet OWSExpirationTimerView *expirationTimerView;
//@property (strong, nonatomic) IBOutlet NSLayoutConstraint *expirationTimerViewWidthConstraint;
@end
@implementation OWSIncomingMessageCell
+ (NSString *)cellReuseIdentifier
@ -27,57 +20,6 @@ NS_ASSUME_NONNULL_BEGIN
return YES;
}
//- (void)awakeFromNib
//{
// [super awakeFromNib];
// self.expirationTimerViewWidthConstraint.constant = 0.0;
//}
//
//- (void)prepareForReuse
//{
// [super prepareForReuse];
// self.expirationTimerViewWidthConstraint.constant = 0.0f;
//
// [self.mediaAdapter setCellVisible:NO];
//
// // Clear this adapter's views IFF this was the last cell to use this adapter.
// [self.mediaAdapter clearCachedMediaViewsIfLastPresentingCell:self];
// [_mediaAdapter setLastPresentingCell:nil];
//
// self.mediaAdapter = nil;
//}
//
//- (void)setMediaAdapter:(nullable id<OWSMessageMediaAdapter>)mediaAdapter
//{
// _mediaAdapter = mediaAdapter;
//
// // Mark this as the last cell to use this adapter.
// [_mediaAdapter setLastPresentingCell:self];
//}
//
//// pragma mark - OWSMessageCollectionViewCell
//
//// TODO:
//- (void)setCellVisible:(BOOL)isVisible
//{
// [self.mediaAdapter setCellVisible:isVisible];
//}
//
//// pragma mark - OWSExpirableMessageView
//
//- (void)startExpirationTimerWithExpiresAtSeconds:(double)expiresAtSeconds
// initialDurationSeconds:(uint32_t)initialDurationSeconds
//{
// self.expirationTimerViewWidthConstraint.constant = OWSExpirableMessageViewTimerWidth;
// [self.expirationTimerView startTimerWithExpiresAtSeconds:expiresAtSeconds
// initialDurationSeconds:initialDurationSeconds];
//}
//
//- (void)stopExpirationTimer
//{
// [self.expirationTimerView stopTimer];
//}
@end
NS_ASSUME_NONNULL_END

View File

@ -4,27 +4,11 @@
#import "ConversationViewCell.h"
//#import "JSQMessagesCollectionViewCell+OWS.h"
//#import "OWSExpirableMessageView.h"
//#import "OWSMessageMediaAdapter.h"
NS_ASSUME_NONNULL_BEGIN
@class OWSExpirationTimerView;
// TODO: Move to source.
static const CGFloat OWSExpirableMessageViewTimerWidth = 10.0f;
@interface OWSMessageCell : ConversationViewCell
// <OWSExpirableMessageView>
@property (nonatomic, readonly) OWSExpirationTimerView *expirationTimerView;
@property (nonatomic, readonly) NSLayoutConstraint *expirationTimerViewWidthConstraint;
- (void)startExpirationTimerWithExpiresAtSeconds:(double)expiresAtSeconds
initialDurationSeconds:(uint32_t)initialDurationSeconds;
- (void)stopExpirationTimer;
@end

View File

@ -8,14 +8,13 @@
#import "ConversationViewItem.h"
#import "NSAttributedString+OWS.h"
#import "OWSAudioMessageView.h"
#import "OWSExpirationTimerView.h"
#import "OWSGenericAttachmentView.h"
#import "Signal-Swift.h"
#import "UIColor+OWS.h"
#import <JSQMessagesViewController/JSQMessagesTimestampFormatter.h>
#import <JSQMessagesViewController/UIColor+JSQMessages.h>
//#import "OWSExpirationTimerView.h"
NS_ASSUME_NONNULL_BEGIN
@interface OWSMessageCell ()
@ -32,11 +31,12 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, nullable) AttachmentPointerView *attachmentPointerView;
@property (nonatomic, nullable) OWSGenericAttachmentView *attachmentView;
@property (nonatomic, nullable) OWSAudioMessageView *audioMessageView;
@property (nonatomic) UIView *footerView;
@property (nonatomic) UILabel *footerLabel;
@property (nonatomic, nullable) OWSExpirationTimerView *expirationTimerView;
@property (nonatomic, nullable) NSArray<NSLayoutConstraint *> *dateHeaderConstraints;
@property (nonatomic, nullable) NSArray<NSLayoutConstraint *> *contentConstraints;
//@property (strong, nonatomic) OWSExpirationTimerView *expirationTimerView;
//@property (strong, nonatomic) NSLayoutConstraint *expirationTimerViewWidthConstraint;
@property (nonatomic, nullable) NSArray<NSLayoutConstraint *> *footerConstraints;
@end
@ -61,8 +61,11 @@ NS_ASSUME_NONNULL_BEGIN
self.payloadView = [UIView containerView];
[self.contentView addSubview:self.payloadView];
self.footerView = [UIView containerView];
[self.contentView addSubview:self.footerView];
self.dateHeaderLabel = [UILabel new];
self.dateHeaderLabel.font = [UIFont ows_regularFontWithSize:16.f];
self.dateHeaderLabel.font = [UIFont ows_regularFontWithSize:12.f];
self.dateHeaderLabel.textAlignment = NSTextAlignmentCenter;
self.dateHeaderLabel.textColor = [UIColor lightGrayColor];
[self.contentView addSubview:self.dateHeaderLabel];
@ -81,15 +84,23 @@ NS_ASSUME_NONNULL_BEGIN
[self.bubbleImageView addSubview:self.textLabel];
OWSAssert(self.textLabel.superview);
self.footerLabel = [UILabel new];
self.footerLabel.font = [UIFont ows_regularFontWithSize:12.f];
self.footerLabel.textColor = [UIColor lightGrayColor];
[self.footerView addSubview:self.footerLabel];
// Hide these views by default.
self.bubbleImageView.hidden = YES;
self.textLabel.hidden = YES;
self.dateHeaderLabel.hidden = YES;
self.footerLabel.hidden = YES;
[self.dateHeaderLabel autoPinEdgeToSuperviewEdge:ALEdgeTop];
[self.payloadView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.dateHeaderLabel];
[self.payloadView autoPinEdgeToSuperviewEdge:ALEdgeBottom];
[self.payloadView autoPinWidthToSuperview];
[self.footerView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.payloadView];
[self.footerView autoPinEdgeToSuperviewEdge:ALEdgeBottom];
[self.footerView autoPinWidthToSuperview];
UITapGestureRecognizer *tap =
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
@ -151,6 +162,7 @@ NS_ASSUME_NONNULL_BEGIN
self.bubbleImageView.image = bubbleImageData.messageBubbleImage;
[self updateDateHeader:contentWidth];
[self updateFooter];
switch (self.cellType) {
case OWSMessageCellType_TextMessage:
@ -183,9 +195,6 @@ NS_ASSUME_NONNULL_BEGIN
}
}
// [self.textLabel addBorderWithColor:[UIColor blueColor]];
// [self.bubbleImageView addBorderWithColor:[UIColor greenColor]];
// dispatch_async(dispatch_get_main_queue(), ^{
// NSLog(@"---- %@", self.viewItem.interaction.debugDescription);
// NSLog(@"cell: %@", NSStringFromCGRect(self.frame));
@ -206,7 +215,7 @@ NS_ASSUME_NONNULL_BEGIN
[dateHeaderDateFormatter setDoesRelativeDateFormatting:YES];
[dateHeaderDateFormatter setDateStyle:NSDateFormatterMediumStyle];
[dateHeaderDateFormatter setTimeStyle:NSDateFormatterNoStyle];
dateHeaderTimeFormatter = [NSDateFormatter new];
[dateHeaderTimeFormatter setLocale:[NSLocale currentLocale]];
[dateHeaderTimeFormatter setDoesRelativeDateFormatting:YES];
@ -257,15 +266,106 @@ NS_ASSUME_NONNULL_BEGIN
}
}
- (CGFloat)footerHeight
{
BOOL showFooter = NO;
TSMessage *message = (TSMessage *)self.viewItem.interaction;
BOOL hasExpirationTimer = message.shouldStartExpireTimer;
if (hasExpirationTimer) {
showFooter = YES;
} else if (!self.isIncoming) {
showFooter = YES;
} else if (self.viewItem.isGroupThread) {
showFooter = YES;
} else {
showFooter = NO;
}
return (showFooter ? MAX(kExpirationTimerViewSize,
self.footerLabel.font.lineHeight)
: 0.f);
}
- (void)updateFooter
{
OWSAssert(self.viewItem.interaction.interactionType == OWSInteractionType_IncomingMessage
|| self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage);
TSMessage *message = (TSMessage *)self.viewItem.interaction;
BOOL hasExpirationTimer = message.shouldStartExpireTimer;
NSAttributedString *attributedText = nil;
if (!self.isIncoming) {
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)message;
NSString *statusMessage =
[MessageRecipientStatusUtils statusMessageWithOutgoingMessage:outgoingMessage referenceView:self];
attributedText = [[NSAttributedString alloc] initWithString:statusMessage attributes:@{}];
} else if (self.viewItem.isGroupThread) {
TSIncomingMessage *incomingMessage = (TSIncomingMessage *)self.viewItem.interaction;
attributedText = [self.delegate attributedContactOrProfileNameForPhoneIdentifier:incomingMessage.authorId];
}
if (!hasExpirationTimer &&
!attributedText) {
self.footerLabel.hidden = YES;
self.footerConstraints = @[
[self.footerView autoSetDimension:ALDimensionHeight toSize:0],
];
return;
}
if (hasExpirationTimer)
{
self.expirationTimerView = [OWSExpirationTimerView new];
[self.footerView addSubview:self.expirationTimerView];
}
if (attributedText) {
self.footerLabel.attributedText = attributedText;
self.footerLabel.hidden = NO;
}
if (hasExpirationTimer &&
attributedText) {
self.footerConstraints = @[
[self.expirationTimerView autoVCenterInSuperview],
[self.footerLabel autoVCenterInSuperview],
(self.isIncoming
? [self.expirationTimerView autoPinLeadingToSuperview]
: [self.expirationTimerView autoPinTrailingToSuperview]),
(self.isIncoming
? [self.footerLabel autoPinLeadingToTrailingOfView:self.expirationTimerView margin:0.f]
: [self.footerLabel autoPinTrailingToLeadingOfView:self.expirationTimerView margin:0.f]),
[self.footerView autoSetDimension:ALDimensionHeight toSize:self.footerHeight],
];
} else if (hasExpirationTimer) {
self.footerConstraints = @[
[self.expirationTimerView autoVCenterInSuperview],
(self.isIncoming
? [self.expirationTimerView autoPinLeadingToSuperview]
: [self.expirationTimerView autoPinTrailingToSuperview]),
[self.footerView autoSetDimension:ALDimensionHeight toSize:self.footerHeight],
];
} else if (attributedText) {
self.footerConstraints = @[
[self.footerLabel autoVCenterInSuperview],
(self.isIncoming
? [self.footerLabel autoPinLeadingToSuperview]
: [self.footerLabel autoPinTrailingToSuperview]),
[self.footerView autoSetDimension:ALDimensionHeight toSize:self.footerHeight],
];
} else {
OWSFail(@"%@ Cell unexpectedly has neither expiration timer nor footer text.", self.logTag);
}
}
- (UIFont *)dateHeaderDateFont
{
// TODO: Refine.
return [UIFont boldSystemFontOfSize:12.0f];
}
- (UIFont *)dateHeaderTimeFont
{
// TODO: Refine.
return [UIFont systemFontOfSize:12.0f];
}
@ -297,6 +397,9 @@ NS_ASSUME_NONNULL_BEGIN
}
self.stillImageView = [[UIImageView alloc] initWithImage:image];
// We need to specify a contentMode since the size of the image
// might not match the aspect ratio of the view.
self.stillImageView.contentMode = UIViewContentModeScaleAspectFill;
// Use trilinear filters for better scaling quality at
// some performance cost.
self.stillImageView.layer.minificationFilter = kCAFilterTrilinear;
@ -323,6 +426,9 @@ NS_ASSUME_NONNULL_BEGIN
self.animatedImageView = [[YYAnimatedImageView alloc] init];
self.animatedImageView.image = animatedImage;
// We need to specify a contentMode since the size of the image
// might not match the aspect ratio of the view.
self.animatedImageView.contentMode = UIViewContentModeScaleAspectFill;
[self replaceBubbleWithView:self.animatedImageView];
[self addAttachmentUploadViewIfNecessary:self.animatedImageView];
}
@ -356,6 +462,9 @@ NS_ASSUME_NONNULL_BEGIN
}
self.stillImageView = [[UIImageView alloc] initWithImage:image];
// We need to specify a contentMode since the size of the image
// might not match the aspect ratio of the view.
self.stillImageView.contentMode = UIViewContentModeScaleAspectFill;
// Use trilinear filters for better scaling quality at
// some performance cost.
self.stillImageView.layer.minificationFilter = kCAFilterTrilinear;
@ -434,15 +543,6 @@ NS_ASSUME_NONNULL_BEGIN
- (void)cropViewToBubbbleShape:(UIView *)view
{
// OWSAssert(CGRectEqualToRect(self.bounds, self.contentView.frame));
// DDLogError(@"cropViewToBubbbleShape: %@ %@", self.viewItem.interaction.uniqueId,
// self.viewItem.interaction.description); DDLogError(@"\t %@ %@ %@ %@",
// NSStringFromCGRect(self.frame),
// NSStringFromCGRect(self.contentView.frame),
// NSStringFromCGRect(view.frame),
// NSStringFromCGRect(view.superview.bounds));
// view.frame = view.superview.bounds;
view.frame = self.bounds;
[JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:view isOutgoing:!self.isIncoming];
}
@ -531,6 +631,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssert(cellSize.width > 0 && cellSize.height > 0);
cellSize.height += self.dateHeaderHeight;
cellSize.height += self.footerHeight;
return cellSize;
}
@ -599,12 +700,16 @@ NS_ASSUME_NONNULL_BEGIN
self.contentConstraints = nil;
[NSLayoutConstraint deactivateConstraints:self.dateHeaderConstraints];
self.dateHeaderConstraints = nil;
[NSLayoutConstraint deactivateConstraints:self.footerConstraints];
self.footerConstraints = nil;
// The text label is used so frequently that we always keep one around.
self.dateHeaderLabel.text = nil;
self.dateHeaderLabel.hidden = YES;
self.textLabel.text = nil;
self.textLabel.hidden = YES;
self.footerLabel.text = nil;
self.footerLabel.hidden = YES;
self.bubbleImageView.image = nil;
self.bubbleImageView.hidden = YES;
@ -621,17 +726,12 @@ NS_ASSUME_NONNULL_BEGIN
[self.audioMessageView removeFromSuperview];
self.audioMessageView = nil;
self.attachmentUploadView = nil;
if (self.expirationTimerView)
[self.expirationTimerView stopTimer];
[self.expirationTimerView removeFromSuperview];
self.expirationTimerView = nil;
}
//- (void)awakeFromNib
//{
// [super awakeFromNib];
// self.expirationTimerViewWidthConstraint.constant = 0.0;
//
// // Our text alignment needs to adapt to RTL.
// self.cellBottomLabel.textAlignment = [self.cellBottomLabel textAlignmentUnnatural];
//}
//
//- (void)prepareForReuse
//{
// [super prepareForReuse];
@ -666,21 +766,43 @@ NS_ASSUME_NONNULL_BEGIN
//{
// return [UIColor whiteColor];
//}
#pragma mark - Notifications
- (void)setIsCellVisible:(BOOL)isCellVisible {
if (self.isCellVisible == isCellVisible) {
return;
}
[super setIsCellVisible:isCellVisible];
if (isCellVisible) {
TSMessage *message = (TSMessage *)self.viewItem.interaction;
if (message.shouldStartExpireTimer) {
uint64_t expirationTimestamp = message.expiresAt;
uint32_t expiresInSeconds = message.expiresInSeconds;
[self.expirationTimerView startTimerWithExpiration:expirationTimestamp
initialDurationSeconds:expiresInSeconds];
} else {
[self.expirationTimerView stopTimer];
}
} else {
[self.expirationTimerView stopTimer];
}
}
// case TSInfoMessageAdapter: {
// // HACK this will get called when we get a new info message, but there's gotta be a better spot for this.
// OWSDisappearingMessagesConfiguration *configuration =
// [OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:self.thread.uniqueId];
// [self setBarButtonItemsForDisappearingMessagesConfiguration:configuration];
//
//// pragma mark - OWSExpirableMessageView
// if (message.shouldStartExpireTimer && [cell conformsToProtocol:@protocol(OWSExpirableMessageView)]) {
// id<OWSExpirableMessageView> expirableView = (id<OWSExpirableMessageView>)cell;
// [expirableView startExpirationTimerWithExpiresAtSeconds:message.expiresAtSeconds
// initialDurationSeconds:message.expiresInSeconds];
// }
//
//- (void)startExpirationTimerWithExpiresAtSeconds:(double)expiresAtSeconds
// initialDurationSeconds:(uint32_t)initialDurationSeconds
//{
// self.expirationTimerViewWidthConstraint.constant = OWSExpirableMessageViewTimerWidth;
// [self.expirationTimerView startTimerWithExpiresAtSeconds:expiresAtSeconds
// initialDurationSeconds:initialDurationSeconds];
//}
//
//- (void)stopExpirationTimer
//{
// [self.expirationTimerView stopTimer];
//}
#pragma mark - Gesture recognizers

View File

@ -21,36 +21,6 @@ NS_ASSUME_NONNULL_BEGIN
return NO;
}
//- (void)prepareForReuse
//{
// [super prepareForReuse];
// self.mediaView.alpha = 1.0;
// self.expirationTimerViewWidthConstraint.constant = 0.0f;
//
// [self.mediaAdapter setCellVisible:NO];
//
// // Clear this adapter's views IFF this was the last cell to use this adapter.
// [self.mediaAdapter clearCachedMediaViewsIfLastPresentingCell:self];
// [_mediaAdapter setLastPresentingCell:nil];
//
// self.mediaAdapter = nil;
//}
//
//// pragma mark - OWSExpirableMessageView
//
//- (void)startExpirationTimerWithExpiresAtSeconds:(double)expiresAtSeconds
// initialDurationSeconds:(uint32_t)initialDurationSeconds
//{
// self.expirationTimerViewWidthConstraint.constant = OWSExpirableMessageViewTimerWidth;
// [self.expirationTimerView startTimerWithExpiresAtSeconds:expiresAtSeconds
// initialDurationSeconds:initialDurationSeconds];
//}
//
//- (void)stopExpirationTimer
//{
// [self.expirationTimerView stopTimer];
//}
@end
NS_ASSUME_NONNULL_END

View File

@ -8,9 +8,6 @@ NS_ASSUME_NONNULL_BEGIN
@class TSThread;
// TODO: Audit this.
extern NSString *const ConversationViewControllerDidAppearNotification;
@interface ConversationViewController : OWSViewController
@property (nonatomic, readonly) TSThread *thread;

View File

@ -101,8 +101,6 @@ static const int kConversationInitialMaxRangeSize = kYapDatabasePageSize * kYapD
static const int kYapDatabaseRangeMaxLength = kYapDatabasePageSize * kYapDatabaseMaxPageCount;
static const int kYapDatabaseRangeMinLength = 0;
NSString *const ConversationViewControllerDidAppearNotification = @"ConversationViewControllerDidAppear";
typedef enum : NSUInteger {
kMediaTypePicture,
kMediaTypeVideo,
@ -505,7 +503,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
- (void)applicationWillEnterForeground:(NSNotification *)notification
{
[self startReadTimer];
[self startExpirationTimerAnimations];
self.isAppInBackground = NO;
}
@ -541,9 +538,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
self.isViewVisible = YES;
// restart any animations that were stopped e.g. while inspecting the contact info screens.
[self startExpirationTimerAnimations];
// We should have already requested contact access at this point, so this should be a no-op
// unless it ever becomes possible to load this VC without going via the HomeViewController.
[self.contactsManager requestSystemContactsOnce];
@ -983,15 +977,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
self.isUserScrolling = NO;
}
- (void)startExpirationTimerAnimations
{
OWSAssert([NSThread isMainThread]);
// This notification should be posted synchronously.
[[NSNotificationCenter defaultCenter] postNotificationName:ConversationViewControllerDidAppearNotification
object:nil];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
@ -1791,6 +1776,14 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
#pragma mark - ConversationViewCellDelegate
- (NSAttributedString *)attributedContactOrProfileNameForPhoneIdentifier:(NSString *)recipientId
{
OWSAssert([NSThread isMainThread]);
OWSAssert(recipientId.length > 0);
return [self.contactsManager attributedContactOrProfileNameForPhoneIdentifier:recipientId];
}
- (void)tappedUnknownContactBlockOfferMessage:(OWSContactOffersInteraction *)interaction
{
if (![self.thread isKindOfClass:[TSContactThread class]]) {
@ -2770,9 +2763,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
}
NSMutableSet<NSNumber *> *rowsThatChangedSize = [[self reloadViewItems] mutableCopy];
for (NSNumber *row in rowsThatChangedSize) {
DDLogError(@"might reload: %@", row);
}
BOOL wasAtBottom = [self isScrolledToBottom];
// We want sending messages to feel snappy. So, if the only
@ -3711,6 +3701,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
_isViewVisible = isViewVisible;
[self updateShouldObserveDBModifications];
[self updateCellsVisible];
}
- (void)setIsAppInBackground:(BOOL)isAppInBackground
@ -3718,6 +3709,15 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
_isAppInBackground = isAppInBackground;
[self updateShouldObserveDBModifications];
[self updateCellsVisible];
}
- (void)updateCellsVisible
{
BOOL isCellVisible = self.isViewVisible && !self.isAppInBackground;
for (ConversationViewCell *cell in self.collectionView.visibleCells) {
cell.isCellVisible = isCellVisible;
}
}
- (void)updateShouldObserveDBModifications
@ -3820,6 +3820,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
NSMutableDictionary<NSString *, ConversationViewItem *> *viewItemMap = [NSMutableDictionary new];
NSUInteger count = [self.messageMappings numberOfItemsInSection:0];
BOOL isGroupThread = self.isGroupConversation;
// TODO: Recycle view items where possible.
// TODO: Distinguish interaction types through some enum.
@ -3835,7 +3836,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
if (viewItem) {
viewItem.lastRow = viewItem.row;
} else {
viewItem = [[ConversationViewItem alloc] initWithTSInteraction:interaction];
viewItem = [[ConversationViewItem alloc] initWithTSInteraction:interaction isGroupThread:isGroupThread];
}
viewItem.row = (NSInteger)row;
[viewItems addObject:viewItem];
@ -3896,6 +3897,57 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
previousViewItemTimestamp = viewItem.interaction.timestampForSorting;
}
// Update the "shouldShowDate" property of the view items.
OWSInteractionType lastInteractionType = OWSInteractionType_Unknown;
for (ConversationViewItem *viewItem in viewItems) {
OWSInteractionType interactionType = viewItem.interaction.interactionType;
lastInteractionType = interactionType;
// BOOL canShowDate = NO;
// switch (viewItem.interaction.interactionType) {
// case OWSInteractionType_Unknown:
// case OWSInteractionType_UnreadIndicator:
// case OWSInteractionType_Offer:
// canShowDate = NO;
// break;
// case OWSInteractionType_IncomingMessage:
// case OWSInteractionType_OutgoingMessage:
// case OWSInteractionType_Error:
// case OWSInteractionType_Info:
// case OWSInteractionType_Call:
// canShowDate = YES;
// break;
// }
//
// BOOL shouldShowDate = NO;
// if (!canShowDate) {
// shouldShowDate = NO;
// shouldShowDateOnNextViewItem = YES;
// } else if (shouldShowDateOnNextViewItem) {
// shouldShowDate = YES;
// shouldShowDateOnNextViewItem = NO;
// } else {
// uint64_t viewItemTimestamp = viewItem.interaction.timestampForSorting;
// OWSAssert(viewItemTimestamp > 0);
// OWSAssert(previousViewItemTimestamp > 0);
// uint64_t timeDifferenceMs = viewItemTimestamp - previousViewItemTimestamp;
// static const uint64_t kShowTimeIntervalMs = 5 * kMinuteInMs;
// if (timeDifferenceMs > kShowTimeIntervalMs) {
// shouldShowDate = YES;
// }
// shouldShowDateOnNextViewItem = NO;
// }
// if (viewItem.shouldShowDate != shouldShowDate) {
// // If this is an existing view item and it has changed size,
// // note that so that we can reload this cell while doing
// // incremental updates.
// if (viewItem.lastRow != NSNotFound) {
// [rowsThatChangedSize addObject:@(viewItem.lastRow)];
// }
// }
// viewItem.shouldShowDate = shouldShowDate;
// previousViewItemTimestamp = viewItem.interaction.timestampForSorting;
}
self.viewItems = viewItems;
self.viewItemMap = viewItemMap;
@ -3957,20 +4009,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
[cell loadForDisplay:self.layout.contentWidth];
return cell;
// case TSInfoMessageAdapter: {
// // HACK this will get called when we get a new info message, but there's gotta be a better spot for this.
// OWSDisappearingMessagesConfiguration *configuration =
// [OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:self.thread.uniqueId];
// [self setBarButtonItemsForDisappearingMessagesConfiguration:configuration];
//
// if (message.shouldStartExpireTimer && [cell conformsToProtocol:@protocol(OWSExpirableMessageView)]) {
// id<OWSExpirableMessageView> expirableView = (id<OWSExpirableMessageView>)cell;
// [expirableView startExpirationTimerWithExpiresAtSeconds:message.expiresAtSeconds
// initialDurationSeconds:message.expiresInSeconds];
// }
//
// return cell;
}
#pragma mark - UICollectionViewDelegate

View File

@ -39,6 +39,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
@property (nonatomic, readonly) TSInteraction *interaction;
@property (nonatomic, readonly) BOOL isGroupThread;
@property (nonatomic) BOOL shouldShowDate;
@property (nonatomic) NSInteger row;
@ -47,7 +49,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
//@property (nonatomic, weak) ConversationViewCell *lastCell;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithTSInteraction:(TSInteraction *)interaction;
- (instancetype)initWithTSInteraction:(TSInteraction *)interaction isGroupThread:(BOOL)isGroupThread;
- (ConversationViewCell *)dequeueCellForCollectionView:(UICollectionView *)collectionView
indexPath:(NSIndexPath *)indexPath;

View File

@ -61,7 +61,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
@implementation ConversationViewItem
- (instancetype)initWithTSInteraction:(TSInteraction *)interaction
- (instancetype)initWithTSInteraction:(TSInteraction *)interaction isGroupThread:(BOOL)isGroupThread
{
self = [super init];
@ -70,6 +70,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
}
_interaction = interaction;
_isGroupThread = isGroupThread;
self.row = NSNotFound;
self.lastRow = NSNotFound;

View File

@ -9,15 +9,6 @@ class MessageMetadataViewController: OWSViewController {
static let TAG = "[MessageMetadataViewController]"
let TAG = "[MessageMetadataViewController]"
enum MessageRecipientState {
case uploading
case sending
case sent
case delivered
case read
case failed
}
// MARK: Properties
let contactsManager: OWSContactsManager
@ -174,7 +165,7 @@ class MessageMetadataViewController: OWSViewController {
let isGroupThread = message.thread.isGroupThread()
let recipientStatusGroups: [MessageRecipientState] = [
let recipientStatusGroups: [MessageRecipientStatus] = [
.read,
.uploading,
.delivered,
@ -194,7 +185,7 @@ class MessageMetadataViewController: OWSViewController {
}
for recipientId in thread.recipientIdentifiers {
let (recipientStatus, statusMessage) = self.recipientStatus(outgoingMessage: outgoingMessage, recipientId: recipientId)
let (recipientStatus, statusMessage) = MessageRecipientStatusUtils.recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage, recipientId: recipientId, referenceView:self.view)
guard recipientStatus == recipientStatusGroup else {
continue
@ -202,7 +193,7 @@ class MessageMetadataViewController: OWSViewController {
if groupRows.count < 1 {
if isGroupThread {
groupRows.append(valueRow(name: MessageRecipientStateName(recipientStatusGroup),
groupRows.append(valueRow(name: MessageRecipientStatusName(recipientStatusGroup),
value:""))
}
@ -424,60 +415,6 @@ class MessageMetadataViewController: OWSViewController {
return rows
}
private func recipientStatus(outgoingMessage: TSOutgoingMessage, recipientId: String) -> (MessageRecipientState, String) {
// Legacy messages don't have "recipient read" state or "per-recipient delivery" state,
// so we fall back to `TSOutgoingMessageState` which is not per-recipient and therefore
// might be misleading.
let recipientReadMap = outgoingMessage.recipientReadMap
if let readTimestamp = recipientReadMap[recipientId] {
assert(outgoingMessage.messageState == .sentToService)
let statusMessage = NSLocalizedString("MESSAGE_STATUS_READ", comment:"message footer for read messages").rtlSafeAppend(" ", referenceView:self.view)
.rtlSafeAppend(
DateUtil.formatPastTimestampRelativeToNow(readTimestamp.uint64Value), referenceView:self.view)
return (.read, statusMessage)
}
let recipientDeliveryMap = outgoingMessage.recipientDeliveryMap
if let deliveryTimestamp = recipientDeliveryMap[recipientId] {
assert(outgoingMessage.messageState == .sentToService)
let statusMessage = NSLocalizedString("MESSAGE_STATUS_DELIVERED",
comment:"message status for message delivered to their recipient.").rtlSafeAppend(" ", referenceView:self.view)
.rtlSafeAppend(
DateUtil.formatPastTimestampRelativeToNow(deliveryTimestamp.uint64Value), referenceView:self.view)
return (.delivered, statusMessage)
}
if outgoingMessage.wasDelivered {
let statusMessage = NSLocalizedString("MESSAGE_STATUS_DELIVERED",
comment:"message status for message delivered to their recipient.")
return (.delivered, statusMessage)
}
if outgoingMessage.messageState == .unsent {
let statusMessage = NSLocalizedString("MESSAGE_STATUS_FAILED", comment:"message footer for failed messages")
return (.failed, statusMessage)
} else if outgoingMessage.messageState == .sentToService ||
outgoingMessage.wasSent(toRecipient:recipientId) {
let statusMessage =
NSLocalizedString("MESSAGE_STATUS_SENT",
comment:"message footer for sent messages")
return (.sent, statusMessage)
} else if outgoingMessage.hasAttachments() {
assert(outgoingMessage.messageState == .attemptingOut)
let statusMessage = NSLocalizedString("MESSAGE_STATUS_UPLOADING",
comment:"message footer while attachment is uploading")
return (.uploading, statusMessage)
} else {
assert(outgoingMessage.messageState == .attemptingOut)
let statusMessage = NSLocalizedString("MESSAGE_STATUS_SENDING",
comment:"message status while message is sending.")
return (.sending, statusMessage)
}
}
private func nameLabel(text: String) -> UILabel {
let label = UILabel()
label.textColor = UIColor.black
@ -595,7 +532,7 @@ class MessageMetadataViewController: OWSViewController {
updateContent()
}
private func MessageRecipientStateName(_ value: MessageRecipientState) -> String {
private func MessageRecipientStatusName(_ value: MessageRecipientStatus) -> String {
switch value {
case .uploading:
return NSLocalizedString("MESSAGE_METADATA_VIEW_MESSAGE_STATUS_UPLOADING",

View File

@ -0,0 +1,135 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
import Foundation
enum MessageRecipientStatus {
case uploading
case sending
case sent
case delivered
case read
case failed
}
class MessageRecipientStatusUtils: NSObject {
// MARK: Initializers
@available(*, unavailable, message:"do not instantiate this class.")
private override init() {
}
public class func recipientStatus(outgoingMessage: TSOutgoingMessage,
recipientId: String,
referenceView: UIView) -> MessageRecipientStatus {
let (messageRecipientStatus, _) = recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage,
recipientId: recipientId,
referenceView: referenceView)
return messageRecipientStatus
}
public class func statusMessage(outgoingMessage: TSOutgoingMessage,
recipientId: String,
referenceView: UIView) -> String {
let (_, statusMessage) = recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage,
recipientId: recipientId,
referenceView: referenceView)
return statusMessage
}
public class func recipientStatusAndStatusMessage(outgoingMessage: TSOutgoingMessage,
recipientId: String,
referenceView: UIView) -> (MessageRecipientStatus, String) {
// Legacy messages don't have "recipient read" state or "per-recipient delivery" state,
// so we fall back to `TSOutgoingMessageState` which is not per-recipient and therefore
// might be misleading.
let recipientReadMap = outgoingMessage.recipientReadMap
if let readTimestamp = recipientReadMap[recipientId] {
assert(outgoingMessage.messageState == .sentToService)
let statusMessage = NSLocalizedString("MESSAGE_STATUS_READ", comment:"message footer for read messages").rtlSafeAppend(" ", referenceView:referenceView)
.rtlSafeAppend(
DateUtil.formatPastTimestampRelativeToNow(readTimestamp.uint64Value), referenceView:referenceView)
return (.read, statusMessage)
}
let recipientDeliveryMap = outgoingMessage.recipientDeliveryMap
if let deliveryTimestamp = recipientDeliveryMap[recipientId] {
assert(outgoingMessage.messageState == .sentToService)
let statusMessage = NSLocalizedString("MESSAGE_STATUS_DELIVERED",
comment:"message status for message delivered to their recipient.").rtlSafeAppend(" ", referenceView:referenceView)
.rtlSafeAppend(
DateUtil.formatPastTimestampRelativeToNow(deliveryTimestamp.uint64Value), referenceView:referenceView)
return (.delivered, statusMessage)
}
if outgoingMessage.wasDelivered {
let statusMessage = NSLocalizedString("MESSAGE_STATUS_DELIVERED",
comment:"message status for message delivered to their recipient.")
return (.delivered, statusMessage)
}
if outgoingMessage.messageState == .unsent {
let statusMessage = NSLocalizedString("MESSAGE_STATUS_FAILED", comment:"message footer for failed messages")
return (.failed, statusMessage)
} else if outgoingMessage.messageState == .sentToService ||
outgoingMessage.wasSent(toRecipient:recipientId) {
let statusMessage =
NSLocalizedString("MESSAGE_STATUS_SENT",
comment:"message footer for sent messages")
return (.sent, statusMessage)
} else if outgoingMessage.hasAttachments() {
assert(outgoingMessage.messageState == .attemptingOut)
let statusMessage = NSLocalizedString("MESSAGE_STATUS_UPLOADING",
comment:"message footer while attachment is uploading")
return (.uploading, statusMessage)
} else {
assert(outgoingMessage.messageState == .attemptingOut)
let statusMessage = NSLocalizedString("MESSAGE_STATUS_SENDING",
comment:"message status while message is sending.")
return (.sending, statusMessage)
}
}
public class func statusMessage(outgoingMessage: TSOutgoingMessage,
referenceView: UIView) -> String {
let recipientReadMap = outgoingMessage.recipientReadMap
if recipientReadMap.count > 0 {
assert(outgoingMessage.messageState == .sentToService)
return NSLocalizedString("MESSAGE_STATUS_READ", comment:"message footer for read messages")
}
let recipientDeliveryMap = outgoingMessage.recipientDeliveryMap
if recipientDeliveryMap.count > 0 {
assert(outgoingMessage.messageState == .sentToService)
return NSLocalizedString("MESSAGE_STATUS_DELIVERED",
comment:"message status for message delivered to their recipient.")
}
if outgoingMessage.wasDelivered {
return NSLocalizedString("MESSAGE_STATUS_DELIVERED",
comment:"message status for message delivered to their recipient.")
}
if outgoingMessage.messageState == .unsent {
return NSLocalizedString("MESSAGE_STATUS_FAILED", comment:"message footer for failed messages")
} else if outgoingMessage.messageState == .sentToService {
return NSLocalizedString("MESSAGE_STATUS_SENT",
comment:"message footer for sent messages")
} else if outgoingMessage.hasAttachments() {
assert(outgoingMessage.messageState == .attemptingOut)
return NSLocalizedString("MESSAGE_STATUS_UPLOADING",
comment:"message footer while attachment is uploading")
} else {
assert(outgoingMessage.messageState == .attemptingOut)
return NSLocalizedString("MESSAGE_STATUS_SENDING",
comment:"message status while message is sending.")
}
}
}