Unfork JSQMessagesViewController

Geting back on upstream fixes a couple bugs (see ##Bugfixes), and also
will make future updates easier.

The unforking process was basically this:

* move custom message types (Calls and DisplayedMessages) classes from our
  custom JSQMVC fork into Signal-iOS.
* Move any method customization into our subclass. Including
  ColletionView stuff, bubble sizing, and gesture behavior

Bug Fixes
---------
* Fix mis-sized incoming media bubbles.

Bubble size was being cached by interaction id. Which broke when
receiving an attachment. The problem is that incoming media messages
were initially the height of a "Downloading Attachment" info message.
Instead we use the mediaHash for media messages to expire the bubble
size when the media changes.

* fix missized bubble when MVC did appear

The MessagesViewController isn't sized correctly until ViewWillAppear.
This caused the first round of bubbles to be rendered incorrectly (they
assumed a larger container than they had).  I think is reflected in the
current version of the app by a reflow occurring shortly after the view
appears.

Chores
------
* bump travis to build with xcode8
* specify RQV development team for device build. required by xcode 8 beta

Cleanup
------
* Refactor messageing XIB so that elements are hangning outside of
  the views frame
* Fix compiler warning with explicit cast
* delete deprecated lineBreakmode, it's the default value anyway.

// FREEBIE
pull/1/head
Michael Kirk 8 years ago
parent 987ce5f832
commit 4d320d6015

@ -9,7 +9,7 @@ target 'Signal' do
pod 'FFCircularProgressView', '~> 0.5'
pod 'SCWaveformView', '~> 1.0'
pod 'DJWActionSheet'
pod 'JSQMessagesViewController', :git => 'https://github.com/WhisperSystems/JSQMessagesViewController', :branch => 'JSignalQ'
pod 'JSQMessagesViewController'
target 'SignalTests' do
inherit! :search_paths
end

@ -30,7 +30,7 @@ PODS:
- DJWActionSheet (1.0.4)
- FFCircularProgressView (0.5)
- HKDFKit (0.0.3)
- JSQMessagesViewController (7.1.0):
- JSQMessagesViewController (7.3.3):
- JSQSystemSoundPlayer (~> 2.0.1)
- JSQSystemSoundPlayer (2.0.1)
- libPhoneNumber-iOS (0.8.14)
@ -115,7 +115,7 @@ PODS:
DEPENDENCIES:
- DJWActionSheet
- FFCircularProgressView (~> 0.5)
- JSQMessagesViewController (from `https://github.com/WhisperSystems/JSQMessagesViewController`, branch `JSignalQ`)
- JSQMessagesViewController
- OpenSSL (~> 1.0.208)
- PastelogKit (~> 1.3)
- SCWaveformView (~> 1.0)
@ -123,18 +123,12 @@ DEPENDENCIES:
- SocketRocket (from `https://github.com/facebook/SocketRocket.git`)
EXTERNAL SOURCES:
JSQMessagesViewController:
:branch: JSignalQ
:git: https://github.com/WhisperSystems/JSQMessagesViewController
SignalServiceKit:
:git: https://github.com/WhisperSystems/SignalServiceKit.git
SocketRocket:
:git: https://github.com/facebook/SocketRocket.git
CHECKOUT OPTIONS:
JSQMessagesViewController:
:commit: 225b1baa11125ea84d4b960d700834b5b0a40ee1
:git: https://github.com/WhisperSystems/JSQMessagesViewController
SignalServiceKit:
:commit: f537b6f19265b0f0845f15b3155cdac4f1913dc6
:git: https://github.com/WhisperSystems/SignalServiceKit.git
@ -150,7 +144,7 @@ SPEC CHECKSUMS:
DJWActionSheet: 2fe54b1298a7f0fe44462233752c76a530e0cd80
FFCircularProgressView: 683a4ab1e1bd613246a3dffa61503ffdebcde8d8
HKDFKit: c058305d6f64b84f28c50bd7aa89574625bcb62a
JSQMessagesViewController: ca11f86fa68ca70835f05e169df9244147c1dc40
JSQMessagesViewController: 0ee3f80237268192a3e8337fd0d787f1a1bf5a7a
JSQSystemSoundPlayer: c5850e77a4363ffd374cd851154b9af93264ed8d
libPhoneNumber-iOS: fb165271ebe7fb32e55da97b83219382f2f9d409
Mantle: bc40bb061d8c2c6fb48d5083e04d928c3b7f73d9
@ -167,6 +161,6 @@ SPEC CHECKSUMS:
UnionFind: c33be5adb12983981d6e827ea94fc7f9e370f52d
YapDatabase: 713d4018cfacbd6e77dd430710ca84730e450980
PODFILE CHECKSUM: 860bce87f11d7ce3a8a80c10f8d35ef83699531e
PODFILE CHECKSUM: 060ff4edf8b7a110984cb2c1ffef3f6e19a6b8b6
COCOAPODS: 1.0.1

@ -9,9 +9,24 @@
/* Begin PBXBuildFile section */
0DD55B166906AF3368995978 /* libPods-Signal.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 80CD5E19DD23200E7926EEA7 /* libPods-Signal.a */; };
30209C98DABCE82064B4EAF5 /* libPods-SignalTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A33D3C7EB4B17BDBD47F0FCC /* libPods-SignalTests.a */; };
453D28B31D32B87100D523F0 /* JSQErrorMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 453D28B01D32B87100D523F0 /* JSQErrorMessage.m */; };
453D28B41D32B87100D523F0 /* JSQInfoMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 453D28B21D32B87100D523F0 /* JSQInfoMessage.m */; };
453D28B71D32BA5F00D523F0 /* JSQDisplayedMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 453D28B61D32BA5F00D523F0 /* JSQDisplayedMessage.m */; };
453D28BA1D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m in Sources */ = {isa = PBXBuildFile; fileRef = 453D28B91D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m */; };
453D28BB1D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m in Sources */ = {isa = PBXBuildFile; fileRef = 453D28B91D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m */; };
45843D1F1D2236B30013E85A /* OWSContactsSearcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 45843D1E1D2236B30013E85A /* OWSContactsSearcher.m */; };
45843D201D2236B30013E85A /* OWSContactsSearcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 45843D1E1D2236B30013E85A /* OWSContactsSearcher.m */; };
45843D221D223BA10013E85A /* OWSContactsSearcherTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 45843D211D223BA10013E85A /* OWSContactsSearcherTest.m */; };
45C681B71D305A580050903A /* JSQCall.m in Sources */ = {isa = PBXBuildFile; fileRef = 45C681B61D305A580050903A /* JSQCall.m */; };
45C681B81D305A580050903A /* JSQCall.m in Sources */ = {isa = PBXBuildFile; fileRef = 45C681B61D305A580050903A /* JSQCall.m */; };
45C681BC1D305C080050903A /* JSQCallCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 45C681BA1D305C080050903A /* JSQCallCollectionViewCell.m */; };
45C681BD1D305C080050903A /* JSQCallCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 45C681BA1D305C080050903A /* JSQCallCollectionViewCell.m */; };
45C681C41D305C9E0050903A /* JSQCallCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 45C681C01D305C9E0050903A /* JSQCallCollectionViewCell.xib */; };
45C681C51D305C9E0050903A /* JSQCallCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 45C681C01D305C9E0050903A /* JSQCallCollectionViewCell.xib */; };
45C681C61D305C9E0050903A /* JSQDisplayedMessageCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 45C681C21D305C9E0050903A /* JSQDisplayedMessageCollectionViewCell.m */; };
45C681C71D305C9E0050903A /* JSQDisplayedMessageCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 45C681C21D305C9E0050903A /* JSQDisplayedMessageCollectionViewCell.m */; };
45C681C81D305C9E0050903A /* JSQDisplayedMessageCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 45C681C31D305C9E0050903A /* JSQDisplayedMessageCollectionViewCell.xib */; };
45C681C91D305C9E0050903A /* JSQDisplayedMessageCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 45C681C31D305C9E0050903A /* JSQDisplayedMessageCollectionViewCell.xib */; };
45CB2FA81CB7146C00E1B343 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 45CB2FA71CB7146C00E1B343 /* Launch Screen.storyboard */; };
4CE0E3771B954546007210CF /* TSAnimatedAdapter.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E3761B954546007210CF /* TSAnimatedAdapter.m */; };
701231B518ECAA4500D456C4 /* EvpMessageDigest.m in Sources */ = {isa = PBXBuildFile; fileRef = 701231B418ECAA4500D456C4 /* EvpMessageDigest.m */; };
@ -491,10 +506,26 @@
/* Begin PBXFileReference section */
453CC0361D08E1A60040EBA3 /* sn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sn; path = translations/sn.lproj/Localizable.strings; sourceTree = "<group>"; };
453D28AF1D32B87100D523F0 /* JSQErrorMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSQErrorMessage.h; sourceTree = "<group>"; };
453D28B01D32B87100D523F0 /* JSQErrorMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSQErrorMessage.m; sourceTree = "<group>"; };
453D28B11D32B87100D523F0 /* JSQInfoMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSQInfoMessage.h; sourceTree = "<group>"; };
453D28B21D32B87100D523F0 /* JSQInfoMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSQInfoMessage.m; sourceTree = "<group>"; };
453D28B51D32BA5F00D523F0 /* JSQDisplayedMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSQDisplayedMessage.h; sourceTree = "<group>"; };
453D28B61D32BA5F00D523F0 /* JSQDisplayedMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSQDisplayedMessage.m; sourceTree = "<group>"; };
453D28B81D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessagesBubblesSizeCalculator.h; sourceTree = "<group>"; };
453D28B91D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessagesBubblesSizeCalculator.m; sourceTree = "<group>"; };
454B35071D08EED80026D658 /* mk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mk; path = translations/mk.lproj/Localizable.strings; sourceTree = "<group>"; };
45843D1D1D2236B30013E85A /* OWSContactsSearcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactsSearcher.h; sourceTree = "<group>"; };
45843D1E1D2236B30013E85A /* OWSContactsSearcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactsSearcher.m; sourceTree = "<group>"; };
45843D211D223BA10013E85A /* OWSContactsSearcherTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactsSearcherTest.m; sourceTree = "<group>"; };
45C681B51D305A580050903A /* JSQCall.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSQCall.h; sourceTree = "<group>"; };
45C681B61D305A580050903A /* JSQCall.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSQCall.m; sourceTree = "<group>"; };
45C681B91D305C080050903A /* JSQCallCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSQCallCollectionViewCell.h; sourceTree = "<group>"; };
45C681BA1D305C080050903A /* JSQCallCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSQCallCollectionViewCell.m; sourceTree = "<group>"; };
45C681C01D305C9E0050903A /* JSQCallCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = JSQCallCollectionViewCell.xib; sourceTree = "<group>"; };
45C681C11D305C9E0050903A /* JSQDisplayedMessageCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSQDisplayedMessageCollectionViewCell.h; sourceTree = "<group>"; };
45C681C21D305C9E0050903A /* JSQDisplayedMessageCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSQDisplayedMessageCollectionViewCell.m; sourceTree = "<group>"; };
45C681C31D305C9E0050903A /* JSQDisplayedMessageCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = JSQDisplayedMessageCollectionViewCell.xib; sourceTree = "<group>"; };
45CB2FA71CB7146C00E1B343 /* Launch Screen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = "Launch Screen.storyboard"; path = "Signal/src/util/Launch Screen.storyboard"; sourceTree = SOURCE_ROOT; };
45E282DE1D08E67800ADD4C8 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gl; path = translations/gl.lproj/Localizable.strings; sourceTree = "<group>"; };
45E282DF1D08E6CC00ADD4C8 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = translations/id.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -1094,6 +1125,16 @@
isa = PBXGroup;
children = (
B62D53F41A23CC8B009AAF82 /* TSMessageAdapters */,
453D28B51D32BA5F00D523F0 /* JSQDisplayedMessage.h */,
453D28B61D32BA5F00D523F0 /* JSQDisplayedMessage.m */,
453D28AF1D32B87100D523F0 /* JSQErrorMessage.h */,
453D28B01D32B87100D523F0 /* JSQErrorMessage.m */,
453D28B11D32B87100D523F0 /* JSQInfoMessage.h */,
453D28B21D32B87100D523F0 /* JSQInfoMessage.m */,
45C681B51D305A580050903A /* JSQCall.h */,
45C681B61D305A580050903A /* JSQCall.m */,
453D28B81D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.h */,
453D28B91D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m */,
);
path = Models;
sourceTree = "<group>";
@ -1638,6 +1679,12 @@
76EB052B18170B33006006FC /* Views */ = {
isa = PBXGroup;
children = (
45C681C11D305C9E0050903A /* JSQDisplayedMessageCollectionViewCell.h */,
45C681C21D305C9E0050903A /* JSQDisplayedMessageCollectionViewCell.m */,
45C681C31D305C9E0050903A /* JSQDisplayedMessageCollectionViewCell.xib */,
45C681B91D305C080050903A /* JSQCallCollectionViewCell.h */,
45C681BA1D305C080050903A /* JSQCallCollectionViewCell.m */,
45C681C01D305C9E0050903A /* JSQCallCollectionViewCell.xib */,
A5509ECB1A69B1D600ABA4BC /* CountryCodeTableViewCell.h */,
A5509ECC1A69B1D600ABA4BC /* CountryCodeTableViewCell.m */,
FCAC963D19FEF99A0046DFC5 /* InboxTableViewCell.h */,
@ -2399,6 +2446,7 @@
A5509ECA1A69AB8B00ABA4BC /* Storyboard.storyboard in Resources */,
A507A3B11A6C60E300BEED0D /* InboxTableViewCell.xib in Resources */,
AD83FF421A73426500B5C81A /* audio_play_button.png in Resources */,
45C681C41D305C9E0050903A /* JSQCallCollectionViewCell.xib in Resources */,
B633C5C41A1D190B0059AC12 /* mute_on@2x.png in Resources */,
B633C5CE1A1D190B0059AC12 /* quit@2x.png in Resources */,
AD83FF441A73426500B5C81A /* audio_pause_button.png in Resources */,
@ -2407,6 +2455,7 @@
B633C59D1A1D190B0059AC12 /* endcall@2x.png in Resources */,
FC5CDF391A3393DD00B47253 /* error_white@2x.png in Resources */,
B633C5D21A1D190B0059AC12 /* savephoto@2x.png in Resources */,
45C681C81D305C9E0050903A /* JSQDisplayedMessageCollectionViewCell.xib in Resources */,
B10C9B611A7049EC00ECA2BF /* play_icon.png in Resources */,
AD83FF401A73426500B5C81A /* audio_pause_button_blue@2x.png in Resources */,
B66DBF4A19D5BBC8006EA940 /* Images.xcassets in Resources */,
@ -2443,6 +2492,8 @@
files = (
B660F6D41C29868000687D6E /* whisperFake.cer in Resources */,
76EB060118170B33006006FC /* InitiateSignal.proto in Resources */,
45C681C91D305C9E0050903A /* JSQDisplayedMessageCollectionViewCell.xib in Resources */,
45C681C51D305C9E0050903A /* JSQCallCollectionViewCell.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -2601,6 +2652,7 @@
76EB05A618170B33006006FC /* RtpPacket.m in Sources */,
76EB064218170B33006006FC /* StringUtil.m in Sources */,
A547DD741A70A87800103EC7 /* DJWActionSheet+OWS.m in Sources */,
45C681B71D305A580050903A /* JSQCall.m in Sources */,
76EB062618170B33006006FC /* Queue.m in Sources */,
D221A09A169C9E5E00537ABF /* main.m in Sources */,
45843D1F1D2236B30013E85A /* OWSContactsSearcher.m in Sources */,
@ -2611,15 +2663,18 @@
76EB05FE18170B33006006FC /* InitiateSignal.pb.m in Sources */,
76EB05CA18170B33006006FC /* RecipientUnavailable.m in Sources */,
E197B61418BBEC1A00F073E5 /* DropoutTracker.m in Sources */,
453D28B41D32B87100D523F0 /* JSQInfoMessage.m in Sources */,
FCAC963C19FEF9280046DFC5 /* SignalsViewController.m in Sources */,
76EB05DA18170B33006006FC /* LowLatencyConnector.m in Sources */,
76EB05EE18170B33006006FC /* CallTermination.m in Sources */,
B66B9F7D1AEAF40500E2E609 /* NotificationSettingsOptionsViewController.m in Sources */,
453D28B31D32B87100D523F0 /* JSQErrorMessage.m in Sources */,
E1CD329618BCFF9900B1A496 /* SoundInstance.m in Sources */,
76EB05B418170B33006006FC /* HashChain.m in Sources */,
76EB05E418170B33006006FC /* UdpSocket.m in Sources */,
76EB058218170B33006006FC /* Environment.m in Sources */,
76EB064418170B33006006FC /* ThreadManager.m in Sources */,
45C681C61D305C9E0050903A /* JSQDisplayedMessageCollectionViewCell.m in Sources */,
E197B61E18BBEC6D00F073E5 /* AudioRouter.m in Sources */,
E197B60D18BBEC1A00F073E5 /* AudioSocket.m in Sources */,
A5D0699B1A50E9CB004CB540 /* ShowGroupMembersViewController.m in Sources */,
@ -2632,6 +2687,7 @@
B63761ED19E1FBE8005735D1 /* HttpRequestOrResponse.m in Sources */,
76EB05A018170B33006006FC /* IpAddress.m in Sources */,
FCAC965119FF0A6E0046DFC5 /* MessagesViewController.m in Sources */,
453D28BA1D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m in Sources */,
B68EF9BB1C0B1EBD009C3DCD /* FLAnimatedImageView.m in Sources */,
A5E9D4BB1A65FAD800E4481C /* TSVideoAttachmentAdapter.m in Sources */,
E197B61118BBEC1A00F073E5 /* AudioProcessor.m in Sources */,
@ -2670,6 +2726,7 @@
E16E5BF018AAC40200B7C403 /* EvpKeyAgreement.m in Sources */,
FCFD25821A154B3800F4C644 /* CodeVerificationViewController.m in Sources */,
B65EDA1219E1BE6400AAA7CB /* RPAPICall.m in Sources */,
453D28B71D32BA5F00D523F0 /* JSQDisplayedMessage.m in Sources */,
76EB05DC18170B33006006FC /* StreamPair.m in Sources */,
76EB064618170B33006006FC /* TimeUtil.m in Sources */,
70BAFD5D190584BE00FA5E0B /* NotificationTracker.m in Sources */,
@ -2707,6 +2764,7 @@
B63761E319E1F487005735D1 /* AFHTTPSessionManager+SignalMethods.m in Sources */,
76EB05CC18170B33006006FC /* ShortAuthenticationStringGenerator.m in Sources */,
E16E5BEF18AAC40200B7C403 /* EC25KeyAgreementProtocol.m in Sources */,
45C681BC1D305C080050903A /* JSQCallCollectionViewCell.m in Sources */,
76EB064018170B33006006FC /* AnonymousTerminator.m in Sources */,
76EB058818170B33006006FC /* PropertyListPreferences.m in Sources */,
76EB05B218170B33006006FC /* DH3KKeyAgreementProtocol.m in Sources */,
@ -2785,6 +2843,7 @@
B660F7341C29988E00687D6E /* RtpSocket.m in Sources */,
B660F7351C29988E00687D6E /* SequenceCounter.m in Sources */,
B660F7361C29988E00687D6E /* SrtpSocket.m in Sources */,
45C681C71D305C9E0050903A /* JSQDisplayedMessageCollectionViewCell.m in Sources */,
B660F7371C29988E00687D6E /* SrtpStream.m in Sources */,
B660F7381C29988E00687D6E /* DH3KKeyAgreementParticipant.m in Sources */,
B660F7391C29988E00687D6E /* DH3KKeyAgreementProtocol.m in Sources */,
@ -2805,6 +2864,7 @@
B660F7481C29988E00687D6E /* RecipientUnavailable.m in Sources */,
45843D201D2236B30013E85A /* OWSContactsSearcher.m in Sources */,
B660F7491C29988E00687D6E /* ShortAuthenticationStringGenerator.m in Sources */,
45C681BD1D305C080050903A /* JSQCallCollectionViewCell.m in Sources */,
B660F74A1C29988E00687D6E /* ZrtpHandshakeResult.m in Sources */,
B660F74B1C29988E00687D6E /* ZrtpHandshakeSocket.m in Sources */,
B660F74C1C29988E00687D6E /* ZrtpInitiator.m in Sources */,
@ -2815,6 +2875,7 @@
B660F7511C29988E00687D6E /* StreamPair.m in Sources */,
B660F7521C29988E00687D6E /* Certificate.m in Sources */,
B660F7531C29988E00687D6E /* NetworkStream.m in Sources */,
453D28BB1D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m in Sources */,
B660F7541C29988E00687D6E /* SecureEndPoint.m in Sources */,
B660F7551C29988E00687D6E /* UdpSocket.m in Sources */,
45843D221D223BA10013E85A /* OWSContactsSearcherTest.m in Sources */,
@ -2835,6 +2896,7 @@
B660F7641C29988E00687D6E /* InitiateSignal.pb.m in Sources */,
B660F7651C29988E00687D6E /* InitiatorSessionDescriptor.m in Sources */,
B660F7661C29988E00687D6E /* ResponderSessionDescriptor.m in Sources */,
45C681B81D305A580050903A /* JSQCall.m in Sources */,
B660F7671C29988E00687D6E /* SignalUtil.m in Sources */,
B660F7681C29988E00687D6E /* CategorizingLogger.m in Sources */,
B660F7691C29988E00687D6E /* DecayingSampleEstimator.m in Sources */,

@ -0,0 +1,76 @@
//
// JSQCall.h
// JSQMessages
//
// Created by Dylan Bourgeois on 20/11/14.
//
#import <Foundation/Foundation.h>
#import "JSQMessageData.h"
#import "TSMessageAdapter.h"
typedef enum : NSUInteger {
kCallOutgoing = 1,
kCallIncoming = 2,
kCallMissed = 3,
kGroupUpdateJoin = 4,
kGroupUpdateLeft = 5,
kGroupUpdate = 6
} CallStatus;
@interface JSQCall : NSObject <JSQMessageData, NSCoding, NSCopying>
/*
* Returns the string Id of the user who initiated the call
*/
@property (copy, nonatomic, readonly) NSString *senderId;
/*
* Returns the display name for user who initiated the call
*/
@property (copy, nonatomic, readonly) NSString *senderDisplayName;
/*
* Returns date of the call
*/
@property (copy, nonatomic, readonly) NSDate *date;
/*
* Returns the call status
* @see CallStatus
*/
@property (nonatomic) CallStatus status;
/*
* Returns message type for adapter
*/
@property (nonatomic) TSMessageAdapterType messageType;
/*
* User can configure whether a thumbnail is used in the display of this cell or not
*/
@property (nonatomic) BOOL useThumbnail;
/**
* String to be displayed
*/
@property (nonatomic, copy) NSString *detailString;
#pragma mark - Initialization
- (instancetype)initWithCallerId:(NSString *)callerId
callerDisplayName:(NSString *)callerDisplayName
date:(NSDate *)date
status:(CallStatus)status
displayString:(NSString*)detailString;
-(NSString*)dateText;
-(UIImage*)thumbnailImage;
@end

@ -0,0 +1,170 @@
//
// JSQCall.m
// JSQMessages
//
// Created by Dylan Bourgeois on 20/11/14.
//
#import "JSQCall.h"
#import "JSQMessagesTimestampFormatter.h"
#import "UIImage+JSQMessages.h"
@implementation JSQCall
#pragma mark - Initialzation
-(instancetype)initWithCallerId:(NSString *)senderId
callerDisplayName:(NSString *)senderDisplayName
date:(NSDate *)date
status:(CallStatus)status
displayString:(NSString *)detailString
{
NSParameterAssert(senderId != nil);
NSParameterAssert(senderDisplayName != nil);
self = [super init];
if (self) {
_senderId = [senderId copy];
_senderDisplayName = [senderDisplayName copy];
_date = [date copy];
_status = status;
_messageType = TSCallAdapter;
_detailString = [detailString stringByAppendingFormat:@" "];
}
return self;
}
-(id)init
{
NSAssert(NO,@"%s is not a valid initializer for %@. Use %@ instead", __PRETTY_FUNCTION__, [self class], NSStringFromSelector(@selector(initWithCallerId:callerDisplayName:date:status:displayString:)));
return nil;
}
-(void)dealloc
{
_senderId = nil;
_senderDisplayName = nil;
_date = nil;
}
-(NSString*)dateText
{
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.timeStyle = NSDateFormatterShortStyle;
dateFormatter.dateStyle = NSDateFormatterMediumStyle;
dateFormatter.doesRelativeDateFormatting = YES;
return [dateFormatter stringFromDate:_date];
}
-(UIImage*)thumbnailImage {
// This relies on those assets being in the project
if(!_useThumbnail) {
return nil;
}
switch (_status) {
case kCallOutgoing:
return [UIImage imageNamed:@"statCallOutgoing--blue"];
break;
case kCallIncoming:
case kCallMissed:
return [UIImage imageNamed:@"statCallIncoming--blue"];
break;
case kGroupUpdate:
return [UIImage imageNamed:@"statRefreshedGroup--blue"];
break;
case kGroupUpdateLeft:
return [UIImage imageNamed:@"statLeftGroup--blue"];
break;
case kGroupUpdateJoin:
return [UIImage imageNamed:@"statJoinedGroup--blue"];
break;
default:
return nil;
break;
}
}
#pragma mark - NSObject
-(BOOL)isEqual:(id)object
{
if (self==object) {
return YES;
}
if (![object isKindOfClass:[self class]])
{
return NO;
}
JSQCall * aCall = (JSQCall*)object;
return [self.senderId isEqualToString:aCall.senderId]
&& [self.senderDisplayName isEqualToString:aCall.senderDisplayName]
&& ([self.date compare:aCall.date] == NSOrderedSame)
&& self.status == aCall.status;
}
-(NSUInteger)hash
{
return self.senderId.hash ^ self.date.hash;
}
-(NSString*)description
{
return [NSString stringWithFormat:@"<%@: senderId=%@, senderDisplayName=%@, date=%@>",
[self class], self.senderId, self.senderDisplayName, self.date];
}
#pragma mark - JSQMessageData
//TODO I'm not sure this is right. It affects bubble rendering.
- (BOOL)isMediaMessage {
return NO;
}
#pragma mark - NSCoding
-(instancetype)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if (self) {
_senderId = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(senderId))];
_senderDisplayName = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(senderDisplayName))];
_date = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(date))];
_status = (CallStatus)[aDecoder decodeIntegerForKey:NSStringFromSelector(@selector(status))];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:self.senderId forKey:NSStringFromSelector(@selector(senderId))];
[aCoder encodeObject:self.senderDisplayName forKey:NSStringFromSelector(@selector(senderDisplayName))];
[aCoder encodeObject:self.date forKey:NSStringFromSelector(@selector(date))];
[aCoder encodeDouble:self.status forKey:NSStringFromSelector(@selector(status))];
}
#pragma mark - NSCopying
-(instancetype)copyWithZone:(NSZone *)zone
{
return [[[self class] allocWithZone:zone]initWithCallerId:self.senderId
callerDisplayName:self.senderDisplayName
date:self.date
status:self.status
displayString:self.detailString];
}
- (NSUInteger)messageHash{
return self.hash;
}
- (NSString *)text{
return _detailString;
}
@end

@ -0,0 +1,45 @@
//
// JSQDisplayedMessage.h
// JSQMessages
//
// Created by Dylan Bourgeois on 29/11/14.
// Copyright (c) 2014 Hexed Bits. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "JSQMessageData.h"
#import "TSMessageAdapter.h"
/* JSQDisplayed message is the parent class for displaying information to the user
* from within the conversation view. Do not use directly :
*
* @see JSQInfoMessage
* @see JSQErrorMessage
*
*/
@interface JSQDisplayedMessage : NSObject <JSQMessageData>
/*
* Returns the unique identifier of the person affected by the displayed message
*/
@property (copy, nonatomic, readonly) NSString *senderId;
/*
* Returns the name of the person affected by the displayed message
*/
@property (copy, nonatomic, readonly) NSString *senderDisplayName;
/*
* Returns date of the displayed message
*/
@property (copy, nonatomic, readonly) NSDate *date;
#pragma mark - Initializer
-(instancetype)initWithSenderId:(NSString*)senderId
senderDisplayName:(NSString*)senderDisplayName
date:(NSDate*)date;
@end

@ -0,0 +1,44 @@
//
// JSQDisplayedMessage.m
// JSQMessages
//
// Created by Dylan Bourgeois on 29/11/14.
// Copyright (c) 2014 Hexed Bits. All rights reserved.
//
#import "JSQDisplayedMessage.h"
@implementation JSQDisplayedMessage
-(id)init
{
NSAssert(NO,@"%s is not a valid initializer for %@. Use %@ instead", __PRETTY_FUNCTION__, [self class], NSStringFromSelector(@selector(initWithSenderId:senderDisplayName:date:)));
return nil;
}
-(instancetype)initWithSenderId:(NSString*)senderId
senderDisplayName:(NSString*)senderDisplayName
date:(NSDate*)date
{
self = [super init];
if (self) {
_senderId = [senderId copy];
_senderDisplayName = [senderDisplayName copy];
_date = [date copy];
}
return self;
}
- (NSUInteger)messageHash
{
return self.date.hash ^ self.senderId.hash;
}
- (BOOL)isMediaMessage
{
return NO;
}
@end

