From 4d320d60150f04a4e6234ca2a3b5ccca58a49356 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 8 Jul 2016 15:25:28 -0700 Subject: [PATCH] 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 --- Podfile | 2 +- Podfile.lock | 14 +- Signal.xcodeproj/project.pbxproj | 62 +++++ Signal/src/Models/JSQCall.h | 76 ++++++ Signal/src/Models/JSQCall.m | 170 +++++++++++++ Signal/src/Models/JSQDisplayedMessage.h | 45 ++++ Signal/src/Models/JSQDisplayedMessage.m | 44 ++++ Signal/src/Models/JSQErrorMessage.h | 36 +++ Signal/src/Models/JSQErrorMessage.m | 75 ++++++ Signal/src/Models/JSQInfoMessage.h | 31 +++ Signal/src/Models/JSQInfoMessage.m | 54 ++++ .../Models/OWSMessagesBubblesSizeCalculator.h | 13 + .../Models/OWSMessagesBubblesSizeCalculator.m | 45 ++++ .../TSMessageAdapaters/TSMessageAdapter.h | 10 + .../TSMessageAdapaters/TSMessageAdapter.m | 12 +- .../view controllers/MessagesViewController.m | 230 ++++++++++++++---- Signal/src/views/JSQCallCollectionViewCell.h | 31 +++ Signal/src/views/JSQCallCollectionViewCell.m | 63 +++++ .../src/views/JSQCallCollectionViewCell.xib | 61 +++++ .../JSQDisplayedMessageCollectionViewCell.h | 21 ++ .../JSQDisplayedMessageCollectionViewCell.m | 63 +++++ .../JSQDisplayedMessageCollectionViewCell.xib | 69 ++++++ 22 files changed, 1161 insertions(+), 66 deletions(-) create mode 100644 Signal/src/Models/JSQCall.h create mode 100644 Signal/src/Models/JSQCall.m create mode 100644 Signal/src/Models/JSQDisplayedMessage.h create mode 100644 Signal/src/Models/JSQDisplayedMessage.m create mode 100644 Signal/src/Models/JSQErrorMessage.h create mode 100644 Signal/src/Models/JSQErrorMessage.m create mode 100644 Signal/src/Models/JSQInfoMessage.h create mode 100644 Signal/src/Models/JSQInfoMessage.m create mode 100644 Signal/src/Models/OWSMessagesBubblesSizeCalculator.h create mode 100644 Signal/src/Models/OWSMessagesBubblesSizeCalculator.m create mode 100644 Signal/src/views/JSQCallCollectionViewCell.h create mode 100644 Signal/src/views/JSQCallCollectionViewCell.m create mode 100644 Signal/src/views/JSQCallCollectionViewCell.xib create mode 100644 Signal/src/views/JSQDisplayedMessageCollectionViewCell.h create mode 100644 Signal/src/views/JSQDisplayedMessageCollectionViewCell.m create mode 100644 Signal/src/views/JSQDisplayedMessageCollectionViewCell.xib diff --git a/Podfile b/Podfile index 0f195570a..aefcc93b4 100644 --- a/Podfile +++ b/Podfile @@ -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 diff --git a/Podfile.lock b/Podfile.lock index 22f0ddbbc..17f976764 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -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 diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 49efdecbb..74bc83e81 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -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 = ""; }; + 453D28AF1D32B87100D523F0 /* JSQErrorMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSQErrorMessage.h; sourceTree = ""; }; + 453D28B01D32B87100D523F0 /* JSQErrorMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSQErrorMessage.m; sourceTree = ""; }; + 453D28B11D32B87100D523F0 /* JSQInfoMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSQInfoMessage.h; sourceTree = ""; }; + 453D28B21D32B87100D523F0 /* JSQInfoMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSQInfoMessage.m; sourceTree = ""; }; + 453D28B51D32BA5F00D523F0 /* JSQDisplayedMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSQDisplayedMessage.h; sourceTree = ""; }; + 453D28B61D32BA5F00D523F0 /* JSQDisplayedMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSQDisplayedMessage.m; sourceTree = ""; }; + 453D28B81D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessagesBubblesSizeCalculator.h; sourceTree = ""; }; + 453D28B91D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessagesBubblesSizeCalculator.m; sourceTree = ""; }; 454B35071D08EED80026D658 /* mk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mk; path = translations/mk.lproj/Localizable.strings; sourceTree = ""; }; 45843D1D1D2236B30013E85A /* OWSContactsSearcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactsSearcher.h; sourceTree = ""; }; 45843D1E1D2236B30013E85A /* OWSContactsSearcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactsSearcher.m; sourceTree = ""; }; 45843D211D223BA10013E85A /* OWSContactsSearcherTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactsSearcherTest.m; sourceTree = ""; }; + 45C681B51D305A580050903A /* JSQCall.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSQCall.h; sourceTree = ""; }; + 45C681B61D305A580050903A /* JSQCall.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSQCall.m; sourceTree = ""; }; + 45C681B91D305C080050903A /* JSQCallCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSQCallCollectionViewCell.h; sourceTree = ""; }; + 45C681BA1D305C080050903A /* JSQCallCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSQCallCollectionViewCell.m; sourceTree = ""; }; + 45C681C01D305C9E0050903A /* JSQCallCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = JSQCallCollectionViewCell.xib; sourceTree = ""; }; + 45C681C11D305C9E0050903A /* JSQDisplayedMessageCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSQDisplayedMessageCollectionViewCell.h; sourceTree = ""; }; + 45C681C21D305C9E0050903A /* JSQDisplayedMessageCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSQDisplayedMessageCollectionViewCell.m; sourceTree = ""; }; + 45C681C31D305C9E0050903A /* JSQDisplayedMessageCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = JSQDisplayedMessageCollectionViewCell.xib; sourceTree = ""; }; 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 = ""; }; 45E282DF1D08E6CC00ADD4C8 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = translations/id.lproj/Localizable.strings; sourceTree = ""; }; @@ -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 = ""; @@ -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 */, diff --git a/Signal/src/Models/JSQCall.h b/Signal/src/Models/JSQCall.h new file mode 100644 index 000000000..5ceb21133 --- /dev/null +++ b/Signal/src/Models/JSQCall.h @@ -0,0 +1,76 @@ +// +// JSQCall.h +// JSQMessages +// +// Created by Dylan Bourgeois on 20/11/14. +// + +#import + +#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 + +/* + * 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 diff --git a/Signal/src/Models/JSQCall.m b/Signal/src/Models/JSQCall.m new file mode 100644 index 000000000..ba4f07a7b --- /dev/null +++ b/Signal/src/Models/JSQCall.m @@ -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 diff --git a/Signal/src/Models/JSQDisplayedMessage.h b/Signal/src/Models/JSQDisplayedMessage.h new file mode 100644 index 000000000..98df957b9 --- /dev/null +++ b/Signal/src/Models/JSQDisplayedMessage.h @@ -0,0 +1,45 @@ +// +// JSQDisplayedMessage.h +// JSQMessages +// +// Created by Dylan Bourgeois on 29/11/14. +// Copyright (c) 2014 Hexed Bits. All rights reserved. +// + +#import +#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 + +/* + * 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 diff --git a/Signal/src/Models/JSQDisplayedMessage.m b/Signal/src/Models/JSQDisplayedMessage.m new file mode 100644 index 000000000..cd92b0670 --- /dev/null +++ b/Signal/src/Models/JSQDisplayedMessage.m @@ -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 diff --git a/Signal/src/Models/JSQErrorMessage.h b/Signal/src/Models/JSQErrorMessage.h new file mode 100644 index 000000000..04430122a --- /dev/null +++ b/Signal/src/Models/JSQErrorMessage.h @@ -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 diff --git a/Signal/src/Models/JSQErrorMessage.m b/Signal/src/Models/JSQErrorMessage.m new file mode 100644 index 000000000..105c4cf68 --- /dev/null +++ b/Signal/src/Models/JSQErrorMessage.m @@ -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 diff --git a/Signal/src/Models/JSQInfoMessage.h b/Signal/src/Models/JSQInfoMessage.h new file mode 100644 index 000000000..ecb5c3453 --- /dev/null +++ b/Signal/src/Models/JSQInfoMessage.h @@ -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 diff --git a/Signal/src/Models/JSQInfoMessage.m b/Signal/src/Models/JSQInfoMessage.m new file mode 100644 index 000000000..20d27918d --- /dev/null +++ b/Signal/src/Models/JSQInfoMessage.m @@ -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 diff --git a/Signal/src/Models/OWSMessagesBubblesSizeCalculator.h b/Signal/src/Models/OWSMessagesBubblesSizeCalculator.h new file mode 100644 index 000000000..f1d4710be --- /dev/null +++ b/Signal/src/Models/OWSMessagesBubblesSizeCalculator.h @@ -0,0 +1,13 @@ +// +// OWSMessagesBubblesSizeCalculator.h +// Signal +// +// Created by Michael Kirk on 7/10/16. +// Copyright © 2016 Open Whisper Systems. All rights reserved. +// + +#import + +@interface OWSMessagesBubblesSizeCalculator : JSQMessagesBubblesSizeCalculator + +@end diff --git a/Signal/src/Models/OWSMessagesBubblesSizeCalculator.m b/Signal/src/Models/OWSMessagesBubblesSizeCalculator.m new file mode 100644 index 000000000..526209fce --- /dev/null +++ b/Signal/src/Models/OWSMessagesBubblesSizeCalculator.m @@ -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)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 diff --git a/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.h b/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.h index c1ccdefa0..d974d9223 100644 --- a/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.h +++ b/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.h @@ -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 + (id)messageViewDataWithInteraction:(TSInteraction *)interaction inThread:(TSThread *)thread; diff --git a/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.m b/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.m index 8a7c051c0..eb5021725 100644 --- a/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.m +++ b/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.m @@ -58,6 +58,7 @@ + (id)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 { diff --git a/Signal/src/view controllers/MessagesViewController.m b/Signal/src/view controllers/MessagesViewController.m index d2c1b145b..a889f7ce9 100644 --- a/Signal/src/view controllers/MessagesViewController.m +++ b/Signal/src/view controllers/MessagesViewController.m @@ -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)collectionView:(JSQMessagesCollectionView *)collectionView - messageBubbleImageDataForItemAtIndexPath:(NSIndexPath *)indexPath { - id 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)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)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)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]; } diff --git a/Signal/src/views/JSQCallCollectionViewCell.h b/Signal/src/views/JSQCallCollectionViewCell.h new file mode 100644 index 000000000..4aebfaa0b --- /dev/null +++ b/Signal/src/views/JSQCallCollectionViewCell.h @@ -0,0 +1,31 @@ +// +// JSQCallCollectionViewCell.h +// JSQMessages +// +// Created by Dylan Bourgeois on 20/11/14. +// + +#import + +#import + +#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 diff --git a/Signal/src/views/JSQCallCollectionViewCell.m b/Signal/src/views/JSQCallCollectionViewCell.m new file mode 100644 index 000000000..ee5ff3390 --- /dev/null +++ b/Signal/src/views/JSQCallCollectionViewCell.m @@ -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 diff --git a/Signal/src/views/JSQCallCollectionViewCell.xib b/Signal/src/views/JSQCallCollectionViewCell.xib new file mode 100644 index 000000000..8901d2a85 --- /dev/null +++ b/Signal/src/views/JSQCallCollectionViewCell.xib @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Signal/src/views/JSQDisplayedMessageCollectionViewCell.h b/Signal/src/views/JSQDisplayedMessageCollectionViewCell.h new file mode 100644 index 000000000..f8437eaf6 --- /dev/null +++ b/Signal/src/views/JSQDisplayedMessageCollectionViewCell.h @@ -0,0 +1,21 @@ +// +// JSQDisplayedMessageCollectionViewCell.h +// JSQMessages +// +// Created by Dylan Bourgeois on 29/11/14. +// Copyright (c) 2014 Hexed Bits. All rights reserved. +// + +#import +#import + +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 diff --git a/Signal/src/views/JSQDisplayedMessageCollectionViewCell.m b/Signal/src/views/JSQDisplayedMessageCollectionViewCell.m new file mode 100644 index 000000000..d3fda83d4 --- /dev/null +++ b/Signal/src/views/JSQDisplayedMessageCollectionViewCell.m @@ -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 + +@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 diff --git a/Signal/src/views/JSQDisplayedMessageCollectionViewCell.xib b/Signal/src/views/JSQDisplayedMessageCollectionViewCell.xib new file mode 100644 index 000000000..7b5ad5770 --- /dev/null +++ b/Signal/src/views/JSQDisplayedMessageCollectionViewCell.xib @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +