mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
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
24 changed files with 1520 additions and 820 deletions
|
@ -42,7 +42,6 @@
|
|||
344F248720069ECB00CFB4F4 /* ModalActivityIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344F248620069ECB00CFB4F4 /* ModalActivityIndicatorViewController.swift */; };
|
||||
344F248A20069F0600CFB4F4 /* ViewControllerUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 344F248820069F0600CFB4F4 /* ViewControllerUtils.h */; };
|
||||
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 */; };
|
||||
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, ); }; };
|
||||
|
@ -194,8 +193,6 @@
|
|||
34FD93701E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 34FD936F1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m */; };
|
||||
4505C2BF1E648EA300CEBF41 /* ExperienceUpgrade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4505C2BE1E648EA300CEBF41 /* ExperienceUpgrade.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 */; };
|
||||
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 */; };
|
||||
|
@ -250,6 +247,7 @@
|
|||
452D1EE81DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452D1EE71DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift */; };
|
||||
452EA09E1EA7ABE00078744B /* AttachmentPointerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452EA09D1EA7ABE00078744B /* AttachmentPointerView.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 */; };
|
||||
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, ); }; };
|
||||
|
@ -290,6 +288,7 @@
|
|||
45A663C51F92EC760027B59E /* GroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A663C41F92EC760027B59E /* GroupTableViewCell.swift */; };
|
||||
45A6DAD61EBBF85500893231 /* ReminderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A6DAD51EBBF85500893231 /* ReminderView.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 */; };
|
||||
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, ); }; };
|
||||
|
@ -630,8 +629,6 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -776,6 +773,7 @@
|
|||
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>"; };
|
||||
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; };
|
||||
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>"; };
|
||||
|
@ -828,6 +826,8 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
|
@ -1319,6 +1319,8 @@
|
|||
34B3F8491E8DF1700035BE1A /* InboxTableViewCell.h */,
|
||||
34B3F84A1E8DF1700035BE1A /* InboxTableViewCell.m */,
|
||||
34B3F84C1E8DF1700035BE1A /* InviteFlow.swift */,
|
||||
45B9EE9A200E91FB005D2F2D /* MediaDetailViewController.h */,
|
||||
45B9EE9B200E91FB005D2F2D /* MediaDetailViewController.m */,
|
||||
34CA1C261F7156F300E51C51 /* MessageDetailViewController.swift */,
|
||||
34B3F84F1E8DF1700035BE1A /* NewContactThreadViewController.h */,
|
||||
34B3F8501E8DF1700035BE1A /* NewContactThreadViewController.m */,
|
||||
|
@ -1561,8 +1563,6 @@
|
|||
children = (
|
||||
34B3F8391E8DF1700035BE1A /* AttachmentSharing.h */,
|
||||
34B3F83A1E8DF1700035BE1A /* AttachmentSharing.m */,
|
||||
34B3F8471E8DF1700035BE1A /* FullImageViewController.h */,
|
||||
34B3F8481E8DF1700035BE1A /* FullImageViewController.m */,
|
||||
45E5471F1FD755E700DFC09E /* AttachmentApprovalViewController.swift */,
|
||||
3400C7901EAF89CD008A8584 /* SharingThreadPickerViewController.h */,
|
||||
3400C7911EAF89CD008A8584 /* SharingThreadPickerViewController.m */,
|
||||
|
@ -1748,6 +1748,7 @@
|
|||
76EB052B18170B33006006FC /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
453034AA200289F50018945D /* VideoPlayerView.swift */,
|
||||
45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */,
|
||||
452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */,
|
||||
34E3E5671EC4B19400495BAC /* AudioProgressView.swift */,
|
||||
|
@ -2071,7 +2072,6 @@
|
|||
346129391FD1B47300532771 /* OWSPreferences.h in Headers */,
|
||||
344D6CED20069E070042AF96 /* NewNonContactConversationViewController.h in Headers */,
|
||||
346129DE1FD5C02A00532771 /* LockInteractionController.h in Headers */,
|
||||
450998671FD8BDA600D89EB3 /* FullImageViewController.h in Headers */,
|
||||
451F8A451FD71570005CB9DA /* BlockListUIUtils.h in Headers */,
|
||||
451F8A4A1FD715D9005CB9DA /* OWSContactAvatarBuilder.h in Headers */,
|
||||
34480B5B1FD0A7E300BC14EF /* SignalMessaging-Prefix.pch in Headers */,
|
||||
|
@ -2775,7 +2775,6 @@
|
|||
344F248720069ECB00CFB4F4 /* ModalActivityIndicatorViewController.swift in Sources */,
|
||||
346129961FD1E30000532771 /* OWSDatabaseMigration.m in Sources */,
|
||||
346129CD1FD2072E00532771 /* UIImage+OWS.m in Sources */,
|
||||
450998661FD8BD9C00D89EB3 /* FullImageViewController.m in Sources */,
|
||||
344D6CEC20069E070042AF96 /* NewNonContactConversationViewController.m in Sources */,
|
||||
346129FB1FD5F31400532771 /* OWS101ExistingUsersBlockOnIdentityChange.m in Sources */,
|
||||
450998651FD8A34D00D89EB3 /* DeviceSleepManager.swift in Sources */,
|
||||
|
@ -2926,6 +2925,7 @@
|
|||
34B3F8771E8DF1700035BE1A /* ContactsPicker.swift in Sources */,
|
||||
45C0DC1B1E68FE9000E04C47 /* UIApplication+OWS.swift in Sources */,
|
||||
45638BDF1F3DDB2200128435 /* MessageSender+Promise.swift in Sources */,
|
||||
453034AB200289F50018945D /* VideoPlayerView.swift in Sources */,
|
||||
45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */,
|
||||
34B3F8911E8DF1710035BE1A /* ShowGroupMembersViewController.m in Sources */,
|
||||
4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */,
|
||||
|
@ -2940,6 +2940,7 @@
|
|||
34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */,
|
||||
45F659731E1BD99C00444429 /* CallKitCallUIAdaptee.swift in Sources */,
|
||||
45BB93381E688E14001E3939 /* UIDevice+featureSupport.swift in Sources */,
|
||||
45B9EE9C200E91FB005D2F2D /* MediaDetailViewController.m in Sources */,
|
||||
458DE9D61DEE3FD00071BB03 /* PeerConnectionClient.swift in Sources */,
|
||||
34D1F0841F8678AA0066283D /* ConversationInputToolbar.m in Sources */,
|
||||
340CB2271EAC25820001CAA1 /* UpdateGroupViewController.m in Sources */,
|
||||
|
|
23
Signal/Images.xcassets/sliderProgressThumb.imageset/Contents.json
vendored
Normal file
23
Signal/Images.xcassets/sliderProgressThumb.imageset/Contents.json
vendored
Normal file
|
@ -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 "DebugUIPage.h"
|
||||
#import "FingerprintViewController.h"
|
||||
#import "FullImageViewController.h"
|
||||
#import "MediaDetailViewController.h"
|
||||
#import "HomeViewController.h"
|
||||
#import "NotificationsManager.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
|
||||
|
@ -18,7 +18,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
- (void)didTapImageViewItem:(ConversationViewItem *)viewItem
|
||||
attachmentStream:(TSAttachmentStream *)attachmentStream
|
||||
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)didTapTruncatedTextMessage:(ConversationViewItem *)conversationItem;
|
||||
- (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"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern const CGFloat OWSMessageCellCornerRadius;
|
||||
|
||||
@interface OWSMessageCell : ConversationViewCell
|
||||
|
||||
+ (NSString *)cellReuseIdentifier;
|
||||
|
|
|
@ -19,6 +19,9 @@
|
|||
|
||||
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
|
||||
|
||||
@property (nonatomic) BOOL isOutgoing;
|
||||
|
@ -1313,7 +1316,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[self.delegate didTapAudioViewItem:self.viewItem attachmentStream:self.attachmentStream];
|
||||
return;
|
||||
case OWSMessageCellType_Video:
|
||||
[self.delegate didTapVideoViewItem:self.viewItem attachmentStream:self.attachmentStream];
|
||||
[self.delegate didTapVideoViewItem:self.viewItem
|
||||
attachmentStream:self.attachmentStream
|
||||
imageView:self.stillImageView];
|
||||
return;
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
#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
|
||||
|
@ -46,8 +46,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
- (NSString *)messageText;
|
||||
- (void)setMessageText:(NSString *_Nullable)value;
|
||||
- (void)clearTextMessage;
|
||||
|
||||
- (nullable NSString *)textInputPrimaryLanguage;
|
||||
- (void)toggleDefaultKeyboard;
|
||||
|
||||
- (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"
|
||||
|
@ -16,7 +16,7 @@
|
|||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
static void *kConversationInputTextViewObservingContext = &kConversationInputTextViewObservingContext;
|
||||
|
||||
static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5;
|
||||
@interface ConversationInputToolbar () <UIGestureRecognizerDelegate, ConversationTextViewToolbarDelegate>
|
||||
|
||||
@property (nonatomic, readonly) UIView *contentView;
|
||||
|
@ -31,6 +31,8 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
|
|||
|
||||
@property (nonatomic) NSArray<NSLayoutConstraint *> *contentContraints;
|
||||
@property (nonatomic) NSValue *lastTextContentSize;
|
||||
@property (nonatomic) CGFloat toolbarHeight;
|
||||
@property (nonatomic) CGFloat textViewHeight;
|
||||
|
||||
#pragma mark - Voice Memo Recording UI
|
||||
|
||||
|
@ -69,18 +71,25 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
|
|||
[self removeKVOObservers];
|
||||
}
|
||||
|
||||
- (CGSize)intrinsicContentSize
|
||||
{
|
||||
CGSize newSize = CGSizeMake(self.bounds.size.width, self.toolbarHeight + ConversationInputToolbarBorderViewHeight);
|
||||
return newSize;
|
||||
}
|
||||
|
||||
- (void)createContents
|
||||
{
|
||||
self.layoutMargins = UIEdgeInsetsZero;
|
||||
|
||||
self.backgroundColor = [UIColor ows_inputToolbarBackgroundColor];
|
||||
self.autoresizingMask = UIViewAutoresizingFlexibleHeight;
|
||||
|
||||
UIView *borderView = [UIView new];
|
||||
borderView.backgroundColor = [UIColor colorWithWhite:238 / 255.f alpha:1.f];
|
||||
[self addSubview:borderView];
|
||||
[borderView autoPinWidthToSuperview];
|
||||
[borderView autoPinEdgeToSuperviewEdge:ALEdgeTop];
|
||||
[borderView autoSetDimension:ALDimensionHeight toSize:0.5f];
|
||||
[borderView autoSetDimension:ALDimensionHeight toSize:ConversationInputToolbarBorderViewHeight];
|
||||
|
||||
_contentView = [UIView containerView];
|
||||
[self addSubview:self.contentView];
|
||||
|
@ -190,6 +199,26 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
|
|||
[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
|
||||
{
|
||||
if (_shouldShowVoiceMemoButton == shouldShowVoiceMemoButton) {
|
||||
|
@ -223,12 +252,16 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
|
|||
const CGFloat kMinTextViewHeight = ceil(self.inputTextView.font.lineHeight
|
||||
+ self.inputTextView.textContainerInset.top + self.inputTextView.textContainerInset.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
|
||||
+ self.inputTextView.contentInset.bottom);
|
||||
const CGFloat textViewHeight = ceil(Clamp(textViewDesiredHeight, kMinTextViewHeight, kMaxTextViewHeight));
|
||||
const CGFloat kMinContentHeight = kMinTextViewHeight + textViewVInset * 2;
|
||||
|
||||
self.textViewHeight = textViewHeight;
|
||||
self.toolbarHeight = textViewHeight + textViewVInset * 2;
|
||||
|
||||
if (self.attachmentToApprove) {
|
||||
OWSAssert(self.attachmentView);
|
||||
|
||||
|
@ -247,14 +280,14 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
|
|||
|
||||
self.contentContraints = @[
|
||||
[self.attachmentView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:textViewVInset],
|
||||
[self.attachmentView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:textViewVInset],
|
||||
[self.attachmentView autoPinBottomToSuperviewWithMargin:textViewVInset],
|
||||
[self.attachmentView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:contentHInset],
|
||||
[self.attachmentView autoSetDimension:ALDimensionHeight toSize:150.f],
|
||||
|
||||
[self.rightButtonWrapper autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.attachmentView],
|
||||
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeRight],
|
||||
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeTop],
|
||||
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeBottom],
|
||||
[self.rightButtonWrapper autoPinBottomToSuperviewWithMargin:0],
|
||||
|
||||
[rightButton autoSetDimension:ALDimensionHeight toSize:kMinContentHeight],
|
||||
[rightButton autoPinLeadingToSuperviewWithMargin:contentHSpacing],
|
||||
|
@ -316,7 +349,7 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
|
|||
self.contentContraints = @[
|
||||
[self.leftButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeLeft],
|
||||
[self.leftButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeTop],
|
||||
[self.leftButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeBottom],
|
||||
[self.leftButtonWrapper autoPinBottomToSuperviewWithMargin:0],
|
||||
|
||||
[leftButton autoSetDimension:ALDimensionHeight toSize:kMinContentHeight],
|
||||
[leftButton autoPinLeadingToSuperviewWithMargin:contentHInset],
|
||||
|
@ -325,18 +358,18 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
|
|||
|
||||
[self.inputTextView autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.leftButtonWrapper],
|
||||
[self.inputTextView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:textViewVInset],
|
||||
[self.inputTextView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:textViewVInset],
|
||||
[self.inputTextView autoPinBottomToSuperviewWithMargin:textViewVInset],
|
||||
[self.inputTextView autoSetDimension:ALDimensionHeight toSize:textViewHeight],
|
||||
|
||||
[self.rightButtonWrapper autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.inputTextView],
|
||||
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeRight],
|
||||
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeTop],
|
||||
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeBottom],
|
||||
[self.rightButtonWrapper autoPinBottomToSuperviewWithMargin:0],
|
||||
|
||||
[rightButton autoSetDimension:ALDimensionHeight toSize:kMinContentHeight],
|
||||
[rightButton autoPinLeadingToSuperviewWithMargin:contentHSpacing],
|
||||
[rightButton autoPinTrailingToSuperviewWithMargin:contentHInset],
|
||||
[rightButton autoPinEdgeToSuperviewEdge:ALEdgeBottom],
|
||||
[rightButton autoPinEdgeToSuperviewEdge:ALEdgeBottom]
|
||||
];
|
||||
|
||||
// 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
|
||||
|| fabs(lastTextContentSize.CGSizeValue.height - textContentSize.height) > 0.1f) {
|
||||
[self ensureContentConstraints];
|
||||
[self invalidateIntrinsicContentSize];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -811,13 +845,6 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
|
|||
- (void)viewWillDisappear:(BOOL)animated
|
||||
{
|
||||
[self.attachmentView viewWillDisappear:animated];
|
||||
|
||||
[self endEditingTextMessage];
|
||||
}
|
||||
|
||||
- (nullable NSString *)textInputPrimaryLanguage
|
||||
{
|
||||
return self.inputTextView.textInputMode.primaryLanguage;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
#import "DateUtil.h"
|
||||
#import "DebugUITableViewController.h"
|
||||
#import "FingerprintViewController.h"
|
||||
#import "FullImageViewController.h"
|
||||
#import "MediaDetailViewController.h"
|
||||
#import "NSAttributedString+OWS.h"
|
||||
#import "NewGroupViewController.h"
|
||||
#import "OWSAudioAttachmentPlayer.h"
|
||||
|
@ -57,7 +57,6 @@
|
|||
#import <JSQMessagesViewController/JSQSystemSoundPlayer+JSQMessages.h>
|
||||
#import <JSQMessagesViewController/UIColor+JSQMessages.h>
|
||||
#import <JSQSystemSoundPlayer/JSQSystemSoundPlayer.h>
|
||||
#import <MediaPlayer/MediaPlayer.h>
|
||||
#import <MobileCoreServices/UTCoreTypes.h>
|
||||
#import <PromiseKit/AnyPromise.h>
|
||||
#import <SignalMessaging/Environment.h>
|
||||
|
@ -175,7 +174,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
@property (nonatomic) NSArray<ConversationViewItem *> *viewItems;
|
||||
@property (nonatomic) NSMutableDictionary<NSString *, ConversationViewItem *> *viewItemCache;
|
||||
|
||||
@property (nonatomic, nullable) MPMoviePlayerController *videoPlayer;
|
||||
@property (nonatomic, nullable) AVAudioRecorder *audioRecorder;
|
||||
@property (nonatomic, nullable) OWSAudioAttachmentPlayer *audioAttachmentPlayer;
|
||||
@property (nonatomic, nullable) NSUUID *voiceMessageUUID;
|
||||
|
@ -220,17 +218,18 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
@property (nonatomic, readonly) BOOL isGroupConversation;
|
||||
@property (nonatomic) BOOL isUserScrolling;
|
||||
|
||||
@property (nonatomic) NSLayoutConstraint *scrollDownButtonButtomConstraint;
|
||||
|
||||
@property (nonatomic) ConversationScrollButton *scrollDownButton;
|
||||
#ifdef DEBUG
|
||||
@property (nonatomic) ConversationScrollButton *scrollUpButton;
|
||||
#endif
|
||||
|
||||
@property (nonatomic) BOOL isViewCompletelyAppeared;
|
||||
@property (nonatomic) BOOL isViewVisible;
|
||||
@property (nonatomic) BOOL isAppInBackground;
|
||||
@property (nonatomic) BOOL shouldObserveDBModifications;
|
||||
@property (nonatomic) BOOL viewHasEverAppeared;
|
||||
@property (nonatomic) BOOL wasScrolledToBottomBeforeKeyboardShow;
|
||||
@property (nonatomic) BOOL wasScrolledToBottomBeforeLayoutChange;
|
||||
@property (nonatomic) BOOL hasUnreadMessages;
|
||||
|
||||
@end
|
||||
|
@ -328,6 +327,10 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
selector:@selector(signalAccountsDidChange:)
|
||||
name:OWSContactsManagerSignalAccountsDidChangeNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(keyboardWillChangeFrame:)
|
||||
name:UIKeyboardWillChangeFrameNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)signalAccountsDidChange:(NSNotification *)notification
|
||||
|
@ -461,13 +464,13 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
{
|
||||
if (_peek) {
|
||||
self.inputToolbar.hidden = YES;
|
||||
[self.inputToolbar endEditing:TRUE];
|
||||
[self dismissKeyBoard];
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.userLeftGroup) {
|
||||
self.inputToolbar.hidden = YES; // user has requested they leave the group. further sends disallowed
|
||||
[self.inputToolbar endEditing:TRUE];
|
||||
[self dismissKeyBoard];
|
||||
} else {
|
||||
self.inputToolbar.hidden = NO;
|
||||
}
|
||||
|
@ -510,6 +513,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
self.collectionView.dataSource = self;
|
||||
self.collectionView.showsVerticalScrollIndicator = YES;
|
||||
self.collectionView.showsHorizontalScrollIndicator = NO;
|
||||
self.collectionView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive;
|
||||
self.collectionView.backgroundColor = [UIColor whiteColor];
|
||||
[self.view addSubview:self.collectionView];
|
||||
[self.collectionView autoPinWidthToSuperview];
|
||||
|
@ -526,10 +530,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
_inputToolbar = [ConversationInputToolbar new];
|
||||
self.inputToolbar.inputToolbarDelegate = self;
|
||||
self.inputToolbar.inputTextViewDelegate = self;
|
||||
[self.view addSubview:self.inputToolbar];
|
||||
[self.inputToolbar autoPinWidthToSuperview];
|
||||
[self.inputToolbar autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.collectionView];
|
||||
[self autoPinViewToBottomGuideOrKeyboard:self.inputToolbar];
|
||||
[self.collectionView autoPinToBottomLayoutGuideOfViewController:self withInset:0];
|
||||
|
||||
self.loadMoreHeader = [UILabel new];
|
||||
self.loadMoreHeader.text = NSLocalizedString(@"CONVERSATION_VIEW_LOADING_MORE_MESSAGES",
|
||||
|
@ -543,6 +544,16 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
[self.loadMoreHeader autoSetDimension:ALDimensionHeight toSize:kLoadMoreHeaderHeight];
|
||||
}
|
||||
|
||||
- (BOOL)canBecomeFirstResponder
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (nullable UIView *)inputAccessoryView
|
||||
{
|
||||
return self.inputToolbar;
|
||||
}
|
||||
|
||||
- (void)registerCellClasses
|
||||
{
|
||||
[self.collectionView registerClass:[OWSSystemMessageCell class]
|
||||
|
@ -892,6 +903,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
}];
|
||||
[actionSheetController addAction:dismissAction];
|
||||
|
||||
[self dismissKeyBoard];
|
||||
[self presentViewController:actionSheetController animated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
@ -1008,6 +1020,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
_callOnOpen = NO;
|
||||
}
|
||||
|
||||
self.isViewCompletelyAppeared = YES;
|
||||
self.viewHasEverAppeared = YES;
|
||||
}
|
||||
|
||||
|
@ -1021,6 +1034,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
|
||||
[super viewWillDisappear:animated];
|
||||
|
||||
self.isViewCompletelyAppeared = NO;
|
||||
[self.inputToolbar viewWillDisappear:animated];
|
||||
}
|
||||
|
||||
|
@ -1038,7 +1052,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
[self markVisibleMessagesAsRead];
|
||||
[self cancelVoiceMemo];
|
||||
[self.cellMediaCache removeAllObjects];
|
||||
[self.inputToolbar endEditingTextMessage];
|
||||
|
||||
self.isUserScrolling = NO;
|
||||
}
|
||||
|
@ -1390,20 +1403,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
|
||||
#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
|
||||
|
||||
/**
|
||||
|
@ -1644,6 +1643,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
|
||||
[actionSheetController addAction:resendMessageAction];
|
||||
|
||||
[self dismissKeyBoard];
|
||||
[self presentViewController:actionSheetController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
@ -1678,6 +1678,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
|
||||
[actionSheetController addAction:resendMessageAction];
|
||||
|
||||
[self dismissKeyBoard];
|
||||
[self presentViewController:actionSheetController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
@ -1805,6 +1806,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
}];
|
||||
[alertController addAction:resetSessionAction];
|
||||
|
||||
[self dismissKeyBoard];
|
||||
[self presentViewController:alertController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
@ -1845,6 +1847,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
}];
|
||||
[actionSheetController addAction:acceptSafetyNumberAction];
|
||||
|
||||
[self dismissKeyBoard];
|
||||
[self presentViewController:actionSheetController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
@ -1874,9 +1877,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
[alertController addAction:callAction];
|
||||
[alertController addAction:[OWSAlerts cancelAction]];
|
||||
|
||||
[[UIApplication sharedApplication].frontmostViewController presentViewController:alertController
|
||||
animated:YES
|
||||
completion:nil];
|
||||
[self dismissKeyBoard];
|
||||
[self presentViewController:alertController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
#pragma mark - ConversationViewCellDelegate
|
||||
|
@ -1925,6 +1927,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
}];
|
||||
[actionSheetController addAction:blockAction];
|
||||
|
||||
[self dismissKeyBoard];
|
||||
[self presentViewController:actionSheetController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
@ -1988,46 +1991,32 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
OWSAssert(attachmentStream);
|
||||
OWSAssert(imageView);
|
||||
|
||||
[self dismissKeyBoard];
|
||||
|
||||
UIWindow *window = [UIApplication sharedApplication].keyWindow;
|
||||
CGRect convertedRect = [imageView convertRect:imageView.bounds toView:window];
|
||||
FullImageViewController *vc = [[FullImageViewController alloc] initWithAttachmentStream:attachmentStream
|
||||
MediaDetailViewController *vc = [[MediaDetailViewController alloc] initWithAttachmentStream:attachmentStream
|
||||
fromRect:convertedRect
|
||||
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();
|
||||
OWSAssert(viewItem);
|
||||
OWSAssert(attachmentStream);
|
||||
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
if (![fileManager fileExistsAtPath:[attachmentStream.mediaURL path]]) {
|
||||
OWSFail(@"%@ Missing video file: %@", self.logTag, attachmentStream.mediaURL);
|
||||
}
|
||||
|
||||
[self dismissKeyBoard];
|
||||
self.videoPlayer = [[MPMoviePlayerController alloc] initWithContentURL:attachmentStream.mediaURL];
|
||||
[_videoPlayer prepareToPlay];
|
||||
UIWindow *window = [UIApplication sharedApplication].keyWindow;
|
||||
CGRect convertedRect = [imageView convertRect:imageView.bounds toView:window];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(moviePlayerWillExitFullscreen:)
|
||||
name:MPMoviePlayerWillExitFullscreenNotification
|
||||
object:_videoPlayer];
|
||||
[[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];
|
||||
MediaDetailViewController *vc = [[MediaDetailViewController alloc] initWithAttachmentStream:attachmentStream
|
||||
fromRect:convertedRect
|
||||
viewItem:viewItem];
|
||||
[vc presentFromViewController:self replacingView:imageView];
|
||||
}
|
||||
|
||||
- (void)didTapAudioViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream
|
||||
|
@ -2108,42 +2097,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
[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
|
||||
|
||||
- (void)didTapSystemMessageWithInteraction:(TSInteraction *)interaction
|
||||
|
@ -2244,7 +2197,9 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
[self.view addSubview:self.scrollDownButton];
|
||||
[self.scrollDownButton autoSetDimension:ALDimensionWidth 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];
|
||||
|
||||
#ifdef DEBUG
|
||||
|
@ -2360,6 +2315,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
[[UIDocumentMenuViewController alloc] initWithDocumentTypes:documentTypes inMode:pickerMode];
|
||||
menuController.delegate = self;
|
||||
|
||||
[self dismissKeyBoard];
|
||||
[self presentViewController:menuController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
@ -2371,6 +2327,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
[[GifPickerViewController alloc] initWithThread:self.thread messageSender:self.messageSender];
|
||||
view.delegate = self;
|
||||
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:view];
|
||||
|
||||
[self dismissKeyBoard];
|
||||
[self presentViewController:navigationController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
@ -2410,6 +2368,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
// post iOS11, document picker has no blue header.
|
||||
[UIUtil applyDefaultSystemAppearence];
|
||||
}
|
||||
|
||||
[self dismissKeyBoard];
|
||||
[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.allowsEditing = NO;
|
||||
picker.delegate = self;
|
||||
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self dismissKeyBoard];
|
||||
[self presentViewController:picker animated:YES completion:[UIUtil modalCompletionBlock]];
|
||||
});
|
||||
}];
|
||||
|
@ -2536,6 +2497,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
picker.delegate = self;
|
||||
picker.mediaTypes = @[ (__bridge NSString *)kUTTypeImage, (__bridge NSString *)kUTTypeMovie ];
|
||||
|
||||
[self dismissKeyBoard];
|
||||
[self presentViewController:picker animated:YES completion:[UIUtil modalCompletionBlock]];
|
||||
}
|
||||
|
||||
|
@ -3098,8 +3060,11 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
const CGFloat kIsAtBottomTolerancePts = 5;
|
||||
// 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);
|
||||
BOOL isScrolledToBottom = (self.collectionView.contentOffset.y > contentOffsetYBottom - kIsAtBottomTolerancePts);
|
||||
CGFloat contentOffsetYBottom
|
||||
= 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;
|
||||
}
|
||||
|
@ -3291,6 +3256,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
|
||||
- (void)attachmentButtonPressed
|
||||
{
|
||||
[self dismissKeyBoard];
|
||||
|
||||
__weak ConversationViewController *weakSelf = self;
|
||||
if ([self isBlockedContactConversation]) {
|
||||
|
@ -3366,6 +3332,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
[gifAction setValue:gifImage forKey:@"image"];
|
||||
[actionSheetController addAction:gifAction];
|
||||
|
||||
[self dismissKeyBoard];
|
||||
[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
|
||||
{
|
||||
OWSAssert(attachment);
|
||||
|
@ -3707,13 +3763,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
return [self.collectionView.collectionViewLayout collectionViewContentSize].height;
|
||||
}
|
||||
|
||||
- (void)scrollToBottomImmediately
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self scrollToBottomAnimated:NO];
|
||||
}
|
||||
|
||||
- (void)scrollToBottomAnimated:(BOOL)animated
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
@ -3721,11 +3770,17 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
if (self.isUserScrolling) {
|
||||
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 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];
|
||||
}
|
||||
|
||||
|
@ -3735,22 +3790,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
{
|
||||
[self updateLastVisibleTimestamp];
|
||||
[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
|
||||
|
@ -3877,10 +3916,10 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
[self messageWasSent:message];
|
||||
|
||||
if (updateKeyboardState) {
|
||||
[self toggleDefaultKeyboard];
|
||||
[self.inputToolbar toggleDefaultKeyboard];
|
||||
}
|
||||
[self clearDraft];
|
||||
[self.inputToolbar clearTextMessage];
|
||||
[self clearDraft];
|
||||
if (didAddToProfileWhitelist) {
|
||||
[self ensureDynamicInteractions];
|
||||
}
|
||||
|
@ -4053,8 +4092,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
- (void)collectionViewWillChangeLayout
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
self.wasScrolledToBottomBeforeLayoutChange = [self isScrolledToBottom];
|
||||
}
|
||||
|
||||
- (void)collectionViewDidChangeLayout
|
||||
|
@ -4062,15 +4099,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
OWSAssertIsOnMainThread();
|
||||
|
||||
[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
|
||||
|
|
|
@ -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"
|
||||
|
@ -68,6 +68,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[self clearState];
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.collectionView.bounds.size.width <= 0.f || self.collectionView.bounds.size.height <= 0.f) {
|
||||
OWSFail(
|
||||
@"%@ 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"
|
||||
|
@ -10,7 +10,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
@class SignalAttachment;
|
||||
@class TSAttachmentStream;
|
||||
|
||||
@interface FullImageViewController : OWSViewController
|
||||
@interface MediaDetailViewController : OWSViewController
|
||||
|
||||
// If viewItem is non-null, long press will show a menu controller.
|
||||
- (instancetype)initWithAttachmentStream:(TSAttachmentStream *)attachmentStream
|
||||
|
@ -19,7 +19,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (instancetype)initWithAttachment:(SignalAttachment *)attachment fromRect:(CGRect)rect;
|
||||
|
||||
- (void)presentFromViewController:(UIViewController *)viewController;
|
||||
- (void)presentFromViewController:(UIViewController *)viewController replacingView:(UIView *)view;
|
||||
|
||||
@end
|
||||
|
986
Signal/src/ViewControllers/MediaDetailViewController.m
Normal file
986
Signal/src/ViewControllers/MediaDetailViewController.m
Normal file
|
@ -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
|
||||
}
|
||||
|
||||
class MessageDetailViewController: OWSViewController, UIScrollViewDelegate {
|
||||
class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, MediaDetailPresenter {
|
||||
|
||||
static let TAG = "[MessageDetailViewController]"
|
||||
let TAG = "[MessageDetailViewController]"
|
||||
|
@ -405,7 +405,7 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate {
|
|||
}
|
||||
|
||||
guard let attachment = TSAttachment.fetch(uniqueId: attachmentId, transaction: transaction) else {
|
||||
owsFail("Missing attachment")
|
||||
Logger.warn("\(TAG) Missing attachment. Was it deleted?")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -414,9 +414,9 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate {
|
|||
|
||||
private func addAttachmentRows() -> [UIView] {
|
||||
var rows = [UIView]()
|
||||
|
||||
|
||||
guard let attachment = self.attachment else {
|
||||
owsFail("no attachment to add.")
|
||||
Logger.warn("\(TAG) Missing attachment. Was it deleted?")
|
||||
return rows
|
||||
}
|
||||
|
||||
|
@ -442,7 +442,8 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate {
|
|||
let contentType = attachment.contentType
|
||||
if let dataUTI = MIMETypeUtil.utiType(forMIMEType: contentType) {
|
||||
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
|
||||
self.mediaMessageView = mediaMessageView
|
||||
rows.append(mediaMessageView)
|
||||
|
@ -751,4 +752,18 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate {
|
|||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
200
Signal/src/views/VideoPlayerView.swift
Normal file
200
Signal/src/views/VideoPlayerView.swift
Normal file
|
@ -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/DebugLogger.h>
|
||||
#import <SignalMessaging/Environment.h>
|
||||
#import <SignalMessaging/FullImageViewController.h>
|
||||
#import <SignalMessaging/NSString+OWS.h>
|
||||
#import <SignalMessaging/OWSAudioAttachmentPlayer.h>
|
||||
#import <SignalMessaging/OWSContactAvatarBuilder.h>
|
||||
|
|
|
@ -202,7 +202,8 @@ public class AttachmentApprovalViewController: OWSViewController, CaptioningTool
|
|||
|
||||
@objc
|
||||
public func playButtonTapped() {
|
||||
mediaMessageView.playVideo()
|
||||
// FIXME - use built in AVPlayer controls like MediaDetailViewController
|
||||
// mediaMessageView.playVideo()
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -14,6 +14,11 @@ public enum MediaMessageViewMode: UInt {
|
|||
case attachmentApproval
|
||||
}
|
||||
|
||||
@objc
|
||||
public protocol MediaDetailPresenter: class {
|
||||
func presentDetails(mediaMessageView: MediaMessageView, fromView: UIView)
|
||||
}
|
||||
|
||||
@objc
|
||||
public class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate {
|
||||
|
||||
|
@ -56,6 +61,8 @@ public class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate {
|
|||
|
||||
@objc
|
||||
public var contentView: UIView?
|
||||
|
||||
private let mediaDetailPresenter: MediaDetailPresenter?
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
|
@ -65,10 +72,15 @@ public class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate {
|
|||
}
|
||||
|
||||
@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)
|
||||
self.mode = mode
|
||||
self.attachment = attachment
|
||||
self.mode = mode
|
||||
self.mediaDetailPresenter = mediaDetailPresenter
|
||||
super.init(frame: CGRect.zero)
|
||||
|
||||
createViews()
|
||||
|
@ -463,14 +475,8 @@ public class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate {
|
|||
guard let fromView = sender.view else {
|
||||
return
|
||||
}
|
||||
guard let fromViewController = CurrentAppContext().frontmostViewController() else {
|
||||
return
|
||||
}
|
||||
|
||||
let window = CurrentAppContext().rootReferenceView
|
||||
let convertedRect = fromView.convert(fromView.bounds, to:window)
|
||||
let viewController = FullImageViewController(attachment:attachment, from:convertedRect)
|
||||
viewController.present(from:fromViewController)
|
||||
|
||||
showMediaDetailViewController(fromView: fromView)
|
||||
}
|
||||
|
||||
// MARK: - Video Playback
|
||||
|
@ -484,60 +490,14 @@ public class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate {
|
|||
guard sender.state == .recognized else {
|
||||
return
|
||||
}
|
||||
|
||||
playVideo()
|
||||
}
|
||||
|
||||
@objc
|
||||
public func playVideo() {
|
||||
guard let dataUrl = attachment.dataUrl else {
|
||||
owsFail("\(self.logTag) attachment is missing dataUrl")
|
||||
guard let fromView = sender.view else {
|
||||
return
|
||||
}
|
||||
|
||||
let filePath = dataUrl.path
|
||||
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)
|
||||
|
||||
showMediaDetailViewController(fromView: fromView)
|
||||
}
|
||||
|
||||
private func moviePlayerWillExitFullscreen() {
|
||||
clearVideoPlayer()
|
||||
}
|
||||
|
||||
private func moviePlayerDidExitFullscreen() {
|
||||
clearVideoPlayer()
|
||||
}
|
||||
|
||||
private func clearVideoPlayer() {
|
||||
videoPlayer?.stop()
|
||||
videoPlayer?.view.removeFromSuperview()
|
||||
videoPlayer = nil
|
||||
OWSAudioAttachmentPlayer.setAudioIgnoresHardwareMuteSwitch(false)
|
||||
func showMediaDetailViewController(fromView: UIView) {
|
||||
self.mediaDetailPresenter?.presentDetails(mediaMessageView: self, fromView: fromView)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,6 +83,9 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value);
|
|||
- (NSLayoutConstraint *)autoPinLeadingToSuperviewWithMargin:(CGFloat)margin;
|
||||
- (NSLayoutConstraint *)autoPinTrailingToSuperview;
|
||||
- (NSLayoutConstraint *)autoPinTrailingToSuperviewWithMargin:(CGFloat)margin;
|
||||
- (NSLayoutConstraint *)autoPinTopToSuperviewWithMargin:(CGFloat)margin;
|
||||
- (NSLayoutConstraint *)autoPinBottomToSuperviewWithMargin:(CGFloat)margin;
|
||||
|
||||
- (NSLayoutConstraint *)autoPinLeadingToTrailingOfView:(UIView *)view;
|
||||
- (NSLayoutConstraint *)autoPinLeadingToTrailingOfView:(UIView *)view margin:(CGFloat)margin;
|
||||
- (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
|
||||
{
|
||||
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"
|
||||
|
|
Loading…
Reference in a new issue