@ -0,0 +1,36 @@
//
// JSQErrorMessage.h
// JSQMessages
//
// Created by Dylan Bourgeois on 29/11/14.
// Copyright (c) 2014 Hexed Bits. All rights reserved.
//
#import "JSQDisplayedMessage.h"
typedef NS_ENUM(NSInteger, JSQErrorMessageType){
JSQErrorMessageNoSession,
JSQErrorMessageWrongTrustedIdentityKey,
JSQErrorMessageInvalidKeyException,
JSQErrorMessageMissingKeyId,
JSQErrorMessageInvalidMessage,
JSQErrorMessageDuplicateMessage,
JSQErrorMessageInvalidVersion
};
@interface JSQErrorMessage : JSQDisplayedMessage
@property (nonatomic) JSQErrorMessageType errorMessageType;
@property (nonatomic) TSMessageAdapterType messageType;
#pragma mark - Initialization
- (instancetype)initWithErrorType:(JSQErrorMessageType)messageType
senderId:(NSString*)senderId
senderDisplayName:(NSString*)senderDisplayName
date:(NSDate*)date;
- (NSString*)text;
@end

@ -0,0 +1,75 @@
//
// JSQErrorMessage.m
// JSQMessages
//
// Created by Dylan Bourgeois on 29/11/14.
// Copyright (c) 2014 Hexed Bits. All rights reserved.
//
#import "JSQErrorMessage.h"
@implementation JSQErrorMessage
- (instancetype)initWithErrorType:(JSQErrorMessageType)messageType
senderId:(NSString *)senderId
senderDisplayName:(NSString *)senderDisplayName
date:(NSDate *)date
{
self = [super initWithSenderId:senderId senderDisplayName:senderDisplayName date:date];
if (self) {
_errorMessageType = messageType;
_messageType = TSErrorMessageAdapter;
}
return self;
}
- (NSString*)text
{
switch (self.errorMessageType) {
case JSQErrorMessageNoSession:
return [NSString stringWithFormat:@"No session error"];
break;
case JSQErrorMessageWrongTrustedIdentityKey:
return [NSString stringWithFormat:@"Error : Wrong trusted identity key for %@.", self.senderDisplayName];
break;
case JSQErrorMessageInvalidKeyException:
return [NSString stringWithFormat:@"Error : Invalid key exception for %@.", self.senderDisplayName];
break;
case JSQErrorMessageMissingKeyId:
return [NSString stringWithFormat:@"Error: Missing key identifier for %@", self.senderDisplayName];
break;
case JSQErrorMessageInvalidMessage:
return [NSString stringWithFormat:@"Error: Invalid message"];
break;
case JSQErrorMessageDuplicateMessage:
return [NSString stringWithFormat:@"Error: Duplicate message"];
break;
case JSQErrorMessageInvalidVersion:
return [NSString stringWithFormat:@"Error: Invalid version for contact %@.", self.senderDisplayName];
break;
default:
return nil;
break;
}
}
- (NSUInteger)hash
{
return self.senderId.hash ^ self.date.hash;
}
- (NSString*)description
{
return [NSString stringWithFormat:@"<%@: senderId=%@, senderDisplayName=%@, date=%@, type=%ld>",
[self class], self.senderId, self.senderDisplayName, self.date, self.errorMessageType];
}
-(TSMessageAdapterType)messageType
{
return TSErrorMessageAdapter;
}
@end

@ -0,0 +1,31 @@
//
// JSQInfoMessage.h
// JSQMessages
//
// Created by Dylan Bourgeois on 29/11/14.
// Copyright (c) 2014 Hexed Bits. All rights reserved.
//
#import "JSQDisplayedMessage.h"
typedef NS_ENUM(NSInteger, JSQInfoMessageType){
JSQInfoMessageTypeSessionDidEnd,
};
@interface JSQInfoMessage : JSQDisplayedMessage
@property (nonatomic) JSQInfoMessageType infoMessageType;
@property (nonatomic) TSMessageAdapterType messageType;
#pragma mark - Initialization
- (instancetype)initWithInfoType:(JSQInfoMessageType)messageType
senderId:(NSString*)senderId
senderDisplayName:(NSString*)senderDisplayName
date:(NSDate*)date;
- (NSString*)text;
@end

@ -0,0 +1,54 @@
//
// JSQInfoMessage.m
// JSQMessages
//
// Created by Dylan Bourgeois on 29/11/14.
// Copyright (c) 2014 Hexed Bits. All rights reserved.
//
#import "JSQInfoMessage.h"
@implementation JSQInfoMessage
- (instancetype)initWithInfoType:(JSQInfoMessageType)messageType
senderId:(NSString *)senderId
senderDisplayName:(NSString *)senderDisplayName
date:(NSDate *)date
{
//@discussion: NSParameterAssert() ?
self = [super initWithSenderId:senderId senderDisplayName:senderDisplayName date:date];
if (self) {
_infoMessageType = messageType;
_messageType = TSInfoMessageAdapter;
}
return self;
}
-(NSString*)text
{
switch (self.infoMessageType) {
case JSQInfoMessageTypeSessionDidEnd:
return [NSString stringWithFormat:@"Session with %@ ended.", self.senderDisplayName];
break;
default:
return nil;
break;
}
}
-(NSUInteger)hash
{
return self.senderId.hash ^ self.date.hash;
}
-(NSString*)description
{
return [NSString stringWithFormat:@"<%@: senderId=%@, senderDisplayName=%@, date=%@, type=%ld>",
[self class], self.senderId, self.senderDisplayName, self.date, self.infoMessageType];
}
@end

