parent
227fd5280d
commit
c2f07bb3d8
|
@ -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 */,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 -
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -4,8 +4,6 @@
|
|||
|
||||
#import "OWSMessageCell.h"
|
||||
|
||||
//#import "OWSExpirableMessageView.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// TODO: Remove this class.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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.")
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue