Merge branch 'charlesmchen/bubbleCollapse'

pull/1/head
Matthew Chen 7 years ago
commit 43e50e33c2

@ -206,12 +206,17 @@
34D2CCD4206294B900CB1A14 /* OWSScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCD3206294B900CB1A14 /* OWSScreenLock.swift */; };
34D2CCDA2062E7D000CB1A14 /* OWSScreenLockUI.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */; };
34D2CCD220618B3000CB1A14 /* OWSBackupLazyRestoreJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCD120618B2F00CB1A14 /* OWSBackupLazyRestoreJob.swift */; };
34D2CCDF206939B400CB1A14 /* DebugUIMessagesAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCDB206939B100CB1A14 /* DebugUIMessagesAction.m */; };
34D2CCE0206939B400CB1A14 /* DebugUIMessagesAssetLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCDC206939B200CB1A14 /* DebugUIMessagesAssetLoader.m */; };
34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */; };
34D8C0271ED3673300188D7C /* DebugUIMessages.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D8C0241ED3673300188D7C /* DebugUIMessages.m */; };
34D8C0281ED3673300188D7C /* DebugUITableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D8C0261ED3673300188D7C /* DebugUITableViewController.m */; };
34D8C02B1ED3685800188D7C /* DebugUIContacts.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D8C02A1ED3685800188D7C /* DebugUIContacts.m */; };
34D99C931F2937CC00D284D6 /* OWSAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D99C911F2937CC00D284D6 /* OWSAnalytics.swift */; };
34DB0BED2011548B007B313F /* OWSDatabaseConverterTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 34DB0BEC2011548B007B313F /* OWSDatabaseConverterTest.m */; };
34DBF003206BD5A500025978 /* OWSMessageTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34DBEFFF206BD5A400025978 /* OWSMessageTextView.m */; };
34DBF004206BD5A500025978 /* OWSBubbleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34DBF001206BD5A500025978 /* OWSBubbleView.m */; };
34DBF007206C3CB200025978 /* OWSBubbleStrokeView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34DBF006206C3CB200025978 /* OWSBubbleStrokeView.m */; };
34E3E5681EC4B19400495BAC /* AudioProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34E3E5671EC4B19400495BAC /* AudioProgressView.swift */; };
34E3EF0D1EFC235B007F6822 /* DebugUIDiskUsage.m in Sources */ = {isa = PBXBuildFile; fileRef = 34E3EF0C1EFC235B007F6822 /* DebugUIDiskUsage.m */; };
34E3EF101EFC2684007F6822 /* DebugUIPage.m in Sources */ = {isa = PBXBuildFile; fileRef = 34E3EF0F1EFC2684007F6822 /* DebugUIPage.m */; };
@ -814,6 +819,11 @@
34D2CCD82062E7D000CB1A14 /* OWSScreenLockUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSScreenLockUI.h; sourceTree = "<group>"; };
34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSScreenLockUI.m; sourceTree = "<group>"; };
34D2CCD120618B2F00CB1A14 /* OWSBackupLazyRestoreJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSBackupLazyRestoreJob.swift; sourceTree = "<group>"; };
34D2CCDB206939B100CB1A14 /* DebugUIMessagesAction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIMessagesAction.m; sourceTree = "<group>"; };
34D2CCDC206939B200CB1A14 /* DebugUIMessagesAssetLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIMessagesAssetLoader.m; sourceTree = "<group>"; };
34D2CCDD206939B200CB1A14 /* DebugUIMessagesAction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIMessagesAction.h; sourceTree = "<group>"; };
34D2CCDE206939B400CB1A14 /* DebugUIMessagesAssetLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIMessagesAssetLoader.h; sourceTree = "<group>"; };
34D2CCE220693A1700CB1A14 /* DebugUIMessagesUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIMessagesUtils.h; sourceTree = "<group>"; };
34D5CCA71EAE3D30005515DB /* AvatarViewHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AvatarViewHelper.h; sourceTree = "<group>"; };
34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AvatarViewHelper.m; sourceTree = "<group>"; };
34D8C0231ED3673300188D7C /* DebugUIMessages.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIMessages.h; sourceTree = "<group>"; };
@ -828,6 +838,12 @@
34D99C911F2937CC00D284D6 /* OWSAnalytics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSAnalytics.swift; sourceTree = "<group>"; };
34DB0BEB2011548A007B313F /* OWSDatabaseConverterTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSDatabaseConverterTest.h; sourceTree = "<group>"; };
34DB0BEC2011548B007B313F /* OWSDatabaseConverterTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDatabaseConverterTest.m; sourceTree = "<group>"; };
34DBEFFF206BD5A400025978 /* OWSMessageTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageTextView.m; sourceTree = "<group>"; };
34DBF000206BD5A400025978 /* OWSMessageTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageTextView.h; sourceTree = "<group>"; };
34DBF001206BD5A500025978 /* OWSBubbleView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBubbleView.m; sourceTree = "<group>"; };
34DBF002206BD5A500025978 /* OWSBubbleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBubbleView.h; sourceTree = "<group>"; };
34DBF005206C3CB100025978 /* OWSBubbleStrokeView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBubbleStrokeView.h; sourceTree = "<group>"; };
34DBF006206C3CB200025978 /* OWSBubbleStrokeView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBubbleStrokeView.m; sourceTree = "<group>"; };
34E3E5671EC4B19400495BAC /* AudioProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioProgressView.swift; sourceTree = "<group>"; };
34E3EF0B1EFC235B007F6822 /* DebugUIDiskUsage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIDiskUsage.h; sourceTree = "<group>"; };
34E3EF0C1EFC235B007F6822 /* DebugUIDiskUsage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIDiskUsage.m; sourceTree = "<group>"; };
@ -1638,6 +1654,10 @@
34D1F0971F867BFC0066283D /* ConversationViewCell.m */,
34D1F0B81F8800D90066283D /* OWSAudioMessageView.h */,
34D1F0B91F8800D90066283D /* OWSAudioMessageView.m */,
34DBF005206C3CB100025978 /* OWSBubbleStrokeView.h */,
34DBF006206C3CB200025978 /* OWSBubbleStrokeView.m */,
34DBF002206BD5A500025978 /* OWSBubbleView.h */,
34DBF001206BD5A500025978 /* OWSBubbleView.m */,
34D1F09A1F867BFC0066283D /* OWSContactOffersCell.h */,
34D1F09B1F867BFC0066283D /* OWSContactOffersCell.m */,
34D1F09C1F867BFC0066283D /* OWSExpirableMessageView.h */,
@ -1647,6 +1667,8 @@
34D1F0B61F87F8850066283D /* OWSGenericAttachmentView.m */,
34D1F0A11F867BFC0066283D /* OWSMessageCell.h */,
34D1F0A21F867BFC0066283D /* OWSMessageCell.m */,
34DBF000206BD5A400025978 /* OWSMessageTextView.h */,
34DBEFFF206BD5A400025978 /* OWSMessageTextView.m */,
34D1F0A51F867BFC0066283D /* OWSSystemMessageCell.h */,
34D1F0A61F867BFC0066283D /* OWSSystemMessageCell.m */,
34D1F0A71F867BFC0066283D /* OWSUnreadIndicatorCell.h */,
@ -1676,6 +1698,11 @@
45B27B852037FFB400A539DF /* DebugUIFileBrowser.swift */,
34D8C0231ED3673300188D7C /* DebugUIMessages.h */,
34D8C0241ED3673300188D7C /* DebugUIMessages.m */,
34D2CCDD206939B200CB1A14 /* DebugUIMessagesAction.h */,
34D2CCDB206939B100CB1A14 /* DebugUIMessagesAction.m */,
34D2CCDE206939B400CB1A14 /* DebugUIMessagesAssetLoader.h */,
34D2CCDC206939B200CB1A14 /* DebugUIMessagesAssetLoader.m */,
34D2CCE220693A1700CB1A14 /* DebugUIMessagesUtils.h */,
341F2C0D1F2B8AE700D07D6B /* DebugUIMisc.h */,
341F2C0E1F2B8AE700D07D6B /* DebugUIMisc.m */,
457C87B72032645C008D52D6 /* DebugUINotifications.swift */,
@ -3113,6 +3140,7 @@
3461293E1FD1D72B00532771 /* ExperienceUpgradeFinder.swift in Sources */,
34D1F0BD1F8D108C0066283D /* AttachmentUploadView.m in Sources */,
452EC6DF205E9E30000E787C /* MediaGalleryViewController.swift in Sources */,
34DBF007206C3CB200025978 /* OWSBubbleStrokeView.m in Sources */,
34D1F0BA1F8800D90066283D /* OWSAudioMessageView.m in Sources */,
34D8C02B1ED3685800188D7C /* DebugUIContacts.m in Sources */,
45C9DEB81DF4E35A0065CA84 /* WebRTCCallMessageHandler.swift in Sources */,
@ -3137,6 +3165,7 @@
34CCAF381F0C0599004084F4 /* AppUpdateNag.m in Sources */,
EF764C351DB67CC5000D9A87 /* UIViewController+Permissions.m in Sources */,
45CD81EF1DC030E7004C9430 /* SyncPushTokensJob.swift in Sources */,
34D2CCE0206939B400CB1A14 /* DebugUIMessagesAssetLoader.m in Sources */,
45794E861E00620000066731 /* CallUIAdapter.swift in Sources */,
340FC8BA204DAC8D007AEB0F /* FingerprintViewScanController.m in Sources */,
4585C4681ED8F8D200896AEA /* SafetyNumberConfirmationAlert.swift in Sources */,
@ -3200,6 +3229,7 @@
45F32C222057297A00A300D5 /* MediaDetailViewController.m in Sources */,
34B3F8851E8DF1700035BE1A /* NewGroupViewController.m in Sources */,
34D8C0271ED3673300188D7C /* DebugUIMessages.m in Sources */,
34DBF003206BD5A500025978 /* OWSMessageTextView.m in Sources */,
34D1F0B41F86D31D0066283D /* ConversationCollectionView.m in Sources */,
34B3F8821E8DF1700035BE1A /* NewContactThreadViewController.m in Sources */,
45D308AD2049A439000189E4 /* PinEntryView.m in Sources */,
@ -3231,6 +3261,7 @@
458DE9D61DEE3FD00071BB03 /* PeerConnectionClient.swift in Sources */,
45F32C242057297A00A300D5 /* MessageDetailViewController.swift in Sources */,
34D1F0841F8678AA0066283D /* ConversationInputToolbar.m in Sources */,
34DBF004206BD5A500025978 /* OWSBubbleView.m in Sources */,
FCC81A981A44558300DFEC7D /* UIDevice+TSHardwareVersion.m in Sources */,
76EB054018170B33006006FC /* AppDelegate.m in Sources */,
34D1F0831F8678AA0066283D /* ConversationInputTextView.m in Sources */,
@ -3246,6 +3277,7 @@
3461299C1FD1EA9E00532771 /* NotificationsManager.m in Sources */,
4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */,
34B3F87E1E8DF1700035BE1A /* InboxTableViewCell.m in Sources */,
34D2CCDF206939B400CB1A14 /* DebugUIMessagesAction.m in Sources */,
340FC8AC204DAC8D007AEB0F /* PrivacySettingsTableViewController.m in Sources */,
34D2CCD4206294B900CB1A14 /* OWSScreenLock.swift in Sources */,
340FC8C5204DE223007AEB0F /* DebugUIBackup.m in Sources */,

@ -22,6 +22,7 @@
#import "OWSBackup.h"
#import "OWSBackupIO.h"
#import "OWSBezierPathView.h"
#import "OWSBubbleView.h"
#import "OWSCallNotificationsAdaptee.h"
#import "OWSDatabaseMigration.h"
#import "OWSMessageCell.h"

@ -1,5 +1,5 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@ -19,7 +19,6 @@ typedef void (^AttachmentStateBlock)(BOOL isAttachmentReady);
@interface AttachmentUploadView : UIView
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachment
superview:(UIView *)superview
attachmentStateCallback:(AttachmentStateBlock _Nullable)attachmentStateCallback;
@end

@ -1,5 +1,5 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "AttachmentUploadView.h"
@ -32,21 +32,16 @@ NS_ASSUME_NONNULL_BEGIN
@implementation AttachmentUploadView
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachment
superview:(UIView *)superview
attachmentStateCallback:(AttachmentStateBlock _Nullable)attachmentStateCallback
{
self = [super init];
if (self) {
OWSAssert(attachment);
OWSAssert(superview);
self.attachment = attachment;
self.attachmentStateCallback = attachmentStateCallback;
[superview addSubview:self];
[self autoPinToSuperviewEdges];
_bezierPathView = [OWSBezierPathView new];
self.bezierPathView.configureShapeLayerBlock = ^(CAShapeLayer *layer, CGRect bounds) {
layer.path = [UIBezierPath bezierPathWithRect:bounds].CGPath;

@ -0,0 +1,20 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@class OWSBubbleView;
@interface OWSBubbleStrokeView : UIView
@property (nonatomic, weak) OWSBubbleView *bubbleView;
@property (nonatomic) UIColor *strokeColor;
@property (nonatomic) CGFloat strokeThickness;
- (void)updateLayers;
@end
NS_ASSUME_NONNULL_END

@ -0,0 +1,114 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSBubbleStrokeView.h"
#import "OWSBubbleView.h"
#import <SignalMessaging/UIView+OWS.h>
NS_ASSUME_NONNULL_BEGIN
@interface OWSBubbleStrokeView ()
@property (nonatomic) CAShapeLayer *shapeLayer;
@end
#pragma mark -
@implementation OWSBubbleStrokeView
- (instancetype)init
{
self = [super init];
if (!self) {
return self;
}
self.opaque = NO;
self.backgroundColor = [UIColor clearColor];
self.shapeLayer = [CAShapeLayer new];
[self.layer addSublayer:self.shapeLayer];
return self;
}
- (void)setStrokeColor:(UIColor *)strokeColor
{
_strokeColor = strokeColor;
[self updateLayers];
}
- (void)setStrokeThickness:(CGFloat)strokeThickness
{
_strokeThickness = strokeThickness;
[self updateLayers];
}
- (void)setFrame:(CGRect)frame
{
BOOL didChange = !CGRectEqualToRect(self.frame, frame);
[super setFrame:frame];
if (didChange || !self.shapeLayer) {
[self updateLayers];
}
}
- (void)setBounds:(CGRect)bounds
{
BOOL didChange = !CGRectEqualToRect(self.bounds, bounds);
[super setBounds:bounds];
if (didChange || !self.shapeLayer) {
[self updateLayers];
}
}
- (void)setCenter:(CGPoint)center
{
[super setCenter:center];
[self updateLayers];
}
- (void)updateLayers
{
OWSAssert(self.shapeLayer);
// Don't fill the shape layer; we just want a stroke around the border.
self.shapeLayer.fillColor = [UIColor clearColor].CGColor;
self.clipsToBounds = YES;
if (!self.bubbleView) {
return;
}
self.shapeLayer.strokeColor = self.strokeColor.CGColor;
self.shapeLayer.lineWidth = self.strokeThickness;
self.shapeLayer.zPosition = 100.f;
UIBezierPath *bezierPath = [UIBezierPath new];
UIBezierPath *boundsBezierPath = [UIBezierPath bezierPathWithRect:self.bounds];
[bezierPath appendPath:boundsBezierPath];
UIBezierPath *bubbleBezierPath = [self.bubbleView maskPath];
// We need to convert between coordinate systems using layers, not views.
CGPoint bubbleOffset = [self.layer convertPoint:CGPointZero fromLayer:self.bubbleView.layer];
CGAffineTransform transform = CGAffineTransformMakeTranslation(bubbleOffset.x, bubbleOffset.y);
[bubbleBezierPath applyTransform:transform];
[bezierPath appendPath:bubbleBezierPath];
self.shapeLayer.path = bezierPath.CGPath;
}
@end
NS_ASSUME_NONNULL_END

@ -0,0 +1,32 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
extern const CGFloat kOWSMessageCellCornerRadius;
extern const CGFloat kBubbleVRounding;
extern const CGFloat kBubbleHRounding;
extern const CGFloat kBubbleThornSideInset;
extern const CGFloat kBubbleThornVInset;
extern const CGFloat kBubbleTextHInset;
extern const CGFloat kBubbleTextVInset;
@class OWSBubbleStrokeView;
@interface OWSBubbleView : UIView
@property (nonatomic, weak, nullable) OWSBubbleStrokeView *bubbleStrokeView;
@property (nonatomic) BOOL isOutgoing;
@property (nonatomic) BOOL hideTail;
@property (nonatomic) BOOL isTruncated;
@property (nonatomic, nullable) UIColor *bubbleColor;
- (UIBezierPath *)maskPath;
@end
NS_ASSUME_NONNULL_END

@ -0,0 +1,205 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSBubbleView.h"
#import "OWSBubbleStrokeView.h"
#import <SignalMessaging/UIView+OWS.h>
NS_ASSUME_NONNULL_BEGIN
const CGFloat kOWSMessageCellCornerRadius = 17;
const CGFloat kBubbleVRounding = kOWSMessageCellCornerRadius;
const CGFloat kBubbleHRounding = kOWSMessageCellCornerRadius;
const CGFloat kBubbleThornSideInset = 5.f;
const CGFloat kBubbleThornVInset = 0;
const CGFloat kBubbleTextHInset = 10.f;
const CGFloat kBubbleTextVInset = 10.f;
@interface OWSBubbleView ()
@property (nonatomic) CAShapeLayer *maskLayer;
@property (nonatomic) CAShapeLayer *shapeLayer;
@end
#pragma mark -
@implementation OWSBubbleView
- (instancetype)init
{
self = [super init];
if (!self) {
return self;
}
self.shapeLayer = [CAShapeLayer new];
[self.layer addSublayer:self.shapeLayer];
self.maskLayer = [CAShapeLayer new];
self.layer.mask = self.maskLayer;
return self;
}
- (void)setIsOutgoing:(BOOL)isOutgoing
{
BOOL didChange = _isOutgoing != isOutgoing;
_isOutgoing = isOutgoing;
if (didChange || !self.shapeLayer) {
[self updateLayers];
}
}
- (void)setHideTail:(BOOL)hideTail
{
BOOL didChange = _hideTail != hideTail;
_hideTail = hideTail;
if (didChange || !self.shapeLayer) {
[self updateLayers];
}
}
- (void)setIsTruncated:(BOOL)isTruncated
{
BOOL didChange = _isTruncated != isTruncated;
_isTruncated = isTruncated;
if (didChange || !self.shapeLayer) {
[self updateLayers];
}
}
- (void)setFrame:(CGRect)frame
{
// We only need to update our layers if the _size_ of this view
// changes since the contents of the layers are in local coordinates.
BOOL didChangeSize = !CGSizeEqualToSize(self.frame.size, frame.size);
[super setFrame:frame];
if (didChangeSize || !self.shapeLayer) {
[self updateLayers];
}
// We always need to inform the "bubble stroke view" (if any) if our
// frame/bounds/center changes. Its contents are not in local coordinates.
[self.bubbleStrokeView updateLayers];
}
- (void)setBounds:(CGRect)bounds
{
// We only need to update our layers if the _size_ of this view
// changes since the contents of the layers are in local coordinates.
BOOL didChangeSize = !CGSizeEqualToSize(self.bounds.size, bounds.size);
[super setBounds:bounds];
if (didChangeSize || !self.shapeLayer) {
[self updateLayers];
}
// We always need to inform the "bubble stroke view" (if any) if our
// frame/bounds/center changes. Its contents are not in local coordinates.
[self.bubbleStrokeView updateLayers];
}
- (void)setCenter:(CGPoint)center
{
[super setCenter:center];
// We always need to inform the "bubble stroke view" (if any) if our
// frame/bounds/center changes. Its contents are not in local coordinates.
[self.bubbleStrokeView updateLayers];
}
- (void)setBubbleColor:(nullable UIColor *)bubbleColor
{
_bubbleColor = bubbleColor;
if (!self.shapeLayer) {
[self updateLayers];
}
self.shapeLayer.fillColor = bubbleColor.CGColor;
}
- (void)updateLayers
{
OWSAssert(self.maskLayer);
OWSAssert(self.shapeLayer);
UIBezierPath *bezierPath = [self maskPath];
self.shapeLayer.fillColor = self.bubbleColor.CGColor;
self.shapeLayer.path = bezierPath.CGPath;
self.maskLayer.path = bezierPath.CGPath;
}
- (UIBezierPath *)maskPath
{
return [self.class maskPathForSize:self.bounds.size
isOutgoing:self.isOutgoing
hideTail:self.hideTail
isTruncated:self.isTruncated
isRTL:self.isRTL];
}
+ (UIBezierPath *)maskPathForSize:(CGSize)size
isOutgoing:(BOOL)isOutgoing
hideTail:(BOOL)hideTail
isTruncated:(BOOL)isTruncated
isRTL:(BOOL)isRTL
{
UIBezierPath *bezierPath = [UIBezierPath new];
CGFloat bubbleLeft = 0.f;
CGFloat bubbleRight = size.width - kBubbleThornSideInset;
CGFloat bubbleTop = 0.f;
CGFloat bubbleBottom = size.height - kBubbleThornVInset;
[bezierPath moveToPoint:CGPointMake(bubbleLeft + kBubbleHRounding, bubbleTop)];
[bezierPath addLineToPoint:CGPointMake(bubbleRight - kBubbleHRounding, bubbleTop)];
[bezierPath addQuadCurveToPoint:CGPointMake(bubbleRight, bubbleTop + kBubbleVRounding)
controlPoint:CGPointMake(bubbleRight, bubbleTop)];
[bezierPath addLineToPoint:CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding)];
[bezierPath addQuadCurveToPoint:CGPointMake(bubbleRight - kBubbleHRounding, bubbleBottom)
controlPoint:CGPointMake(bubbleRight, bubbleBottom)];
[bezierPath addLineToPoint:CGPointMake(bubbleLeft + kBubbleHRounding, bubbleBottom)];
[bezierPath addQuadCurveToPoint:CGPointMake(bubbleLeft, bubbleBottom - kBubbleVRounding)
controlPoint:CGPointMake(bubbleLeft, bubbleBottom)];
[bezierPath addLineToPoint:CGPointMake(bubbleLeft, bubbleTop + kBubbleVRounding)];
[bezierPath addQuadCurveToPoint:CGPointMake(bubbleLeft + kBubbleHRounding, bubbleTop)
controlPoint:CGPointMake(bubbleLeft, bubbleTop)];
if (!hideTail) {
// Thorn Tip
CGPoint thornTip = CGPointMake(size.width + 1, size.height);
CGPoint thornA = CGPointMake(bubbleRight - kBubbleHRounding * 0.5f, bubbleBottom - kBubbleVRounding);
CGPoint thornB = CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding);
[bezierPath moveToPoint:thornTip];
[bezierPath addQuadCurveToPoint:thornA controlPoint:CGPointMake(thornA.x, bubbleBottom)];
[bezierPath addLineToPoint:thornB];
[bezierPath addQuadCurveToPoint:thornTip controlPoint:CGPointMake(thornB.x, bubbleBottom)];
[bezierPath addLineToPoint:thornTip];
}
// Horizontal Flip If Necessary
BOOL shouldFlip = isOutgoing == isRTL;
if (shouldFlip) {
CGAffineTransform flipTransform = CGAffineTransformMakeTranslation(size.width, 0.0);
flipTransform = CGAffineTransformScale(flipTransform, -1.0, 1.0);
[bezierPath applyTransform:flipTransform];
}
return bezierPath;
}
@end
NS_ASSUME_NONNULL_END

@ -6,8 +6,6 @@
NS_ASSUME_NONNULL_BEGIN
extern const CGFloat OWSMessageCellCornerRadius;
@interface OWSMessageCell : ConversationViewCell
+ (NSString *)cellReuseIdentifier;

@ -0,0 +1,13 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@interface OWSMessageTextView : UITextView
@property (nonatomic) BOOL shouldIgnoreEvents;
@end
NS_ASSUME_NONNULL_END

@ -0,0 +1,65 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSMessageTextView.h"
NS_ASSUME_NONNULL_BEGIN
@implementation OWSMessageTextView
// Our message text views are never used for editing;
// suppress their ability to become first responder
// so that tapping on them doesn't hide keyboard.
- (BOOL)canBecomeFirstResponder
{
return NO;
}
// Ignore interactions with the text view _except_ taps on links.
//
// We want to disable "partial" selection of text in the message
// and we want to enable "tap to resend" by tapping on a message.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *_Nullable)event
{
if (self.shouldIgnoreEvents) {
// We ignore all events for failed messages so that users
// can tap-to-resend even "all link" messages.
return NO;
}
// Find the nearest text position to the event.
UITextPosition *_Nullable position = [self closestPositionToPoint:point];
if (!position) {
return NO;
}
// Find the range of the character in the text which contains the event.
//
// Try every layout direction (this might not be necessary).
UITextRange *_Nullable range = nil;
for (NSNumber *textLayoutDirection in @[
@(UITextLayoutDirectionLeft),
@(UITextLayoutDirectionRight),
@(UITextLayoutDirectionUp),
@(UITextLayoutDirectionDown),
]) {
range = [self.tokenizer rangeEnclosingPosition:position
withGranularity:UITextGranularityCharacter
inDirection:(UITextDirection)textLayoutDirection.intValue];
if (range) {
break;
}
}
if (!range) {
return NO;
}
// Ignore the event unless it occurred inside a link.
NSInteger startIndex = [self offsetFromPosition:self.beginningOfDocument toPosition:range.start];
BOOL result =
[self.attributedText attribute:NSLinkAttributeName atIndex:(NSUInteger)startIndex effectiveRange:nil] != nil;
return result;
}
@end
NS_ASSUME_NONNULL_END

@ -2274,6 +2274,14 @@ typedef enum : NSUInteger {
- (void)scrollDownButtonTapped
{
#ifdef DEBUG
CGPoint contentOffset = self.collectionView.contentOffset;
contentOffset.y += self.collectionView.height
- (self.collectionView.contentInset.top + self.collectionView.contentInset.bottom);
[self.collectionView setContentOffset:contentOffset animated:NO];
return;
#endif
NSIndexPath *indexPathOfUnreadMessagesIndicator = [self indexPathOfUnreadMessagesIndicator];
if (indexPathOfUnreadMessagesIndicator != nil) {
NSInteger unreadRow = indexPathOfUnreadMessagesIndicator.row;
@ -4455,8 +4463,10 @@ typedef enum : NSUInteger {
// Update the "shouldShowDate" property of the view items.
OWSInteractionType lastInteractionType = OWSInteractionType_Unknown;
MessageRecipientStatus lastRecipientStatus = MessageRecipientStatusUploading;
NSString *_Nullable lastIncomingSenderId = nil;
for (ConversationViewItem *viewItem in viewItems.reverseObjectEnumerator) {
BOOL shouldHideRecipientStatus = NO;
BOOL shouldHideBubbleTail = NO;
OWSInteractionType interactionType = viewItem.interaction.interactionType;
if (interactionType == OWSInteractionType_OutgoingMessage) {
@ -4465,14 +4475,23 @@ typedef enum : NSUInteger {
[MessageRecipientStatusUtils recipientStatusWithOutgoingMessage:outgoingMessage];
if (outgoingMessage.messageState == TSOutgoingMessageStateUnsent) {
// always sow "failed to send" status
// always show "failed to send" status
shouldHideRecipientStatus = NO;
} else {
shouldHideRecipientStatus
= (interactionType == lastInteractionType && recipientStatus == lastRecipientStatus);
}
shouldHideBubbleTail = (interactionType == lastInteractionType && recipientStatus == lastRecipientStatus);
lastRecipientStatus = recipientStatus;
} else if (interactionType == OWSInteractionType_IncomingMessage) {
TSIncomingMessage *incomingMessage = (TSIncomingMessage *)viewItem.interaction;
NSString *incomingSenderId = incomingMessage.authorId;
OWSAssert(incomingSenderId.length > 0);
shouldHideBubbleTail = (interactionType == lastInteractionType &&
[NSObject isNullableObject:lastIncomingSenderId equalTo:incomingSenderId]);
lastIncomingSenderId = incomingSenderId;
}
lastInteractionType = interactionType;
@ -4483,6 +4502,7 @@ typedef enum : NSUInteger {
[rowsThatChangedSize addObject:@(viewItem.previousRow)];
}
viewItem.shouldHideRecipientStatus = shouldHideRecipientStatus;
viewItem.shouldHideBubbleTail = shouldHideBubbleTail;
}
self.viewItems = viewItems;

@ -43,9 +43,10 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
@property (nonatomic, readonly) TSInteraction *interaction;
@property (nonatomic, readonly) BOOL isGroupThread;
@property (nonatomic, readonly) BOOL hasText;
@property (nonatomic, readonly) BOOL hasBodyText;
@property (nonatomic) BOOL shouldShowDate;
@property (nonatomic) BOOL shouldHideRecipientStatus;
@property (nonatomic) BOOL shouldHideBubbleTail;
@property (nonatomic) NSInteger row;
// During updates, we sometimes need the previous row index
@ -79,7 +80,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
// These methods only apply to text & attachment messages.
- (OWSMessageCellType)messageCellType;
- (nullable DisplayableText *)displayableText;
- (nullable DisplayableText *)displayableBodyText;
- (nullable TSAttachmentStream *)attachmentStream;
- (nullable TSAttachmentPointer *)attachmentPointer;
- (CGSize)mediaSize;

@ -55,11 +55,10 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
@property (nonatomic) BOOL hasViewState;
@property (nonatomic) OWSMessageCellType messageCellType;
@property (nonatomic, nullable) DisplayableText *displayableText;
@property (nonatomic, nullable) DisplayableText *displayableBodyText;
@property (nonatomic, nullable) TSAttachmentStream *attachmentStream;
@property (nonatomic, nullable) TSAttachmentPointer *attachmentPointer;
@property (nonatomic) CGSize mediaSize;
@property (nonatomic) BOOL hasText;
@end
@ -95,7 +94,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
self.hasViewState = NO;
self.messageCellType = OWSMessageCellType_Unknown;
self.displayableText = nil;
self.displayableBodyText = nil;
self.attachmentStream = nil;
self.attachmentPointer = nil;
self.mediaSize = CGSizeZero;
@ -105,6 +104,11 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
[self ensureViewState:transaction];
}
- (BOOL)hasBodyText
{
return _displayableBodyText != nil;
}
- (void)setShouldShowDate:(BOOL)shouldShowDate
{
if (_shouldShowDate == shouldShowDate) {
@ -127,6 +131,17 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
[self clearCachedLayoutState];
}
- (void)setShouldHideBubbleTail:(BOOL)shouldHideBubbleTail
{
if (_shouldHideBubbleTail == shouldHideBubbleTail) {
return;
}
_shouldHideBubbleTail = shouldHideBubbleTail;
[self clearCachedLayoutState];
}
- (void)clearCachedLayoutState
{
self.cachedCellSize = nil;
@ -264,7 +279,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
// TODO: Now that we're caching the displayable text on the view items,
// I don't think we need this cache any more.
- (NSCache *)displayableTextCache
- (NSCache *)displayableBodyTextCache
{
static NSCache *cache = nil;
static dispatch_once_t onceToken;
@ -276,44 +291,45 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
return cache;
}
- (DisplayableText *)displayableTextForText:(NSString *)text interactionId:(NSString *)interactionId
- (DisplayableText *)displayableBodyTextForText:(NSString *)text interactionId:(NSString *)interactionId
{
OWSAssert(text);
OWSAssert(interactionId.length > 0);
return [self displayableTextForInteractionId:interactionId
textBlock:^{
return text;
}];
return [self displayableBodyTextForInteractionId:interactionId
textBlock:^{
return text;
}];
}
- (DisplayableText *)displayableTextForAttachmentStream:(TSAttachmentStream *)attachmentStream
interactionId:(NSString *)interactionId
- (DisplayableText *)displayableBodyTextForOversizeTextAttachment:(TSAttachmentStream *)attachmentStream
interactionId:(NSString *)interactionId
{
OWSAssert(attachmentStream);
OWSAssert(interactionId.length > 0);
return [self displayableTextForInteractionId:interactionId
textBlock:^{
NSData *textData = [NSData dataWithContentsOfURL:attachmentStream.mediaURL];
NSString *text =
[[NSString alloc] initWithData:textData encoding:NSUTF8StringEncoding];
return text;
}];
return
[self displayableBodyTextForInteractionId:interactionId
textBlock:^{
NSData *textData = [NSData dataWithContentsOfURL:attachmentStream.mediaURL];
NSString *text =
[[NSString alloc] initWithData:textData encoding:NSUTF8StringEncoding];
return text;
}];
}
- (DisplayableText *)displayableTextForInteractionId:(NSString *)interactionId
textBlock:(NSString * (^_Nonnull)(void))textBlock
- (DisplayableText *)displayableBodyTextForInteractionId:(NSString *)interactionId
textBlock:(NSString * (^_Nonnull)(void))textBlock
{
OWSAssert(interactionId.length > 0);
DisplayableText *_Nullable displayableText = [[self displayableTextCache] objectForKey:interactionId];
if (!displayableText) {
DisplayableText *_Nullable displayableBodyText = [[self displayableBodyTextCache] objectForKey:interactionId];
if (!displayableBodyText) {
NSString *text = textBlock();
displayableText = [DisplayableText displayableText:text];
[[self displayableTextCache] setObject:displayableText forKey:interactionId];
displayableBodyText = [DisplayableText displayableText:text];
[[self displayableBodyTextCache] setObject:displayableBodyText forKey:interactionId];
}
return displayableText;
return displayableBodyText;
}
- (nullable TSAttachment *)firstAttachmentIfAnyOfMessage:(TSMessage *)message
@ -353,9 +369,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
if ([attachment.contentType isEqualToString:OWSMimeTypeOversizeTextMessage]) {
self.messageCellType = OWSMessageCellType_OversizeTextMessage;
self.displayableText =
[self displayableTextForAttachmentStream:self.attachmentStream interactionId:message.uniqueId];
self.hasText = YES;
self.displayableBodyText = [self displayableBodyTextForOversizeTextAttachment:self.attachmentStream
interactionId:message.uniqueId];
} else if ([self.attachmentStream isAnimated] || [self.attachmentStream isImage] ||
[self.attachmentStream isVideo]) {
if ([self.attachmentStream isAnimated]) {
@ -392,16 +407,20 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
}
}
// Ignore message body for oversize text attachments.
if (message.body.length > 0) {
self.hasText = YES;
if (self.hasBodyText) {
OWSFail(@"%@ oversize text message has unexpected caption.", self.logTag);
}
// If we haven't already assigned an attachment type at this point, message.body isn't a caption,
// it's a stand-alone text message.
if (self.messageCellType == OWSMessageCellType_Unknown) {
OWSAssert(message.attachmentIds.count == 0);
self.messageCellType = OWSMessageCellType_TextMessage;
}
self.displayableText = [self displayableTextForText:message.body interactionId:message.uniqueId];
OWSAssert(self.displayableText);
self.displayableBodyText = [self displayableBodyTextForText:message.body interactionId:message.uniqueId];
OWSAssert(self.displayableBodyText);
}
if (self.messageCellType == OWSMessageCellType_Unknown) {
@ -409,8 +428,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
// are rendered like empty text messages, but without any interactivity.
DDLogWarn(@"%@ Treating unknown message as empty text message: %@", self.logTag, message.description);
self.messageCellType = OWSMessageCellType_TextMessage;
self.hasText = YES;
self.displayableText = [[DisplayableText alloc] initWithFullText:@"" displayText:@"" isTextTruncated:NO];
self.displayableBodyText = [[DisplayableText alloc] initWithFullText:@"" displayText:@"" isTextTruncated:NO];
}
}
@ -421,16 +439,16 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
return _messageCellType;
}
- (nullable DisplayableText *)displayableText
- (nullable DisplayableText *)displayableBodyText
{
OWSAssertIsOnMainThread();
OWSAssert(self.hasViewState);
OWSAssert(_displayableText);
OWSAssert(_displayableText.displayText);
OWSAssert(_displayableText.fullText);
OWSAssert(_displayableBodyText);
OWSAssert(_displayableBodyText.displayText);
OWSAssert(_displayableBodyText.fullText);
return _displayableText;
return _displayableBodyText;
}
- (nullable TSAttachmentStream *)attachmentStream
@ -533,13 +551,13 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
- (BOOL)canPerformAction:(SEL)action
{
if (action == self.copyTextActionSelector) {
return [self hasTextActionContent];
return [self hasBodyTextActionContent];
} else if (action == self.copyMediaActionSelector) {
return [self hasMediaActionContent];
} else if (action == self.saveMediaActionSelector) {
return [self canSaveMedia];
} else if (action == self.shareTextActionSelector) {
return [self hasTextActionContent];
return [self hasBodyTextActionContent];
} else if (action == self.shareMediaActionSelector) {
return [self hasMediaActionContent];
} else if (action == self.deleteActionSelector) {
@ -561,8 +579,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
case OWSMessageCellType_Audio:
case OWSMessageCellType_Video:
case OWSMessageCellType_GenericAttachment: {
OWSAssert(self.displayableText);
[UIPasteboard.generalPasteboard setString:self.displayableText.fullText];
OWSAssert(self.displayableBodyText);
[UIPasteboard.generalPasteboard setString:self.displayableBodyText.fullText];
break;
}
case OWSMessageCellType_DownloadingAttachment: {
@ -620,8 +638,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
case OWSMessageCellType_Audio:
case OWSMessageCellType_Video:
case OWSMessageCellType_GenericAttachment: {
OWSAssert(self.displayableText);
[AttachmentSharing showShareUIForText:self.displayableText.fullText];
OWSAssert(self.displayableBodyText);
[AttachmentSharing showShareUIForText:self.displayableBodyText.fullText];
break;
}
case OWSMessageCellType_DownloadingAttachment: {
@ -728,9 +746,9 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
[self.interaction remove];
}
- (BOOL)hasTextActionContent
- (BOOL)hasBodyTextActionContent
{
return self.hasText && self.displayableText.fullText.length > 0;
return self.hasBodyText && self.displayableBodyText.fullText.length > 0;
}
- (BOOL)hasMediaActionContent

@ -1,13 +1,11 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "DebugUIPage.h"
NS_ASSUME_NONNULL_BEGIN
@class TSThread;
@interface DebugUIMessages : DebugUIPage
@end

File diff suppressed because it is too large Load Diff

@ -0,0 +1,62 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "DebugUIMessagesUtils.h"
NS_ASSUME_NONNULL_BEGIN
@interface DebugUIMessagesAction : NSObject
@property (nonatomic) NSString *label;
- (void)prepareAndPerformNTimes:(NSUInteger)count;
@end
#pragma mark -
@interface DebugUIMessagesSingleAction : DebugUIMessagesAction
+ (DebugUIMessagesAction *)actionWithLabel:(NSString *)label
staggeredActionBlock:(StaggeredActionBlock)staggeredActionBlock;
+ (DebugUIMessagesAction *)actionWithLabel:(NSString *)label
unstaggeredActionBlock:(UnstaggeredActionBlock)unstaggeredActionBlock;
+ (DebugUIMessagesAction *)actionWithLabel:(NSString *)label
staggeredActionBlock:(StaggeredActionBlock)staggeredActionBlock
prepareBlock:(ActionPrepareBlock)prepareBlock;
+ (DebugUIMessagesAction *)actionWithLabel:(NSString *)label
unstaggeredActionBlock:(UnstaggeredActionBlock)unstaggeredActionBlock
prepareBlock:(ActionPrepareBlock)prepareBlock;
@end
#pragma mark -
typedef NS_ENUM(NSUInteger, SubactionMode) {
SubactionMode_Random = 0,
SubactionMode_Ordered,
};
@interface DebugUIMessagesGroupAction : DebugUIMessagesAction
@property (nonatomic, readonly) SubactionMode subactionMode;
@property (nonatomic, readonly, nullable) NSArray<DebugUIMessagesAction *> *subactions;
// Given a group of subactions, perform a single random subaction each time.
+ (DebugUIMessagesAction *)randomGroupActionWithLabel:(NSString *)label
subactions:(NSArray<DebugUIMessagesAction *> *)subactions;
// Given a group of subactions, perform the subactions in order.
//
// If prepareAndPerformNTimes: is called with count == subactions.count, all of the subactions
// are performed exactly once.
+ (DebugUIMessagesAction *)allGroupActionWithLabel:(NSString *)label
subactions:(NSArray<DebugUIMessagesAction *> *)subactions;
@end
NS_ASSUME_NONNULL_END

@ -0,0 +1,288 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "DebugUIMessagesAction.h"
#import <SignalServiceKit/OWSPrimaryStorage.h>
NS_ASSUME_NONNULL_BEGIN
@class DebugUIMessagesSingleAction;
@interface DebugUIMessagesAction ()
@end
#pragma mark -
@interface DebugUIMessagesSingleAction ()
@property (nonatomic, nullable) ActionPrepareBlock prepareBlock;
// "Single" actions should have exactly one "staggered" or "unstaggered" action block.
@property (nonatomic, nullable) StaggeredActionBlock staggeredActionBlock;
@property (nonatomic, nullable) UnstaggeredActionBlock unstaggeredActionBlock;
@end
#pragma mark -
@implementation DebugUIMessagesAction
- (DebugUIMessagesSingleAction *)nextActionToPerform
{
return (DebugUIMessagesSingleAction *)self;
}
- (void)prepare:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure
{
OWSAssert(success);
OWSAssert(failure);
OWS_ABSTRACT_METHOD();
success();
}
- (void)prepareAndPerformNTimes:(NSUInteger)count
{
DDLogInfo(@"%@ %@ prepareAndPerformNTimes: %zd", self.logTag, self.label, count);
[DDLog flushLog];
[self prepare:^{
[self performNTimes:count
success:^{
}
failure:^{
}];
}
failure:^{
}];
}
- (void)performNTimes:(NSUInteger)countParam success:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure
{
OWSAssert(success);
OWSAssert(failure);
DDLogInfo(@"%@ %@ performNTimes: %zd", self.logTag, self.label, countParam);
[DDLog flushLog];
if (countParam < 1) {
success();
return;
}
__block NSUInteger count = countParam;
[OWSPrimaryStorage.sharedManager.newDatabaseConnection readWriteWithBlock:^(
YapDatabaseReadWriteTransaction *transaction) {
NSUInteger batchSize = 0;
while (count > 0) {
NSUInteger index = count;
DebugUIMessagesSingleAction *action = [self nextActionToPerform];
OWSAssert([action isKindOfClass:[DebugUIMessagesSingleAction class]]);
if (action.staggeredActionBlock) {
OWSAssert(!action.unstaggeredActionBlock);
action.staggeredActionBlock(index,
transaction,
^{
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
DDLogInfo(@"%@ %@ performNTimes success: %zd", self.logTag, self.label, count);
[self performNTimes:count - 1 success:success failure:failure];
});
},
failure);
break;
} else {
OWSAssert(action.unstaggeredActionBlock);
// TODO: We could check result for failure.
action.unstaggeredActionBlock(index, transaction);
const NSUInteger kMaxBatchSize = 2500;
batchSize++;
if (batchSize >= kMaxBatchSize) {
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
DDLogInfo(@"%@ %@ performNTimes success: %zd", self.logTag, self.label, count);
[self performNTimes:count - 1 success:success failure:failure];
});
break;
}
count--;
}
}
}];
}
@end
#pragma mark -
@implementation DebugUIMessagesSingleAction
+ (DebugUIMessagesAction *)actionWithLabel:(NSString *)label
staggeredActionBlock:(StaggeredActionBlock)staggeredActionBlock
{
OWSAssert(label.length > 0);
OWSAssert(staggeredActionBlock);
DebugUIMessagesSingleAction *instance = [DebugUIMessagesSingleAction new];
instance.label = label;
instance.staggeredActionBlock = staggeredActionBlock;
return instance;
}
+ (DebugUIMessagesAction *)actionWithLabel:(NSString *)label
unstaggeredActionBlock:(UnstaggeredActionBlock)unstaggeredActionBlock
{
OWSAssert(label.length > 0);
OWSAssert(unstaggeredActionBlock);
DebugUIMessagesSingleAction *instance = [DebugUIMessagesSingleAction new];
instance.label = label;
instance.unstaggeredActionBlock = unstaggeredActionBlock;
return instance;
}
+ (DebugUIMessagesAction *)actionWithLabel:(NSString *)label
staggeredActionBlock:(StaggeredActionBlock)staggeredActionBlock
prepareBlock:(ActionPrepareBlock)prepareBlock
{
OWSAssert(label.length > 0);
OWSAssert(staggeredActionBlock);
OWSAssert(prepareBlock);
DebugUIMessagesSingleAction *instance = [DebugUIMessagesSingleAction new];
instance.label = label;
instance.staggeredActionBlock = staggeredActionBlock;
instance.prepareBlock = prepareBlock;
return instance;
}
+ (DebugUIMessagesAction *)actionWithLabel:(NSString *)label
unstaggeredActionBlock:(UnstaggeredActionBlock)unstaggeredActionBlock
prepareBlock:(ActionPrepareBlock)prepareBlock
{
OWSAssert(label.length > 0);
OWSAssert(unstaggeredActionBlock);
OWSAssert(prepareBlock);
DebugUIMessagesSingleAction *instance = [DebugUIMessagesSingleAction new];
instance.label = label;
instance.unstaggeredActionBlock = unstaggeredActionBlock;
instance.prepareBlock = prepareBlock;
return instance;
}
- (void)prepare:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure
{
OWSAssert(success);
OWSAssert(failure);
if (self.prepareBlock) {
self.prepareBlock(success, failure);
} else {
success();
}
}
@end
#pragma mark -
@interface DebugUIMessagesGroupAction ()
@property (nonatomic) SubactionMode subactionMode;
@property (nonatomic, nullable) NSArray<DebugUIMessagesAction *> *subactions;
@property (nonatomic) NSUInteger subactionIndex;
@end
#pragma mark -
@implementation DebugUIMessagesGroupAction
- (DebugUIMessagesSingleAction *)nextActionToPerform
{
OWSAssert(self.subactions.count > 0);
switch (self.subactionMode) {
case SubactionMode_Random: {
DebugUIMessagesAction *subaction = self.subactions[arc4random_uniform((uint32_t)self.subactions.count)];
OWSAssert(subaction);
return subaction.nextActionToPerform;
}
case SubactionMode_Ordered: {
DebugUIMessagesAction *subaction = self.subactions[self.subactionIndex];
OWSAssert(subaction);
self.subactionIndex = (self.subactionIndex + 1) % self.subactions.count;
return subaction.nextActionToPerform;
}
}
}
- (void)prepare:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure
{
OWSAssert(success);
OWSAssert(failure);
[DebugUIMessagesGroupAction prepareSubactions:[self.subactions mutableCopy] success:success failure:failure];
}
+ (void)prepareSubactions:(NSMutableArray<DebugUIMessagesAction *> *)unpreparedSubactions
success:(ActionSuccessBlock)success
failure:(ActionFailureBlock)failure
{
OWSAssert(success);
OWSAssert(failure);
if (unpreparedSubactions.count < 1) {
return success();
}
DebugUIMessagesAction *nextAction = unpreparedSubactions.lastObject;
[unpreparedSubactions removeLastObject];
DDLogInfo(@"%@ preparing: %@", self.logTag, nextAction.label);
[DDLog flushLog];
[nextAction prepare:^{
[self prepareSubactions:unpreparedSubactions success:success failure:failure];
}
failure:^{
}];
}
+ (DebugUIMessagesAction *)randomGroupActionWithLabel:(NSString *)label
subactions:(NSArray<DebugUIMessagesAction *> *)subactions
{
OWSAssert(label.length > 0);
OWSAssert(subactions.count > 0);
DebugUIMessagesGroupAction *instance = [DebugUIMessagesGroupAction new];
instance.label = label;
instance.subactions = subactions;
instance.subactionMode = SubactionMode_Random;
return instance;
}
+ (DebugUIMessagesAction *)allGroupActionWithLabel:(NSString *)label
subactions:(NSArray<DebugUIMessagesAction *> *)subactions
{
OWSAssert(label.length > 0);
OWSAssert(subactions.count > 0);
DebugUIMessagesGroupAction *instance = [DebugUIMessagesGroupAction new];
instance.label = label;
instance.subactions = subactions;
instance.subactionMode = SubactionMode_Ordered;
return instance;
}
@end
NS_ASSUME_NONNULL_END

@ -0,0 +1,43 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "DebugUIMessagesUtils.h"
NS_ASSUME_NONNULL_BEGIN
@interface DebugUIMessagesAssetLoader : NSObject
@property (nonatomic) NSString *filename;
@property (nonatomic) NSString *mimeType;
@property (nonatomic) ActionPrepareBlock prepareBlock;
@property (nonatomic, nullable) NSString *filePath;
#pragma mark -
+ (instancetype)jpegInstance;
+ (instancetype)gifInstance;
+ (instancetype)largeGifInstance;
+ (instancetype)mp3Instance;
+ (instancetype)mp4Instance;
+ (instancetype)compactPortraitPngInstance;
+ (instancetype)compactLandscapePngInstance;
+ (instancetype)tallPortraitPngInstance;
+ (instancetype)wideLandscapePngInstance;
+ (instancetype)largePngInstance;
+ (instancetype)tinyPngInstance;
+ (instancetype)pngInstanceWithSize:(CGSize)size
backgroundColor:(UIColor *)backgroundColor
textColor:(UIColor *)textColor
label:(NSString *)label;
+ (instancetype)tinyPdfInstance;
+ (instancetype)largePdfInstance;
+ (instancetype)missingPngInstance;
+ (instancetype)missingPdfInstance;
+ (instancetype)oversizeTextInstance;
@end
NS_ASSUME_NONNULL_END

@ -0,0 +1,523 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "DebugUIMessagesAssetLoader.h"
#import <AFNetworking/AFHTTPSessionManager.h>
#import <AFNetworking/AFNetworking.h>
#import <Curve25519Kit/Randomness.h>
#import <SignalServiceKit/MIMETypeUtil.h>
#import <SignalServiceKit/OWSFileSystem.h>
NS_ASSUME_NONNULL_BEGIN
@implementation DebugUIMessagesAssetLoader
+ (DebugUIMessagesAssetLoader *)fakeAssetLoaderWithUrl:(NSString *)fileUrl mimeType:(NSString *)mimeType
{
OWSAssert(fileUrl.length > 0);
OWSAssert(mimeType.length > 0);
DebugUIMessagesAssetLoader *instance = [DebugUIMessagesAssetLoader new];
instance.mimeType = mimeType;
instance.filename = [NSURL URLWithString:fileUrl].lastPathComponent;
__weak DebugUIMessagesAssetLoader *weakSelf = instance;
instance.prepareBlock = ^(ActionSuccessBlock success, ActionFailureBlock failure) {
[weakSelf ensureURLAssetLoaded:fileUrl success:success failure:failure];
};
return instance;
}
- (void)ensureURLAssetLoaded:(NSString *)fileUrl success:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure
{
OWSAssert(success);
OWSAssert(failure);
OWSAssert(self.filename.length > 0);
OWSAssert(self.mimeType.length > 0);
if (self.filePath) {
success();
return;
}
// Use a predictable file path so that we reuse the cache between app launches.
NSString *temporaryDirectory = NSTemporaryDirectory();
NSString *cacheDirectory = [temporaryDirectory stringByAppendingPathComponent:@"cached_random_files"];
[OWSFileSystem ensureDirectoryExists:cacheDirectory];
NSString *filePath = [cacheDirectory stringByAppendingPathComponent:self.filename];
if ([NSFileManager.defaultManager fileExistsAtPath:filePath]) {
self.filePath = filePath;
return success();
}
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
AFHTTPSessionManager *sessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:configuration];
sessionManager.responseSerializer = [AFHTTPResponseSerializer serializer];
OWSAssert(sessionManager.responseSerializer);
[sessionManager GET:fileUrl
parameters:nil
progress:nil
success:^(NSURLSessionDataTask *task, NSData *_Nullable responseObject) {
if ([responseObject writeToFile:filePath atomically:YES]) {
self.filePath = filePath;
OWSAssert([NSFileManager.defaultManager fileExistsAtPath:filePath]);
success();
} else {
OWSFail(@"Error write url response [%@]: %@", fileUrl, filePath);
failure();
}
}
failure:^(NSURLSessionDataTask *_Nullable task, NSError *requestError) {
OWSFail(@"Error downloading url[%@]: %@", fileUrl, requestError);
failure();
}];
}
#pragma mark -
+ (DebugUIMessagesAssetLoader *)fakePngAssetLoaderWithImageSize:(CGSize)imageSize
backgroundColor:(UIColor *)backgroundColor
textColor:(UIColor *)textColor
label:(NSString *)label
{
OWSAssert(imageSize.width > 0);
OWSAssert(imageSize.height > 0);
OWSAssert(backgroundColor);
OWSAssert(textColor);
OWSAssert(label.length > 0);
DebugUIMessagesAssetLoader *instance = [DebugUIMessagesAssetLoader new];
instance.mimeType = OWSMimeTypeImagePng;
instance.filename = @"image.png";
__weak DebugUIMessagesAssetLoader *weakSelf = instance;
instance.prepareBlock = ^(ActionSuccessBlock success, ActionFailureBlock failure) {
[weakSelf ensurePngAssetLoaded:imageSize
backgroundColor:backgroundColor
textColor:textColor
label:label
success:success
failure:failure];
};
return instance;
}
- (void)ensurePngAssetLoaded:(CGSize)imageSize
backgroundColor:(UIColor *)backgroundColor
textColor:(UIColor *)textColor
label:(NSString *)label
success:(ActionSuccessBlock)success
failure:(ActionFailureBlock)failure
{
OWSAssert(success);
OWSAssert(failure);
OWSAssert(self.filename.length > 0);
OWSAssert(self.mimeType.length > 0);
OWSAssert(imageSize.width > 0 && imageSize.height > 0);
OWSAssert(backgroundColor);
OWSAssert(textColor);
OWSAssert(label.length > 0);
if (self.filePath) {
success();
return;
}
@autoreleasepool {
NSString *filePath = [OWSFileSystem temporaryFilePathWithFileExtension:@"png"];
UIImage *image =
[self createRandomPngWithSize:imageSize backgroundColor:backgroundColor textColor:textColor label:label];
NSData *pngData = UIImagePNGRepresentation(image);
[pngData writeToFile:filePath atomically:YES];
self.filePath = filePath;
OWSAssert([NSFileManager.defaultManager fileExistsAtPath:filePath]);
success();
}
}
- (nullable UIImage *)createRandomPngWithSize:(CGSize)imageSize
backgroundColor:(UIColor *)backgroundColor
textColor:(UIColor *)textColor
label:(NSString *)label
{
OWSAssert(imageSize.width > 0 && imageSize.height > 0);
OWSAssert(backgroundColor);
OWSAssert(textColor);
OWSAssert(label.length > 0);
@autoreleasepool {
CGRect frame = CGRectZero;
frame.size = imageSize;
CGFloat smallDimension = MIN(imageSize.width, imageSize.height);
UIFont *font = [UIFont boldSystemFontOfSize:smallDimension * 0.5f];
NSDictionary *textAttributes = @{ NSFontAttributeName : font, NSForegroundColorAttributeName : textColor };
CGRect textFrame =
[label boundingRectWithSize:frame.size
options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
attributes:textAttributes
context:nil];
UIGraphicsBeginImageContextWithOptions(frame.size, NO, [UIScreen mainScreen].scale);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, backgroundColor.CGColor);
CGContextFillRect(context, frame);
[label drawAtPoint:CGPointMake(CGRectGetMidX(frame) - CGRectGetMidX(textFrame),
CGRectGetMidY(frame) - CGRectGetMidY(textFrame))
withAttributes:textAttributes];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
}
#pragma mark -
+ (DebugUIMessagesAssetLoader *)fakeRandomAssetLoaderWithLength:(NSUInteger)dataLength mimeType:(NSString *)mimeType
{
OWSAssert(dataLength > 0);
OWSAssert(mimeType.length > 0);
DebugUIMessagesAssetLoader *instance = [DebugUIMessagesAssetLoader new];
instance.mimeType = mimeType;
NSString *fileExtension = [MIMETypeUtil fileExtensionForMIMEType:mimeType];
OWSAssert(fileExtension.length > 0);
instance.filename = [@"attachment" stringByAppendingPathExtension:fileExtension];
__weak DebugUIMessagesAssetLoader *weakSelf = instance;
instance.prepareBlock = ^(ActionSuccessBlock success, ActionFailureBlock failure) {
[weakSelf ensureRandomAssetLoaded:dataLength success:success failure:failure];
};
return instance;
}
- (void)ensureRandomAssetLoaded:(NSUInteger)dataLength
success:(ActionSuccessBlock)success
failure:(ActionFailureBlock)failure
{
OWSAssert(dataLength > 0);
OWSAssert(success);
OWSAssert(failure);
OWSAssert(self.filename.length > 0);
OWSAssert(self.mimeType.length > 0);
if (self.filePath) {
success();
return;
}
@autoreleasepool {
NSString *fileExtension = [MIMETypeUtil fileExtensionForMIMEType:self.mimeType];
OWSAssert(fileExtension.length > 0);
NSData *data = [Randomness generateRandomBytes:(int)dataLength];
OWSAssert(data);
NSString *filePath = [OWSFileSystem temporaryFilePathWithFileExtension:fileExtension];
BOOL didWrite = [data writeToFile:filePath atomically:YES];
OWSAssert(didWrite);
self.filePath = filePath;
OWSAssert([NSFileManager.defaultManager fileExistsAtPath:filePath]);
}
success();
}
#pragma mark -
+ (DebugUIMessagesAssetLoader *)fakeMissingAssetLoaderWithMimeType:(NSString *)mimeType
{
OWSAssert(mimeType.length > 0);
DebugUIMessagesAssetLoader *instance = [DebugUIMessagesAssetLoader new];
instance.mimeType = mimeType;
NSString *fileExtension = [MIMETypeUtil fileExtensionForMIMEType:mimeType];
OWSAssert(fileExtension.length > 0);
instance.filename = [@"attachment" stringByAppendingPathExtension:fileExtension];
__weak DebugUIMessagesAssetLoader *weakSelf = instance;
instance.prepareBlock = ^(ActionSuccessBlock success, ActionFailureBlock failure) {
[weakSelf ensureMissingAssetLoaded:success failure:failure];
};
return instance;
}
- (void)ensureMissingAssetLoaded:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure
{
OWSAssert(success);
OWSAssert(failure);
OWSAssert(self.filename.length > 0);
OWSAssert(self.mimeType.length > 0);
if (self.filePath) {
success();
return;
}
NSString *fileExtension = [MIMETypeUtil fileExtensionForMIMEType:self.mimeType];
OWSAssert(fileExtension.length > 0);
NSString *filePath = [OWSFileSystem temporaryFilePathWithFileExtension:fileExtension];
BOOL didCreate = [NSFileManager.defaultManager createFileAtPath:filePath contents:nil attributes:nil];
OWSAssert(didCreate);
self.filePath = filePath;
OWSAssert([NSFileManager.defaultManager fileExistsAtPath:filePath]);
success();
}
#pragma mark -
+ (DebugUIMessagesAssetLoader *)fakeOversizeTextAssetLoader
{
DebugUIMessagesAssetLoader *instance = [DebugUIMessagesAssetLoader new];
instance.mimeType = OWSMimeTypeOversizeTextMessage;
instance.filename = @"attachment.txt";
__weak DebugUIMessagesAssetLoader *weakSelf = instance;
instance.prepareBlock = ^(ActionSuccessBlock success, ActionFailureBlock failure) {
[weakSelf ensureOversizeTextAssetLoaded:success failure:failure];
};
return instance;
}
- (void)ensureOversizeTextAssetLoaded:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure
{
OWSAssert(success);
OWSAssert(failure);
OWSAssert(self.filename.length > 0);
OWSAssert(self.mimeType.length > 0);
if (self.filePath) {
success();
return;
}
NSMutableString *message = [NSMutableString new];
for (NSUInteger i = 0; i < 32; i++) {
[message appendString:@"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rutrum, nulla "
@"vitae pretium hendrerit, tellus turpis pharetra libero, vitae sodales tortor ante vel "
@"sem. Fusce sed nisl a lorem gravida tincidunt. Suspendisse efficitur non quam ac "
@"sodales. Aenean ut velit maximus, posuere sem a, accumsan nunc. Donec ullamcorper "
@"turpis lorem. Quisque dignissim purus eu placerat ultricies. Proin at urna eget mi "
@"semper congue. Aenean non elementum ex. Praesent pharetra quam at sem vestibulum, "
@"vestibulum ornare dolor elementum. Vestibulum massa tortor, scelerisque sit amet "
@"pulvinar a, rhoncus vitae nisl. Sed mi nunc, tempus at varius in, malesuada vitae "
@"dui. Vivamus efficitur pulvinar erat vitae congue. Proin vehicula turpis non felis "
@"congue facilisis. Nullam aliquet dapibus ligula ac mollis. Etiam sit amet posuere "
@"lorem, in rhoncus nisi.\n\n"];
}
NSString *fileExtension = @"txt";
NSString *filePath = [OWSFileSystem temporaryFilePathWithFileExtension:fileExtension];
NSData *data = [message dataUsingEncoding:NSUTF8StringEncoding];
OWSAssert(data);
BOOL didWrite = [data writeToFile:filePath atomically:YES];
OWSAssert(didWrite);
self.filePath = filePath;
OWSAssert([NSFileManager.defaultManager fileExistsAtPath:filePath]);
success();
}
#pragma mark -
+ (instancetype)jpegInstance
{
static DebugUIMessagesAssetLoader *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [DebugUIMessagesAssetLoader
fakeAssetLoaderWithUrl:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-jpg.JPG"
mimeType:@"image/jpeg"];
});
return instance;
}
+ (instancetype)gifInstance
{
static DebugUIMessagesAssetLoader *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [DebugUIMessagesAssetLoader
fakeAssetLoaderWithUrl:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-gif.gif"
mimeType:@"image/gif"];
});
return instance;
}
+ (instancetype)largeGifInstance
{
static DebugUIMessagesAssetLoader *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance =
[DebugUIMessagesAssetLoader fakeAssetLoaderWithUrl:@"https://i.giphy.com/media/LTw0F3GAdaao8/source.gif"
mimeType:@"image/gif"];
});
return instance;
}
+ (instancetype)mp3Instance
{
static DebugUIMessagesAssetLoader *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [DebugUIMessagesAssetLoader
fakeAssetLoaderWithUrl:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-mp3.mp3"
mimeType:@"audio/mp3"];
});
return instance;
}
+ (instancetype)mp4Instance
{
static DebugUIMessagesAssetLoader *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [DebugUIMessagesAssetLoader
fakeAssetLoaderWithUrl:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-mp4.mp4"
mimeType:@"video/mp4"];
});
return instance;
}
+ (instancetype)compactPortraitPngInstance
{
static DebugUIMessagesAssetLoader *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [DebugUIMessagesAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(60, 100)
backgroundColor:[UIColor blueColor]
textColor:[UIColor whiteColor]
label:@"P"];
});
return instance;
}
+ (instancetype)compactLandscapePngInstance
{
static DebugUIMessagesAssetLoader *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [DebugUIMessagesAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(100, 60)
backgroundColor:[UIColor greenColor]
textColor:[UIColor whiteColor]
label:@"L"];
});
return instance;
}
+ (instancetype)tallPortraitPngInstance
{
static DebugUIMessagesAssetLoader *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [DebugUIMessagesAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(10, 100)
backgroundColor:[UIColor yellowColor]
textColor:[UIColor whiteColor]
label:@"P"];
});
return instance;
}
+ (instancetype)wideLandscapePngInstance
{
static DebugUIMessagesAssetLoader *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [DebugUIMessagesAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(100, 10)
backgroundColor:[UIColor purpleColor]
textColor:[UIColor whiteColor]
label:@"L"];
});
return instance;
}
+ (instancetype)largePngInstance
{
static DebugUIMessagesAssetLoader *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [DebugUIMessagesAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(4000, 4000)
backgroundColor:[UIColor brownColor]
textColor:[UIColor whiteColor]
label:@"B"];
});
return instance;
}
+ (instancetype)tinyPngInstance
{
static DebugUIMessagesAssetLoader *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [DebugUIMessagesAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(2, 2)
backgroundColor:[UIColor cyanColor]
textColor:[UIColor whiteColor]
label:@"T"];
});
return instance;
}
+ (instancetype)pngInstanceWithSize:(CGSize)size
backgroundColor:(UIColor *)backgroundColor
textColor:(UIColor *)textColor
label:(NSString *)label
{
return [DebugUIMessagesAssetLoader fakePngAssetLoaderWithImageSize:size
backgroundColor:backgroundColor
textColor:textColor
label:label];
}
+ (instancetype)tinyPdfInstance
{
static DebugUIMessagesAssetLoader *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [DebugUIMessagesAssetLoader fakeRandomAssetLoaderWithLength:256 mimeType:@"application/pdf"];
});
return instance;
}
+ (instancetype)largePdfInstance
{
static DebugUIMessagesAssetLoader *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance =
[DebugUIMessagesAssetLoader fakeRandomAssetLoaderWithLength:4 * 1024 * 1024 mimeType:@"application/pdf"];
});
return instance;
}
+ (instancetype)missingPngInstance
{
static DebugUIMessagesAssetLoader *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [DebugUIMessagesAssetLoader fakeMissingAssetLoaderWithMimeType:OWSMimeTypeImagePng];
});
return instance;
}
+ (instancetype)missingPdfInstance
{
static DebugUIMessagesAssetLoader *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [DebugUIMessagesAssetLoader fakeMissingAssetLoaderWithMimeType:@"application/pdf"];
});
return instance;
}
+ (instancetype)oversizeTextInstance
{
static DebugUIMessagesAssetLoader *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [DebugUIMessagesAssetLoader fakeOversizeTextAssetLoader];
});
return instance;
}
@end
NS_ASSUME_NONNULL_END

@ -0,0 +1,18 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@class YapDatabaseReadWriteTransaction;
typedef void (^ActionSuccessBlock)(void);
typedef void (^ActionFailureBlock)(void);
typedef void (^ActionPrepareBlock)(ActionSuccessBlock success, ActionFailureBlock failure);
typedef void (^StaggeredActionBlock)(NSUInteger index,
YapDatabaseReadWriteTransaction *transaction,
ActionSuccessBlock success,
ActionFailureBlock failure);
typedef void (^UnstaggeredActionBlock)(NSUInteger index, YapDatabaseReadWriteTransaction *transaction);
NS_ASSUME_NONNULL_END

@ -325,7 +325,7 @@ class MediaGalleryViewController: UINavigationController, MediaGalleryDataSource
detailView.backgroundColor = .clear
self.view.backgroundColor = .clear
self.presentationView.layer.cornerRadius = OWSMessageCellCornerRadius
self.presentationView.layer.cornerRadius = kOWSMessageCellCornerRadius
fromViewController.present(self, animated: false) {
@ -478,7 +478,7 @@ class MediaGalleryViewController: UINavigationController, MediaGalleryDataSource
if changedItems {
self.presentationView.alpha = 0
} else {
self.presentationView.layer.cornerRadius = OWSMessageCellCornerRadius
self.presentationView.layer.cornerRadius = kOWSMessageCellCornerRadius
}
},
completion: { (_: Bool) in

@ -307,10 +307,10 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi
}
private func displayableTextIfText() -> String? {
guard viewItem.hasText else {
guard viewItem.hasBodyText else {
return nil
}
guard let displayableText = viewItem.displayableText() else {
guard let displayableText = viewItem.displayableBodyText() else {
return nil
}
let messageBody = displayableText.fullText

@ -5,7 +5,6 @@
import Foundation
import JSQMessagesViewController
import SignalServiceKit
import SignalMessaging
@objc
public class OWSMessagesBubbleImageFactory: NSObject {
@ -21,22 +20,22 @@ public class OWSMessagesBubbleImageFactory: NSObject {
}()
public lazy var incoming: JSQMessagesBubbleImage = {
let color = UIColor.jsq_messageBubbleLightGray()!
let color = bubbleColorIncoming
return self.incoming(color: color)
}()
public lazy var outgoing: JSQMessagesBubbleImage = {
let color = UIColor.ows_materialBlue
let color = bubbleColorOutgoingSent
return self.outgoing(color: color)
}()
public lazy var currentlyOutgoing: JSQMessagesBubbleImage = {
let color = UIColor.ows_fadedBlue
let color = bubbleColorOutgoingSending
return self.outgoing(color: color)
}()
public lazy var outgoingFailed: JSQMessagesBubbleImage = {
let color = UIColor.gray
let color = bubbleColorOutgoingUnsent
return self.outgoing(color: color)
}()
@ -58,6 +57,32 @@ public class OWSMessagesBubbleImageFactory: NSObject {
}
}
public static let bubbleColorIncoming = UIColor.jsq_messageBubbleLightGray()!
public static let bubbleColorOutgoingUnsent = UIColor.gray
public static let bubbleColorOutgoingSending = UIColor.ows_fadedBlue
public static let bubbleColorOutgoingSent = UIColor.ows_materialBlue
public func bubbleColor(message: TSMessage) -> UIColor {
if message is TSIncomingMessage {
return OWSMessagesBubbleImageFactory.bubbleColorIncoming
} else if let outgoingMessage = message as? TSOutgoingMessage {
switch outgoingMessage.messageState {
case .unsent:
return OWSMessagesBubbleImageFactory.bubbleColorOutgoingUnsent
case .attemptingOut:
return OWSMessagesBubbleImageFactory.bubbleColorOutgoingSending
default:
return OWSMessagesBubbleImageFactory.bubbleColorOutgoingSent
}
} else {
owsFail("Unexpected message type: \(message)")
return UIColor.ows_materialBlue
}
}
private func outgoing(color: UIColor) -> JSQMessagesBubbleImage {
if isRTL {
return jsqFactory.incomingMessagesBubbleImage(with: color)

@ -1,5 +1,5 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "TSYapDatabaseObject.h"
@ -133,6 +133,8 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (void)unarchiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)removeAllThreadInteractionsWithTransaction:(YapDatabaseReadWriteTransaction *)transaction;
#pragma mark Drafts
/**

@ -49,8 +49,13 @@ NS_ASSUME_NONNULL_BEGIN
- (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
[self removeAllThreadInteractionsWithTransaction:transaction];
[super removeWithTransaction:transaction];
}
- (void)removeAllThreadInteractionsWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
// We can't safely delete interactions while enumerating them, so
// we collect and delete separately.
//

@ -132,26 +132,42 @@ void runAsyncRegistrationsForStorage(OWSStorage *storage)
{
OWSAssert(completion);
[((OWSDatabase *)self.database)collectRegistrationConnections];
runAsyncRegistrationsForStorage(self);
DDLogVerbose(@"%@ async registrations enqueued.", self.logTag);
// Block until all async registrations are complete.
//
// NOTE: This has to happen on the "registration connection" for this
// NOTE: This has to happen on the "registration connections" for this
// database.
YapDatabaseConnection *dbConnection = self.registrationConnection;
OWSAssert(self.registrationConnection);
[dbConnection flushTransactionsWithCompletionQueue:dispatch_get_main_queue()
completionBlock:^{
OWSAssert(!self.areAsyncRegistrationsComplete);
DDLogVerbose(@"%@ async registrations complete.", self.logTag);
self.areAsyncRegistrationsComplete = YES;
completion();
}];
NSMutableSet<YapDatabaseConnection *> *pendingRegistrationConnectionSet =
[[((OWSDatabase *)self.database)clearCollectedRegistrationConnections] mutableCopy];
DDLogVerbose(@"%@ flushing registration connections: %zd.", self.logTag, pendingRegistrationConnectionSet.count);
dispatch_async(dispatch_get_main_queue(), ^{
for (YapDatabaseConnection *dbConnection in pendingRegistrationConnectionSet) {
[dbConnection
flushTransactionsWithCompletionQueue:dispatch_get_main_queue()
completionBlock:^{
OWSAssertIsOnMainThread();
OWSAssert(!self.areAsyncRegistrationsComplete);
[pendingRegistrationConnectionSet removeObject:dbConnection];
if (pendingRegistrationConnectionSet.count > 0) {
DDLogVerbose(@"%@ registration connection flushed.", self.logTag);
return;
}
DDLogVerbose(@"%@ async registrations complete.", self.logTag);
self.areAsyncRegistrationsComplete = YES;
completion();
}];
}
});
}
+ (void)protectFiles

@ -2,7 +2,7 @@
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import <YapDatabase/YapDatabaseConnection.h>
#import <YapDatabase/YapDatabase.h>
NS_ASSUME_NONNULL_BEGIN
@ -34,6 +34,26 @@ extern NSString *const StorageIsReadyNotification;
#pragma mark -
@interface OWSDatabase : YapDatabase
- (instancetype)init NS_UNAVAILABLE;
- (id)initWithPath:(NSString *)inPath
serializer:(nullable YapDatabaseSerializer)inSerializer
deserializer:(YapDatabaseDeserializer)inDeserializer
options:(YapDatabaseOptions *)inOptions
delegate:(id<OWSDatabaseConnectionDelegate>)delegate NS_DESIGNATED_INITIALIZER;
// Starts collecting references to the registration connections.
- (void)collectRegistrationConnections;
// Stops collecting references to the registration connections and returns
// all collected connections.
- (NSSet<YapDatabaseConnection *> *)clearCollectedRegistrationConnections;
@end
#pragma mark -
@interface OWSStorage : NSObject
- (instancetype)init NS_UNAVAILABLE;

@ -130,17 +130,11 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void);
#pragma mark -
@interface OWSDatabase : YapDatabase
@interface OWSDatabase ()
@property (atomic, weak) id<OWSDatabaseConnectionDelegate> delegate;
- (instancetype)init NS_UNAVAILABLE;
- (id)initWithPath:(NSString *)inPath
serializer:(nullable YapDatabaseSerializer)inSerializer
deserializer:(YapDatabaseDeserializer)inDeserializer
options:(YapDatabaseOptions *)inOptions
delegate:(id<OWSDatabaseConnectionDelegate>)delegate NS_DESIGNATED_INITIALIZER;
@property (nonatomic, readonly, nullable) NSMutableSet<YapDatabaseConnection *> *registrationConnectionSet;
@end
@ -191,9 +185,29 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void);
((OWSDatabaseConnection *)connection).canWriteBeforeStorageReady = YES;
#endif
[self.registrationConnectionSet addObject:connection];
return connection;
}
- (void)collectRegistrationConnections
{
OWSAssert(!self.registrationConnectionSet);
_registrationConnectionSet = [NSMutableSet set];
}
- (NSSet<YapDatabaseConnection *> *)clearCollectedRegistrationConnections
{
OWSAssert(self.registrationConnectionSet);
NSSet<YapDatabaseConnection *> *registrationConnectionSetCopy = [self.registrationConnectionSet copy];
_registrationConnectionSet = nil;
return registrationConnectionSetCopy;
}
@end
#pragma mark -

@ -12,7 +12,7 @@ extern const NSTimeInterval kDayInterval;
extern const NSTimeInterval kWeekInterval;
extern const NSTimeInterval kMonthInterval;
#define kSecondInMs 1000
#define kSecondInMs ((uint64_t)1000)
#define kMinuteInMs (kSecondInMs * 60)
#define kHourInMs (kMinuteInMs * 60)
#define kDayInMs (kHourInMs * 24)

Loading…
Cancel
Save