@ -0,0 +1,13 @@
//
// OWSMessagesBubblesSizeCalculator.h
// Signal
//
// Created by Michael Kirk on 7/10/16.
// Copyright © 2016 Open Whisper Systems. All rights reserved.
//
#import <JSQMessagesViewController/JSQMessagesBubblesSizeCalculator.h>
@interface OWSMessagesBubblesSizeCalculator : JSQMessagesBubblesSizeCalculator
@end

@ -0,0 +1,45 @@
//
// OWSMessagesBubblesSizeCalculator.m
// Signal
//
// Created by Michael Kirk on 7/10/16.
// Copyright © 2016 Open Whisper Systems. All rights reserved.
//
#import "OWSMessagesBubblesSizeCalculator.h"
#import "TSMessageAdapter.h"
#import "JSQDisplayedMessageCollectionViewCell.h"
@implementation OWSMessagesBubblesSizeCalculator
/**
* Computes and returns the size of the `messageBubbleImageView` property
* of a `JSQMessagesCollectionViewCell` for the specified messageData at indexPath.
*
* @param messageData A message data object.
* @param indexPath The index path at which messageData is located.
* @param layout The layout object asking for this information.
*
* @return A sizes that specifies the required dimensions to display the entire message contents.
* Note, this is *not* the entire cell, but only its message bubble.
*/
- (CGSize)messageBubbleSizeForMessageData:(id<JSQMessageData>)messageData
atIndexPath:(NSIndexPath *)indexPath
withLayout:(JSQMessagesCollectionViewFlowLayout *)layout
{
CGSize superSize = [super messageBubbleSizeForMessageData:messageData
atIndexPath:indexPath
withLayout:layout];
TSMessageAdapter *message = (TSMessageAdapter *)messageData;
if (message.messageType == TSInfoMessageAdapter ||
message.messageType == TSErrorMessageAdapter) {
// Prevent cropping message text by accounting for message container/icon
superSize.height = OWSDisplayedMessageCellHeight;
}
return superSize;
}
@end

@ -15,6 +15,16 @@
#define ME_MESSAGE_IDENTIFIER @"Me";
typedef NS_ENUM(NSInteger, TSMessageAdapterType) {
TSIncomingMessageAdapter,
TSOutgoingMessageAdapter,
TSCallAdapter,
TSInfoMessageAdapter,
TSErrorMessageAdapter,
TSMediaAttachmentAdapter,
TSGenericTextMessageAdapter, //Used when message direction is unknown (outgoing or incoming)
};
@interface TSMessageAdapter : NSObject <JSQMessageData>
+ (id<JSQMessageData>)messageViewDataWithInteraction:(TSInteraction *)interaction inThread:(TSThread *)thread;

