mirror of https://github.com/oxen-io/session-ios
Merge branch 'charlesmchen/footerView'
commit
5e676c13d2
@ -1,26 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_hourglass_empty.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_hourglass_empty@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_hourglass_empty@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 271 B |
Binary file not shown.
Before Width: | Height: | Size: 315 B |
Binary file not shown.
Before Width: | Height: | Size: 421 B |
@ -1,26 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_hourglass_full.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_hourglass_full@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_hourglass_full@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 207 B |
Binary file not shown.
Before Width: | Height: | Size: 231 B |
Binary file not shown.
Before Width: | Height: | Size: 367 B |
@ -1,5 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
// TODO:
|
@ -1,25 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern const CGFloat kExpirationTimerViewSize;
|
||||
|
||||
@interface OWSExpirationTimerView : UIView
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
|
||||
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE;
|
||||
- (instancetype)initWithExpiration:(uint64_t)expirationTimestamp
|
||||
initialDurationSeconds:(uint32_t)initialDurationSeconds NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
- (void)ensureAnimations;
|
||||
|
||||
- (void)clearAnimations;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,192 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSExpirationTimerView.h"
|
||||
#import "ConversationViewController.h"
|
||||
#import "NSDate+OWS.h"
|
||||
#import "OWSMath.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
#import <SignalServiceKit/NSTimer+OWS.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
const CGFloat kExpirationTimerViewSize = 16.f;
|
||||
|
||||
@interface OWSExpirationTimerView ()
|
||||
|
||||
@property (nonatomic) uint32_t initialDurationSeconds;
|
||||
@property (nonatomic) uint64_t expirationTimestamp;
|
||||
|
||||
@property (nonatomic, readonly) UIImageView *emptyHourglassImageView;
|
||||
@property (nonatomic, readonly) UIImageView *fullHourglassImageView;
|
||||
@property (nonatomic, nullable) CAGradientLayer *maskLayer;
|
||||
@property (nonatomic, nullable) NSTimer *animationTimer;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSExpirationTimerView
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (instancetype)initWithExpiration:(uint64_t)expirationTimestamp initialDurationSeconds:(uint32_t)initialDurationSeconds
|
||||
{
|
||||
self = [super initWithFrame:CGRectZero];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
self.expirationTimestamp = expirationTimestamp;
|
||||
self.initialDurationSeconds = initialDurationSeconds;
|
||||
|
||||
[self commonInit];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)commonInit
|
||||
{
|
||||
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:2.f];
|
||||
[self.emptyHourglassImageView autoHCenterInSuperview];
|
||||
[self.emptyHourglassImageView autoPinToSquareAspectRatio];
|
||||
[self.fullHourglassImageView autoPinHeightToSuperviewWithMargin:2.f];
|
||||
[self.fullHourglassImageView autoHCenterInSuperview];
|
||||
[self.fullHourglassImageView autoPinToSquareAspectRatio];
|
||||
[self autoSetDimension:ALDimensionWidth toSize:kExpirationTimerViewSize];
|
||||
[self autoSetDimension:ALDimensionHeight toSize:kExpirationTimerViewSize];
|
||||
}
|
||||
|
||||
- (void)clearAnimations
|
||||
{
|
||||
[self.layer removeAllAnimations];
|
||||
[self.maskLayer removeAllAnimations];
|
||||
[self.maskLayer removeFromSuperlayer];
|
||||
self.maskLayer = nil;
|
||||
[self.fullHourglassImageView.layer.mask removeFromSuperlayer];
|
||||
self.fullHourglassImageView.layer.mask = nil;
|
||||
self.layer.opacity = 1.f;
|
||||
self.emptyHourglassImageView.hidden = YES;
|
||||
self.fullHourglassImageView.hidden = YES;
|
||||
[self.animationTimer invalidate];
|
||||
self.animationTimer = nil;
|
||||
}
|
||||
|
||||
- (void)setFrame:(CGRect)frame {
|
||||
BOOL sizeDidChange = CGSizeEqualToSize(self.frame.size, frame.size);
|
||||
[super setFrame:frame];
|
||||
if (sizeDidChange) {
|
||||
[self ensureAnimations];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setBounds:(CGRect)bounds {
|
||||
BOOL sizeDidChange = CGSizeEqualToSize(self.bounds.size, bounds.size);
|
||||
[super setBounds:bounds];
|
||||
if (sizeDidChange) {
|
||||
[self ensureAnimations];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)ensureAnimations
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
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:^(BOOL finished) {
|
||||
self.layer.opacity = 1.f;
|
||||
}];
|
||||
return;
|
||||
}
|
||||
|
||||
self.emptyHourglassImageView.hidden = NO;
|
||||
self.fullHourglassImageView.hidden = NO;
|
||||
|
||||
CAGradientLayer *maskLayer = [CAGradientLayer new];
|
||||
maskLayer.anchorPoint = CGPointZero;
|
||||
maskLayer.frame = self.fullHourglassImageView.bounds;
|
||||
self.maskLayer = maskLayer;
|
||||
self.fullHourglassImageView.layer.mask = maskLayer;
|
||||
|
||||
// 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.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);
|
||||
|
||||
if (self.initialDurationSeconds == 0) {
|
||||
OWSFail(@"initialDurationSeconds was unexpectedly 0");
|
||||
return;
|
||||
}
|
||||
|
||||
CGFloat ratioRemaining = (CGFloat)secondsLeft / (CGFloat)self.initialDurationSeconds;
|
||||
CGFloat ratioComplete = CGFloatClamp((CGFloat)1.0 - ratioRemaining, 0, 1.0);
|
||||
CGPoint startPosition = CGPointMake(0, self.fullHourglassImageView.height * ratioComplete);
|
||||
|
||||
// We offset the bottom slightly to make sure the duration of the perceived animation is correct.
|
||||
// We're accounting for:
|
||||
// - the bottom pixel of the two images is the outline of the hourglass. Because the outline is identical in the full vs empty hourglass this wouldn't be perceptible.
|
||||
// - the top pixel is not visible due to our softening gradient layer.
|
||||
CGPoint endPosition = CGPointMake(0, self.fullHourglassImageView.height - 2);
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -0,0 +1,19 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
@class ConversationViewItem;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSMessageFooterView : UIStackView
|
||||
|
||||
- (void)configureWithConversationViewItem:(ConversationViewItem *)viewItem;
|
||||
|
||||
- (CGSize)measureWithConversationViewItem:(ConversationViewItem *)viewItem;
|
||||
|
||||
- (void)setHasShadows:(BOOL)hasShadows viewItem:(ConversationViewItem *)viewItem;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -0,0 +1,182 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSMessageFooterView.h"
|
||||
#import "DateUtil.h"
|
||||
#import "Signal-Swift.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSMessageFooterView ()
|
||||
|
||||
@property (nonatomic) UILabel *timestampLabel;
|
||||
@property (nonatomic) UIView *spacerView;
|
||||
@property (nonatomic) UILabel *statusLabel;
|
||||
@property (nonatomic) UIView *statusIndicatorView;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OWSMessageFooterView
|
||||
|
||||
// `[UIView init]` invokes `[self initWithFrame:...]`.
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
[self commontInit];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)commontInit
|
||||
{
|
||||
// Ensure only called once.
|
||||
OWSAssert(!self.timestampLabel);
|
||||
|
||||
self.layoutMargins = UIEdgeInsetsZero;
|
||||
|
||||
self.axis = UILayoutConstraintAxisHorizontal;
|
||||
self.spacing = self.hSpacing;
|
||||
self.alignment = UIStackViewAlignmentCenter;
|
||||
|
||||
self.timestampLabel = [UILabel new];
|
||||
// TODO: Color
|
||||
self.timestampLabel.textColor = [UIColor lightGrayColor];
|
||||
[self addArrangedSubview:self.timestampLabel];
|
||||
|
||||
self.spacerView = [UIView new];
|
||||
[self.spacerView setContentHuggingLow];
|
||||
[self addArrangedSubview:self.spacerView];
|
||||
|
||||
self.statusLabel = [UILabel new];
|
||||
// TODO: Color
|
||||
self.statusLabel.textColor = [UIColor lightGrayColor];
|
||||
[self addArrangedSubview:self.statusLabel];
|
||||
|
||||
self.statusIndicatorView = [UIView new];
|
||||
[self.statusIndicatorView autoSetDimension:ALDimensionWidth toSize:self.statusIndicatorSize];
|
||||
[self.statusIndicatorView autoSetDimension:ALDimensionHeight toSize:self.statusIndicatorSize];
|
||||
self.statusIndicatorView.layer.cornerRadius = self.statusIndicatorSize * 0.5f;
|
||||
[self addArrangedSubview:self.statusIndicatorView];
|
||||
}
|
||||
|
||||
- (void)configureFonts
|
||||
{
|
||||
self.timestampLabel.font = UIFont.ows_dynamicTypeCaption2Font;
|
||||
self.statusLabel.font = UIFont.ows_dynamicTypeCaption2Font;
|
||||
}
|
||||
|
||||
- (CGFloat)statusIndicatorSize
|
||||
{
|
||||
// TODO: Review constant.
|
||||
return 12.f;
|
||||
}
|
||||
|
||||
- (CGFloat)hSpacing
|
||||
{
|
||||
// TODO: Review constant.
|
||||
return 8.f;
|
||||
}
|
||||
|
||||
#pragma mark - Load
|
||||
|
||||
- (void)configureWithConversationViewItem:(ConversationViewItem *)viewItem
|
||||
{
|
||||
OWSAssert(viewItem);
|
||||
|
||||
[self configureLabelsWithConversationViewItem:viewItem];
|
||||
|
||||
// TODO:
|
||||
self.statusIndicatorView.backgroundColor = [UIColor orangeColor];
|
||||
|
||||
BOOL isOutgoing = (viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage);
|
||||
for (UIView *subview in @[
|
||||
self.spacerView,
|
||||
self.statusLabel,
|
||||
self.statusIndicatorView,
|
||||
]) {
|
||||
subview.hidden = !isOutgoing;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)configureLabelsWithConversationViewItem:(ConversationViewItem *)viewItem
|
||||
{
|
||||
OWSAssert(viewItem);
|
||||
|
||||
[self configureFonts];
|
||||
|
||||
self.timestampLabel.text = [DateUtil formatTimestampShort:viewItem.interaction.timestamp];
|
||||
self.statusLabel.text = [self messageStatusTextForConversationViewItem:viewItem];
|
||||
}
|
||||
|
||||
- (CGSize)measureWithConversationViewItem:(ConversationViewItem *)viewItem
|
||||
{
|
||||
OWSAssert(viewItem);
|
||||
|
||||
[self configureLabelsWithConversationViewItem:viewItem];
|
||||
|
||||
CGSize result = CGSizeZero;
|
||||
result.height
|
||||
= MAX(self.timestampLabel.font.lineHeight, MAX(self.statusLabel.font.lineHeight, self.statusIndicatorSize));
|
||||
if (viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) {
|
||||
result.width = ([self.timestampLabel sizeThatFits:CGSizeZero].width +
|
||||
[self.statusLabel sizeThatFits:CGSizeZero].width + self.statusIndicatorSize + self.hSpacing * 3.f);
|
||||
} else {
|
||||
result.width = [self.timestampLabel sizeThatFits:CGSizeZero].width;
|
||||
}
|
||||
return CGSizeCeil(result);
|
||||
}
|
||||
|
||||
- (nullable NSString *)messageStatusTextForConversationViewItem:(ConversationViewItem *)viewItem
|
||||
{
|
||||
OWSAssert(viewItem);
|
||||
if (viewItem.interaction.interactionType != OWSInteractionType_OutgoingMessage) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)viewItem.interaction;
|
||||
NSString *statusMessage =
|
||||
[MessageRecipientStatusUtils receiptMessageWithOutgoingMessage:outgoingMessage referenceView:self];
|
||||
return statusMessage;
|
||||
}
|
||||
|
||||
#pragma mark - Shadows
|
||||
|
||||
- (void)setHasShadows:(BOOL)hasShadows viewItem:(ConversationViewItem *)viewItem
|
||||
{
|
||||
// TODO: Constants
|
||||
for (UIView *subview in @[
|
||||
self.timestampLabel,
|
||||
self.statusLabel,
|
||||
self.statusIndicatorView,
|
||||
]) {
|
||||
if (hasShadows) {
|
||||
subview.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||
subview.layer.shadowOpacity = 0.35f;
|
||||
subview.layer.shadowOffset = CGSizeZero;
|
||||
subview.layer.shadowRadius = 0.5f;
|
||||
} else {
|
||||
subview.layer.shadowColor = nil;
|
||||
subview.layer.shadowOpacity = 0.f;
|
||||
subview.layer.shadowOffset = CGSizeZero;
|
||||
subview.layer.shadowRadius = 0.f;
|
||||
}
|
||||
}
|
||||
|
||||
UIColor *textColor;
|
||||
if (hasShadows) {
|
||||
textColor = [UIColor whiteColor];
|
||||
} else if (viewItem.interaction.interactionType == OWSInteractionType_IncomingMessage) {
|
||||
// TODO:
|
||||
textColor = [UIColor lightGrayColor];
|
||||
} else {
|
||||
textColor = [UIColor whiteColor];
|
||||
}
|
||||
self.timestampLabel.textColor = textColor;
|
||||
self.statusLabel.textColor = textColor;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
Loading…
Reference in New Issue