WIP Merge tag '2.19.4.4'
- restore video playback in fullscreen This was a large merge, so I'm opting to make some changes in separate commits.
This commit is contained in:
commit
a423fe8a0e
|
@ -42,7 +42,6 @@
|
||||||
344F248720069ECB00CFB4F4 /* ModalActivityIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344F248620069ECB00CFB4F4 /* ModalActivityIndicatorViewController.swift */; };
|
344F248720069ECB00CFB4F4 /* ModalActivityIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344F248620069ECB00CFB4F4 /* ModalActivityIndicatorViewController.swift */; };
|
||||||
344F248A20069F0600CFB4F4 /* ViewControllerUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 344F248820069F0600CFB4F4 /* ViewControllerUtils.h */; };
|
344F248A20069F0600CFB4F4 /* ViewControllerUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 344F248820069F0600CFB4F4 /* ViewControllerUtils.h */; };
|
||||||
344F248B20069F0600CFB4F4 /* ViewControllerUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 344F248920069F0600CFB4F4 /* ViewControllerUtils.m */; };
|
344F248B20069F0600CFB4F4 /* ViewControllerUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 344F248920069F0600CFB4F4 /* ViewControllerUtils.m */; };
|
||||||
3456D2C41FFFCC70001EA55D /* OWSBackupViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3456D2C21FFFCC6F001EA55D /* OWSBackupViewController.m */; };
|
|
||||||
3461284B1FD0B94000532771 /* SAELoadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3461284A1FD0B93F00532771 /* SAELoadViewController.swift */; };
|
3461284B1FD0B94000532771 /* SAELoadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3461284A1FD0B93F00532771 /* SAELoadViewController.swift */; };
|
||||||
346129341FD1A88700532771 /* OWSSwiftUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346129331FD1A88700532771 /* OWSSwiftUtils.swift */; };
|
346129341FD1A88700532771 /* OWSSwiftUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346129331FD1A88700532771 /* OWSSwiftUtils.swift */; };
|
||||||
346129391FD1B47300532771 /* OWSPreferences.h in Headers */ = {isa = PBXBuildFile; fileRef = 346129371FD1B47200532771 /* OWSPreferences.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
346129391FD1B47300532771 /* OWSPreferences.h in Headers */ = {isa = PBXBuildFile; fileRef = 346129371FD1B47200532771 /* OWSPreferences.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
@ -194,8 +193,6 @@
|
||||||
34FD93701E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 34FD936F1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m */; };
|
34FD93701E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 34FD936F1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m */; };
|
||||||
4505C2BF1E648EA300CEBF41 /* ExperienceUpgrade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4505C2BE1E648EA300CEBF41 /* ExperienceUpgrade.swift */; };
|
4505C2BF1E648EA300CEBF41 /* ExperienceUpgrade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4505C2BE1E648EA300CEBF41 /* ExperienceUpgrade.swift */; };
|
||||||
450998651FD8A34D00D89EB3 /* DeviceSleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348F2EAD1F0D21BC00D4ECE0 /* DeviceSleepManager.swift */; };
|
450998651FD8A34D00D89EB3 /* DeviceSleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348F2EAD1F0D21BC00D4ECE0 /* DeviceSleepManager.swift */; };
|
||||||
450998661FD8BD9C00D89EB3 /* FullImageViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8481E8DF1700035BE1A /* FullImageViewController.m */; };
|
|
||||||
450998671FD8BDA600D89EB3 /* FullImageViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 34B3F8471E8DF1700035BE1A /* FullImageViewController.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
|
||||||
450998681FD8C0FF00D89EB3 /* AttachmentSharing.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F83A1E8DF1700035BE1A /* AttachmentSharing.m */; };
|
450998681FD8C0FF00D89EB3 /* AttachmentSharing.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F83A1E8DF1700035BE1A /* AttachmentSharing.m */; };
|
||||||
450998691FD8C10200D89EB3 /* AttachmentSharing.h in Headers */ = {isa = PBXBuildFile; fileRef = 34B3F8391E8DF1700035BE1A /* AttachmentSharing.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
450998691FD8C10200D89EB3 /* AttachmentSharing.h in Headers */ = {isa = PBXBuildFile; fileRef = 34B3F8391E8DF1700035BE1A /* AttachmentSharing.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
4509E79A1DD653700025A59F /* WebRTC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4509E7991DD653700025A59F /* WebRTC.framework */; };
|
4509E79A1DD653700025A59F /* WebRTC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4509E7991DD653700025A59F /* WebRTC.framework */; };
|
||||||
|
@ -250,6 +247,7 @@
|
||||||
452D1EE81DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452D1EE71DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift */; };
|
452D1EE81DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452D1EE71DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift */; };
|
||||||
452EA09E1EA7ABE00078744B /* AttachmentPointerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */; };
|
452EA09E1EA7ABE00078744B /* AttachmentPointerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */; };
|
||||||
452ECA4D1E087E7200E2F016 /* MessageFetcherJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */; };
|
452ECA4D1E087E7200E2F016 /* MessageFetcherJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */; };
|
||||||
|
453034AB200289F50018945D /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 453034AA200289F50018945D /* VideoPlayerView.swift */; };
|
||||||
4535186B1FC635DD00210559 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4535186A1FC635DD00210559 /* ShareViewController.swift */; };
|
4535186B1FC635DD00210559 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4535186A1FC635DD00210559 /* ShareViewController.swift */; };
|
||||||
4535186E1FC635DD00210559 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4535186C1FC635DD00210559 /* MainInterface.storyboard */; };
|
4535186E1FC635DD00210559 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4535186C1FC635DD00210559 /* MainInterface.storyboard */; };
|
||||||
453518721FC635DD00210559 /* SignalShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 453518681FC635DD00210559 /* SignalShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
453518721FC635DD00210559 /* SignalShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 453518681FC635DD00210559 /* SignalShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
|
@ -290,6 +288,7 @@
|
||||||
45A663C51F92EC760027B59E /* GroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A663C41F92EC760027B59E /* GroupTableViewCell.swift */; };
|
45A663C51F92EC760027B59E /* GroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A663C41F92EC760027B59E /* GroupTableViewCell.swift */; };
|
||||||
45A6DAD61EBBF85500893231 /* ReminderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A6DAD51EBBF85500893231 /* ReminderView.swift */; };
|
45A6DAD61EBBF85500893231 /* ReminderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A6DAD51EBBF85500893231 /* ReminderView.swift */; };
|
||||||
45AE48511E0732D6004D96C2 /* TurnServerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45AE48501E0732D6004D96C2 /* TurnServerInfo.swift */; };
|
45AE48511E0732D6004D96C2 /* TurnServerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45AE48501E0732D6004D96C2 /* TurnServerInfo.swift */; };
|
||||||
|
45B9EE9C200E91FB005D2F2D /* MediaDetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 45B9EE9B200E91FB005D2F2D /* MediaDetailViewController.m */; };
|
||||||
45BB93381E688E14001E3939 /* UIDevice+featureSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45BB93371E688E14001E3939 /* UIDevice+featureSupport.swift */; };
|
45BB93381E688E14001E3939 /* UIDevice+featureSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45BB93371E688E14001E3939 /* UIDevice+featureSupport.swift */; };
|
||||||
45BC829D1FD9C4B400011CF3 /* ShareViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45BC829C1FD9C4B400011CF3 /* ShareViewDelegate.swift */; };
|
45BC829D1FD9C4B400011CF3 /* ShareViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45BC829C1FD9C4B400011CF3 /* ShareViewDelegate.swift */; };
|
||||||
45BD60821DE9547E00A8F436 /* Contacts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 45BD60811DE9547E00A8F436 /* Contacts.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
|
45BD60821DE9547E00A8F436 /* Contacts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 45BD60811DE9547E00A8F436 /* Contacts.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
|
||||||
|
@ -630,8 +629,6 @@
|
||||||
34B3F8441E8DF1700035BE1A /* ExperienceUpgradesPageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExperienceUpgradesPageViewController.swift; sourceTree = "<group>"; };
|
34B3F8441E8DF1700035BE1A /* ExperienceUpgradesPageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExperienceUpgradesPageViewController.swift; sourceTree = "<group>"; };
|
||||||
34B3F8451E8DF1700035BE1A /* FingerprintViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FingerprintViewController.h; sourceTree = "<group>"; };
|
34B3F8451E8DF1700035BE1A /* FingerprintViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FingerprintViewController.h; sourceTree = "<group>"; };
|
||||||
34B3F8461E8DF1700035BE1A /* FingerprintViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FingerprintViewController.m; sourceTree = "<group>"; };
|
34B3F8461E8DF1700035BE1A /* FingerprintViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FingerprintViewController.m; sourceTree = "<group>"; };
|
||||||
34B3F8471E8DF1700035BE1A /* FullImageViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FullImageViewController.h; sourceTree = "<group>"; };
|
|
||||||
34B3F8481E8DF1700035BE1A /* FullImageViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FullImageViewController.m; sourceTree = "<group>"; };
|
|
||||||
34B3F8491E8DF1700035BE1A /* InboxTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InboxTableViewCell.h; sourceTree = "<group>"; };
|
34B3F8491E8DF1700035BE1A /* InboxTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InboxTableViewCell.h; sourceTree = "<group>"; };
|
||||||
34B3F84A1E8DF1700035BE1A /* InboxTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InboxTableViewCell.m; sourceTree = "<group>"; };
|
34B3F84A1E8DF1700035BE1A /* InboxTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InboxTableViewCell.m; sourceTree = "<group>"; };
|
||||||
34B3F84C1E8DF1700035BE1A /* InviteFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InviteFlow.swift; sourceTree = "<group>"; };
|
34B3F84C1E8DF1700035BE1A /* InviteFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InviteFlow.swift; sourceTree = "<group>"; };
|
||||||
|
@ -776,6 +773,7 @@
|
||||||
452D1EE71DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MesssagesBubblesSizeCalculatorTest.swift; path = Models/MesssagesBubblesSizeCalculatorTest.swift; sourceTree = "<group>"; };
|
452D1EE71DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MesssagesBubblesSizeCalculatorTest.swift; path = Models/MesssagesBubblesSizeCalculatorTest.swift; sourceTree = "<group>"; };
|
||||||
452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentPointerView.swift; sourceTree = "<group>"; };
|
452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentPointerView.swift; sourceTree = "<group>"; };
|
||||||
452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageFetcherJob.swift; sourceTree = "<group>"; };
|
452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageFetcherJob.swift; sourceTree = "<group>"; };
|
||||||
|
453034AA200289F50018945D /* VideoPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = "<group>"; };
|
||||||
453518681FC635DD00210559 /* SignalShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SignalShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
453518681FC635DD00210559 /* SignalShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SignalShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
4535186A1FC635DD00210559 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
4535186A1FC635DD00210559 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||||
4535186D1FC635DD00210559 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
|
4535186D1FC635DD00210559 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
|
||||||
|
@ -828,6 +826,8 @@
|
||||||
45A6DAD51EBBF85500893231 /* ReminderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReminderView.swift; sourceTree = "<group>"; };
|
45A6DAD51EBBF85500893231 /* ReminderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReminderView.swift; sourceTree = "<group>"; };
|
||||||
45AE48501E0732D6004D96C2 /* TurnServerInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TurnServerInfo.swift; sourceTree = "<group>"; };
|
45AE48501E0732D6004D96C2 /* TurnServerInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TurnServerInfo.swift; sourceTree = "<group>"; };
|
||||||
45B201741DAECBFD00C461E0 /* Signal-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Signal-Bridging-Header.h"; sourceTree = "<group>"; };
|
45B201741DAECBFD00C461E0 /* Signal-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Signal-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
|
45B9EE9A200E91FB005D2F2D /* MediaDetailViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MediaDetailViewController.h; sourceTree = "<group>"; };
|
||||||
|
45B9EE9B200E91FB005D2F2D /* MediaDetailViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MediaDetailViewController.m; sourceTree = "<group>"; };
|
||||||
45BB93371E688E14001E3939 /* UIDevice+featureSupport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIDevice+featureSupport.swift"; sourceTree = "<group>"; };
|
45BB93371E688E14001E3939 /* UIDevice+featureSupport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIDevice+featureSupport.swift"; sourceTree = "<group>"; };
|
||||||
45BC829C1FD9C4B400011CF3 /* ShareViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewDelegate.swift; sourceTree = "<group>"; };
|
45BC829C1FD9C4B400011CF3 /* ShareViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewDelegate.swift; sourceTree = "<group>"; };
|
||||||
45BD60811DE9547E00A8F436 /* Contacts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Contacts.framework; path = System/Library/Frameworks/Contacts.framework; sourceTree = SDKROOT; };
|
45BD60811DE9547E00A8F436 /* Contacts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Contacts.framework; path = System/Library/Frameworks/Contacts.framework; sourceTree = SDKROOT; };
|
||||||
|
@ -1319,6 +1319,8 @@
|
||||||
34B3F8491E8DF1700035BE1A /* InboxTableViewCell.h */,
|
34B3F8491E8DF1700035BE1A /* InboxTableViewCell.h */,
|
||||||
34B3F84A1E8DF1700035BE1A /* InboxTableViewCell.m */,
|
34B3F84A1E8DF1700035BE1A /* InboxTableViewCell.m */,
|
||||||
34B3F84C1E8DF1700035BE1A /* InviteFlow.swift */,
|
34B3F84C1E8DF1700035BE1A /* InviteFlow.swift */,
|
||||||
|
45B9EE9A200E91FB005D2F2D /* MediaDetailViewController.h */,
|
||||||
|
45B9EE9B200E91FB005D2F2D /* MediaDetailViewController.m */,
|
||||||
34CA1C261F7156F300E51C51 /* MessageDetailViewController.swift */,
|
34CA1C261F7156F300E51C51 /* MessageDetailViewController.swift */,
|
||||||
34B3F84F1E8DF1700035BE1A /* NewContactThreadViewController.h */,
|
34B3F84F1E8DF1700035BE1A /* NewContactThreadViewController.h */,
|
||||||
34B3F8501E8DF1700035BE1A /* NewContactThreadViewController.m */,
|
34B3F8501E8DF1700035BE1A /* NewContactThreadViewController.m */,
|
||||||
|
@ -1561,8 +1563,6 @@
|
||||||
children = (
|
children = (
|
||||||
34B3F8391E8DF1700035BE1A /* AttachmentSharing.h */,
|
34B3F8391E8DF1700035BE1A /* AttachmentSharing.h */,
|
||||||
34B3F83A1E8DF1700035BE1A /* AttachmentSharing.m */,
|
34B3F83A1E8DF1700035BE1A /* AttachmentSharing.m */,
|
||||||
34B3F8471E8DF1700035BE1A /* FullImageViewController.h */,
|
|
||||||
34B3F8481E8DF1700035BE1A /* FullImageViewController.m */,
|
|
||||||
45E5471F1FD755E700DFC09E /* AttachmentApprovalViewController.swift */,
|
45E5471F1FD755E700DFC09E /* AttachmentApprovalViewController.swift */,
|
||||||
3400C7901EAF89CD008A8584 /* SharingThreadPickerViewController.h */,
|
3400C7901EAF89CD008A8584 /* SharingThreadPickerViewController.h */,
|
||||||
3400C7911EAF89CD008A8584 /* SharingThreadPickerViewController.m */,
|
3400C7911EAF89CD008A8584 /* SharingThreadPickerViewController.m */,
|
||||||
|
@ -1748,6 +1748,7 @@
|
||||||
76EB052B18170B33006006FC /* Views */ = {
|
76EB052B18170B33006006FC /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
453034AA200289F50018945D /* VideoPlayerView.swift */,
|
||||||
45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */,
|
45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */,
|
||||||
452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */,
|
452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */,
|
||||||
34E3E5671EC4B19400495BAC /* AudioProgressView.swift */,
|
34E3E5671EC4B19400495BAC /* AudioProgressView.swift */,
|
||||||
|
@ -2071,7 +2072,6 @@
|
||||||
346129391FD1B47300532771 /* OWSPreferences.h in Headers */,
|
346129391FD1B47300532771 /* OWSPreferences.h in Headers */,
|
||||||
344D6CED20069E070042AF96 /* NewNonContactConversationViewController.h in Headers */,
|
344D6CED20069E070042AF96 /* NewNonContactConversationViewController.h in Headers */,
|
||||||
346129DE1FD5C02A00532771 /* LockInteractionController.h in Headers */,
|
346129DE1FD5C02A00532771 /* LockInteractionController.h in Headers */,
|
||||||
450998671FD8BDA600D89EB3 /* FullImageViewController.h in Headers */,
|
|
||||||
451F8A451FD71570005CB9DA /* BlockListUIUtils.h in Headers */,
|
451F8A451FD71570005CB9DA /* BlockListUIUtils.h in Headers */,
|
||||||
451F8A4A1FD715D9005CB9DA /* OWSContactAvatarBuilder.h in Headers */,
|
451F8A4A1FD715D9005CB9DA /* OWSContactAvatarBuilder.h in Headers */,
|
||||||
34480B5B1FD0A7E300BC14EF /* SignalMessaging-Prefix.pch in Headers */,
|
34480B5B1FD0A7E300BC14EF /* SignalMessaging-Prefix.pch in Headers */,
|
||||||
|
@ -2775,7 +2775,6 @@
|
||||||
344F248720069ECB00CFB4F4 /* ModalActivityIndicatorViewController.swift in Sources */,
|
344F248720069ECB00CFB4F4 /* ModalActivityIndicatorViewController.swift in Sources */,
|
||||||
346129961FD1E30000532771 /* OWSDatabaseMigration.m in Sources */,
|
346129961FD1E30000532771 /* OWSDatabaseMigration.m in Sources */,
|
||||||
346129CD1FD2072E00532771 /* UIImage+OWS.m in Sources */,
|
346129CD1FD2072E00532771 /* UIImage+OWS.m in Sources */,
|
||||||
450998661FD8BD9C00D89EB3 /* FullImageViewController.m in Sources */,
|
|
||||||
344D6CEC20069E070042AF96 /* NewNonContactConversationViewController.m in Sources */,
|
344D6CEC20069E070042AF96 /* NewNonContactConversationViewController.m in Sources */,
|
||||||
346129FB1FD5F31400532771 /* OWS101ExistingUsersBlockOnIdentityChange.m in Sources */,
|
346129FB1FD5F31400532771 /* OWS101ExistingUsersBlockOnIdentityChange.m in Sources */,
|
||||||
450998651FD8A34D00D89EB3 /* DeviceSleepManager.swift in Sources */,
|
450998651FD8A34D00D89EB3 /* DeviceSleepManager.swift in Sources */,
|
||||||
|
@ -2926,6 +2925,7 @@
|
||||||
34B3F8771E8DF1700035BE1A /* ContactsPicker.swift in Sources */,
|
34B3F8771E8DF1700035BE1A /* ContactsPicker.swift in Sources */,
|
||||||
45C0DC1B1E68FE9000E04C47 /* UIApplication+OWS.swift in Sources */,
|
45C0DC1B1E68FE9000E04C47 /* UIApplication+OWS.swift in Sources */,
|
||||||
45638BDF1F3DDB2200128435 /* MessageSender+Promise.swift in Sources */,
|
45638BDF1F3DDB2200128435 /* MessageSender+Promise.swift in Sources */,
|
||||||
|
453034AB200289F50018945D /* VideoPlayerView.swift in Sources */,
|
||||||
45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */,
|
45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */,
|
||||||
34B3F8911E8DF1710035BE1A /* ShowGroupMembersViewController.m in Sources */,
|
34B3F8911E8DF1710035BE1A /* ShowGroupMembersViewController.m in Sources */,
|
||||||
4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */,
|
4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */,
|
||||||
|
@ -2940,6 +2940,7 @@
|
||||||
34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */,
|
34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */,
|
||||||
45F659731E1BD99C00444429 /* CallKitCallUIAdaptee.swift in Sources */,
|
45F659731E1BD99C00444429 /* CallKitCallUIAdaptee.swift in Sources */,
|
||||||
45BB93381E688E14001E3939 /* UIDevice+featureSupport.swift in Sources */,
|
45BB93381E688E14001E3939 /* UIDevice+featureSupport.swift in Sources */,
|
||||||
|
45B9EE9C200E91FB005D2F2D /* MediaDetailViewController.m in Sources */,
|
||||||
458DE9D61DEE3FD00071BB03 /* PeerConnectionClient.swift in Sources */,
|
458DE9D61DEE3FD00071BB03 /* PeerConnectionClient.swift in Sources */,
|
||||||
34D1F0841F8678AA0066283D /* ConversationInputToolbar.m in Sources */,
|
34D1F0841F8678AA0066283D /* ConversationInputToolbar.m in Sources */,
|
||||||
340CB2271EAC25820001CAA1 /* UpdateGroupViewController.m in Sources */,
|
340CB2271EAC25820001CAA1 /* UpdateGroupViewController.m in Sources */,
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "VideoPlayer_Slider_Thumb_15x15_@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "VideoPlayer_Slider_Thumb_15x15_@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "VideoPlayer_Slider_Thumb_15x15_@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
BIN
Signal/Images.xcassets/sliderProgressThumb.imageset/VideoPlayer_Slider_Thumb_15x15_@1x.png
vendored
Normal file
BIN
Signal/Images.xcassets/sliderProgressThumb.imageset/VideoPlayer_Slider_Thumb_15x15_@1x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
BIN
Signal/Images.xcassets/sliderProgressThumb.imageset/VideoPlayer_Slider_Thumb_15x15_@2x.png
vendored
Normal file
BIN
Signal/Images.xcassets/sliderProgressThumb.imageset/VideoPlayer_Slider_Thumb_15x15_@2x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
BIN
Signal/Images.xcassets/sliderProgressThumb.imageset/VideoPlayer_Slider_Thumb_15x15_@3x.png
vendored
Normal file
BIN
Signal/Images.xcassets/sliderProgressThumb.imageset/VideoPlayer_Slider_Thumb_15x15_@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
|
@ -11,7 +11,7 @@
|
||||||
#import "DateUtil.h"
|
#import "DateUtil.h"
|
||||||
#import "DebugUIPage.h"
|
#import "DebugUIPage.h"
|
||||||
#import "FingerprintViewController.h"
|
#import "FingerprintViewController.h"
|
||||||
#import "FullImageViewController.h"
|
#import "MediaDetailViewController.h"
|
||||||
#import "HomeViewController.h"
|
#import "HomeViewController.h"
|
||||||
#import "NotificationsManager.h"
|
#import "NotificationsManager.h"
|
||||||
#import "OWSAnyTouchGestureRecognizer.h"
|
#import "OWSAnyTouchGestureRecognizer.h"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
@ -18,7 +18,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
- (void)didTapImageViewItem:(ConversationViewItem *)viewItem
|
- (void)didTapImageViewItem:(ConversationViewItem *)viewItem
|
||||||
attachmentStream:(TSAttachmentStream *)attachmentStream
|
attachmentStream:(TSAttachmentStream *)attachmentStream
|
||||||
imageView:(UIView *)imageView;
|
imageView:(UIView *)imageView;
|
||||||
- (void)didTapVideoViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream;
|
- (void)didTapVideoViewItem:(ConversationViewItem *)viewItem
|
||||||
|
attachmentStream:(TSAttachmentStream *)attachmentStream
|
||||||
|
imageView:(UIView *)imageView;
|
||||||
- (void)didTapAudioViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream;
|
- (void)didTapAudioViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream;
|
||||||
- (void)didTapTruncatedTextMessage:(ConversationViewItem *)conversationItem;
|
- (void)didTapTruncatedTextMessage:(ConversationViewItem *)conversationItem;
|
||||||
- (void)didTapFailedIncomingAttachment:(ConversationViewItem *)viewItem
|
- (void)didTapFailedIncomingAttachment:(ConversationViewItem *)viewItem
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
//
|
//
|
||||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import "ConversationViewCell.h"
|
#import "ConversationViewCell.h"
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
extern const CGFloat OWSMessageCellCornerRadius;
|
||||||
|
|
||||||
@interface OWSMessageCell : ConversationViewCell
|
@interface OWSMessageCell : ConversationViewCell
|
||||||
|
|
||||||
+ (NSString *)cellReuseIdentifier;
|
+ (NSString *)cellReuseIdentifier;
|
||||||
|
|
|
@ -19,6 +19,9 @@
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
// This approximates the curve of our message bubbles, which makes the animation feel a little smoother.
|
||||||
|
const CGFloat OWSMessageCellCornerRadius = 17;
|
||||||
|
|
||||||
@interface BubbleMaskingView : UIView
|
@interface BubbleMaskingView : UIView
|
||||||
|
|
||||||
@property (nonatomic) BOOL isOutgoing;
|
@property (nonatomic) BOOL isOutgoing;
|
||||||
|
@ -1313,7 +1316,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
[self.delegate didTapAudioViewItem:self.viewItem attachmentStream:self.attachmentStream];
|
[self.delegate didTapAudioViewItem:self.viewItem attachmentStream:self.attachmentStream];
|
||||||
return;
|
return;
|
||||||
case OWSMessageCellType_Video:
|
case OWSMessageCellType_Video:
|
||||||
[self.delegate didTapVideoViewItem:self.viewItem attachmentStream:self.attachmentStream];
|
[self.delegate didTapVideoViewItem:self.viewItem
|
||||||
|
attachmentStream:self.attachmentStream
|
||||||
|
imageView:self.stillImageView];
|
||||||
return;
|
return;
|
||||||
case OWSMessageCellType_GenericAttachment:
|
case OWSMessageCellType_GenericAttachment:
|
||||||
#ifdef DEBUG
|
#ifdef DEBUG
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
@ -46,8 +46,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
- (NSString *)messageText;
|
- (NSString *)messageText;
|
||||||
- (void)setMessageText:(NSString *_Nullable)value;
|
- (void)setMessageText:(NSString *_Nullable)value;
|
||||||
- (void)clearTextMessage;
|
- (void)clearTextMessage;
|
||||||
|
- (void)toggleDefaultKeyboard;
|
||||||
- (nullable NSString *)textInputPrimaryLanguage;
|
|
||||||
|
|
||||||
- (void)updateFontSizes;
|
- (void)updateFontSizes;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import "ConversationInputToolbar.h"
|
#import "ConversationInputToolbar.h"
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
static void *kConversationInputTextViewObservingContext = &kConversationInputTextViewObservingContext;
|
static void *kConversationInputTextViewObservingContext = &kConversationInputTextViewObservingContext;
|
||||||
|
static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5;
|
||||||
@interface ConversationInputToolbar () <UIGestureRecognizerDelegate, ConversationTextViewToolbarDelegate>
|
@interface ConversationInputToolbar () <UIGestureRecognizerDelegate, ConversationTextViewToolbarDelegate>
|
||||||
|
|
||||||
@property (nonatomic, readonly) UIView *contentView;
|
@property (nonatomic, readonly) UIView *contentView;
|
||||||
|
@ -31,6 +31,8 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
|
||||||
|
|
||||||
@property (nonatomic) NSArray<NSLayoutConstraint *> *contentContraints;
|
@property (nonatomic) NSArray<NSLayoutConstraint *> *contentContraints;
|
||||||
@property (nonatomic) NSValue *lastTextContentSize;
|
@property (nonatomic) NSValue *lastTextContentSize;
|
||||||
|
@property (nonatomic) CGFloat toolbarHeight;
|
||||||
|
@property (nonatomic) CGFloat textViewHeight;
|
||||||
|
|
||||||
#pragma mark - Voice Memo Recording UI
|
#pragma mark - Voice Memo Recording UI
|
||||||
|
|
||||||
|
@ -69,18 +71,25 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
|
||||||
[self removeKVOObservers];
|
[self removeKVOObservers];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (CGSize)intrinsicContentSize
|
||||||
|
{
|
||||||
|
CGSize newSize = CGSizeMake(self.bounds.size.width, self.toolbarHeight + ConversationInputToolbarBorderViewHeight);
|
||||||
|
return newSize;
|
||||||
|
}
|
||||||
|
|
||||||
- (void)createContents
|
- (void)createContents
|
||||||
{
|
{
|
||||||
self.layoutMargins = UIEdgeInsetsZero;
|
self.layoutMargins = UIEdgeInsetsZero;
|
||||||
|
|
||||||
self.backgroundColor = [UIColor ows_inputToolbarBackgroundColor];
|
self.backgroundColor = [UIColor ows_inputToolbarBackgroundColor];
|
||||||
|
self.autoresizingMask = UIViewAutoresizingFlexibleHeight;
|
||||||
|
|
||||||
UIView *borderView = [UIView new];
|
UIView *borderView = [UIView new];
|
||||||
borderView.backgroundColor = [UIColor colorWithWhite:238 / 255.f alpha:1.f];
|
borderView.backgroundColor = [UIColor colorWithWhite:238 / 255.f alpha:1.f];
|
||||||
[self addSubview:borderView];
|
[self addSubview:borderView];
|
||||||
[borderView autoPinWidthToSuperview];
|
[borderView autoPinWidthToSuperview];
|
||||||
[borderView autoPinEdgeToSuperviewEdge:ALEdgeTop];
|
[borderView autoPinEdgeToSuperviewEdge:ALEdgeTop];
|
||||||
[borderView autoSetDimension:ALDimensionHeight toSize:0.5f];
|
[borderView autoSetDimension:ALDimensionHeight toSize:ConversationInputToolbarBorderViewHeight];
|
||||||
|
|
||||||
_contentView = [UIView containerView];
|
_contentView = [UIView containerView];
|
||||||
[self addSubview:self.contentView];
|
[self addSubview:self.contentView];
|
||||||
|
@ -190,6 +199,26 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
|
||||||
[self.inputTextView.undoManager removeAllActions];
|
[self.inputTextView.undoManager removeAllActions];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)toggleDefaultKeyboard
|
||||||
|
{
|
||||||
|
// Primary language is nil for the emoji keyboard.
|
||||||
|
if (!self.inputTextView.textInputMode.primaryLanguage) {
|
||||||
|
// Stay on emoji keyboard after sending
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we want to toggle back to default keyboard if the user had the numeric keyboard present.
|
||||||
|
|
||||||
|
// Momentarily switch to a non-default keyboard, else reloadInputViews
|
||||||
|
// will not affect the displayed keyboard. In practice this isn't perceptable to the user.
|
||||||
|
// The alternative would be to dismiss-and-pop the keyboard, but that can cause a more pronounced animation.
|
||||||
|
self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
|
||||||
|
[self.inputTextView reloadInputViews];
|
||||||
|
|
||||||
|
self.inputTextView.keyboardType = UIKeyboardTypeDefault;
|
||||||
|
[self.inputTextView reloadInputViews];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)setShouldShowVoiceMemoButton:(BOOL)shouldShowVoiceMemoButton
|
- (void)setShouldShowVoiceMemoButton:(BOOL)shouldShowVoiceMemoButton
|
||||||
{
|
{
|
||||||
if (_shouldShowVoiceMemoButton == shouldShowVoiceMemoButton) {
|
if (_shouldShowVoiceMemoButton == shouldShowVoiceMemoButton) {
|
||||||
|
@ -223,12 +252,16 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
|
||||||
const CGFloat kMinTextViewHeight = ceil(self.inputTextView.font.lineHeight
|
const CGFloat kMinTextViewHeight = ceil(self.inputTextView.font.lineHeight
|
||||||
+ self.inputTextView.textContainerInset.top + self.inputTextView.textContainerInset.bottom
|
+ self.inputTextView.textContainerInset.top + self.inputTextView.textContainerInset.bottom
|
||||||
+ self.inputTextView.contentInset.top + self.inputTextView.contentInset.bottom);
|
+ self.inputTextView.contentInset.top + self.inputTextView.contentInset.bottom);
|
||||||
const CGFloat kMaxTextViewHeight = 100.f;
|
// Exactly 4 lines of text with default sizing.
|
||||||
|
const CGFloat kMaxTextViewHeight = 98.f;
|
||||||
const CGFloat textViewDesiredHeight = (self.inputTextView.contentSize.height + self.inputTextView.contentInset.top
|
const CGFloat textViewDesiredHeight = (self.inputTextView.contentSize.height + self.inputTextView.contentInset.top
|
||||||
+ self.inputTextView.contentInset.bottom);
|
+ self.inputTextView.contentInset.bottom);
|
||||||
const CGFloat textViewHeight = ceil(Clamp(textViewDesiredHeight, kMinTextViewHeight, kMaxTextViewHeight));
|
const CGFloat textViewHeight = ceil(Clamp(textViewDesiredHeight, kMinTextViewHeight, kMaxTextViewHeight));
|
||||||
const CGFloat kMinContentHeight = kMinTextViewHeight + textViewVInset * 2;
|
const CGFloat kMinContentHeight = kMinTextViewHeight + textViewVInset * 2;
|
||||||
|
|
||||||
|
self.textViewHeight = textViewHeight;
|
||||||
|
self.toolbarHeight = textViewHeight + textViewVInset * 2;
|
||||||
|
|
||||||
if (self.attachmentToApprove) {
|
if (self.attachmentToApprove) {
|
||||||
OWSAssert(self.attachmentView);
|
OWSAssert(self.attachmentView);
|
||||||
|
|
||||||
|
@ -247,14 +280,14 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
|
||||||
|
|
||||||
self.contentContraints = @[
|
self.contentContraints = @[
|
||||||
[self.attachmentView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:textViewVInset],
|
[self.attachmentView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:textViewVInset],
|
||||||
[self.attachmentView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:textViewVInset],
|
[self.attachmentView autoPinBottomToSuperviewWithMargin:textViewVInset],
|
||||||
[self.attachmentView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:contentHInset],
|
[self.attachmentView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:contentHInset],
|
||||||
[self.attachmentView autoSetDimension:ALDimensionHeight toSize:150.f],
|
[self.attachmentView autoSetDimension:ALDimensionHeight toSize:150.f],
|
||||||
|
|
||||||
[self.rightButtonWrapper autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.attachmentView],
|
[self.rightButtonWrapper autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.attachmentView],
|
||||||
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeRight],
|
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeRight],
|
||||||
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeTop],
|
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeTop],
|
||||||
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeBottom],
|
[self.rightButtonWrapper autoPinBottomToSuperviewWithMargin:0],
|
||||||
|
|
||||||
[rightButton autoSetDimension:ALDimensionHeight toSize:kMinContentHeight],
|
[rightButton autoSetDimension:ALDimensionHeight toSize:kMinContentHeight],
|
||||||
[rightButton autoPinLeadingToSuperviewWithMargin:contentHSpacing],
|
[rightButton autoPinLeadingToSuperviewWithMargin:contentHSpacing],
|
||||||
|
@ -316,7 +349,7 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
|
||||||
self.contentContraints = @[
|
self.contentContraints = @[
|
||||||
[self.leftButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeLeft],
|
[self.leftButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeLeft],
|
||||||
[self.leftButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeTop],
|
[self.leftButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeTop],
|
||||||
[self.leftButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeBottom],
|
[self.leftButtonWrapper autoPinBottomToSuperviewWithMargin:0],
|
||||||
|
|
||||||
[leftButton autoSetDimension:ALDimensionHeight toSize:kMinContentHeight],
|
[leftButton autoSetDimension:ALDimensionHeight toSize:kMinContentHeight],
|
||||||
[leftButton autoPinLeadingToSuperviewWithMargin:contentHInset],
|
[leftButton autoPinLeadingToSuperviewWithMargin:contentHInset],
|
||||||
|
@ -325,18 +358,18 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
|
||||||
|
|
||||||
[self.inputTextView autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.leftButtonWrapper],
|
[self.inputTextView autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.leftButtonWrapper],
|
||||||
[self.inputTextView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:textViewVInset],
|
[self.inputTextView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:textViewVInset],
|
||||||
[self.inputTextView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:textViewVInset],
|
[self.inputTextView autoPinBottomToSuperviewWithMargin:textViewVInset],
|
||||||
[self.inputTextView autoSetDimension:ALDimensionHeight toSize:textViewHeight],
|
[self.inputTextView autoSetDimension:ALDimensionHeight toSize:textViewHeight],
|
||||||
|
|
||||||
[self.rightButtonWrapper autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.inputTextView],
|
[self.rightButtonWrapper autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.inputTextView],
|
||||||
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeRight],
|
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeRight],
|
||||||
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeTop],
|
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeTop],
|
||||||
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeBottom],
|
[self.rightButtonWrapper autoPinBottomToSuperviewWithMargin:0],
|
||||||
|
|
||||||
[rightButton autoSetDimension:ALDimensionHeight toSize:kMinContentHeight],
|
[rightButton autoSetDimension:ALDimensionHeight toSize:kMinContentHeight],
|
||||||
[rightButton autoPinLeadingToSuperviewWithMargin:contentHSpacing],
|
[rightButton autoPinLeadingToSuperviewWithMargin:contentHSpacing],
|
||||||
[rightButton autoPinTrailingToSuperviewWithMargin:contentHInset],
|
[rightButton autoPinTrailingToSuperviewWithMargin:contentHInset],
|
||||||
[rightButton autoPinEdgeToSuperviewEdge:ALEdgeBottom],
|
[rightButton autoPinEdgeToSuperviewEdge:ALEdgeBottom]
|
||||||
];
|
];
|
||||||
|
|
||||||
// Layout immediately, unless the input toolbar hasn't even been laid out yet.
|
// Layout immediately, unless the input toolbar hasn't even been laid out yet.
|
||||||
|
@ -709,6 +742,7 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
|
||||||
if (!lastTextContentSize || fabs(lastTextContentSize.CGSizeValue.width - textContentSize.width) > 0.1f
|
if (!lastTextContentSize || fabs(lastTextContentSize.CGSizeValue.width - textContentSize.width) > 0.1f
|
||||||
|| fabs(lastTextContentSize.CGSizeValue.height - textContentSize.height) > 0.1f) {
|
|| fabs(lastTextContentSize.CGSizeValue.height - textContentSize.height) > 0.1f) {
|
||||||
[self ensureContentConstraints];
|
[self ensureContentConstraints];
|
||||||
|
[self invalidateIntrinsicContentSize];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -811,13 +845,6 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
|
||||||
- (void)viewWillDisappear:(BOOL)animated
|
- (void)viewWillDisappear:(BOOL)animated
|
||||||
{
|
{
|
||||||
[self.attachmentView viewWillDisappear:animated];
|
[self.attachmentView viewWillDisappear:animated];
|
||||||
|
|
||||||
[self endEditingTextMessage];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (nullable NSString *)textInputPrimaryLanguage
|
|
||||||
{
|
|
||||||
return self.inputTextView.textInputMode.primaryLanguage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
#import "DateUtil.h"
|
#import "DateUtil.h"
|
||||||
#import "DebugUITableViewController.h"
|
#import "DebugUITableViewController.h"
|
||||||
#import "FingerprintViewController.h"
|
#import "FingerprintViewController.h"
|
||||||
#import "FullImageViewController.h"
|
#import "MediaDetailViewController.h"
|
||||||
#import "NSAttributedString+OWS.h"
|
#import "NSAttributedString+OWS.h"
|
||||||
#import "NewGroupViewController.h"
|
#import "NewGroupViewController.h"
|
||||||
#import "OWSAudioAttachmentPlayer.h"
|
#import "OWSAudioAttachmentPlayer.h"
|
||||||
|
@ -57,7 +57,6 @@
|
||||||
#import <JSQMessagesViewController/JSQSystemSoundPlayer+JSQMessages.h>
|
#import <JSQMessagesViewController/JSQSystemSoundPlayer+JSQMessages.h>
|
||||||
#import <JSQMessagesViewController/UIColor+JSQMessages.h>
|
#import <JSQMessagesViewController/UIColor+JSQMessages.h>
|
||||||
#import <JSQSystemSoundPlayer/JSQSystemSoundPlayer.h>
|
#import <JSQSystemSoundPlayer/JSQSystemSoundPlayer.h>
|
||||||
#import <MediaPlayer/MediaPlayer.h>
|
|
||||||
#import <MobileCoreServices/UTCoreTypes.h>
|
#import <MobileCoreServices/UTCoreTypes.h>
|
||||||
#import <PromiseKit/AnyPromise.h>
|
#import <PromiseKit/AnyPromise.h>
|
||||||
#import <SignalMessaging/Environment.h>
|
#import <SignalMessaging/Environment.h>
|
||||||
|
@ -175,7 +174,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
@property (nonatomic) NSArray<ConversationViewItem *> *viewItems;
|
@property (nonatomic) NSArray<ConversationViewItem *> *viewItems;
|
||||||
@property (nonatomic) NSMutableDictionary<NSString *, ConversationViewItem *> *viewItemCache;
|
@property (nonatomic) NSMutableDictionary<NSString *, ConversationViewItem *> *viewItemCache;
|
||||||
|
|
||||||
@property (nonatomic, nullable) MPMoviePlayerController *videoPlayer;
|
|
||||||
@property (nonatomic, nullable) AVAudioRecorder *audioRecorder;
|
@property (nonatomic, nullable) AVAudioRecorder *audioRecorder;
|
||||||
@property (nonatomic, nullable) OWSAudioAttachmentPlayer *audioAttachmentPlayer;
|
@property (nonatomic, nullable) OWSAudioAttachmentPlayer *audioAttachmentPlayer;
|
||||||
@property (nonatomic, nullable) NSUUID *voiceMessageUUID;
|
@property (nonatomic, nullable) NSUUID *voiceMessageUUID;
|
||||||
|
@ -220,17 +218,18 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
@property (nonatomic, readonly) BOOL isGroupConversation;
|
@property (nonatomic, readonly) BOOL isGroupConversation;
|
||||||
@property (nonatomic) BOOL isUserScrolling;
|
@property (nonatomic) BOOL isUserScrolling;
|
||||||
|
|
||||||
|
@property (nonatomic) NSLayoutConstraint *scrollDownButtonButtomConstraint;
|
||||||
|
|
||||||
@property (nonatomic) ConversationScrollButton *scrollDownButton;
|
@property (nonatomic) ConversationScrollButton *scrollDownButton;
|
||||||
#ifdef DEBUG
|
#ifdef DEBUG
|
||||||
@property (nonatomic) ConversationScrollButton *scrollUpButton;
|
@property (nonatomic) ConversationScrollButton *scrollUpButton;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@property (nonatomic) BOOL isViewCompletelyAppeared;
|
||||||
@property (nonatomic) BOOL isViewVisible;
|
@property (nonatomic) BOOL isViewVisible;
|
||||||
@property (nonatomic) BOOL isAppInBackground;
|
@property (nonatomic) BOOL isAppInBackground;
|
||||||
@property (nonatomic) BOOL shouldObserveDBModifications;
|
@property (nonatomic) BOOL shouldObserveDBModifications;
|
||||||
@property (nonatomic) BOOL viewHasEverAppeared;
|
@property (nonatomic) BOOL viewHasEverAppeared;
|
||||||
@property (nonatomic) BOOL wasScrolledToBottomBeforeKeyboardShow;
|
|
||||||
@property (nonatomic) BOOL wasScrolledToBottomBeforeLayoutChange;
|
|
||||||
@property (nonatomic) BOOL hasUnreadMessages;
|
@property (nonatomic) BOOL hasUnreadMessages;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -328,6 +327,10 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
selector:@selector(signalAccountsDidChange:)
|
selector:@selector(signalAccountsDidChange:)
|
||||||
name:OWSContactsManagerSignalAccountsDidChangeNotification
|
name:OWSContactsManagerSignalAccountsDidChangeNotification
|
||||||
object:nil];
|
object:nil];
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
|
selector:@selector(keyboardWillChangeFrame:)
|
||||||
|
name:UIKeyboardWillChangeFrameNotification
|
||||||
|
object:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)signalAccountsDidChange:(NSNotification *)notification
|
- (void)signalAccountsDidChange:(NSNotification *)notification
|
||||||
|
@ -461,13 +464,13 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
{
|
{
|
||||||
if (_peek) {
|
if (_peek) {
|
||||||
self.inputToolbar.hidden = YES;
|
self.inputToolbar.hidden = YES;
|
||||||
[self.inputToolbar endEditing:TRUE];
|
[self dismissKeyBoard];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.userLeftGroup) {
|
if (self.userLeftGroup) {
|
||||||
self.inputToolbar.hidden = YES; // user has requested they leave the group. further sends disallowed
|
self.inputToolbar.hidden = YES; // user has requested they leave the group. further sends disallowed
|
||||||
[self.inputToolbar endEditing:TRUE];
|
[self dismissKeyBoard];
|
||||||
} else {
|
} else {
|
||||||
self.inputToolbar.hidden = NO;
|
self.inputToolbar.hidden = NO;
|
||||||
}
|
}
|
||||||
|
@ -510,6 +513,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
self.collectionView.dataSource = self;
|
self.collectionView.dataSource = self;
|
||||||
self.collectionView.showsVerticalScrollIndicator = YES;
|
self.collectionView.showsVerticalScrollIndicator = YES;
|
||||||
self.collectionView.showsHorizontalScrollIndicator = NO;
|
self.collectionView.showsHorizontalScrollIndicator = NO;
|
||||||
|
self.collectionView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive;
|
||||||
self.collectionView.backgroundColor = [UIColor whiteColor];
|
self.collectionView.backgroundColor = [UIColor whiteColor];
|
||||||
[self.view addSubview:self.collectionView];
|
[self.view addSubview:self.collectionView];
|
||||||
[self.collectionView autoPinWidthToSuperview];
|
[self.collectionView autoPinWidthToSuperview];
|
||||||
|
@ -526,10 +530,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
_inputToolbar = [ConversationInputToolbar new];
|
_inputToolbar = [ConversationInputToolbar new];
|
||||||
self.inputToolbar.inputToolbarDelegate = self;
|
self.inputToolbar.inputToolbarDelegate = self;
|
||||||
self.inputToolbar.inputTextViewDelegate = self;
|
self.inputToolbar.inputTextViewDelegate = self;
|
||||||
[self.view addSubview:self.inputToolbar];
|
[self.collectionView autoPinToBottomLayoutGuideOfViewController:self withInset:0];
|
||||||
[self.inputToolbar autoPinWidthToSuperview];
|
|
||||||
[self.inputToolbar autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.collectionView];
|
|
||||||
[self autoPinViewToBottomGuideOrKeyboard:self.inputToolbar];
|
|
||||||
|
|
||||||
self.loadMoreHeader = [UILabel new];
|
self.loadMoreHeader = [UILabel new];
|
||||||
self.loadMoreHeader.text = NSLocalizedString(@"CONVERSATION_VIEW_LOADING_MORE_MESSAGES",
|
self.loadMoreHeader.text = NSLocalizedString(@"CONVERSATION_VIEW_LOADING_MORE_MESSAGES",
|
||||||
|
@ -543,6 +544,16 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
[self.loadMoreHeader autoSetDimension:ALDimensionHeight toSize:kLoadMoreHeaderHeight];
|
[self.loadMoreHeader autoSetDimension:ALDimensionHeight toSize:kLoadMoreHeaderHeight];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (BOOL)canBecomeFirstResponder
|
||||||
|
{
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (nullable UIView *)inputAccessoryView
|
||||||
|
{
|
||||||
|
return self.inputToolbar;
|
||||||
|
}
|
||||||
|
|
||||||
- (void)registerCellClasses
|
- (void)registerCellClasses
|
||||||
{
|
{
|
||||||
[self.collectionView registerClass:[OWSSystemMessageCell class]
|
[self.collectionView registerClass:[OWSSystemMessageCell class]
|
||||||
|
@ -892,6 +903,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
}];
|
}];
|
||||||
[actionSheetController addAction:dismissAction];
|
[actionSheetController addAction:dismissAction];
|
||||||
|
|
||||||
|
[self dismissKeyBoard];
|
||||||
[self presentViewController:actionSheetController animated:YES completion:nil];
|
[self presentViewController:actionSheetController animated:YES completion:nil];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1008,6 +1020,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
_callOnOpen = NO;
|
_callOnOpen = NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.isViewCompletelyAppeared = YES;
|
||||||
self.viewHasEverAppeared = YES;
|
self.viewHasEverAppeared = YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1021,6 +1034,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
|
|
||||||
[super viewWillDisappear:animated];
|
[super viewWillDisappear:animated];
|
||||||
|
|
||||||
|
self.isViewCompletelyAppeared = NO;
|
||||||
[self.inputToolbar viewWillDisappear:animated];
|
[self.inputToolbar viewWillDisappear:animated];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1038,7 +1052,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
[self markVisibleMessagesAsRead];
|
[self markVisibleMessagesAsRead];
|
||||||
[self cancelVoiceMemo];
|
[self cancelVoiceMemo];
|
||||||
[self.cellMediaCache removeAllObjects];
|
[self.cellMediaCache removeAllObjects];
|
||||||
[self.inputToolbar endEditingTextMessage];
|
|
||||||
|
|
||||||
self.isUserScrolling = NO;
|
self.isUserScrolling = NO;
|
||||||
}
|
}
|
||||||
|
@ -1390,20 +1403,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
|
|
||||||
#pragma mark - JSQMessagesViewController method overrides
|
#pragma mark - JSQMessagesViewController method overrides
|
||||||
|
|
||||||
- (void)toggleDefaultKeyboard
|
|
||||||
{
|
|
||||||
// Primary language is nil for the emoji keyboard & we want to stay on it after sending
|
|
||||||
if (!self.inputToolbar.textInputPrimaryLanguage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The JSQ event listeners cause a bounce animation, so we temporarily disable them.
|
|
||||||
[self setShouldIgnoreKeyboardChanges:YES];
|
|
||||||
[self dismissKeyBoard];
|
|
||||||
[self popKeyBoard];
|
|
||||||
[self setShouldIgnoreKeyboardChanges:NO];
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - Dynamic Text
|
#pragma mark - Dynamic Text
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1644,6 +1643,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
|
|
||||||
[actionSheetController addAction:resendMessageAction];
|
[actionSheetController addAction:resendMessageAction];
|
||||||
|
|
||||||
|
[self dismissKeyBoard];
|
||||||
[self presentViewController:actionSheetController animated:YES completion:nil];
|
[self presentViewController:actionSheetController animated:YES completion:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1678,6 +1678,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
|
|
||||||
[actionSheetController addAction:resendMessageAction];
|
[actionSheetController addAction:resendMessageAction];
|
||||||
|
|
||||||
|
[self dismissKeyBoard];
|
||||||
[self presentViewController:actionSheetController animated:YES completion:nil];
|
[self presentViewController:actionSheetController animated:YES completion:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1805,6 +1806,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
}];
|
}];
|
||||||
[alertController addAction:resetSessionAction];
|
[alertController addAction:resetSessionAction];
|
||||||
|
|
||||||
|
[self dismissKeyBoard];
|
||||||
[self presentViewController:alertController animated:YES completion:nil];
|
[self presentViewController:alertController animated:YES completion:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1845,6 +1847,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
}];
|
}];
|
||||||
[actionSheetController addAction:acceptSafetyNumberAction];
|
[actionSheetController addAction:acceptSafetyNumberAction];
|
||||||
|
|
||||||
|
[self dismissKeyBoard];
|
||||||
[self presentViewController:actionSheetController animated:YES completion:nil];
|
[self presentViewController:actionSheetController animated:YES completion:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1874,9 +1877,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
[alertController addAction:callAction];
|
[alertController addAction:callAction];
|
||||||
[alertController addAction:[OWSAlerts cancelAction]];
|
[alertController addAction:[OWSAlerts cancelAction]];
|
||||||
|
|
||||||
[[UIApplication sharedApplication].frontmostViewController presentViewController:alertController
|
[self dismissKeyBoard];
|
||||||
animated:YES
|
[self presentViewController:alertController animated:YES completion:nil];
|
||||||
completion:nil];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - ConversationViewCellDelegate
|
#pragma mark - ConversationViewCellDelegate
|
||||||
|
@ -1925,6 +1927,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
}];
|
}];
|
||||||
[actionSheetController addAction:blockAction];
|
[actionSheetController addAction:blockAction];
|
||||||
|
|
||||||
|
[self dismissKeyBoard];
|
||||||
[self presentViewController:actionSheetController animated:YES completion:nil];
|
[self presentViewController:actionSheetController animated:YES completion:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1988,46 +1991,32 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
OWSAssert(attachmentStream);
|
OWSAssert(attachmentStream);
|
||||||
OWSAssert(imageView);
|
OWSAssert(imageView);
|
||||||
|
|
||||||
|
[self dismissKeyBoard];
|
||||||
|
|
||||||
UIWindow *window = [UIApplication sharedApplication].keyWindow;
|
UIWindow *window = [UIApplication sharedApplication].keyWindow;
|
||||||
CGRect convertedRect = [imageView convertRect:imageView.bounds toView:window];
|
CGRect convertedRect = [imageView convertRect:imageView.bounds toView:window];
|
||||||
FullImageViewController *vc = [[FullImageViewController alloc] initWithAttachmentStream:attachmentStream
|
MediaDetailViewController *vc = [[MediaDetailViewController alloc] initWithAttachmentStream:attachmentStream
|
||||||
fromRect:convertedRect
|
fromRect:convertedRect
|
||||||
viewItem:viewItem];
|
viewItem:viewItem];
|
||||||
[vc presentFromViewController:self];
|
[vc presentFromViewController:self replacingView:imageView];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)didTapVideoViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream
|
- (void)didTapVideoViewItem:(ConversationViewItem *)viewItem
|
||||||
|
attachmentStream:(TSAttachmentStream *)attachmentStream
|
||||||
|
imageView:(UIImageView *)imageView
|
||||||
{
|
{
|
||||||
OWSAssertIsOnMainThread();
|
OWSAssertIsOnMainThread();
|
||||||
OWSAssert(viewItem);
|
OWSAssert(viewItem);
|
||||||
OWSAssert(attachmentStream);
|
OWSAssert(attachmentStream);
|
||||||
|
|
||||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
||||||
if (![fileManager fileExistsAtPath:[attachmentStream.mediaURL path]]) {
|
|
||||||
OWSFail(@"%@ Missing video file: %@", self.logTag, attachmentStream.mediaURL);
|
|
||||||
}
|
|
||||||
|
|
||||||
[self dismissKeyBoard];
|
[self dismissKeyBoard];
|
||||||
self.videoPlayer = [[MPMoviePlayerController alloc] initWithContentURL:attachmentStream.mediaURL];
|
UIWindow *window = [UIApplication sharedApplication].keyWindow;
|
||||||
[_videoPlayer prepareToPlay];
|
CGRect convertedRect = [imageView convertRect:imageView.bounds toView:window];
|
||||||
|
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
MediaDetailViewController *vc = [[MediaDetailViewController alloc] initWithAttachmentStream:attachmentStream
|
||||||
selector:@selector(moviePlayerWillExitFullscreen:)
|
fromRect:convertedRect
|
||||||
name:MPMoviePlayerWillExitFullscreenNotification
|
viewItem:viewItem];
|
||||||
object:_videoPlayer];
|
[vc presentFromViewController:self replacingView:imageView];
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
||||||
selector:@selector(moviePlayerDidExitFullscreen:)
|
|
||||||
name:MPMoviePlayerDidExitFullscreenNotification
|
|
||||||
object:_videoPlayer];
|
|
||||||
|
|
||||||
_videoPlayer.controlStyle = MPMovieControlStyleDefault;
|
|
||||||
_videoPlayer.shouldAutoplay = YES;
|
|
||||||
[self.view addSubview:_videoPlayer.view];
|
|
||||||
// We can't animate from the cell media frame;
|
|
||||||
// MPMoviePlayerController will animate a crop of its
|
|
||||||
// contents rather than scaling them.
|
|
||||||
_videoPlayer.view.frame = self.view.bounds;
|
|
||||||
[_videoPlayer setFullscreen:YES animated:NO];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)didTapAudioViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream
|
- (void)didTapAudioViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream
|
||||||
|
@ -2108,42 +2097,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
[self.navigationController pushViewController:view animated:YES];
|
[self.navigationController pushViewController:view animated:YES];
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - Video Playback
|
|
||||||
|
|
||||||
// There's more than one way to exit the fullscreen video playback.
|
|
||||||
// There's a done button, a "toggle fullscreen" button and I think
|
|
||||||
// there's some gestures too. These fire slightly different notifications.
|
|
||||||
// We want to hide & clean up the video player immediately in all of
|
|
||||||
// these cases.
|
|
||||||
- (void)moviePlayerWillExitFullscreen:(id)sender
|
|
||||||
{
|
|
||||||
DDLogDebug(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
|
|
||||||
|
|
||||||
[self clearVideoPlayer];
|
|
||||||
}
|
|
||||||
|
|
||||||
// See comment on moviePlayerWillExitFullscreen:
|
|
||||||
- (void)moviePlayerDidExitFullscreen:(id)sender
|
|
||||||
{
|
|
||||||
DDLogDebug(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
|
|
||||||
|
|
||||||
[self clearVideoPlayer];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)clearVideoPlayer
|
|
||||||
{
|
|
||||||
[_videoPlayer stop];
|
|
||||||
[_videoPlayer.view removeFromSuperview];
|
|
||||||
self.videoPlayer = nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)setVideoPlayer:(MPMoviePlayerController *_Nullable)videoPlayer
|
|
||||||
{
|
|
||||||
_videoPlayer = videoPlayer;
|
|
||||||
|
|
||||||
[ViewControllerUtils setAudioIgnoresHardwareMuteSwitch:videoPlayer != nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - System Messages
|
#pragma mark - System Messages
|
||||||
|
|
||||||
- (void)didTapSystemMessageWithInteraction:(TSInteraction *)interaction
|
- (void)didTapSystemMessageWithInteraction:(TSInteraction *)interaction
|
||||||
|
@ -2244,7 +2197,9 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
[self.view addSubview:self.scrollDownButton];
|
[self.view addSubview:self.scrollDownButton];
|
||||||
[self.scrollDownButton autoSetDimension:ALDimensionWidth toSize:ConversationScrollButton.buttonSize];
|
[self.scrollDownButton autoSetDimension:ALDimensionWidth toSize:ConversationScrollButton.buttonSize];
|
||||||
[self.scrollDownButton autoSetDimension:ALDimensionHeight toSize:ConversationScrollButton.buttonSize];
|
[self.scrollDownButton autoSetDimension:ALDimensionHeight toSize:ConversationScrollButton.buttonSize];
|
||||||
[self.scrollDownButton autoPinEdge:ALEdgeBottom toEdge:ALEdgeTop ofView:self.inputToolbar];
|
|
||||||
|
self.scrollDownButtonButtomConstraint =
|
||||||
|
[self.scrollDownButton autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.collectionView];
|
||||||
[self.scrollDownButton autoPinEdgeToSuperviewEdge:ALEdgeTrailing];
|
[self.scrollDownButton autoPinEdgeToSuperviewEdge:ALEdgeTrailing];
|
||||||
|
|
||||||
#ifdef DEBUG
|
#ifdef DEBUG
|
||||||
|
@ -2360,6 +2315,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
[[UIDocumentMenuViewController alloc] initWithDocumentTypes:documentTypes inMode:pickerMode];
|
[[UIDocumentMenuViewController alloc] initWithDocumentTypes:documentTypes inMode:pickerMode];
|
||||||
menuController.delegate = self;
|
menuController.delegate = self;
|
||||||
|
|
||||||
|
[self dismissKeyBoard];
|
||||||
[self presentViewController:menuController animated:YES completion:nil];
|
[self presentViewController:menuController animated:YES completion:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2371,6 +2327,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
[[GifPickerViewController alloc] initWithThread:self.thread messageSender:self.messageSender];
|
[[GifPickerViewController alloc] initWithThread:self.thread messageSender:self.messageSender];
|
||||||
view.delegate = self;
|
view.delegate = self;
|
||||||
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:view];
|
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:view];
|
||||||
|
|
||||||
|
[self dismissKeyBoard];
|
||||||
[self presentViewController:navigationController animated:YES completion:nil];
|
[self presentViewController:navigationController animated:YES completion:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2410,6 +2368,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
// post iOS11, document picker has no blue header.
|
// post iOS11, document picker has no blue header.
|
||||||
[UIUtil applyDefaultSystemAppearence];
|
[UIUtil applyDefaultSystemAppearence];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[self dismissKeyBoard];
|
||||||
[self presentViewController:documentPicker animated:YES completion:nil];
|
[self presentViewController:documentPicker animated:YES completion:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2515,8 +2475,9 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
picker.mediaTypes = @[ (__bridge NSString *)kUTTypeImage, (__bridge NSString *)kUTTypeMovie ];
|
picker.mediaTypes = @[ (__bridge NSString *)kUTTypeImage, (__bridge NSString *)kUTTypeMovie ];
|
||||||
picker.allowsEditing = NO;
|
picker.allowsEditing = NO;
|
||||||
picker.delegate = self;
|
picker.delegate = self;
|
||||||
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[self dismissKeyBoard];
|
||||||
[self presentViewController:picker animated:YES completion:[UIUtil modalCompletionBlock]];
|
[self presentViewController:picker animated:YES completion:[UIUtil modalCompletionBlock]];
|
||||||
});
|
});
|
||||||
}];
|
}];
|
||||||
|
@ -2536,6 +2497,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
picker.delegate = self;
|
picker.delegate = self;
|
||||||
picker.mediaTypes = @[ (__bridge NSString *)kUTTypeImage, (__bridge NSString *)kUTTypeMovie ];
|
picker.mediaTypes = @[ (__bridge NSString *)kUTTypeImage, (__bridge NSString *)kUTTypeMovie ];
|
||||||
|
|
||||||
|
[self dismissKeyBoard];
|
||||||
[self presentViewController:picker animated:YES completion:[UIUtil modalCompletionBlock]];
|
[self presentViewController:picker animated:YES completion:[UIUtil modalCompletionBlock]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3098,8 +3060,11 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
const CGFloat kIsAtBottomTolerancePts = 5;
|
const CGFloat kIsAtBottomTolerancePts = 5;
|
||||||
// Note the usage of MAX() to handle the case where there isn't enough
|
// Note the usage of MAX() to handle the case where there isn't enough
|
||||||
// content to fill the collection view at its current size.
|
// content to fill the collection view at its current size.
|
||||||
CGFloat contentOffsetYBottom = MAX(0.f, contentHeight - self.collectionView.bounds.size.height);
|
CGFloat contentOffsetYBottom
|
||||||
BOOL isScrolledToBottom = (self.collectionView.contentOffset.y > contentOffsetYBottom - kIsAtBottomTolerancePts);
|
= MAX(0.f, contentHeight + self.collectionView.contentInset.bottom - self.collectionView.bounds.size.height);
|
||||||
|
|
||||||
|
CGFloat distanceFromBottom = contentOffsetYBottom - self.collectionView.contentOffset.y;
|
||||||
|
BOOL isScrolledToBottom = distanceFromBottom <= kIsAtBottomTolerancePts;
|
||||||
|
|
||||||
return isScrolledToBottom;
|
return isScrolledToBottom;
|
||||||
}
|
}
|
||||||
|
@ -3291,6 +3256,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
|
|
||||||
- (void)attachmentButtonPressed
|
- (void)attachmentButtonPressed
|
||||||
{
|
{
|
||||||
|
[self dismissKeyBoard];
|
||||||
|
|
||||||
__weak ConversationViewController *weakSelf = self;
|
__weak ConversationViewController *weakSelf = self;
|
||||||
if ([self isBlockedContactConversation]) {
|
if ([self isBlockedContactConversation]) {
|
||||||
|
@ -3366,6 +3332,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
[gifAction setValue:gifImage forKey:@"image"];
|
[gifAction setValue:gifImage forKey:@"image"];
|
||||||
[actionSheetController addAction:gifAction];
|
[actionSheetController addAction:gifAction];
|
||||||
|
|
||||||
|
[self dismissKeyBoard];
|
||||||
[self presentViewController:actionSheetController animated:true completion:nil];
|
[self presentViewController:actionSheetController animated:true completion:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3675,6 +3642,95 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)keyboardWillChangeFrame:(NSNotification *)notification
|
||||||
|
{
|
||||||
|
// `willChange` is the correct keyboard notifiation to observe when adjusting contentInset
|
||||||
|
// in lockstep with the keyboard presentation animation. `didChange` results in the contentInset
|
||||||
|
// not adjusting until after the keyboard is fully up.
|
||||||
|
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
|
||||||
|
[self handleKeyboardNotification:notification];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)handleKeyboardNotification:(NSNotification *)notification
|
||||||
|
{
|
||||||
|
AssertIsOnMainThread();
|
||||||
|
|
||||||
|
NSDictionary *userInfo = [notification userInfo];
|
||||||
|
|
||||||
|
NSValue *_Nullable keyboardBeginFrameValue = userInfo[UIKeyboardFrameBeginUserInfoKey];
|
||||||
|
if (!keyboardBeginFrameValue) {
|
||||||
|
OWSFail(@"%@ Missing keyboard begin frame", self.logTag);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSValue *_Nullable keyboardEndFrameValue = userInfo[UIKeyboardFrameEndUserInfoKey];
|
||||||
|
if (!keyboardEndFrameValue) {
|
||||||
|
OWSFail(@"%@ Missing keyboard end frame", self.logTag);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CGRect keyboardEndFrame = [keyboardEndFrameValue CGRectValue];
|
||||||
|
|
||||||
|
UIEdgeInsets oldInsets = self.collectionView.contentInset;
|
||||||
|
UIEdgeInsets newInsets = oldInsets;
|
||||||
|
|
||||||
|
// bottomLayoutGuide accounts for extra offset needed on iPhoneX
|
||||||
|
newInsets.bottom = keyboardEndFrame.size.height - self.bottomLayoutGuide.length;
|
||||||
|
|
||||||
|
BOOL wasScrolledToBottom = [self isScrolledToBottom];
|
||||||
|
|
||||||
|
void (^adjustInsets)(void) = ^(void) {
|
||||||
|
self.collectionView.contentInset = newInsets;
|
||||||
|
self.collectionView.scrollIndicatorInsets = newInsets;
|
||||||
|
|
||||||
|
// Note there is a bug in iOS11.2 which where switching to the emoji keyboard
|
||||||
|
// does not fire a UIKeyboardFrameWillChange notification. In that case, the scroll
|
||||||
|
// down button gets mostly obscured by the keyboard.
|
||||||
|
// RADAR: #36297652
|
||||||
|
self.scrollDownButtonButtomConstraint.constant = -1 * newInsets.bottom;
|
||||||
|
[self.scrollDownButton setNeedsLayout];
|
||||||
|
[self.scrollDownButton layoutIfNeeded];
|
||||||
|
// HACK: I've made the assumption that we are already in the context of an animation, in which case the
|
||||||
|
// above should be sufficient to smoothly move the scrollDown button in step with the keyboard presentation
|
||||||
|
// animation. Yet, setting the constraint doesn't animate the movement of the button - it "jumps" to it's final
|
||||||
|
// position. So here we manually lay out the scroll down button frame (seemingly redundantly), which allows it
|
||||||
|
// to be smoothly animated.
|
||||||
|
CGRect newButtonFrame = self.scrollDownButton.frame;
|
||||||
|
newButtonFrame.origin.y
|
||||||
|
= self.scrollDownButton.superview.height - (newInsets.bottom + self.scrollDownButton.height);
|
||||||
|
self.scrollDownButton.frame = newButtonFrame;
|
||||||
|
|
||||||
|
// Adjust content offset to prevent the presented keyboard from obscuring content.
|
||||||
|
if (wasScrolledToBottom) {
|
||||||
|
// If we were scrolled to the bottom, don't do any fancy math. Just stay at the bottom.
|
||||||
|
[self scrollToBottomAnimated:NO];
|
||||||
|
} else {
|
||||||
|
// If we were scrolled away from the bottom, shift the content in lockstep with the
|
||||||
|
// keyboard, up to the limits of the content bounds.
|
||||||
|
CGFloat insetChange = newInsets.bottom - oldInsets.bottom;
|
||||||
|
CGFloat oldYOffset = self.collectionView.contentOffset.y;
|
||||||
|
CGFloat newYOffset = Clamp(oldYOffset + insetChange, 0, self.safeContentHeight);
|
||||||
|
CGPoint newOffset = CGPointMake(0, newYOffset);
|
||||||
|
|
||||||
|
// If the user is dismissing the keyboard via interactive scrolling, any additional conset offset feels
|
||||||
|
// redundant, so we only adjust content offset when *presenting* the keyboard (i.e. when insetChange > 0).
|
||||||
|
if (insetChange > 0 && newYOffset > keyboardEndFrame.origin.y) {
|
||||||
|
[self.collectionView setContentOffset:newOffset animated:NO];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (self.isViewCompletelyAppeared) {
|
||||||
|
adjustInsets();
|
||||||
|
} else {
|
||||||
|
// Even though we are scrolling without explicitly animating, the notification seems to occur within the context
|
||||||
|
// of a system animation, which is desirable when the view is visible, because the user sees the content rise
|
||||||
|
// in sync with the keyboard. However, when the view hasn't yet been presented, the animation conflicts and the
|
||||||
|
// result is that initial load causes the collection cells to visably "animate" to their final position once the
|
||||||
|
// view appears.
|
||||||
|
[UIView performWithoutAnimation:adjustInsets];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
- (void)didApproveAttachment:(SignalAttachment *)attachment
|
- (void)didApproveAttachment:(SignalAttachment *)attachment
|
||||||
{
|
{
|
||||||
OWSAssert(attachment);
|
OWSAssert(attachment);
|
||||||
|
@ -3707,13 +3763,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
return [self.collectionView.collectionViewLayout collectionViewContentSize].height;
|
return [self.collectionView.collectionViewLayout collectionViewContentSize].height;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)scrollToBottomImmediately
|
|
||||||
{
|
|
||||||
OWSAssertIsOnMainThread();
|
|
||||||
|
|
||||||
[self scrollToBottomAnimated:NO];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)scrollToBottomAnimated:(BOOL)animated
|
- (void)scrollToBottomAnimated:(BOOL)animated
|
||||||
{
|
{
|
||||||
OWSAssertIsOnMainThread();
|
OWSAssertIsOnMainThread();
|
||||||
|
@ -3721,11 +3770,17 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
if (self.isUserScrolling) {
|
if (self.isUserScrolling) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure the view is fully layed out before we try to scroll to the bottom, since
|
||||||
|
// we use the collectionView bounds to determine where the "bottom" is.
|
||||||
|
[self.view layoutIfNeeded];
|
||||||
|
|
||||||
CGFloat contentHeight = self.safeContentHeight;
|
CGFloat contentHeight = self.safeContentHeight;
|
||||||
CGFloat dstY = MAX(0, contentHeight - self.collectionView.height);
|
|
||||||
[self.collectionView setContentOffset:CGPointMake(0, dstY) animated:animated];
|
|
||||||
|
|
||||||
|
CGFloat dstY
|
||||||
|
= MAX(0, contentHeight + self.collectionView.contentInset.bottom - self.collectionView.bounds.size.height);
|
||||||
|
|
||||||
|
[self.collectionView setContentOffset:CGPointMake(0, dstY) animated:NO];
|
||||||
[self didScrollToBottom];
|
[self didScrollToBottom];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3735,22 +3790,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
{
|
{
|
||||||
[self updateLastVisibleTimestamp];
|
[self updateLastVisibleTimestamp];
|
||||||
[self autoLoadMoreIfNecessary];
|
[self autoLoadMoreIfNecessary];
|
||||||
|
|
||||||
if (self.isUserScrolling && [self isScrolledAwayFromBottom]) {
|
|
||||||
[self.inputToolbar endEditingTextMessage];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// See the comments on isScrolledToBottom.
|
|
||||||
- (BOOL)isScrolledAwayFromBottom
|
|
||||||
{
|
|
||||||
CGFloat contentHeight = self.safeContentHeight;
|
|
||||||
// Note the usage of MAX() to handle the case where there isn't enough
|
|
||||||
// content to fill the collection view at its current size.
|
|
||||||
CGFloat contentOffsetYBottom = MAX(0.f, contentHeight - self.collectionView.bounds.size.height);
|
|
||||||
const CGFloat kThreshold = 250;
|
|
||||||
BOOL isScrolledAwayFromBottom = (self.collectionView.contentOffset.y < contentOffsetYBottom - kThreshold);
|
|
||||||
return isScrolledAwayFromBottom;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
|
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
|
||||||
|
@ -3877,10 +3916,10 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
[self messageWasSent:message];
|
[self messageWasSent:message];
|
||||||
|
|
||||||
if (updateKeyboardState) {
|
if (updateKeyboardState) {
|
||||||
[self toggleDefaultKeyboard];
|
[self.inputToolbar toggleDefaultKeyboard];
|
||||||
}
|
}
|
||||||
[self clearDraft];
|
|
||||||
[self.inputToolbar clearTextMessage];
|
[self.inputToolbar clearTextMessage];
|
||||||
|
[self clearDraft];
|
||||||
if (didAddToProfileWhitelist) {
|
if (didAddToProfileWhitelist) {
|
||||||
[self ensureDynamicInteractions];
|
[self ensureDynamicInteractions];
|
||||||
}
|
}
|
||||||
|
@ -4053,8 +4092,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
- (void)collectionViewWillChangeLayout
|
- (void)collectionViewWillChangeLayout
|
||||||
{
|
{
|
||||||
OWSAssertIsOnMainThread();
|
OWSAssertIsOnMainThread();
|
||||||
|
|
||||||
self.wasScrolledToBottomBeforeLayoutChange = [self isScrolledToBottom];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)collectionViewDidChangeLayout
|
- (void)collectionViewDidChangeLayout
|
||||||
|
@ -4062,15 +4099,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
||||||
OWSAssertIsOnMainThread();
|
OWSAssertIsOnMainThread();
|
||||||
|
|
||||||
[self updateLastVisibleTimestamp];
|
[self updateLastVisibleTimestamp];
|
||||||
|
|
||||||
// JSQMessageView has glitchy behavior. When presenting/dismissing view
|
|
||||||
// controllers, the size of the input toolbar and/or collection view can
|
|
||||||
// repeatedly change, leaving scroll state in an invalid state. The
|
|
||||||
// simplest fix that covers most cases is to ensure that we remain
|
|
||||||
// "scrolled to bottom" across these changes.
|
|
||||||
if (self.wasScrolledToBottomBeforeLayoutChange) {
|
|
||||||
[self scrollToBottomImmediately];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - View Items
|
#pragma mark - View Items
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import "ConversationViewLayout.h"
|
#import "ConversationViewLayout.h"
|
||||||
|
@ -68,6 +68,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
[self clearState];
|
[self clearState];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.collectionView.bounds.size.width <= 0.f || self.collectionView.bounds.size.height <= 0.f) {
|
if (self.collectionView.bounds.size.width <= 0.f || self.collectionView.bounds.size.height <= 0.f) {
|
||||||
OWSFail(
|
OWSFail(
|
||||||
@"%@ Collection view has invalid size: %@", self.logTag, NSStringFromCGRect(self.collectionView.bounds));
|
@"%@ Collection view has invalid size: %@", self.logTag, NSStringFromCGRect(self.collectionView.bounds));
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import "OWSViewController.h"
|
#import "OWSViewController.h"
|
||||||
|
@ -10,7 +10,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
@class SignalAttachment;
|
@class SignalAttachment;
|
||||||
@class TSAttachmentStream;
|
@class TSAttachmentStream;
|
||||||
|
|
||||||
@interface FullImageViewController : OWSViewController
|
@interface MediaDetailViewController : OWSViewController
|
||||||
|
|
||||||
// If viewItem is non-null, long press will show a menu controller.
|
// If viewItem is non-null, long press will show a menu controller.
|
||||||
- (instancetype)initWithAttachmentStream:(TSAttachmentStream *)attachmentStream
|
- (instancetype)initWithAttachmentStream:(TSAttachmentStream *)attachmentStream
|
||||||
|
@ -19,7 +19,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
- (instancetype)initWithAttachment:(SignalAttachment *)attachment fromRect:(CGRect)rect;
|
- (instancetype)initWithAttachment:(SignalAttachment *)attachment fromRect:(CGRect)rect;
|
||||||
|
|
||||||
- (void)presentFromViewController:(UIViewController *)viewController;
|
- (void)presentFromViewController:(UIViewController *)viewController replacingView:(UIView *)view;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -0,0 +1,986 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "MediaDetailViewController.h"
|
||||||
|
#import "AttachmentSharing.h"
|
||||||
|
#import "ConversationViewController.h"
|
||||||
|
#import "ConversationViewItem.h"
|
||||||
|
#import "OWSMessageCell.h"
|
||||||
|
#import "Signal-Swift.h"
|
||||||
|
#import "TSAttachmentStream.h"
|
||||||
|
#import "TSInteraction.h"
|
||||||
|
#import "UIColor+OWS.h"
|
||||||
|
#import "UIUtil.h"
|
||||||
|
#import "UIView+OWS.h"
|
||||||
|
#import <AVKit/AVKit.h>
|
||||||
|
#import <MediaPlayer/MPMoviePlayerViewController.h>
|
||||||
|
#import <MediaPlayer/MediaPlayer.h>
|
||||||
|
#import <SignalServiceKit/NSData+Image.h>
|
||||||
|
#import <YYImage/YYImage.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
// In order to use UIMenuController, the view from which it is
|
||||||
|
// presented must have certain custom behaviors.
|
||||||
|
@interface AttachmentMenuView : UIView
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
#pragma mark -
|
||||||
|
|
||||||
|
@implementation AttachmentMenuView
|
||||||
|
|
||||||
|
- (BOOL)canBecomeFirstResponder {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only use custom actions in UIMenuController.
|
||||||
|
- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender
|
||||||
|
{
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
#pragma mark -
|
||||||
|
|
||||||
|
@interface MediaDetailViewController () <UIScrollViewDelegate, UIGestureRecognizerDelegate, PlayerProgressBarDelegate>
|
||||||
|
|
||||||
|
@property (nonatomic) UIScrollView *scrollView;
|
||||||
|
@property (nonatomic) UIView *mediaView;
|
||||||
|
@property (nonatomic) UIView *presentationView;
|
||||||
|
@property (nonatomic) UIView *replacingView;
|
||||||
|
@property (nonatomic) UIButton *shareButton;
|
||||||
|
|
||||||
|
@property (nonatomic) CGRect originRect;
|
||||||
|
@property (nonatomic) NSData *fileData;
|
||||||
|
|
||||||
|
@property (nonatomic, nullable) TSAttachmentStream *attachmentStream;
|
||||||
|
@property (nonatomic, nullable) SignalAttachment *attachment;
|
||||||
|
@property (nonatomic, nullable) ConversationViewItem *viewItem;
|
||||||
|
|
||||||
|
@property (nonatomic) UIToolbar *footerBar;
|
||||||
|
@property (nonatomic) BOOL areToolbarsHidden;
|
||||||
|
|
||||||
|
@property (nonatomic, nullable) AVPlayer *videoPlayer;
|
||||||
|
@property (nonatomic, nullable) UIButton *playVideoButton;
|
||||||
|
@property (nonatomic, nullable) PlayerProgressBar *videoProgressBar;
|
||||||
|
@property (nonatomic, nullable) UIBarButtonItem *videoPlayBarButton;
|
||||||
|
@property (nonatomic, nullable) UIBarButtonItem *videoPauseBarButton;
|
||||||
|
|
||||||
|
@property (nonatomic, nullable) NSArray<NSLayoutConstraint *> *presentationViewConstraints;
|
||||||
|
@property (nonatomic, nullable) NSLayoutConstraint *mediaViewBottomConstraint;
|
||||||
|
@property (nonatomic, nullable) NSLayoutConstraint *mediaViewLeadingConstraint;
|
||||||
|
@property (nonatomic, nullable) NSLayoutConstraint *mediaViewTopConstraint;
|
||||||
|
@property (nonatomic, nullable) NSLayoutConstraint *mediaViewTrailingConstraint;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation MediaDetailViewController
|
||||||
|
|
||||||
|
- (instancetype)initWithAttachmentStream:(TSAttachmentStream *)attachmentStream
|
||||||
|
fromRect:(CGRect)rect
|
||||||
|
viewItem:(ConversationViewItem *_Nullable)viewItem
|
||||||
|
{
|
||||||
|
self = [super initWithNibName:nil bundle:nil];
|
||||||
|
|
||||||
|
if (self) {
|
||||||
|
self.attachmentStream = attachmentStream;
|
||||||
|
self.originRect = rect;
|
||||||
|
self.viewItem = viewItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)initWithAttachment:(SignalAttachment *)attachment fromRect:(CGRect)rect
|
||||||
|
{
|
||||||
|
self = [super initWithNibName:nil bundle:nil];
|
||||||
|
|
||||||
|
if (self) {
|
||||||
|
self.attachment = attachment;
|
||||||
|
self.originRect = rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSURL *_Nullable)attachmentUrl
|
||||||
|
{
|
||||||
|
if (self.attachmentStream) {
|
||||||
|
return self.attachmentStream.mediaURL;
|
||||||
|
} else if (self.attachment) {
|
||||||
|
return self.attachment.dataUrl;
|
||||||
|
} else {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSData *)fileData
|
||||||
|
{
|
||||||
|
if (!_fileData) {
|
||||||
|
NSURL *_Nullable url = self.attachmentUrl;
|
||||||
|
if (url) {
|
||||||
|
_fileData = [NSData dataWithContentsOfURL:url];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _fileData;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIImage *)image {
|
||||||
|
if (self.attachmentStream) {
|
||||||
|
return self.attachmentStream.image;
|
||||||
|
} else if (self.attachment) {
|
||||||
|
if (self.isVideo) {
|
||||||
|
return self.attachment.videoPreview;
|
||||||
|
} else {
|
||||||
|
return self.attachment.image;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)isAnimated
|
||||||
|
{
|
||||||
|
if (self.attachmentStream) {
|
||||||
|
return self.attachmentStream.isAnimated;
|
||||||
|
} else if (self.attachment) {
|
||||||
|
return self.attachment.isAnimatedImage;
|
||||||
|
} else {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)isVideo
|
||||||
|
{
|
||||||
|
if (self.attachmentStream) {
|
||||||
|
return self.attachmentStream.isVideo;
|
||||||
|
} else if (self.attachment) {
|
||||||
|
return self.attachment.isVideo;
|
||||||
|
} else {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)loadView
|
||||||
|
{
|
||||||
|
self.view = [AttachmentMenuView new];
|
||||||
|
self.view.backgroundColor = [UIColor clearColor];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)viewDidLoad
|
||||||
|
{
|
||||||
|
[super viewDidLoad];
|
||||||
|
|
||||||
|
[self createContents];
|
||||||
|
[self initializeGestureRecognizers];
|
||||||
|
|
||||||
|
// Even though bars are opaque, we want content to be layed out behind them.
|
||||||
|
// The bars might obscure part of the content, but they can easily be hidden by tapping
|
||||||
|
// The alternative would be that content would shift when the navbars hide.
|
||||||
|
self.extendedLayoutIncludesOpaqueBars = YES;
|
||||||
|
|
||||||
|
// TODO better title.
|
||||||
|
self.title = @"Attachment";
|
||||||
|
|
||||||
|
self.navigationItem.leftBarButtonItem =
|
||||||
|
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemStop
|
||||||
|
target:self
|
||||||
|
action:@selector(didTapDismissButton:)];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)viewWillDisappear:(BOOL)animated {
|
||||||
|
[super viewWillDisappear:animated];
|
||||||
|
|
||||||
|
if ([UIMenuController sharedMenuController].isMenuVisible) {
|
||||||
|
[[UIMenuController sharedMenuController] setMenuVisible:NO
|
||||||
|
animated:NO];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)viewDidLayoutSubviews
|
||||||
|
{
|
||||||
|
[super viewDidLayoutSubviews];
|
||||||
|
|
||||||
|
[self updateMinZoomScale];
|
||||||
|
[self centerMediaViewConstraints];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)updateMinZoomScale
|
||||||
|
{
|
||||||
|
CGSize viewSize = self.scrollView.bounds.size;
|
||||||
|
UIImage *image = self.image;
|
||||||
|
OWSAssert(image);
|
||||||
|
|
||||||
|
if (image.size.width == 0 || image.size.height == 0) {
|
||||||
|
OWSFail(@"%@ Invalid image dimensions. %@", self.logTag, NSStringFromCGSize(image.size));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CGFloat scaleWidth = viewSize.width / image.size.width;
|
||||||
|
CGFloat scaleHeight = viewSize.height / image.size.height;
|
||||||
|
CGFloat minScale = MIN(scaleWidth, scaleHeight);
|
||||||
|
|
||||||
|
if (minScale != self.scrollView.minimumZoomScale) {
|
||||||
|
self.scrollView.minimumZoomScale = minScale;
|
||||||
|
self.scrollView.maximumZoomScale = minScale * 8;
|
||||||
|
self.scrollView.zoomScale = minScale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Initializers
|
||||||
|
|
||||||
|
- (void)createContents
|
||||||
|
{
|
||||||
|
CGFloat kFooterHeight = 44;
|
||||||
|
|
||||||
|
UIScrollView *scrollView = [UIScrollView new];
|
||||||
|
[self.view addSubview:scrollView];
|
||||||
|
self.scrollView = scrollView;
|
||||||
|
scrollView.delegate = self;
|
||||||
|
|
||||||
|
scrollView.showsVerticalScrollIndicator = NO;
|
||||||
|
scrollView.showsHorizontalScrollIndicator = NO;
|
||||||
|
scrollView.decelerationRate = UIScrollViewDecelerationRateFast;
|
||||||
|
self.automaticallyAdjustsScrollViewInsets = NO;
|
||||||
|
|
||||||
|
[scrollView autoPinToSuperviewEdges];
|
||||||
|
|
||||||
|
if (self.isAnimated) {
|
||||||
|
if ([self.fileData ows_isValidImage]) {
|
||||||
|
YYImage *animatedGif = [YYImage imageWithData:self.fileData];
|
||||||
|
YYAnimatedImageView *animatedView = [YYAnimatedImageView new];
|
||||||
|
animatedView.image = animatedGif;
|
||||||
|
self.mediaView = animatedView;
|
||||||
|
} else {
|
||||||
|
self.mediaView = [UIImageView new];
|
||||||
|
}
|
||||||
|
} else if (self.isVideo) {
|
||||||
|
self.mediaView = [self buildVideoPlayerView];
|
||||||
|
} else {
|
||||||
|
// Present the static image using standard UIImageView
|
||||||
|
UIImageView *imageView = [[UIImageView alloc] initWithImage:self.image];
|
||||||
|
|
||||||
|
self.mediaView = imageView;
|
||||||
|
}
|
||||||
|
|
||||||
|
OWSAssert(self.mediaView);
|
||||||
|
|
||||||
|
[scrollView addSubview:self.mediaView];
|
||||||
|
self.mediaViewLeadingConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeLeading];
|
||||||
|
self.mediaViewTopConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeTop];
|
||||||
|
self.mediaViewTrailingConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeTrailing];
|
||||||
|
self.mediaViewBottomConstraint = [self.mediaView autoPinEdgeToSuperviewEdge:ALEdgeBottom];
|
||||||
|
|
||||||
|
self.mediaView.contentMode = UIViewContentModeScaleAspectFit;
|
||||||
|
self.mediaView.userInteractionEnabled = YES;
|
||||||
|
self.mediaView.clipsToBounds = YES;
|
||||||
|
self.mediaView.layer.allowsEdgeAntialiasing = YES;
|
||||||
|
self.mediaView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
|
|
||||||
|
// Use trilinear filters for better scaling quality at
|
||||||
|
// some performance cost.
|
||||||
|
self.mediaView.layer.minificationFilter = kCAFilterTrilinear;
|
||||||
|
self.mediaView.layer.magnificationFilter = kCAFilterTrilinear;
|
||||||
|
|
||||||
|
// The presentationView is only used during present/dismiss animations.
|
||||||
|
// It's a static image of the media content.
|
||||||
|
UIImageView *presentationView = [[UIImageView alloc] initWithImage:self.image];
|
||||||
|
self.presentationView = presentationView;
|
||||||
|
|
||||||
|
[self.view addSubview:presentationView];
|
||||||
|
presentationView.hidden = YES;
|
||||||
|
presentationView.clipsToBounds = YES;
|
||||||
|
presentationView.layer.allowsEdgeAntialiasing = YES;
|
||||||
|
presentationView.layer.minificationFilter = kCAFilterTrilinear;
|
||||||
|
presentationView.layer.magnificationFilter = kCAFilterTrilinear;
|
||||||
|
presentationView.contentMode = UIViewContentModeScaleAspectFit;
|
||||||
|
|
||||||
|
[self applyInitialMediaViewConstraints];
|
||||||
|
|
||||||
|
if (self.isVideo) {
|
||||||
|
if (@available(iOS 9, *)) {
|
||||||
|
PlayerProgressBar *videoProgressBar = [PlayerProgressBar new];
|
||||||
|
videoProgressBar.delegate = self;
|
||||||
|
videoProgressBar.player = self.videoPlayer;
|
||||||
|
|
||||||
|
self.videoProgressBar = videoProgressBar;
|
||||||
|
[self.view addSubview:videoProgressBar];
|
||||||
|
[videoProgressBar autoPinWidthToSuperview];
|
||||||
|
[videoProgressBar autoPinToTopLayoutGuideOfViewController:self withInset:0];
|
||||||
|
CGFloat kVideoProgressBarHeight = 44;
|
||||||
|
[videoProgressBar autoSetDimension:ALDimensionHeight toSize:kVideoProgressBarHeight];
|
||||||
|
}
|
||||||
|
|
||||||
|
UIButton *playVideoButton = [UIButton new];
|
||||||
|
self.playVideoButton = playVideoButton;
|
||||||
|
|
||||||
|
[playVideoButton addTarget:self action:@selector(playVideo) forControlEvents:UIControlEventTouchUpInside];
|
||||||
|
|
||||||
|
UIImage *playImage = [UIImage imageNamed:@"play_button"];
|
||||||
|
[playVideoButton setBackgroundImage:playImage forState:UIControlStateNormal];
|
||||||
|
playVideoButton.contentMode = UIViewContentModeScaleAspectFill;
|
||||||
|
|
||||||
|
[self.view addSubview:playVideoButton];
|
||||||
|
|
||||||
|
CGFloat playVideoButtonWidth = ScaleFromIPhone5(70);
|
||||||
|
[playVideoButton autoSetDimensionsToSize:CGSizeMake(playVideoButtonWidth, playVideoButtonWidth)];
|
||||||
|
[playVideoButton autoCenterInSuperview];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Don't show footer bar after tapping approval-view
|
||||||
|
if (self.viewItem) {
|
||||||
|
UIToolbar *footerBar = [UIToolbar new];
|
||||||
|
_footerBar = footerBar;
|
||||||
|
footerBar.barTintColor = [UIColor ows_signalBrandBlueColor];
|
||||||
|
self.videoPlayBarButton =
|
||||||
|
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPlay
|
||||||
|
target:self
|
||||||
|
action:@selector(didPressPlayBarButton:)];
|
||||||
|
self.videoPauseBarButton =
|
||||||
|
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPause
|
||||||
|
target:self
|
||||||
|
action:@selector(didPressPauseBarButton:)];
|
||||||
|
[self updateFooterBarButtonItemsWithIsPlayingVideo:YES];
|
||||||
|
[self.view addSubview:footerBar];
|
||||||
|
|
||||||
|
[footerBar autoPinWidthToSuperview];
|
||||||
|
[footerBar autoPinToBottomLayoutGuideOfViewController:self withInset:0];
|
||||||
|
[footerBar autoSetDimension:ALDimensionHeight toSize:kFooterHeight];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)updateFooterBarButtonItemsWithIsPlayingVideo:(BOOL)isPlayingVideo
|
||||||
|
{
|
||||||
|
OWSAssert(self.footerBar);
|
||||||
|
|
||||||
|
NSMutableArray<UIBarButtonItem *> *toolbarItems = [NSMutableArray new];
|
||||||
|
|
||||||
|
[toolbarItems addObjectsFromArray:@[
|
||||||
|
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction
|
||||||
|
target:self
|
||||||
|
action:@selector(didPressShare:)],
|
||||||
|
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil],
|
||||||
|
]];
|
||||||
|
|
||||||
|
if (self.isVideo) {
|
||||||
|
// bar button video controls only work on iOS9+
|
||||||
|
if (@available(iOS 9.0, *)) {
|
||||||
|
UIBarButtonItem *playerButton = isPlayingVideo ? self.videoPauseBarButton : self.videoPlayBarButton;
|
||||||
|
[toolbarItems addObjectsFromArray:@[
|
||||||
|
playerButton,
|
||||||
|
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
|
||||||
|
target:nil
|
||||||
|
action:nil],
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[toolbarItems addObject:[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemTrash
|
||||||
|
target:self
|
||||||
|
action:@selector(didPressDelete:)]];
|
||||||
|
|
||||||
|
[self.footerBar setItems:toolbarItems animated:NO];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)applyInitialMediaViewConstraints
|
||||||
|
{
|
||||||
|
if (self.presentationViewConstraints.count > 0) {
|
||||||
|
[NSLayoutConstraint deactivateConstraints:self.presentationViewConstraints];
|
||||||
|
}
|
||||||
|
|
||||||
|
CGRect convertedRect = [self.presentationView.superview convertRect:self.originRect
|
||||||
|
fromView:[UIApplication sharedApplication].keyWindow];
|
||||||
|
|
||||||
|
NSMutableArray<NSLayoutConstraint *> *presentationViewConstraints = [NSMutableArray new];
|
||||||
|
self.presentationViewConstraints = presentationViewConstraints;
|
||||||
|
|
||||||
|
[presentationViewConstraints
|
||||||
|
addObjectsFromArray:[self.presentationView autoSetDimensionsToSize:convertedRect.size]];
|
||||||
|
[presentationViewConstraints addObjectsFromArray:@[
|
||||||
|
[self.presentationView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:convertedRect.origin.y],
|
||||||
|
[self.presentationView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:convertedRect.origin.x]
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)applyFinalMediaViewConstraints
|
||||||
|
{
|
||||||
|
if (self.presentationViewConstraints.count > 0) {
|
||||||
|
[NSLayoutConstraint deactivateConstraints:self.presentationViewConstraints];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableArray<NSLayoutConstraint *> *presentationViewConstraints = [NSMutableArray new];
|
||||||
|
self.presentationViewConstraints = presentationViewConstraints;
|
||||||
|
|
||||||
|
[presentationViewConstraints addObjectsFromArray:@[
|
||||||
|
[self.presentationView autoPinEdgeToSuperviewEdge:ALEdgeLeading],
|
||||||
|
[self.presentationView autoPinEdgeToSuperviewEdge:ALEdgeTop],
|
||||||
|
[self.presentationView autoPinEdgeToSuperviewEdge:ALEdgeTrailing],
|
||||||
|
[self.presentationView autoPinEdgeToSuperviewEdge:ALEdgeBottom]
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIView *)buildVideoPlayerView
|
||||||
|
{
|
||||||
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||||
|
if (![fileManager fileExistsAtPath:[self.attachmentUrl path]]) {
|
||||||
|
OWSFail(@"%@ Missing video file: %@", self.logTag, self.attachmentStream.mediaURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@available(iOS 9.0, *)) {
|
||||||
|
AVPlayer *player = [[AVPlayer alloc] initWithURL:self.attachmentUrl];
|
||||||
|
[player seekToTime:kCMTimeZero];
|
||||||
|
self.videoPlayer = player;
|
||||||
|
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
|
selector:@selector(playerItemDidPlayToCompletion:)
|
||||||
|
name:AVPlayerItemDidPlayToEndTimeNotification
|
||||||
|
object:player.currentItem];
|
||||||
|
|
||||||
|
VideoPlayerView *playerView = [VideoPlayerView new];
|
||||||
|
playerView.player = player;
|
||||||
|
|
||||||
|
[NSLayoutConstraint autoSetPriority:UILayoutPriorityDefaultLow
|
||||||
|
forConstraints:^{
|
||||||
|
[playerView autoSetDimensionsToSize:self.image.size];
|
||||||
|
}];
|
||||||
|
|
||||||
|
return playerView;
|
||||||
|
} else {
|
||||||
|
return [[UIImageView alloc] initWithImage:self.image];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setAreToolbarsHidden:(BOOL)areToolbarsHidden
|
||||||
|
{
|
||||||
|
if (_areToolbarsHidden == areToolbarsHidden) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_areToolbarsHidden = areToolbarsHidden;
|
||||||
|
|
||||||
|
// Hiding the status bar affects the positioing of the navbar. We don't want to show that in an animation, it's
|
||||||
|
// better to just have everythign "flit" in/out.
|
||||||
|
[[UIApplication sharedApplication] setStatusBarHidden:areToolbarsHidden withAnimation:UIStatusBarAnimationNone];
|
||||||
|
[self.navigationController setNavigationBarHidden:areToolbarsHidden animated:NO];
|
||||||
|
self.videoProgressBar.hidden = areToolbarsHidden;
|
||||||
|
|
||||||
|
// We don't animate the background color change because the old color shows through momentarily
|
||||||
|
// behind where the status bar "used to be".
|
||||||
|
self.view.backgroundColor = areToolbarsHidden ? UIColor.blackColor : UIColor.whiteColor;
|
||||||
|
|
||||||
|
[UIView animateWithDuration:0.1
|
||||||
|
animations:^(void) {
|
||||||
|
self.footerBar.alpha = areToolbarsHidden ? 0 : 1;
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)initializeGestureRecognizers
|
||||||
|
{
|
||||||
|
UITapGestureRecognizer *doubleTap =
|
||||||
|
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didDoubleTapImage:)];
|
||||||
|
doubleTap.numberOfTapsRequired = 2;
|
||||||
|
[self.view addGestureRecognizer:doubleTap];
|
||||||
|
|
||||||
|
UITapGestureRecognizer *singleTap =
|
||||||
|
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapImage:)];
|
||||||
|
[singleTap requireGestureRecognizerToFail:doubleTap];
|
||||||
|
|
||||||
|
[self.view addGestureRecognizer:singleTap];
|
||||||
|
|
||||||
|
// UISwipeGestureRecognizer supposedly supports multiple directions,
|
||||||
|
// but in practice it works better if you use a separate GR for each
|
||||||
|
// direction.
|
||||||
|
for (NSNumber *direction in @[
|
||||||
|
@(UISwipeGestureRecognizerDirectionRight),
|
||||||
|
@(UISwipeGestureRecognizerDirectionLeft),
|
||||||
|
@(UISwipeGestureRecognizerDirectionUp),
|
||||||
|
@(UISwipeGestureRecognizerDirectionDown),
|
||||||
|
]) {
|
||||||
|
UISwipeGestureRecognizer *swipe =
|
||||||
|
[[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(didSwipeImage:)];
|
||||||
|
swipe.direction = (UISwipeGestureRecognizerDirection) direction.integerValue;
|
||||||
|
swipe.delegate = self;
|
||||||
|
[self.view addGestureRecognizer:swipe];
|
||||||
|
}
|
||||||
|
|
||||||
|
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self
|
||||||
|
action:@selector(longPressGesture:)];
|
||||||
|
longPress.delegate = self;
|
||||||
|
[self.view addGestureRecognizer:longPress];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Gesture Recognizers
|
||||||
|
|
||||||
|
- (void)didTapDismissButton:(id)sender
|
||||||
|
{
|
||||||
|
[self dismissSelfAnimated:YES completion:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)didTapImage:(id)sender
|
||||||
|
{
|
||||||
|
DDLogVerbose(@"%@ did tap image.", self.logTag);
|
||||||
|
self.areToolbarsHidden = !self.areToolbarsHidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)didDoubleTapImage:(UITapGestureRecognizer *)gesture
|
||||||
|
{
|
||||||
|
DDLogVerbose(@"%@ did double tap image.", self.logTag);
|
||||||
|
if (self.scrollView.zoomScale == self.scrollView.minimumZoomScale) {
|
||||||
|
CGFloat kDoubleTapZoomScale = 2;
|
||||||
|
|
||||||
|
CGFloat zoomWidth = self.scrollView.width / kDoubleTapZoomScale;
|
||||||
|
CGFloat zoomHeight = self.scrollView.height / kDoubleTapZoomScale;
|
||||||
|
|
||||||
|
// center zoom rect around tapLocation
|
||||||
|
CGPoint tapLocation = [gesture locationInView:self.scrollView];
|
||||||
|
CGFloat zoomX = MAX(0, tapLocation.x - zoomWidth / 2);
|
||||||
|
CGFloat zoomY = MAX(0, tapLocation.y - zoomHeight / 2);
|
||||||
|
|
||||||
|
CGRect zoomRect = CGRectMake(zoomX, zoomY, zoomWidth, zoomHeight);
|
||||||
|
|
||||||
|
CGRect translatedRect = [self.mediaView convertRect:zoomRect fromView:self.scrollView];
|
||||||
|
|
||||||
|
[self.scrollView zoomToRect:translatedRect animated:YES];
|
||||||
|
} else {
|
||||||
|
// If already zoomed in at all, zoom out all the way.
|
||||||
|
[self.scrollView setZoomScale:self.scrollView.minimumZoomScale animated:YES];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)didSwipeImage:(UIGestureRecognizer *)sender
|
||||||
|
{
|
||||||
|
// Ignore if image is zoomed in at all.
|
||||||
|
// e.g. otherwise, for example, if the image is horizontally larger than the scroll
|
||||||
|
// view, but fits vertically, swiping left/right will scroll the image, but swiping up/down
|
||||||
|
// would dismiss the image. That would not be intuitive.
|
||||||
|
if (self.scrollView.zoomScale != self.scrollView.minimumZoomScale) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self dismissSelfAnimated:YES completion:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)longPressGesture:(UIGestureRecognizer *)sender {
|
||||||
|
// We "eagerly" respond when the long press begins, not when it ends.
|
||||||
|
if (sender.state == UIGestureRecognizerStateBegan) {
|
||||||
|
if (!self.viewItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self.view becomeFirstResponder];
|
||||||
|
|
||||||
|
if ([UIMenuController sharedMenuController].isMenuVisible) {
|
||||||
|
[[UIMenuController sharedMenuController] setMenuVisible:NO
|
||||||
|
animated:NO];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSArray *menuItems = self.viewItem.mediaMenuControllerItems;
|
||||||
|
[UIMenuController sharedMenuController].menuItems = menuItems;
|
||||||
|
CGPoint location = [sender locationInView:self.view];
|
||||||
|
CGRect targetRect = CGRectMake(location.x,
|
||||||
|
location.y,
|
||||||
|
1, 1);
|
||||||
|
[[UIMenuController sharedMenuController] setTargetRect:targetRect
|
||||||
|
inView:self.view];
|
||||||
|
[[UIMenuController sharedMenuController] setMenuVisible:YES
|
||||||
|
animated:YES];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)didPressShare:(id)sender
|
||||||
|
{
|
||||||
|
DDLogInfo(@"%@: didPressShare", self.logTag);
|
||||||
|
if (!self.viewItem) {
|
||||||
|
OWSFail(@"share should only be available when a viewItem is present");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self.viewItem shareMediaAction];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)didPressDelete:(id)sender
|
||||||
|
{
|
||||||
|
DDLogInfo(@"%@: didPressDelete", self.logTag);
|
||||||
|
if (!self.viewItem) {
|
||||||
|
OWSFail(@"delete should only be available when a viewItem is present");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIAlertController *actionSheet =
|
||||||
|
[UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
|
||||||
|
|
||||||
|
[actionSheet
|
||||||
|
addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_DELETE_TITLE", nil)
|
||||||
|
style:UIAlertActionStyleDestructive
|
||||||
|
handler:^(UIAlertAction *action) {
|
||||||
|
OWSAssert([self.presentingViewController
|
||||||
|
isKindOfClass:[UINavigationController class]]);
|
||||||
|
UINavigationController *navController
|
||||||
|
= (UINavigationController *)self.presentingViewController;
|
||||||
|
|
||||||
|
if ([navController.topViewController
|
||||||
|
isKindOfClass:[ConversationViewController class]]) {
|
||||||
|
[self dismissSelfAnimated:YES
|
||||||
|
completion:^{
|
||||||
|
[self.viewItem deleteAction];
|
||||||
|
}];
|
||||||
|
} else if ([navController.topViewController
|
||||||
|
isKindOfClass:[MessageDetailViewController class]]) {
|
||||||
|
[self dismissSelfAnimated:NO
|
||||||
|
completion:^{
|
||||||
|
[self.viewItem deleteAction];
|
||||||
|
}];
|
||||||
|
[navController popViewControllerAnimated:YES];
|
||||||
|
} else {
|
||||||
|
OWSFail(@"Unexpected presentation context.");
|
||||||
|
[self dismissSelfAnimated:YES
|
||||||
|
completion:^{
|
||||||
|
[self.viewItem deleteAction];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}]];
|
||||||
|
|
||||||
|
[actionSheet addAction:[OWSAlerts cancelAction]];
|
||||||
|
|
||||||
|
[self presentViewController:actionSheet animated:YES completion:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender
|
||||||
|
{
|
||||||
|
if (self.viewItem == nil) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already in detail view, so no link to "info"
|
||||||
|
if (action == self.viewItem.metadataActionSelector) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
return [self.viewItem canPerformAction:action];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)copyMediaAction:(nullable id)sender
|
||||||
|
{
|
||||||
|
if (!self.viewItem) {
|
||||||
|
OWSFail(@"copy should only be available when a viewItem is present");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self.viewItem copyMediaAction];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)shareMediaAction:(nullable id)sender
|
||||||
|
{
|
||||||
|
if (!self.viewItem) {
|
||||||
|
OWSFail(@"share should only be available when a viewItem is present");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self didPressShare:sender];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)saveMediaAction:(nullable id)sender
|
||||||
|
{
|
||||||
|
if (!self.viewItem) {
|
||||||
|
OWSFail(@"save should only be available when a viewItem is present");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self.viewItem saveMediaAction];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)deleteAction:(nullable id)sender
|
||||||
|
{
|
||||||
|
if (!self.viewItem) {
|
||||||
|
OWSFail(@"delete should only be available when a viewItem is present");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self didPressDelete:sender];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)didPressPlayBarButton:(id)sender
|
||||||
|
{
|
||||||
|
OWSAssert(self.isVideo);
|
||||||
|
OWSAssert(self.videoPlayer);
|
||||||
|
[self playVideo];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)didPressPauseBarButton:(id)sender
|
||||||
|
{
|
||||||
|
OWSAssert(self.isVideo);
|
||||||
|
OWSAssert(self.videoPlayer);
|
||||||
|
[self pauseVideo];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Presentation
|
||||||
|
|
||||||
|
- (void)presentFromViewController:(UIViewController *)viewController replacingView:(UIView *)view;
|
||||||
|
{
|
||||||
|
self.replacingView = view;
|
||||||
|
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:self];
|
||||||
|
|
||||||
|
// UIModalPresentationCustom retains the current view context behind our VC, allowing us to manually
|
||||||
|
// animate in our view, over the existing context, similar to a cross disolve, but allowing us to have
|
||||||
|
// more fine grained control
|
||||||
|
navController.modalPresentationStyle = UIModalPresentationCustom;
|
||||||
|
navController.navigationBar.barTintColor = UIColor.ows_materialBlueColor;
|
||||||
|
navController.navigationBar.translucent = NO;
|
||||||
|
navController.navigationBar.opaque = YES;
|
||||||
|
|
||||||
|
self.view.userInteractionEnabled = NO;
|
||||||
|
|
||||||
|
// We want to animate the tapped media from it's position in the previous VC
|
||||||
|
// to it's resting place in the center of this view controller.
|
||||||
|
//
|
||||||
|
// Rather than animating the actual media view in place, we animate the presentationView, which is a static
|
||||||
|
// image of the media content. Animating the actual media view is problematic for a couple reasons:
|
||||||
|
// 1. The media view ultimately lives in a zoomable scrollView. Getting both original positioning and the final positioning
|
||||||
|
// correct, involves manipulating the zoomScale and position simultaneously, which results in non-linear movement,
|
||||||
|
// especially noticeable on high resolution images.
|
||||||
|
// 2. For Video views, the AVPlayerLayer content does not scale with the presentation animation. So you instead get a full scale
|
||||||
|
// video, wherein only the cropping is animated.
|
||||||
|
// Using a simple image view allows us to address both these problems relatively easily.
|
||||||
|
self.view.alpha = 0.0;
|
||||||
|
|
||||||
|
self.mediaView.hidden = YES;
|
||||||
|
self.presentationView.hidden = NO;
|
||||||
|
self.presentationView.layer.cornerRadius = OWSMessageCellCornerRadius;
|
||||||
|
|
||||||
|
[viewController presentViewController:navController
|
||||||
|
animated:NO
|
||||||
|
completion:^{
|
||||||
|
|
||||||
|
// 1. Fade in the entire view.
|
||||||
|
[UIView animateWithDuration:0.1
|
||||||
|
animations:^{
|
||||||
|
self.replacingView.alpha = 0.0;
|
||||||
|
self.view.alpha = 1.0;
|
||||||
|
}];
|
||||||
|
|
||||||
|
[self.presentationView.superview layoutIfNeeded];
|
||||||
|
[self applyFinalMediaViewConstraints];
|
||||||
|
|
||||||
|
// 2. Animate imageView from it's initial position, which should match where it was
|
||||||
|
// in the presenting view to it's final position, front and center in this view. This
|
||||||
|
// animation duration intentionally overlaps the previous
|
||||||
|
[UIView animateWithDuration:0.2
|
||||||
|
delay:0.08
|
||||||
|
options:UIViewAnimationOptionCurveEaseOut
|
||||||
|
animations:^(void) {
|
||||||
|
self.presentationView.layer.cornerRadius = 0;
|
||||||
|
[self.presentationView.superview layoutIfNeeded];
|
||||||
|
|
||||||
|
// We must lay out once *before* we centerMediaViewConstraints
|
||||||
|
// because it uses the imageView.frame to build the constraints
|
||||||
|
// that will center the imageView, and then once again *after*
|
||||||
|
// to ensure that the centered constraints are applied.
|
||||||
|
[self centerMediaViewConstraints];
|
||||||
|
[self.mediaView.superview layoutIfNeeded];
|
||||||
|
self.view.backgroundColor = UIColor.whiteColor;
|
||||||
|
}
|
||||||
|
completion:^(BOOL finished) {
|
||||||
|
// HACK: Setting the frame to itself *seems* like it should be a no-op, but
|
||||||
|
// it ensures the content is drawn at the right frame. In particular I was reproducibly
|
||||||
|
// some images squished (they were EXIF rotated, maybe relateed).
|
||||||
|
// similar to this report: https://stackoverflow.com/questions/27961884/swift-uiimageview-stretched-aspect
|
||||||
|
self.mediaView.frame = self.mediaView.frame;
|
||||||
|
|
||||||
|
// At this point our presentation view should be overlayed perfectly
|
||||||
|
// with our media view. Swapping them out should be imperceptible.
|
||||||
|
self.mediaView.hidden = NO;
|
||||||
|
self.presentationView.hidden = YES;
|
||||||
|
|
||||||
|
self.view.userInteractionEnabled = YES;
|
||||||
|
|
||||||
|
if (self.isVideo) {
|
||||||
|
[self playVideo];
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)dismissSelfAnimated:(BOOL)isAnimated completion:(void (^_Nullable)(void))completion
|
||||||
|
{
|
||||||
|
|
||||||
|
self.view.userInteractionEnabled = NO;
|
||||||
|
[UIApplication sharedApplication].statusBarHidden = NO;
|
||||||
|
|
||||||
|
// Swapping mediaView for presentationView will be perceptible if we're not zoomed out all the way.
|
||||||
|
if (self.scrollView.zoomScale != self.scrollView.minimumZoomScale) {
|
||||||
|
[self.scrollView setZoomScale:self.scrollView.minimumZoomScale animated:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
self.mediaView.hidden = YES;
|
||||||
|
self.presentationView.hidden = NO;
|
||||||
|
|
||||||
|
// Move the presentationView back to it's initial position, i.e. where
|
||||||
|
// it sits on the screen in the conversation view.
|
||||||
|
[self applyInitialMediaViewConstraints];
|
||||||
|
|
||||||
|
if (isAnimated) {
|
||||||
|
[UIView animateWithDuration:0.18
|
||||||
|
delay:0.0
|
||||||
|
options:UIViewAnimationOptionCurveEaseOut
|
||||||
|
animations:^(void) {
|
||||||
|
[self.presentationView.superview layoutIfNeeded];
|
||||||
|
self.presentationView.layer.cornerRadius = OWSMessageCellCornerRadius;
|
||||||
|
|
||||||
|
// In case user has hidden bars, which changes background to black.
|
||||||
|
self.view.backgroundColor = UIColor.whiteColor;
|
||||||
|
|
||||||
|
}
|
||||||
|
completion:nil];
|
||||||
|
|
||||||
|
[UIView animateWithDuration:0.1
|
||||||
|
delay:0.15
|
||||||
|
options:UIViewAnimationOptionCurveEaseInOut
|
||||||
|
animations:^(void) {
|
||||||
|
|
||||||
|
OWSAssert(self.replacingView);
|
||||||
|
self.replacingView.alpha = 1.0;
|
||||||
|
|
||||||
|
// fade out content and toolbars
|
||||||
|
self.navigationController.view.alpha = 0.0;
|
||||||
|
}
|
||||||
|
completion:^(BOOL finished) {
|
||||||
|
[self.presentingViewController dismissViewControllerAnimated:NO completion:completion];
|
||||||
|
}];
|
||||||
|
|
||||||
|
} else {
|
||||||
|
self.replacingView.alpha = 1.0;
|
||||||
|
[self.presentingViewController dismissViewControllerAnimated:NO completion:completion];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - UIScrollViewDelegate
|
||||||
|
|
||||||
|
- (nullable UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
|
||||||
|
{
|
||||||
|
return self.mediaView;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)centerMediaViewConstraints
|
||||||
|
{
|
||||||
|
OWSAssert(self.scrollView);
|
||||||
|
|
||||||
|
CGSize scrollViewSize = self.scrollView.bounds.size;
|
||||||
|
CGSize imageViewSize = self.mediaView.frame.size;
|
||||||
|
|
||||||
|
CGFloat yOffset = MAX(0, (scrollViewSize.height - imageViewSize.height) / 2);
|
||||||
|
self.mediaViewTopConstraint.constant = yOffset;
|
||||||
|
self.mediaViewBottomConstraint.constant = yOffset;
|
||||||
|
|
||||||
|
CGFloat xOffset = MAX(0, (scrollViewSize.width - imageViewSize.width) / 2);
|
||||||
|
self.mediaViewLeadingConstraint.constant = xOffset;
|
||||||
|
self.mediaViewTrailingConstraint.constant = xOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)scrollViewDidZoom:(UIScrollView *)scrollView
|
||||||
|
{
|
||||||
|
[self centerMediaViewConstraints];
|
||||||
|
[self.view layoutIfNeeded];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Video Playback
|
||||||
|
|
||||||
|
- (void)playVideo
|
||||||
|
{
|
||||||
|
if (@available(iOS 9, *)) {
|
||||||
|
OWSAssert(self.videoPlayer);
|
||||||
|
AVPlayer *player = self.videoPlayer;
|
||||||
|
|
||||||
|
[self updateFooterBarButtonItemsWithIsPlayingVideo:YES];
|
||||||
|
self.playVideoButton.hidden = YES;
|
||||||
|
self.areToolbarsHidden = YES;
|
||||||
|
|
||||||
|
OWSAssert(player.currentItem);
|
||||||
|
AVPlayerItem *item = player.currentItem;
|
||||||
|
if (CMTIME_COMPARE_INLINE(item.currentTime, ==, item.duration)) {
|
||||||
|
// Rewind for repeated plays
|
||||||
|
[player seekToTime:kCMTimeZero];
|
||||||
|
}
|
||||||
|
|
||||||
|
[player play];
|
||||||
|
} else {
|
||||||
|
[self legacyPlayVideo];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)pauseVideo
|
||||||
|
{
|
||||||
|
OWSAssert(self.isVideo);
|
||||||
|
OWSAssert(self.videoPlayer);
|
||||||
|
|
||||||
|
[self updateFooterBarButtonItemsWithIsPlayingVideo:NO];
|
||||||
|
[self.videoPlayer pause];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)playerItemDidPlayToCompletion:(NSNotification *)notification
|
||||||
|
{
|
||||||
|
OWSAssert(self.isVideo);
|
||||||
|
OWSAssert(self.videoPlayer);
|
||||||
|
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
|
||||||
|
|
||||||
|
self.areToolbarsHidden = NO;
|
||||||
|
self.playVideoButton.hidden = NO;
|
||||||
|
|
||||||
|
[self updateFooterBarButtonItemsWithIsPlayingVideo:NO];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)playerProgressBarDidStartScrubbing:(PlayerProgressBar *)playerProgressBar
|
||||||
|
{
|
||||||
|
OWSAssert(self.videoPlayer);
|
||||||
|
[self.videoPlayer pause];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)playerProgressBar:(PlayerProgressBar *)playerProgressBar scrubbedToTime:(CMTime)time
|
||||||
|
{
|
||||||
|
OWSAssert(self.videoPlayer);
|
||||||
|
[self.videoPlayer seekToTime:time];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)playerProgressBar:(PlayerProgressBar *)playerProgressBar
|
||||||
|
didFinishScrubbingAtTime:(CMTime)time
|
||||||
|
shouldResumePlayback:(BOOL)shouldResumePlayback
|
||||||
|
{
|
||||||
|
OWSAssert(self.videoPlayer);
|
||||||
|
[self.videoPlayer seekToTime:time];
|
||||||
|
|
||||||
|
if (shouldResumePlayback) {
|
||||||
|
[self.videoPlayer play];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark iOS8 Video Playback
|
||||||
|
|
||||||
|
// AVPlayer was introduced in iOS9, so on iOS8 we fall back to MPMoviePlayer
|
||||||
|
// This causes an unforutnate "double present" since we present the full screen view and then the MPMovie view over top.
|
||||||
|
// And similarly a double dismiss.
|
||||||
|
- (void)legacyPlayVideo
|
||||||
|
{
|
||||||
|
if (@available(iOS 9.0, *)) {
|
||||||
|
OWSFail(@"legacy video is for iOS8 only");
|
||||||
|
}
|
||||||
|
MPMoviePlayerViewController *vc = [[MPMoviePlayerViewController alloc] initWithContentURL:self.attachmentUrl];
|
||||||
|
|
||||||
|
[self presentViewController:vc animated:YES completion:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Saving images to Camera Roll
|
||||||
|
|
||||||
|
- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo {
|
||||||
|
if (error) {
|
||||||
|
DDLogWarn(@"There was a problem saving <%@> to camera roll from %s ",
|
||||||
|
error.localizedDescription,
|
||||||
|
__PRETTY_FUNCTION__);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
|
@ -12,7 +12,7 @@ enum MessageMetadataViewMode: UInt {
|
||||||
case focusOnMetadata
|
case focusOnMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
class MessageDetailViewController: OWSViewController, UIScrollViewDelegate {
|
class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, MediaDetailPresenter {
|
||||||
|
|
||||||
static let TAG = "[MessageDetailViewController]"
|
static let TAG = "[MessageDetailViewController]"
|
||||||
let TAG = "[MessageDetailViewController]"
|
let TAG = "[MessageDetailViewController]"
|
||||||
|
@ -405,7 +405,7 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let attachment = TSAttachment.fetch(uniqueId: attachmentId, transaction: transaction) else {
|
guard let attachment = TSAttachment.fetch(uniqueId: attachmentId, transaction: transaction) else {
|
||||||
owsFail("Missing attachment")
|
Logger.warn("\(TAG) Missing attachment. Was it deleted?")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -414,9 +414,9 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate {
|
||||||
|
|
||||||
private func addAttachmentRows() -> [UIView] {
|
private func addAttachmentRows() -> [UIView] {
|
||||||
var rows = [UIView]()
|
var rows = [UIView]()
|
||||||
|
|
||||||
guard let attachment = self.attachment else {
|
guard let attachment = self.attachment else {
|
||||||
owsFail("no attachment to add.")
|
Logger.warn("\(TAG) Missing attachment. Was it deleted?")
|
||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -442,7 +442,8 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate {
|
||||||
let contentType = attachment.contentType
|
let contentType = attachment.contentType
|
||||||
if let dataUTI = MIMETypeUtil.utiType(forMIMEType: contentType) {
|
if let dataUTI = MIMETypeUtil.utiType(forMIMEType: contentType) {
|
||||||
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI, imageQuality: .original)
|
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI, imageQuality: .original)
|
||||||
let mediaMessageView = MediaMessageView(attachment: attachment, mode: .small)
|
let mediaMessageView = MediaMessageView(attachment: attachment, mode: .small, mediaDetailPresenter: self)
|
||||||
|
|
||||||
mediaMessageView.backgroundColor = UIColor.white
|
mediaMessageView.backgroundColor = UIColor.white
|
||||||
self.mediaMessageView = mediaMessageView
|
self.mediaMessageView = mediaMessageView
|
||||||
rows.append(mediaMessageView)
|
rows.append(mediaMessageView)
|
||||||
|
@ -751,4 +752,18 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate {
|
||||||
|
|
||||||
updateTextLayout()
|
updateTextLayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: MediaDetailPresenter
|
||||||
|
|
||||||
|
public func presentDetails(mediaMessageView: MediaMessageView, fromView: UIView) {
|
||||||
|
let window = UIApplication.shared.keyWindow
|
||||||
|
let convertedRect = fromView.convert(fromView.bounds, to:window)
|
||||||
|
guard let attachmentStream = self.attachmentStream else {
|
||||||
|
owsFail("attachment stream unexpectedly nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let mediaDetailViewController = MediaDetailViewController(attachmentStream: attachmentStream, from: convertedRect, viewItem: self.viewItem)
|
||||||
|
mediaDetailViewController.present(from: self, replacing: fromView)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,200 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@available(iOS 9.0, *)
|
||||||
|
@objc
|
||||||
|
public class VideoPlayerView: UIView {
|
||||||
|
var player: AVPlayer? {
|
||||||
|
get {
|
||||||
|
return playerLayer.player
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
playerLayer.player = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var playerLayer: AVPlayerLayer {
|
||||||
|
return layer as! AVPlayerLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override UIView property
|
||||||
|
override public static var layerClass: AnyClass {
|
||||||
|
return AVPlayerLayer.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 9.0, *)
|
||||||
|
@objc
|
||||||
|
public protocol PlayerProgressBarDelegate {
|
||||||
|
func playerProgressBarDidStartScrubbing(_ playerProgressBar: PlayerProgressBar)
|
||||||
|
func playerProgressBar(_ playerProgressBar: PlayerProgressBar, scrubbedToTime time: CMTime)
|
||||||
|
func playerProgressBar(_ playerProgressBar: PlayerProgressBar, didFinishScrubbingAtTime time: CMTime, shouldResumePlayback: Bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 9.0, *)
|
||||||
|
@objc
|
||||||
|
public class PlayerProgressBar: UIView {
|
||||||
|
public let TAG = "[PlayerProgressBar]"
|
||||||
|
|
||||||
|
@objc
|
||||||
|
public weak var delegate: PlayerProgressBarDelegate?
|
||||||
|
|
||||||
|
private lazy var formatter: DateComponentsFormatter = {
|
||||||
|
let formatter = DateComponentsFormatter()
|
||||||
|
formatter.unitsStyle = .positional
|
||||||
|
formatter.allowedUnits = [.minute, .second ]
|
||||||
|
formatter.zeroFormattingBehavior = [ .pad ]
|
||||||
|
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: Subviews
|
||||||
|
private let positionLabel = UILabel()
|
||||||
|
private let remainingLabel = UILabel()
|
||||||
|
private let slider = UISlider()
|
||||||
|
private let blurEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
|
||||||
|
weak private var progressObserver: AnyObject?
|
||||||
|
|
||||||
|
private let kPreferredTimeScale: CMTimeScale = 100
|
||||||
|
|
||||||
|
public var player: AVPlayer? {
|
||||||
|
didSet {
|
||||||
|
guard let item = player?.currentItem else {
|
||||||
|
owsFail("No player item")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slider.minimumValue = 0
|
||||||
|
|
||||||
|
let duration: CMTime = item.asset.duration
|
||||||
|
slider.maximumValue = Float(CMTimeGetSeconds(duration))
|
||||||
|
|
||||||
|
// OPTIMIZE We need a high frequency observer for smooth slider updates,
|
||||||
|
// but could use a much less frequent observer for label updates
|
||||||
|
progressObserver = player?.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.01, preferredTimescale: kPreferredTimeScale), queue: nil, using: { [weak self] (_) in
|
||||||
|
self?.updateState()
|
||||||
|
}) as AnyObject
|
||||||
|
updateState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override public init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
// Background
|
||||||
|
backgroundColor = UIColor.lightGray.withAlphaComponent(0.5)
|
||||||
|
if !UIAccessibilityIsReduceTransparencyEnabled() {
|
||||||
|
addSubview(blurEffectView)
|
||||||
|
blurEffectView.autoPinToSuperviewEdges()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure controls
|
||||||
|
|
||||||
|
let kLabelFont = UIFont.monospacedDigitSystemFont(ofSize: 12, weight: UIFontWeightRegular)
|
||||||
|
positionLabel.font = kLabelFont
|
||||||
|
remainingLabel.font = kLabelFont
|
||||||
|
|
||||||
|
// We use a smaller thumb for the progress slider.
|
||||||
|
slider.setThumbImage(#imageLiteral(resourceName: "sliderProgressThumb"), for: .normal)
|
||||||
|
slider.maximumTrackTintColor = UIColor.ows_black
|
||||||
|
slider.minimumTrackTintColor = UIColor.ows_black
|
||||||
|
|
||||||
|
slider.addTarget(self, action: #selector(handleSliderTouchDown), for: .touchDown)
|
||||||
|
slider.addTarget(self, action: #selector(handleSliderTouchUp), for: .touchUpInside)
|
||||||
|
slider.addTarget(self, action: #selector(handleSliderTouchUp), for: .touchUpOutside)
|
||||||
|
slider.addTarget(self, action: #selector(handleSliderValueChanged), for: .valueChanged)
|
||||||
|
|
||||||
|
// Layout Subviews
|
||||||
|
|
||||||
|
addSubview(positionLabel)
|
||||||
|
addSubview(remainingLabel)
|
||||||
|
addSubview(slider)
|
||||||
|
|
||||||
|
positionLabel.autoPinEdge(toSuperviewMargin: .leading)
|
||||||
|
positionLabel.autoVCenterInSuperview()
|
||||||
|
|
||||||
|
let kSliderMargin: CGFloat = 8
|
||||||
|
|
||||||
|
slider.autoPinEdge(.leading, to: .trailing, of: positionLabel, withOffset: kSliderMargin)
|
||||||
|
slider.autoVCenterInSuperview()
|
||||||
|
|
||||||
|
remainingLabel.autoPinEdge(.leading, to: .trailing, of: slider, withOffset: kSliderMargin)
|
||||||
|
remainingLabel.autoPinEdge(toSuperviewMargin: .trailing)
|
||||||
|
remainingLabel.autoVCenterInSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Gesture handling
|
||||||
|
|
||||||
|
var wasPlayingWhenScrubbingStarted: Bool = false
|
||||||
|
|
||||||
|
@objc
|
||||||
|
private func handleSliderTouchDown(_ slider: UISlider) {
|
||||||
|
guard let player = self.player else {
|
||||||
|
owsFail("player was nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.wasPlayingWhenScrubbingStarted = (player.rate != 0) && (player.error == nil)
|
||||||
|
|
||||||
|
self.delegate?.playerProgressBarDidStartScrubbing(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
private func handleSliderTouchUp(_ slider: UISlider) {
|
||||||
|
let sliderTime = time(slider: slider)
|
||||||
|
self.delegate?.playerProgressBar(self, didFinishScrubbingAtTime: sliderTime, shouldResumePlayback:wasPlayingWhenScrubbingStarted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
private func handleSliderValueChanged(_ slider: UISlider) {
|
||||||
|
let sliderTime = time(slider: slider)
|
||||||
|
self.delegate?.playerProgressBar(self, scrubbedToTime: sliderTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Render cycle
|
||||||
|
|
||||||
|
private func updateState() {
|
||||||
|
guard let player = player else {
|
||||||
|
owsFail("\(TAG) player isn't set.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let item = player.currentItem else {
|
||||||
|
owsFail("\(TAG) player has no item.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let position = player.currentTime()
|
||||||
|
let positionSeconds: Float64 = CMTimeGetSeconds(position)
|
||||||
|
positionLabel.text = formatter.string(from: positionSeconds)
|
||||||
|
|
||||||
|
let duration: CMTime = item.asset.duration
|
||||||
|
let remainingTime = duration - position
|
||||||
|
let remainingSeconds = CMTimeGetSeconds(remainingTime)
|
||||||
|
|
||||||
|
guard let remainingString = formatter.string(from: remainingSeconds) else {
|
||||||
|
owsFail("unable to format time remaining")
|
||||||
|
remainingLabel.text = "0:00"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// show remaining time as negative
|
||||||
|
remainingLabel.text = "-\(remainingString)"
|
||||||
|
|
||||||
|
slider.setValue(Float(positionSeconds), animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Util
|
||||||
|
|
||||||
|
private func time(slider: UISlider) -> CMTime {
|
||||||
|
let seconds: Double = Double(slider.value)
|
||||||
|
return CMTime(seconds: seconds, preferredTimescale: kPreferredTimeScale)
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,6 @@ FOUNDATION_EXPORT const unsigned char SignalMessagingVersionString[];
|
||||||
#import <SignalMessaging/ContactsViewHelper.h>
|
#import <SignalMessaging/ContactsViewHelper.h>
|
||||||
#import <SignalMessaging/DebugLogger.h>
|
#import <SignalMessaging/DebugLogger.h>
|
||||||
#import <SignalMessaging/Environment.h>
|
#import <SignalMessaging/Environment.h>
|
||||||
#import <SignalMessaging/FullImageViewController.h>
|
|
||||||
#import <SignalMessaging/NSString+OWS.h>
|
#import <SignalMessaging/NSString+OWS.h>
|
||||||
#import <SignalMessaging/OWSAudioAttachmentPlayer.h>
|
#import <SignalMessaging/OWSAudioAttachmentPlayer.h>
|
||||||
#import <SignalMessaging/OWSContactAvatarBuilder.h>
|
#import <SignalMessaging/OWSContactAvatarBuilder.h>
|
||||||
|
|
|
@ -202,7 +202,8 @@ public class AttachmentApprovalViewController: OWSViewController, CaptioningTool
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
public func playButtonTapped() {
|
public func playButtonTapped() {
|
||||||
mediaMessageView.playVideo()
|
// FIXME - use built in AVPlayer controls like MediaDetailViewController
|
||||||
|
// mediaMessageView.playVideo()
|
||||||
}
|
}
|
||||||
|
|
||||||
func cancelPressed(sender: UIButton) {
|
func cancelPressed(sender: UIButton) {
|
||||||
|
|
|
@ -1,578 +0,0 @@
|
||||||
//
|
|
||||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "FullImageViewController.h"
|
|
||||||
#import "AttachmentSharing.h"
|
|
||||||
#import "ConversationViewItem.h"
|
|
||||||
#import "TSAttachmentStream.h"
|
|
||||||
#import "TSInteraction.h"
|
|
||||||
#import "UIColor+OWS.h"
|
|
||||||
#import "UIUtil.h"
|
|
||||||
#import "UIView+OWS.h"
|
|
||||||
#import <SignalMessaging/SignalMessaging-Swift.h>
|
|
||||||
#import <SignalServiceKit/AppContext.h>
|
|
||||||
#import <SignalServiceKit/NSData+Image.h>
|
|
||||||
#import <YYImage/YYImage.h>
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
|
||||||
|
|
||||||
#define kMinZoomScale 1.0f
|
|
||||||
#define kMaxZoomScale 8.0f
|
|
||||||
|
|
||||||
#define kBackgroundAlpha 0.6f
|
|
||||||
|
|
||||||
// In order to use UIMenuController, the view from which it is
|
|
||||||
// presented must have certain custom behaviors.
|
|
||||||
@interface AttachmentMenuView : UIView
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
#pragma mark -
|
|
||||||
|
|
||||||
@implementation AttachmentMenuView
|
|
||||||
|
|
||||||
- (BOOL)canBecomeFirstResponder
|
|
||||||
{
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We only use custom actions in UIMenuController.
|
|
||||||
- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender
|
|
||||||
{
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
#pragma mark -
|
|
||||||
|
|
||||||
@interface FullImageViewController () <UIScrollViewDelegate, UIGestureRecognizerDelegate>
|
|
||||||
|
|
||||||
@property (nonatomic) UIView *backgroundView;
|
|
||||||
@property (nonatomic) UIScrollView *scrollView;
|
|
||||||
@property (nonatomic) UIImageView *imageView;
|
|
||||||
@property (nonatomic) UIButton *shareButton;
|
|
||||||
@property (nonatomic) UIView *contentView;
|
|
||||||
|
|
||||||
@property (nonatomic) CGRect originRect;
|
|
||||||
@property (nonatomic) BOOL isPresenting;
|
|
||||||
@property (nonatomic) NSData *fileData;
|
|
||||||
|
|
||||||
@property (nonatomic, nullable) TSAttachmentStream *attachmentStream;
|
|
||||||
@property (nonatomic, nullable) SignalAttachment *attachment;
|
|
||||||
@property (nonatomic, nullable) ConversationViewItem *viewItem;
|
|
||||||
|
|
||||||
@property (nonatomic) UIToolbar *footerBar;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation FullImageViewController
|
|
||||||
|
|
||||||
- (instancetype)initWithAttachmentStream:(TSAttachmentStream *)attachmentStream
|
|
||||||
fromRect:(CGRect)rect
|
|
||||||
viewItem:(ConversationViewItem *_Nullable)viewItem
|
|
||||||
{
|
|
||||||
|
|
||||||
self = [super initWithNibName:nil bundle:nil];
|
|
||||||
|
|
||||||
if (self) {
|
|
||||||
self.attachmentStream = attachmentStream;
|
|
||||||
self.originRect = rect;
|
|
||||||
self.viewItem = viewItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (instancetype)initWithAttachment:(SignalAttachment *)attachment fromRect:(CGRect)rect
|
|
||||||
{
|
|
||||||
|
|
||||||
self = [super initWithNibName:nil bundle:nil];
|
|
||||||
|
|
||||||
if (self) {
|
|
||||||
self.attachment = attachment;
|
|
||||||
self.originRect = rect;
|
|
||||||
}
|
|
||||||
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSURL *_Nullable)attachmentUrl
|
|
||||||
{
|
|
||||||
if (self.attachmentStream) {
|
|
||||||
return self.attachmentStream.mediaURL;
|
|
||||||
} else if (self.attachment) {
|
|
||||||
return self.attachment.dataUrl;
|
|
||||||
} else {
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSData *)fileData
|
|
||||||
{
|
|
||||||
if (!_fileData) {
|
|
||||||
NSURL *_Nullable url = self.attachmentUrl;
|
|
||||||
if (url) {
|
|
||||||
_fileData = [NSData dataWithContentsOfURL:url];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return _fileData;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (UIImage *)image
|
|
||||||
{
|
|
||||||
if (self.attachmentStream) {
|
|
||||||
return self.attachmentStream.image;
|
|
||||||
} else if (self.attachment) {
|
|
||||||
return self.attachment.image;
|
|
||||||
} else {
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)isAnimated
|
|
||||||
{
|
|
||||||
if (self.attachmentStream) {
|
|
||||||
return self.attachmentStream.isAnimated;
|
|
||||||
} else if (self.attachment) {
|
|
||||||
return self.attachment.isAnimatedImage;
|
|
||||||
} else {
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)loadView
|
|
||||||
{
|
|
||||||
self.view = [AttachmentMenuView new];
|
|
||||||
self.view.backgroundColor = [UIColor colorWithWhite:0 alpha:kBackgroundAlpha];
|
|
||||||
self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)viewDidLoad
|
|
||||||
{
|
|
||||||
[super viewDidLoad];
|
|
||||||
|
|
||||||
[self initializeBackground];
|
|
||||||
[self initializeContentViewAndFooterBar];
|
|
||||||
[self initializeScrollView];
|
|
||||||
[self initializeImageView];
|
|
||||||
[self initializeGestureRecognizers];
|
|
||||||
|
|
||||||
[self populateImageView:self.image];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)viewWillDisappear:(BOOL)animated
|
|
||||||
{
|
|
||||||
[super viewWillDisappear:animated];
|
|
||||||
|
|
||||||
if ([UIMenuController sharedMenuController].isMenuVisible) {
|
|
||||||
[[UIMenuController sharedMenuController] setMenuVisible:NO animated:NO];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - Initializers
|
|
||||||
|
|
||||||
- (void)initializeBackground
|
|
||||||
{
|
|
||||||
self.imageView.backgroundColor = [UIColor colorWithWhite:0 alpha:kBackgroundAlpha];
|
|
||||||
|
|
||||||
self.backgroundView = [UIView new];
|
|
||||||
self.backgroundView.backgroundColor = [UIColor colorWithWhite:0 alpha:kBackgroundAlpha];
|
|
||||||
[self.view addSubview:self.backgroundView];
|
|
||||||
[self.backgroundView autoPinEdgesToSuperviewEdges];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)initializeContentViewAndFooterBar
|
|
||||||
{
|
|
||||||
self.contentView = [UIView new];
|
|
||||||
[self.backgroundView addSubview:self.contentView];
|
|
||||||
[self.contentView autoPinWidthToSuperview];
|
|
||||||
[self.contentView autoPinToTopLayoutGuideOfViewController:self withInset:0];
|
|
||||||
|
|
||||||
self.footerBar = [UIToolbar new];
|
|
||||||
_footerBar.barTintColor = [UIColor ows_signalBrandBlueColor];
|
|
||||||
if (CurrentAppContext().isMainApp) {
|
|
||||||
[self.footerBar setItems:@[
|
|
||||||
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
|
|
||||||
target:nil
|
|
||||||
action:nil],
|
|
||||||
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction
|
|
||||||
target:self
|
|
||||||
action:@selector(shareWasPressed:)],
|
|
||||||
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
|
|
||||||
target:nil
|
|
||||||
action:nil],
|
|
||||||
]
|
|
||||||
animated:NO];
|
|
||||||
}
|
|
||||||
[self.backgroundView addSubview:self.footerBar];
|
|
||||||
[self.footerBar autoPinWidthToSuperview];
|
|
||||||
[self.footerBar autoPinToBottomLayoutGuideOfViewController:self withInset:0];
|
|
||||||
[self.footerBar autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.contentView];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)shareWasPressed:(id)sender
|
|
||||||
{
|
|
||||||
DDLogInfo(@"%@: sharing image.", self.logTag);
|
|
||||||
OWSAssert(CurrentAppContext().isMainApp);
|
|
||||||
|
|
||||||
[AttachmentSharing showShareUIForURL:self.attachmentUrl];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)initializeScrollView
|
|
||||||
{
|
|
||||||
self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
|
|
||||||
self.scrollView.delegate = self;
|
|
||||||
self.scrollView.zoomScale = 1.0f;
|
|
||||||
self.scrollView.maximumZoomScale = kMaxZoomScale;
|
|
||||||
self.scrollView.scrollEnabled = NO;
|
|
||||||
[self.contentView addSubview:self.scrollView];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)initializeImageView
|
|
||||||
{
|
|
||||||
if (self.isAnimated) {
|
|
||||||
if ([self.fileData ows_isValidImage]) {
|
|
||||||
YYImage *animatedGif = [YYImage imageWithData:self.fileData];
|
|
||||||
YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc] init];
|
|
||||||
imageView.image = animatedGif;
|
|
||||||
imageView.frame = self.originRect;
|
|
||||||
imageView.contentMode = UIViewContentModeScaleAspectFill;
|
|
||||||
imageView.clipsToBounds = YES;
|
|
||||||
self.imageView = imageView;
|
|
||||||
} else {
|
|
||||||
self.imageView = [[UIImageView alloc] initWithFrame:self.originRect];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Present the static image using standard UIImageView
|
|
||||||
self.imageView = [[UIImageView alloc] initWithFrame:self.originRect];
|
|
||||||
self.imageView.contentMode = UIViewContentModeScaleAspectFill;
|
|
||||||
self.imageView.userInteractionEnabled = YES;
|
|
||||||
self.imageView.clipsToBounds = YES;
|
|
||||||
self.imageView.layer.allowsEdgeAntialiasing = YES;
|
|
||||||
// Use trilinear filters for better scaling quality at
|
|
||||||
// some performance cost.
|
|
||||||
self.imageView.layer.minificationFilter = kCAFilterTrilinear;
|
|
||||||
self.imageView.layer.magnificationFilter = kCAFilterTrilinear;
|
|
||||||
}
|
|
||||||
|
|
||||||
[self.scrollView addSubview:self.imageView];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)populateImageView:(UIImage *)image
|
|
||||||
{
|
|
||||||
if (image && !self.isAnimated) {
|
|
||||||
self.imageView.image = image;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)initializeGestureRecognizers
|
|
||||||
{
|
|
||||||
UITapGestureRecognizer *singleTap =
|
|
||||||
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(imageDismissGesture:)];
|
|
||||||
singleTap.delegate = self;
|
|
||||||
[self.view addGestureRecognizer:singleTap];
|
|
||||||
|
|
||||||
UITapGestureRecognizer *doubleTap =
|
|
||||||
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(imageDismissGesture:)];
|
|
||||||
doubleTap.numberOfTapsRequired = 2;
|
|
||||||
doubleTap.delegate = self;
|
|
||||||
[self.view addGestureRecognizer:doubleTap];
|
|
||||||
|
|
||||||
// UISwipeGestureRecognizer supposedly supports multiple directions,
|
|
||||||
// but in practice it works better if you use a separate GR for each
|
|
||||||
// direction.
|
|
||||||
for (NSNumber *direction in @[
|
|
||||||
@(UISwipeGestureRecognizerDirectionRight),
|
|
||||||
@(UISwipeGestureRecognizerDirectionLeft),
|
|
||||||
@(UISwipeGestureRecognizerDirectionUp),
|
|
||||||
@(UISwipeGestureRecognizerDirectionDown),
|
|
||||||
]) {
|
|
||||||
UISwipeGestureRecognizer *swipe =
|
|
||||||
[[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(imageDismissGesture:)];
|
|
||||||
swipe.direction = (UISwipeGestureRecognizerDirection)direction.integerValue;
|
|
||||||
swipe.delegate = self;
|
|
||||||
[self.view addGestureRecognizer:swipe];
|
|
||||||
}
|
|
||||||
|
|
||||||
UILongPressGestureRecognizer *longPress =
|
|
||||||
[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressGesture:)];
|
|
||||||
longPress.delegate = self;
|
|
||||||
[self.view addGestureRecognizer:longPress];
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - Gesture Recognizers
|
|
||||||
|
|
||||||
- (void)imageDismissGesture:(UIGestureRecognizer *)sender
|
|
||||||
{
|
|
||||||
if (sender.state == UIGestureRecognizerStateRecognized) {
|
|
||||||
[self dismiss];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)longPressGesture:(UIGestureRecognizer *)sender
|
|
||||||
{
|
|
||||||
// We "eagerly" respond when the long press begins, not when it ends.
|
|
||||||
if (sender.state == UIGestureRecognizerStateBegan) {
|
|
||||||
if (!self.viewItem) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
[self.view becomeFirstResponder];
|
|
||||||
|
|
||||||
if ([UIMenuController sharedMenuController].isMenuVisible) {
|
|
||||||
[[UIMenuController sharedMenuController] setMenuVisible:NO animated:NO];
|
|
||||||
}
|
|
||||||
|
|
||||||
NSArray *menuItems = self.viewItem.mediaMenuControllerItems;
|
|
||||||
[UIMenuController sharedMenuController].menuItems = menuItems;
|
|
||||||
CGPoint location = [sender locationInView:self.view];
|
|
||||||
CGRect targetRect = CGRectMake(location.x, location.y, 1, 1);
|
|
||||||
[[UIMenuController sharedMenuController] setTargetRect:targetRect inView:self.view];
|
|
||||||
[[UIMenuController sharedMenuController] setMenuVisible:YES animated:YES];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender
|
|
||||||
{
|
|
||||||
if (action == self.viewItem.metadataActionSelector) {
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
return [self.viewItem canPerformAction:action];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)copyMediaAction:(nullable id)sender
|
|
||||||
{
|
|
||||||
[self.viewItem copyMediaAction];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)shareMediaAction:(nullable id)sender
|
|
||||||
{
|
|
||||||
[self.viewItem shareMediaAction];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)saveMediaAction:(nullable id)sender
|
|
||||||
{
|
|
||||||
[self.viewItem saveMediaAction];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)deleteAction:(nullable id)sender
|
|
||||||
{
|
|
||||||
[self.viewItem deleteAction];
|
|
||||||
|
|
||||||
[self dismiss];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)canBecomeFirstResponder
|
|
||||||
{
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - Presentation
|
|
||||||
|
|
||||||
- (void)presentFromViewController:(UIViewController *)viewController
|
|
||||||
{
|
|
||||||
_isPresenting = YES;
|
|
||||||
self.view.userInteractionEnabled = NO;
|
|
||||||
[self.view addSubview:self.imageView];
|
|
||||||
self.modalPresentationStyle = UIModalPresentationOverCurrentContext;
|
|
||||||
self.view.alpha = 0;
|
|
||||||
|
|
||||||
[viewController
|
|
||||||
presentViewController:self
|
|
||||||
animated:NO
|
|
||||||
completion:^{
|
|
||||||
UIView *window = CurrentAppContext().rootReferenceView;
|
|
||||||
// During the presentation animation, we want to seamlessly animate the image
|
|
||||||
// from its location in the conversation view. To do so, we need a
|
|
||||||
// consistent coordinate system, so we pass the `originRect` in the
|
|
||||||
// coordinate system of the window.
|
|
||||||
self.imageView.frame = [self.view convertRect:self.originRect fromView:window];
|
|
||||||
|
|
||||||
[UIView animateWithDuration:0.25f
|
|
||||||
delay:0
|
|
||||||
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseOut
|
|
||||||
animations:^() {
|
|
||||||
self.view.alpha = 1.0f;
|
|
||||||
// During the presentation animation, we want to seamlessly animate the image
|
|
||||||
// to its resting location in this view. We use `resizedFrameForImageView`
|
|
||||||
// to determine its size "at rest" in the content view, and then convert
|
|
||||||
// from the content view's coordinate system to the root view coordinate
|
|
||||||
// system because the image view is temporarily hosted by the root view during
|
|
||||||
// the presentation animation.
|
|
||||||
self.imageView.frame = [self resizedFrameForImageView:self.image.size];
|
|
||||||
self.imageView.center =
|
|
||||||
[self.contentView convertPoint:self.contentView.center fromView:self.contentView];
|
|
||||||
}
|
|
||||||
completion:^(BOOL completed) {
|
|
||||||
self.scrollView.frame = self.contentView.bounds;
|
|
||||||
[self.scrollView addSubview:self.imageView];
|
|
||||||
[self updateLayouts];
|
|
||||||
self.view.userInteractionEnabled = YES;
|
|
||||||
_isPresenting = NO;
|
|
||||||
}];
|
|
||||||
[UIUtil modalCompletionBlock]();
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)dismiss
|
|
||||||
{
|
|
||||||
self.view.userInteractionEnabled = NO;
|
|
||||||
[UIView animateWithDuration:0.25f
|
|
||||||
delay:0
|
|
||||||
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveLinear
|
|
||||||
animations:^() {
|
|
||||||
self.backgroundView.backgroundColor = [UIColor clearColor];
|
|
||||||
self.scrollView.alpha = 0;
|
|
||||||
self.view.alpha = 0;
|
|
||||||
}
|
|
||||||
completion:^(BOOL completed) {
|
|
||||||
[self.presentingViewController dismissViewControllerAnimated:NO completion:nil];
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - Update Layout
|
|
||||||
|
|
||||||
- (void)viewDidLayoutSubviews
|
|
||||||
{
|
|
||||||
[self updateLayouts];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)updateLayouts
|
|
||||||
{
|
|
||||||
if (_isPresenting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.scrollView.frame = self.contentView.bounds;
|
|
||||||
self.imageView.frame = [self resizedFrameForImageView:self.image.size];
|
|
||||||
self.scrollView.contentSize = self.imageView.frame.size;
|
|
||||||
self.scrollView.contentInset = [self contentInsetForScrollView:self.scrollView.zoomScale];
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - Resizing
|
|
||||||
|
|
||||||
- (CGRect)resizedFrameForImageView:(CGSize)imageSize
|
|
||||||
{
|
|
||||||
CGRect frame = self.contentView.bounds;
|
|
||||||
CGSize screenSize
|
|
||||||
= CGSizeMake(frame.size.width * self.scrollView.zoomScale, frame.size.height * self.scrollView.zoomScale);
|
|
||||||
CGSize targetSize = screenSize;
|
|
||||||
|
|
||||||
if ([self isImagePortrait]) {
|
|
||||||
if ([self getAspectRatioForCGSize:screenSize] < [self getAspectRatioForCGSize:imageSize]) {
|
|
||||||
targetSize.width = screenSize.height / [self getAspectRatioForCGSize:imageSize];
|
|
||||||
} else {
|
|
||||||
targetSize.height = screenSize.width * [self getAspectRatioForCGSize:imageSize];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ([self getAspectRatioForCGSize:screenSize] > [self getAspectRatioForCGSize:imageSize]) {
|
|
||||||
targetSize.height = screenSize.width * [self getAspectRatioForCGSize:imageSize];
|
|
||||||
} else {
|
|
||||||
targetSize.width = screenSize.height / [self getAspectRatioForCGSize:imageSize];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.size = targetSize;
|
|
||||||
frame.origin = CGPointMake(0, 0);
|
|
||||||
return frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (UIEdgeInsets)contentInsetForScrollView:(CGFloat)targetZoomScale
|
|
||||||
{
|
|
||||||
UIEdgeInsets inset = UIEdgeInsetsZero;
|
|
||||||
|
|
||||||
CGSize boundsSize = self.scrollView.bounds.size;
|
|
||||||
CGSize contentSize = self.image.size;
|
|
||||||
CGSize minSize;
|
|
||||||
|
|
||||||
if ([self isImagePortrait]) {
|
|
||||||
if ([self getAspectRatioForCGSize:boundsSize] < [self getAspectRatioForCGSize:contentSize]) {
|
|
||||||
minSize.height = boundsSize.height;
|
|
||||||
minSize.width = minSize.height / [self getAspectRatioForCGSize:contentSize];
|
|
||||||
} else {
|
|
||||||
minSize.width = boundsSize.width;
|
|
||||||
minSize.height = minSize.width * [self getAspectRatioForCGSize:contentSize];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ([self getAspectRatioForCGSize:boundsSize] > [self getAspectRatioForCGSize:contentSize]) {
|
|
||||||
minSize.width = boundsSize.width;
|
|
||||||
minSize.height = minSize.width * [self getAspectRatioForCGSize:contentSize];
|
|
||||||
} else {
|
|
||||||
minSize.height = boundsSize.height;
|
|
||||||
minSize.width = minSize.height / [self getAspectRatioForCGSize:contentSize];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CGSize finalSize = self.view.bounds.size;
|
|
||||||
|
|
||||||
minSize.width *= targetZoomScale;
|
|
||||||
minSize.height *= targetZoomScale;
|
|
||||||
|
|
||||||
if (minSize.height > finalSize.height && minSize.width > finalSize.width) {
|
|
||||||
inset = UIEdgeInsetsZero;
|
|
||||||
} else {
|
|
||||||
CGFloat dy = boundsSize.height - minSize.height;
|
|
||||||
CGFloat dx = boundsSize.width - minSize.width;
|
|
||||||
|
|
||||||
dy = (dy > 0) ? dy : 0;
|
|
||||||
dx = (dx > 0) ? dx : 0;
|
|
||||||
|
|
||||||
inset.top = dy / 2.0f;
|
|
||||||
inset.bottom = dy / 2.0f;
|
|
||||||
inset.left = dx / 2.0f;
|
|
||||||
inset.right = dx / 2.0f;
|
|
||||||
}
|
|
||||||
return inset;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - UIScrollViewDelegate
|
|
||||||
|
|
||||||
- (nullable UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
|
|
||||||
{
|
|
||||||
return self.imageView;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)scrollViewDidZoom:(UIScrollView *)scrollView
|
|
||||||
{
|
|
||||||
scrollView.contentInset = [self contentInsetForScrollView:scrollView.zoomScale];
|
|
||||||
|
|
||||||
if (self.scrollView.scrollEnabled == NO) {
|
|
||||||
self.scrollView.scrollEnabled = YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(nullable UIView *)view atScale:(CGFloat)scale
|
|
||||||
{
|
|
||||||
self.scrollView.scrollEnabled = (scale > 1);
|
|
||||||
self.scrollView.contentInset = [self contentInsetForScrollView:scale];
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - Utility
|
|
||||||
|
|
||||||
- (BOOL)isImagePortrait
|
|
||||||
{
|
|
||||||
return ([self getAspectRatioForCGSize:self.image.size] > 1.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (CGFloat)getAspectRatioForCGSize:(CGSize)size
|
|
||||||
{
|
|
||||||
return size.height / size.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - Saving images to Camera Roll
|
|
||||||
|
|
||||||
- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo
|
|
||||||
{
|
|
||||||
if (error) {
|
|
||||||
DDLogWarn(@"There was a problem saving <%@> to camera roll from %s ",
|
|
||||||
error.localizedDescription,
|
|
||||||
__PRETTY_FUNCTION__);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
@ -14,6 +14,11 @@ public enum MediaMessageViewMode: UInt {
|
||||||
case attachmentApproval
|
case attachmentApproval
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
public protocol MediaDetailPresenter: class {
|
||||||
|
func presentDetails(mediaMessageView: MediaMessageView, fromView: UIView)
|
||||||
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
public class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate {
|
public class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate {
|
||||||
|
|
||||||
|
@ -56,6 +61,8 @@ public class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate {
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
public var contentView: UIView?
|
public var contentView: UIView?
|
||||||
|
|
||||||
|
private let mediaDetailPresenter: MediaDetailPresenter?
|
||||||
|
|
||||||
// MARK: Initializers
|
// MARK: Initializers
|
||||||
|
|
||||||
|
@ -65,10 +72,15 @@ public class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
public required init(attachment: SignalAttachment, mode: MediaMessageViewMode) {
|
public convenience init(attachment: SignalAttachment, mode: MediaMessageViewMode) {
|
||||||
|
self.init(attachment: attachment, mode: mode, mediaDetailPresenter: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
public required init(attachment: SignalAttachment, mode: MediaMessageViewMode, mediaDetailPresenter: MediaDetailPresenter?) {
|
||||||
assert(!attachment.hasError)
|
assert(!attachment.hasError)
|
||||||
self.mode = mode
|
|
||||||
self.attachment = attachment
|
self.attachment = attachment
|
||||||
|
self.mode = mode
|
||||||
|
self.mediaDetailPresenter = mediaDetailPresenter
|
||||||
super.init(frame: CGRect.zero)
|
super.init(frame: CGRect.zero)
|
||||||
|
|
||||||
createViews()
|
createViews()
|
||||||
|
@ -463,14 +475,8 @@ public class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate {
|
||||||
guard let fromView = sender.view else {
|
guard let fromView = sender.view else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let fromViewController = CurrentAppContext().frontmostViewController() else {
|
|
||||||
return
|
showMediaDetailViewController(fromView: fromView)
|
||||||
}
|
|
||||||
|
|
||||||
let window = CurrentAppContext().rootReferenceView
|
|
||||||
let convertedRect = fromView.convert(fromView.bounds, to:window)
|
|
||||||
let viewController = FullImageViewController(attachment:attachment, from:convertedRect)
|
|
||||||
viewController.present(from:fromViewController)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Video Playback
|
// MARK: - Video Playback
|
||||||
|
@ -484,60 +490,14 @@ public class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate {
|
||||||
guard sender.state == .recognized else {
|
guard sender.state == .recognized else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
guard let fromView = sender.view else {
|
||||||
playVideo()
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc
|
|
||||||
public func playVideo() {
|
|
||||||
guard let dataUrl = attachment.dataUrl else {
|
|
||||||
owsFail("\(self.logTag) attachment is missing dataUrl")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let filePath = dataUrl.path
|
showMediaDetailViewController(fromView: fromView)
|
||||||
guard FileManager.default.fileExists(atPath: filePath) else {
|
|
||||||
owsFail("\(self.logTag) file at \(filePath) doesn't exist")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let videoPlayer = MPMoviePlayerController(contentURL: dataUrl) else {
|
|
||||||
owsFail("\(self.logTag) unable to build moview player controller")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
videoPlayer.prepareToPlay()
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(forName: .MPMoviePlayerWillExitFullscreen, object: nil, queue: nil) { [weak self] _ in
|
|
||||||
self?.moviePlayerWillExitFullscreen()
|
|
||||||
}
|
|
||||||
NotificationCenter.default.addObserver(forName: .MPMoviePlayerDidExitFullscreen, object: nil, queue: nil) { [weak self] _ in
|
|
||||||
self?.moviePlayerDidExitFullscreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
videoPlayer.controlStyle = .default
|
|
||||||
videoPlayer.shouldAutoplay = true
|
|
||||||
|
|
||||||
self.addSubview(videoPlayer.view)
|
|
||||||
videoPlayer.view.frame = self.bounds
|
|
||||||
self.videoPlayer = videoPlayer
|
|
||||||
videoPlayer.view.autoPinToSuperviewEdges()
|
|
||||||
OWSAudioAttachmentPlayer.setAudioIgnoresHardwareMuteSwitch(true)
|
|
||||||
videoPlayer.setFullscreen(true, animated:false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func moviePlayerWillExitFullscreen() {
|
func showMediaDetailViewController(fromView: UIView) {
|
||||||
clearVideoPlayer()
|
self.mediaDetailPresenter?.presentDetails(mediaMessageView: self, fromView: fromView)
|
||||||
}
|
|
||||||
|
|
||||||
private func moviePlayerDidExitFullscreen() {
|
|
||||||
clearVideoPlayer()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func clearVideoPlayer() {
|
|
||||||
videoPlayer?.stop()
|
|
||||||
videoPlayer?.view.removeFromSuperview()
|
|
||||||
videoPlayer = nil
|
|
||||||
OWSAudioAttachmentPlayer.setAudioIgnoresHardwareMuteSwitch(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,6 +83,9 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value);
|
||||||
- (NSLayoutConstraint *)autoPinLeadingToSuperviewWithMargin:(CGFloat)margin;
|
- (NSLayoutConstraint *)autoPinLeadingToSuperviewWithMargin:(CGFloat)margin;
|
||||||
- (NSLayoutConstraint *)autoPinTrailingToSuperview;
|
- (NSLayoutConstraint *)autoPinTrailingToSuperview;
|
||||||
- (NSLayoutConstraint *)autoPinTrailingToSuperviewWithMargin:(CGFloat)margin;
|
- (NSLayoutConstraint *)autoPinTrailingToSuperviewWithMargin:(CGFloat)margin;
|
||||||
|
- (NSLayoutConstraint *)autoPinTopToSuperviewWithMargin:(CGFloat)margin;
|
||||||
|
- (NSLayoutConstraint *)autoPinBottomToSuperviewWithMargin:(CGFloat)margin;
|
||||||
|
|
||||||
- (NSLayoutConstraint *)autoPinLeadingToTrailingOfView:(UIView *)view;
|
- (NSLayoutConstraint *)autoPinLeadingToTrailingOfView:(UIView *)view;
|
||||||
- (NSLayoutConstraint *)autoPinLeadingToTrailingOfView:(UIView *)view margin:(CGFloat)margin;
|
- (NSLayoutConstraint *)autoPinLeadingToTrailingOfView:(UIView *)view margin:(CGFloat)margin;
|
||||||
- (NSLayoutConstraint *)autoPinTrailingToLeadingOfView:(UIView *)view;
|
- (NSLayoutConstraint *)autoPinTrailingToLeadingOfView:(UIView *)view;
|
||||||
|
|
|
@ -299,6 +299,32 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (NSLayoutConstraint *)autoPinBottomToSuperviewWithMargin:(CGFloat)margin
|
||||||
|
{
|
||||||
|
if (@available(iOS 9.0, *)) {
|
||||||
|
NSLayoutConstraint *constraint =
|
||||||
|
[self.bottomAnchor constraintEqualToAnchor:self.superview.layoutMarginsGuide.bottomAnchor
|
||||||
|
constant:-margin];
|
||||||
|
constraint.active = YES;
|
||||||
|
return constraint;
|
||||||
|
} else {
|
||||||
|
return [self autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:margin];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSLayoutConstraint *)autoPinTopToSuperviewWithMargin:(CGFloat)margin
|
||||||
|
{
|
||||||
|
if (@available(iOS 9.0, *)) {
|
||||||
|
NSLayoutConstraint *constraint =
|
||||||
|
[self.topAnchor constraintEqualToAnchor:self.superview.layoutMarginsGuide.topAnchor
|
||||||
|
constant:margin];
|
||||||
|
constraint.active = YES;
|
||||||
|
return constraint;
|
||||||
|
} else {
|
||||||
|
return [self autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:margin];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
- (NSLayoutConstraint *)autoPinLeadingToTrailingOfView:(UIView *)view
|
- (NSLayoutConstraint *)autoPinLeadingToTrailingOfView:(UIView *)view
|
||||||
{
|
{
|
||||||
OWSAssert(view);
|
OWSAssert(view);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import "OWSOrphanedDataCleaner.h"
|
#import "OWSOrphanedDataCleaner.h"
|
||||||
|
|
Loading…
Reference in New Issue