@ -58,6 +58,7 @@
+ (id<JSQMessageData>)messageViewDataWithInteraction:(TSInteraction *)interaction inThread:(TSThread *)thread {
TSMessageAdapter *adapter = [[TSMessageAdapter alloc] init];
adapter.messageDate = interaction.date;
// TODO casting a string to an integer? At least need a comment here explaining why we are doing this.
adapter.identifier = (NSUInteger)interaction.uniqueId;
if ([thread isKindOfClass:[TSContactThread class]]) {
@ -230,7 +231,7 @@
if (self.thread) {
return _thread.name;
}
return self.senderDisplayName;
return _senderDisplayName;
}
- (NSDate *)date {
@ -249,8 +250,13 @@
return self.messageBody;
}
- (NSUInteger)messageHash {
return self.identifier;
- (NSUInteger)messageHash
{
if (self.isMediaMessage) {
return [self.mediaItem mediaHash];
} else {
return self.identifier;
}
}
- (NSInteger)messageState {

@ -20,6 +20,7 @@
#import "FingerprintViewController.h"
#import "FullImageViewController.h"
#import "JSQCallCollectionViewCell.h"
#import "JSQDisplayedMessageCollectionViewCell.h"
#import "MessagesViewController.h"
#import "NSDate+millisecondTimeStamp.h"
#import "NewGroupViewController.h"
@ -30,7 +31,16 @@
#import "TSAttachmentPointer.h"
#import "TSContentAdapters.h"
#import "TSDatabaseView.h"
#import "OWSMessagesBubblesSizeCalculator.h"
//TODO should JSQInfoMessage be rolled into JSQDisplayedMessageCollectionViewCell?
#import "JSQInfoMessage.h"
#import "TSInfoMessage.h"
//TODO should JSQErrorMessage be rolled into JSQDisplayedMessageCollectionViewCell?
#import "JSQErrorMessage.h"
#import "TSErrorMessage.h"
//TODO should JSQCall be rolled into JSQCallCollectionViewCell?
#import "JSQCall.h"
#import "TSCall.h"
#import "TSIncomingMessage.h"
#import "TSInvalidIdentityKeyErrorMessage.h"
#import "TSMessagesManager+attachments.h"
@ -154,6 +164,12 @@ typedef enum : NSUInteger {
- (void)viewDidLoad {
[super viewDidLoad];
// JSQMVC width is 375px at this point (as specified by the xib), but this causes
// our initial bubble calculations to be off since they happen before the containing
// view is layed out. https://github.com/jessesquires/JSQMessagesViewController/issues/1257
// Resetting here makes sure we've got a good initial width.
[self resetFrame];
[self.navigationController.navigationBar setTranslucent:NO];
self.messageAdapterCache = [[NSCache alloc] init];
@ -182,13 +198,22 @@ typedef enum : NSUInteger {
[self initializeTextView];
[JSQMessagesCollectionViewCell registerMenuAction:@selector(delete:)];
self.collectionView.collectionViewLayout.bubbleSizeCalculator = [[OWSMessagesBubblesSizeCalculator alloc] init];
[self initializeCollectionViewLayout];
[self registerCustomMessageNibs];
self.senderId = ME_MESSAGE_IDENTIFIER;
self.senderDisplayName = ME_MESSAGE_IDENTIFIER;
}
- (void)registerCustomMessageNibs
{
[self.collectionView registerNib:[JSQCallCollectionViewCell nib]
forCellWithReuseIdentifier:[JSQCallCollectionViewCell cellReuseIdentifier]];
[self.collectionView registerNib:[JSQDisplayedMessageCollectionViewCell nib]
forCellWithReuseIdentifier:[JSQDisplayedMessageCollectionViewCell cellReuseIdentifier]];
}
- (void)toggleObservers:(BOOL)shouldObserve {
@ -499,13 +524,10 @@ typedef enum : NSUInteger {
- (void)initializeBubbles {
JSQMessagesBubbleImageFactory *bubbleFactory = [[JSQMessagesBubbleImageFactory alloc] init];
self.incomingBubbleImageData = [bubbleFactory incomingMessagesBubbleImageWithColor:[UIColor jsq_messageBubbleLightGrayColor]];
self.outgoingBubbleImageData = [bubbleFactory outgoingMessagesBubbleImageWithColor:[UIColor ows_materialBlueColor]];
self.incomingBubbleImageData =
[bubbleFactory incomingMessagesBubbleImageWithColor:[UIColor jsq_messageBubbleLightGrayColor]];
self.currentlyOutgoingBubbleImageData =
[bubbleFactory outgoingMessageFailedBubbleImageWithColor:[UIColor ows_fadedBlueColor]];
self.outgoingMessageFailedImageData = [bubbleFactory outgoingMessageFailedBubbleImageWithColor:[UIColor grayColor]];
self.currentlyOutgoingBubbleImageData = [bubbleFactory outgoingMessagesBubbleImageWithColor:[UIColor ows_fadedBlueColor]];
self.outgoingMessageFailedImageData = [bubbleFactory outgoingMessagesBubbleImageWithColor:[UIColor grayColor]];
}
- (void)initializeCollectionViewLayout {
@ -686,11 +708,13 @@ typedef enum : NSUInteger {
}
- (id<JSQMessageBubbleImageDataSource>)collectionView:(JSQMessagesCollectionView *)collectionView
messageBubbleImageDataForItemAtIndexPath:(NSIndexPath *)indexPath {
id<JSQMessageData> message = [self messageAtIndexPath:indexPath];
messageBubbleImageDataForItemAtIndexPath:(NSIndexPath *)indexPath
{
TSInteraction *message = [self interactionAtIndexPath:indexPath];
if ([message.senderId isEqualToString:self.senderId]) {
switch (message.messageState) {
if ([message isKindOfClass:[TSOutgoingMessage class]]) {
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)message;
switch (outgoingMessage.messageState) {
case TSOutgoingMessageStateUnsent:
return self.outgoingMessageFailedImageData;
case TSOutgoingMessageStateAttemptingOut:
@ -711,25 +735,45 @@ typedef enum : NSUInteger {
#pragma mark - UICollectionView DataSource
- (UICollectionViewCell *)collectionView:(JSQMessagesCollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath {
TSMessageAdapter *msg = [self messageAtIndexPath:indexPath];
switch (msg.messageType) {
case TSIncomingMessageAdapter:
return [self loadIncomingMessageCellForMessage:msg atIndexPath:indexPath];
case TSOutgoingMessageAdapter:
return [self loadOutgoingCellForMessage:msg atIndexPath:indexPath];
case TSCallAdapter:
return [self loadCallCellForCall:msg atIndexPath:indexPath];
case TSInfoMessageAdapter:
return [self loadInfoMessageCellForMessage:msg atIndexPath:indexPath];
case TSErrorMessageAdapter:
return [self loadErrorMessageCellForMessage:msg atIndexPath:indexPath];
cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
TSMessageAdapter *message = [self messageAtIndexPath:indexPath];
NSParameterAssert(message != nil);
JSQMessagesCollectionViewCell *cell;
switch (message.messageType) {
case TSCallAdapter: {
DDLogDebug(@"building cell for Call");
JSQCall *call = (JSQCall *)message;
cell = [self loadCallCellForCall:call atIndexPath:indexPath];
} break;
case TSInfoMessageAdapter: {
DDLogDebug(@"building cell for InfoMessage");
JSQInfoMessage *infoMessage = (JSQInfoMessage *)message;
cell = [self loadInfoMessageCellForMessage:infoMessage atIndexPath:indexPath];
} break;
case TSErrorMessageAdapter: {
DDLogDebug(@"building cell for ErrorMessage");
JSQErrorMessage *errorMessage = (JSQErrorMessage *)message;
cell = [self loadErrorMessageCellForMessage:errorMessage atIndexPath:indexPath];
} break;
case TSIncomingMessageAdapter: {
DDLogDebug(@"building cell for incoming message: %@", message);
cell = [self loadIncomingMessageCellForMessage:message atIndexPath:indexPath];
default:
DDLogError(@"Something went wrong");
return nil;
} break;
case TSOutgoingMessageAdapter: {
DDLogDebug(@"building cell for incoming message: %@", message);
cell = [self loadOutgoingCellForMessage:message atIndexPath:indexPath];
} break;
default: {
DDLogDebug(@"using default cell constructor for message: %@", message);
cell = (JSQMessagesCollectionViewCell *)[super collectionView:collectionView cellForItemAtIndexPath:indexPath];
} break;
}
cell.delegate = collectionView;
return cell;
}
#pragma mark - Loading message cells
@ -764,26 +808,84 @@ typedef enum : NSUInteger {
return cell;
}
- (JSQCallCollectionViewCell *)loadCallCellForCall:(id<JSQMessageData>)call atIndexPath:(NSIndexPath *)indexPath {
JSQCallCollectionViewCell *cell =
(JSQCallCollectionViewCell *)[super collectionView:self.collectionView cellForItemAtIndexPath:indexPath];
return cell;
- (JSQCallCollectionViewCell *)loadCallCellForCall:(JSQCall *)call
atIndexPath:(NSIndexPath *)indexPath
{
JSQCallCollectionViewCell *callCell = [self.collectionView dequeueReusableCellWithReuseIdentifier:[JSQCallCollectionViewCell cellReuseIdentifier]
forIndexPath:indexPath];
NSString *text = call.date != nil ? [call text] : call.senderDisplayName;
NSString *allText = call.date != nil ? [text stringByAppendingString:[call dateText]] : text;
UIFont *boldFont = [UIFont fontWithName:@"HelveticaNeue-Medium" size:12.0f];
UIFont *regularFont = [UIFont fontWithName:@"HelveticaNeue-Light" size:12.0f];
//TODO declarative dict
NSDictionary *attrs = [NSDictionary dictionaryWithObjectsAndKeys:boldFont, NSFontAttributeName, nil];
NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:allText
attributes:attrs];
if([call date]!=nil) {
// Not a group meta message
NSDictionary *subAttrs = [NSDictionary dictionaryWithObjectsAndKeys:
regularFont, NSFontAttributeName, nil];
const NSRange range = NSMakeRange([text length],[[call dateText] length]);
[attributedText setAttributes:subAttrs range:range];
BOOL isOutgoing = [self.senderId isEqualToString:call.senderId];
if (isOutgoing)
{
callCell.outgoingCallImageView.image = [call thumbnailImage];
} else {
callCell.incomingCallImageView.image = [call thumbnailImage];
}
} else {
// TODO wrt comment, does it make sense to receive a group meta message in a *call* or was this copy/paste misfire?
// A group meta message
callCell.incomingCallImageView.image = [call thumbnailImage];
}
callCell.cellLabel.attributedText = attributedText;
callCell.cellLabel.numberOfLines = 0; // uses as many lines as it needs
// TODO is this a constant somewhere else already?
callCell.cellLabel.textColor = [UIColor colorWithRed:32.f/255.f green:144.f/255.f blue:234.f/255.f alpha:1.f];
callCell.layer.shouldRasterize = YES;
callCell.layer.rasterizationScale = [UIScreen mainScreen].scale;
return callCell;
}
- (JSQDisplayedMessageCollectionViewCell *)loadInfoMessageCellForMessage:(id<JSQMessageData>)message
atIndexPath:(NSIndexPath *)indexPath {
JSQDisplayedMessageCollectionViewCell *cell =
(JSQDisplayedMessageCollectionViewCell *)[super collectionView:self.collectionView
cellForItemAtIndexPath:indexPath];
return cell;
- (JSQDisplayedMessageCollectionViewCell *)loadInfoMessageCellForMessage:(JSQInfoMessage *)infoMessage
atIndexPath:(NSIndexPath *)indexPath
{
JSQDisplayedMessageCollectionViewCell *infoCell = [self.collectionView dequeueReusableCellWithReuseIdentifier:[JSQDisplayedMessageCollectionViewCell cellReuseIdentifier]
forIndexPath:indexPath];
infoCell.cellLabel.text = [infoMessage text];
infoCell.cellLabel.textColor = [UIColor darkGrayColor];
// TODO is this a constant somewhere else already?
infoCell.textContainer.layer.borderColor = [[UIColor colorWithRed:239.f/255.f green:189.f/255.f blue:88.f/255.f alpha:1.0f] CGColor];
infoCell.headerImageView.image = [UIImage imageNamed:@"warning_white"];
infoCell.layer.shouldRasterize = YES;
infoCell.layer.rasterizationScale = [UIScreen mainScreen].scale;
return infoCell;
}
- (JSQDisplayedMessageCollectionViewCell *)loadErrorMessageCellForMessage:(id<JSQMessageData>)message
- (JSQDisplayedMessageCollectionViewCell *)loadErrorMessageCellForMessage:(JSQErrorMessage *)errorMessage
atIndexPath:(NSIndexPath *)indexPath {
JSQDisplayedMessageCollectionViewCell *cell =
(JSQDisplayedMessageCollectionViewCell *)[super collectionView:self.collectionView
cellForItemAtIndexPath:indexPath];
return cell;
JSQDisplayedMessageCollectionViewCell *errorCell = [self.collectionView dequeueReusableCellWithReuseIdentifier:[JSQDisplayedMessageCollectionViewCell cellReuseIdentifier]
forIndexPath:indexPath];
errorCell.cellLabel.text = [errorMessage text];
errorCell.cellLabel.textColor = [UIColor darkGrayColor];
// TODO is this a constant somewhere else already?
errorCell.textContainer.layer.borderColor = [[UIColor colorWithRed:195.f/255.f green:0 blue:22.f/255.f alpha:1.0f] CGColor];
errorCell.headerImageView.image = [UIImage imageNamed:@"error_white"];
errorCell.layer.shouldRasterize = YES;
errorCell.layer.rasterizationScale = [UIScreen mainScreen].scale;
return errorCell;
}
#pragma mark - Adjusting cell label heights
@ -831,9 +933,11 @@ typedef enum : NSUInteger {
TSMessageAdapter *currentMessage = [self messageAtIndexPath:indexPath];
// If message failed, say that message should be tapped to retry;
if (currentMessage.messageType == TSOutgoingMessageAdapter &&
currentMessage.messageState == TSOutgoingMessageStateUnsent) {
return YES;
if (currentMessage.messageType == TSOutgoingMessageAdapter) {
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)currentMessage;
if(outgoingMessage.messageState == TSOutgoingMessageStateUnsent) {
return YES;
}
}
if ([self.thread isKindOfClass:[TSGroupThread class]]) {
@ -868,7 +972,13 @@ typedef enum : NSUInteger {
}
- (BOOL)isMessageOutgoingAndDelivered:(TSMessageAdapter *)message {
return message.messageType == TSOutgoingMessageAdapter && message.messageState == TSOutgoingMessageStateDelivered;
if (message.messageType == TSOutgoingMessageAdapter) {
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)message;
if(outgoingMessage.messageState == TSOutgoingMessageStateDelivered) {
return YES;
}
}
return NO;
}
@ -879,11 +989,14 @@ typedef enum : NSUInteger {
textAttachment.bounds = CGRectMake(0, 0, 11.0f, 10.0f);
if ([self shouldShowMessageStatusAtIndexPath:indexPath]) {
if (msg.messageType == TSOutgoingMessageAdapter && msg.messageState == TSOutgoingMessageStateUnsent) {
NSMutableAttributedString *attrStr =
if (msg.messageType == TSOutgoingMessageAdapter) {
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)msg;
if(outgoingMessage.messageState == TSOutgoingMessageStateUnsent) {
NSMutableAttributedString *attrStr =
[[NSMutableAttributedString alloc] initWithString:NSLocalizedString(@"FAILED_SENDING_TEXT", nil)];
[attrStr appendAttributedString:[NSAttributedString attributedStringWithAttachment:textAttachment]];
return attrStr;
[attrStr appendAttributedString:[NSAttributedString attributedStringWithAttachment:textAttachment]];
return attrStr;
}
}
if ([self.thread isKindOfClass:[TSGroupThread class]]) {
@ -922,6 +1035,12 @@ typedef enum : NSUInteger {
#pragma mark - Actions
- (void)collectionView:(JSQMessagesCollectionView *)collectionView didTapCellAtIndexPath:(NSIndexPath *)indexPath touchLocation:(CGPoint)touchLocation
{
// Pass info/error message tapping to bubble tapping handler
[self collectionView:collectionView didTapMessageBubbleAtIndexPath:indexPath];
}
- (void)collectionView:(JSQMessagesCollectionView *)collectionView
didTapMessageBubbleAtIndexPath:(NSIndexPath *)indexPath {
TSMessageAdapter *messageItem =
@ -929,10 +1048,13 @@ typedef enum : NSUInteger {
TSInteraction *interaction = [self interactionAtIndexPath:indexPath];
switch (messageItem.messageType) {
case TSOutgoingMessageAdapter:
if (messageItem.messageState == TSOutgoingMessageStateUnsent) {
case TSOutgoingMessageAdapter: {
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)messageItem;
if (outgoingMessage.messageState == TSOutgoingMessageStateUnsent) {
[self handleUnsentMessageTap:(TSOutgoingMessage *)interaction];
}
}
// No `break` as we want to fall through to capture tapping on media items
case TSIncomingMessageAdapter: {
BOOL isMediaMessage = [messageItem isMediaMessage];
@ -1111,11 +1233,14 @@ typedef enum : NSUInteger {
case TSCallAdapter:
break;
default:
DDLogDebug(@"Unhandled bubble touch for interaction: %@.", interaction);
break;
}
}
- (void)handleWarningTap:(TSInteraction *)interaction {
//TODO why is handle warning tap expecting a TSIncomingMessage? I assumed it was for info messages, but maybe those aren't actionable.
// Looks like we create an InfoMessage "attachment is downloading" and tapping on it may restart a stalled fetch
if ([interaction isKindOfClass:[TSIncomingMessage class]]) {
TSIncomingMessage *message = (TSIncomingMessage *)interaction;
@ -1129,6 +1254,7 @@ typedef enum : NSUInteger {
if ([attachment isKindOfClass:[TSAttachmentPointer class]]) {
TSAttachmentPointer *pointer = (TSAttachmentPointer *)attachment;
// FIXME possible for pointer to get stuck in isDownloading state if app is closed while downloading.
if (!pointer.isDownloading) {
[[TSMessagesManager sharedManager] retrieveAttachment:pointer messageId:message.uniqueId];
}

@ -0,0 +1,31 @@
//
// JSQCallCollectionViewCell.h
// JSQMessages
//
// Created by Dylan Bourgeois on 20/11/14.
//
#import <UIKit/UIKit.h>
#import <JSQMessagesViewController/JSQMessagesCollectionViewCell.h>
#define kCallCellHeight 40.0f
#define kCallCellWidth 400.0f
@interface JSQCallCollectionViewCell : JSQMessagesCollectionViewCell
//TODO can we use an existing label from JSQMessagesCollectionViewCell?
@property (weak, nonatomic, readonly) JSQMessagesLabel *cellLabel;
@property (weak, nonatomic, readonly) UIImageView *outgoingCallImageView;
@property (weak, nonatomic, readonly) UIImageView *incomingCallImageView;
#pragma mark - Class methods
+ (UINib *)nib;
+ (NSString *)cellReuseIdentifier;
@end

@ -0,0 +1,63 @@
//
// JSQCallCollectionViewCell.m
// JSQMessages
//
// Created by Dylan Bourgeois on 20/11/14.
//
#import "JSQCallCollectionViewCell.h"
#import "UIView+JSQMessages.h"
@interface JSQCallCollectionViewCell ()
@property (weak, nonatomic) IBOutlet JSQMessagesLabel *cellLabel;
@property (weak, nonatomic) IBOutlet UIImageView *outgoingCallImageView;
@property (weak, nonatomic) IBOutlet UIImageView *incomingCallImageView;
@end
@implementation JSQCallCollectionViewCell
#pragma mark - Class Methods
+ (UINib *)nib
{
return [UINib nibWithNibName:NSStringFromClass([self class]) bundle:[NSBundle mainBundle]];
}
+ (NSString *)cellReuseIdentifier
{
return NSStringFromClass([self class]);
}
#pragma mark - Initializer
-(void)awakeFromNib
{
[super awakeFromNib];
[self setTranslatesAutoresizingMaskIntoConstraints:NO];
self.backgroundColor = [UIColor whiteColor];
self.cellLabel.textAlignment = NSTextAlignmentCenter;
self.cellLabel.font = [UIFont fontWithName:@"HelveticaNeue-Light" size:14.0f];
self.cellLabel.textColor = [UIColor lightGrayColor];
}
-(void)dealloc
{
_cellLabel = nil;
}
#pragma mark - Collection view cell
-(void)prepareForReuse
{
[super prepareForReuse];
self.cellLabel.text = nil;
}
@end

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="Efo-Hk-7Hw" customClass="JSQCallCollectionViewCell">
<rect key="frame" x="0.0" y="0.0" width="320" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="320" height="20"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="YEr-eC-P6i" customClass="JSQMessagesLabel">
<rect key="frame" x="39" y="0.0" width="242" height="20"/>
<constraints>
<constraint firstAttribute="height" constant="20" id="7nw-w2-91p"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="o2l-Ms-1mk">
<rect key="frame" x="281" y="0.0" width="20" height="20"/>
<constraints>
<constraint firstAttribute="height" constant="20" id="78L-mQ-gEo"/>
<constraint firstAttribute="width" constant="20" id="olH-5o-XyR"/>
</constraints>
</imageView>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="H8m-r4-eEC">
<rect key="frame" x="19" y="0.0" width="20" height="20"/>
<constraints>
<constraint firstAttribute="height" constant="20" id="Qay-jM-aBk"/>
<constraint firstAttribute="width" constant="20" id="RpE-jJ-cYX"/>
</constraints>
</imageView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</view>
<constraints>
<constraint firstItem="YEr-eC-P6i" firstAttribute="centerY" secondItem="o2l-Ms-1mk" secondAttribute="centerY" id="8wg-Tg-9Nh"/>
<constraint firstAttribute="trailing" secondItem="YEr-eC-P6i" secondAttribute="trailing" constant="39" id="Hj7-z5-WMM"/>
<constraint firstItem="YEr-eC-P6i" firstAttribute="leading" secondItem="H8m-r4-eEC" secondAttribute="trailing" id="ZZJ-jL-10d"/>
<constraint firstItem="YEr-eC-P6i" firstAttribute="top" secondItem="Efo-Hk-7Hw" secondAttribute="top" id="rEI-cY-6lx"/>
<constraint firstItem="YEr-eC-P6i" firstAttribute="leading" secondItem="Efo-Hk-7Hw" secondAttribute="leading" constant="39" id="uNd-aK-1hE"/>
<constraint firstItem="YEr-eC-P6i" firstAttribute="centerY" secondItem="H8m-r4-eEC" secondAttribute="centerY" id="wg8-V9-pBf"/>
<constraint firstItem="o2l-Ms-1mk" firstAttribute="leading" secondItem="YEr-eC-P6i" secondAttribute="trailing" id="wmR-MD-QeR"/>
</constraints>
<size key="customSize" width="320" height="20"/>
<connections>
<outlet property="cellLabel" destination="YEr-eC-P6i" id="jii-8O-zLL"/>
<outlet property="incomingCallImageView" destination="H8m-r4-eEC" id="hVW-Ng-BnU"/>
<outlet property="outgoingCallImageView" destination="o2l-Ms-1mk" id="Q5m-uX-80H"/>
</connections>
<point key="canvasLocation" x="219" y="435"/>
</collectionViewCell>
</objects>
</document>

@ -0,0 +1,21 @@
//
// JSQDisplayedMessageCollectionViewCell.h
// JSQMessages
//
// Created by Dylan Bourgeois on 29/11/14.
// Copyright (c) 2014 Hexed Bits. All rights reserved.
//
#import <UIKit/UIKit.h>
#import <JSQMessagesViewController/JSQMessagesCollectionViewCell.h>
static const CGFloat OWSDisplayedMessageCellHeight = 70.0f;
@interface JSQDisplayedMessageCollectionViewCell : JSQMessagesCollectionViewCell
// TODO can we use existing label from superclass?
@property (weak, nonatomic, readonly) JSQMessagesLabel * cellLabel;
@property (weak, nonatomic, readonly) UIImageView * headerImageView;
@property (strong, nonatomic, readonly) UIView *textContainer;
@end

@ -0,0 +1,63 @@
//
// JSQDisplayedMessageCollectionViewCell.m
// JSQMessages
//
// Created by Dylan Bourgeois on 29/11/14.
// Copyright (c) 2014 Hexed Bits. All rights reserved.
//
#import "JSQDisplayedMessageCollectionViewCell.h"
#import <JSQMessagesViewController/UIView+JSQMessages.h>
@interface JSQDisplayedMessageCollectionViewCell ()
@property(weak, nonatomic) IBOutlet JSQMessagesLabel* cellLabel;
@property (weak, nonatomic) IBOutlet UIImageView* headerImageView;
@property (strong, nonatomic) IBOutlet UIView *textContainer;
@end
@implementation JSQDisplayedMessageCollectionViewCell
#pragma mark - Class Methods
+ (UINib *)nib
{
return [UINib nibWithNibName:NSStringFromClass([self class]) bundle:[NSBundle mainBundle]];
}
+ (NSString *)cellReuseIdentifier
{
return NSStringFromClass([self class]);
}
#pragma mark - Initializer
-(void)awakeFromNib
{
[super awakeFromNib];
[self setTranslatesAutoresizingMaskIntoConstraints:NO];
self.backgroundColor = [UIColor whiteColor];
// self.cellLabelHeightConstraint.constant = 0.0f;
self.textContainer.layer.borderColor = [[UIColor lightGrayColor] CGColor];
self.textContainer.layer.borderWidth = 0.75f;
self.textContainer.layer.cornerRadius = 5.0f;
self.cellLabel.textAlignment = NSTextAlignmentCenter;
self.cellLabel.font = [UIFont fontWithName:@"HelveticaNeue-Light" size:14.0f];
self.cellLabel.textColor = [UIColor lightGrayColor];
}
#pragma mark - Collection view cell
-(void)prepareForReuse
{
[super prepareForReuse];
self.cellLabel.text = nil;
}
@end

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
<capability name="Alignment constraints with different attributes" minToolsVersion="5.1"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="eMU-z2-CzM" customClass="JSQDisplayedMessageCollectionViewCell">
<rect key="frame" x="0.0" y="0.0" width="320" height="70"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="320" height="70"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="qCf-bs-dBd" userLabel="textContainer">
<rect key="frame" x="0.0" y="22" width="320" height="48"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Info Message" textAlignment="center" lineBreakMode="wordWrap" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="OVa-Xw-5vl" customClass="JSQMessagesLabel">
<rect key="frame" x="8" y="12" width="304" height="28"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="14" id="fed-2c-dqd"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstItem="OVa-Xw-5vl" firstAttribute="leading" secondItem="qCf-bs-dBd" secondAttribute="leading" constant="8" id="2IE-8k-czI"/>
<constraint firstAttribute="bottom" secondItem="OVa-Xw-5vl" secondAttribute="bottom" constant="8" id="MtI-jW-t1x"/>
<constraint firstAttribute="trailing" secondItem="OVa-Xw-5vl" secondAttribute="trailing" constant="8" id="Y8z-8G-PLt"/>
<constraint firstItem="OVa-Xw-5vl" firstAttribute="top" secondItem="qCf-bs-dBd" secondAttribute="top" constant="12" id="v5B-tB-pOB"/>
</constraints>
</view>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="warning_white.png" translatesAutoresizingMaskIntoConstraints="NO" id="ePO-Cy-jUE">
<rect key="frame" x="143" y="0.0" width="35" height="35"/>
<constraints>
<constraint firstAttribute="height" constant="35" id="Llx-81-oyV"/>
<constraint firstAttribute="width" constant="35" id="Nth-3D-Wo9"/>
</constraints>
</imageView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</view>
<constraints>
<constraint firstItem="ePO-Cy-jUE" firstAttribute="top" secondItem="eMU-z2-CzM" secondAttribute="top" id="D28-BQ-Qam"/>
<constraint firstAttribute="trailing" secondItem="qCf-bs-dBd" secondAttribute="trailing" id="F3T-1l-nCg"/>
<constraint firstItem="qCf-bs-dBd" firstAttribute="leading" secondItem="eMU-z2-CzM" secondAttribute="leading" id="OzF-VM-85V"/>
<constraint firstAttribute="bottom" secondItem="qCf-bs-dBd" secondAttribute="bottom" id="PNq-zm-usq"/>
<constraint firstItem="qCf-bs-dBd" firstAttribute="top" secondItem="ePO-Cy-jUE" secondAttribute="centerY" constant="4" id="UzU-DS-8WZ"/>
<constraint firstItem="ePO-Cy-jUE" firstAttribute="centerX" secondItem="eMU-z2-CzM" secondAttribute="centerX" id="qtQ-mS-o6z"/>
</constraints>
<size key="customSize" width="320" height="55"/>
<connections>
<outlet property="cellLabel" destination="OVa-Xw-5vl" id="7PC-oj-dQZ"/>
<outlet property="headerImageView" destination="ePO-Cy-jUE" id="4uq-2C-V7U"/>
<outlet property="textContainer" destination="qCf-bs-dBd" id="fL7-LO-El1"/>
</connections>
<point key="canvasLocation" x="219" y="433"/>
</collectionViewCell>
</objects>
<resources>
<image name="warning_white.png" width="100" height="100"/>
</resources>
</document>
Loading…
Cancel
Save