Merge remote-tracking branch 'private/release/2.40.0'
This commit is contained in:
commit
0b4728ba3f
@ -536,6 +536,7 @@
|
||||
768A1A2B17FC9CD300E00ED8 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 768A1A2A17FC9CD300E00ED8 /* libz.dylib */; };
|
||||
76C87F19181EFCE600C4ACAB /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; };
|
||||
76EB054018170B33006006FC /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 76EB03C318170B33006006FC /* AppDelegate.m */; };
|
||||
8811CF842295D8DA00FF6549 /* VolumeButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8811CF832295D8DA00FF6549 /* VolumeButtons.swift */; };
|
||||
954AEE6A1DF33E01002E5410 /* ContactsPickerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 954AEE681DF33D32002E5410 /* ContactsPickerTest.swift */; };
|
||||
A10FDF79184FB4BB007FF963 /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; };
|
||||
A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */; };
|
||||
@ -1317,6 +1318,7 @@
|
||||
76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = System/Library/Frameworks/MediaPlayer.framework; sourceTree = SDKROOT; };
|
||||
76EB03C218170B33006006FC /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
|
||||
76EB03C318170B33006006FC /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
|
||||
8811CF832295D8DA00FF6549 /* VolumeButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeButtons.swift; sourceTree = "<group>"; };
|
||||
8981C8F64D94D3C52EB67A2C /* Pods-SignalTests.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalTests.test.xcconfig"; path = "Pods/Target Support Files/Pods-SignalTests/Pods-SignalTests.test.xcconfig"; sourceTree = "<group>"; };
|
||||
8EEE74B0753448C085B48721 /* Pods-SignalMessaging.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalMessaging.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SignalMessaging/Pods-SignalMessaging.app store release.xcconfig"; sourceTree = "<group>"; };
|
||||
948239851C08032C842937CC /* Pods-SignalMessaging.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalMessaging.test.xcconfig"; path = "Pods/Target Support Files/Pods-SignalMessaging/Pods-SignalMessaging.test.xcconfig"; sourceTree = "<group>"; };
|
||||
@ -2511,6 +2513,7 @@
|
||||
34E5DC8020D8050D00C08145 /* RegistrationUtils.h */,
|
||||
34E5DC8120D8050D00C08145 /* RegistrationUtils.m */,
|
||||
4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */,
|
||||
8811CF832295D8DA00FF6549 /* VolumeButtons.swift */,
|
||||
FCFA64B11A24F29E0007FB87 /* UI Categories */,
|
||||
);
|
||||
path = util;
|
||||
@ -3701,6 +3704,7 @@
|
||||
450D19131F85236600970622 /* RemoteVideoView.m in Sources */,
|
||||
34129B8621EF877A005457A8 /* LinkPreviewView.swift in Sources */,
|
||||
34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */,
|
||||
8811CF842295D8DA00FF6549 /* VolumeButtons.swift in Sources */,
|
||||
45DF5DF21DDB843F00C936C7 /* CompareSafetyNumbersActivity.swift in Sources */,
|
||||
451166C01FD86B98000739BA /* AccountManager.swift in Sources */,
|
||||
3430FE181F7751D4000EC51B /* GiphyAPI.swift in Sources */,
|
||||
|
||||
@ -47,7 +47,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2.40.0.11</string>
|
||||
<string>2.40.0.12</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LOGS_EMAIL</key>
|
||||
|
||||
@ -277,9 +277,23 @@ const CGFloat kMaxTextViewHeight = 98;
|
||||
[vStackWrapper setContentHuggingHorizontalLow];
|
||||
[vStackWrapper setCompressionResistanceHorizontalLow];
|
||||
|
||||
// Media Stack
|
||||
UIStackView *mediaStack = [[UIStackView alloc] initWithArrangedSubviews:@[
|
||||
self.sendButton,
|
||||
self.cameraButton,
|
||||
self.voiceMemoButton,
|
||||
]];
|
||||
mediaStack.axis = UILayoutConstraintAxisHorizontal;
|
||||
mediaStack.alignment = UIStackViewAlignmentCenter;
|
||||
[mediaStack setContentHuggingHorizontalHigh];
|
||||
[mediaStack setCompressionResistanceHorizontalHigh];
|
||||
|
||||
// H Stack
|
||||
UIStackView *hStack = [[UIStackView alloc]
|
||||
initWithArrangedSubviews:@[ self.cameraButton, vStackWrapper, self.attachmentButton, self.sendButton ]];
|
||||
UIStackView *hStack = [[UIStackView alloc] initWithArrangedSubviews:@[
|
||||
self.attachmentButton,
|
||||
vStackWrapper,
|
||||
mediaStack,
|
||||
]];
|
||||
hStack.axis = UILayoutConstraintAxisHorizontal;
|
||||
hStack.layoutMarginsRelativeArrangement = YES;
|
||||
hStack.layoutMargins = UIEdgeInsetsMake(6, 6, 6, 6);
|
||||
@ -289,8 +303,9 @@ const CGFloat kMaxTextViewHeight = 98;
|
||||
// Suggested Stickers
|
||||
const CGFloat suggestedStickerSize = 48;
|
||||
const CGFloat suggestedStickerSpacing = 12;
|
||||
_suggestedStickerView =
|
||||
[[StickerHorizontalListView alloc] initWithCellSize:suggestedStickerSize spacing:suggestedStickerSpacing];
|
||||
_suggestedStickerView = [[StickerHorizontalListView alloc] initWithCellSize:suggestedStickerSize
|
||||
cellInset:0
|
||||
spacing:suggestedStickerSpacing];
|
||||
self.suggestedStickerView.backgroundColor = UIColor.clearColor;
|
||||
self.suggestedStickerView.contentInset = UIEdgeInsetsMake(
|
||||
suggestedStickerSpacing, suggestedStickerSpacing, suggestedStickerSpacing, suggestedStickerSpacing);
|
||||
@ -323,12 +338,9 @@ const CGFloat kMaxTextViewHeight = 98;
|
||||
self.preservesSuperviewLayoutMargins = NO;
|
||||
|
||||
// Input buttons
|
||||
[self addSubview:self.voiceMemoButton];
|
||||
[self.voiceMemoButton autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.inputTextView];
|
||||
[self.voiceMemoButton autoPinEdge:ALEdgeTrailing toEdge:ALEdgeTrailing ofView:vStackWrapper withOffset:-4];
|
||||
[self addSubview:self.stickerButton];
|
||||
[self.stickerButton autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.voiceMemoButton];
|
||||
[self.voiceMemoButton autoPinLeadingToTrailingEdgeOfView:self.stickerButton offset:0];
|
||||
[self.stickerButton autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.inputTextView];
|
||||
[self.stickerButton autoPinEdge:ALEdgeTrailing toEdge:ALEdgeTrailing ofView:vStackWrapper withOffset:-4];
|
||||
|
||||
// Border
|
||||
//
|
||||
@ -570,41 +582,31 @@ const CGFloat kMaxTextViewHeight = 98;
|
||||
|
||||
- (void)ensureButtonVisibilityWithIsAnimated:(BOOL)isAnimated doLayout:(BOOL)doLayout
|
||||
{
|
||||
void (^ensureViewHiddenState)(UIView *, BOOL) = ^(UIView *subview, BOOL hidden) {
|
||||
if (subview.isHidden != hidden) {
|
||||
subview.hidden = hidden;
|
||||
}
|
||||
};
|
||||
|
||||
void (^updateBlock)(void) = ^{
|
||||
ensureViewHiddenState(self.attachmentButton, NO);
|
||||
|
||||
BOOL hasTextInput = self.inputTextView.trimmedText.length > 0;
|
||||
if (hasTextInput) {
|
||||
if (!self.attachmentButton.isHidden) {
|
||||
self.attachmentButton.hidden = YES;
|
||||
}
|
||||
if (!self.voiceMemoButton.isHidden) {
|
||||
self.voiceMemoButton.hidden = YES;
|
||||
}
|
||||
if (self.sendButton.isHidden) {
|
||||
self.sendButton.hidden = NO;
|
||||
}
|
||||
ensureViewHiddenState(self.cameraButton, YES);
|
||||
ensureViewHiddenState(self.voiceMemoButton, YES);
|
||||
ensureViewHiddenState(self.sendButton, NO);
|
||||
} else {
|
||||
if (self.attachmentButton.isHidden) {
|
||||
self.attachmentButton.hidden = NO;
|
||||
}
|
||||
if (self.voiceMemoButton.isHidden) {
|
||||
self.voiceMemoButton.hidden = NO;
|
||||
}
|
||||
if (!self.sendButton.isHidden) {
|
||||
self.sendButton.hidden = YES;
|
||||
}
|
||||
ensureViewHiddenState(self.cameraButton, NO);
|
||||
ensureViewHiddenState(self.voiceMemoButton, NO);
|
||||
ensureViewHiddenState(self.sendButton, YES);
|
||||
}
|
||||
|
||||
BOOL hideStickerButton = hasTextInput || self.quotedReply != nil || !StickerManager.shared.isStickerSendEnabled;
|
||||
if (hideStickerButton) {
|
||||
if (!self.stickerButton.isHidden) {
|
||||
self.stickerButton.hidden = YES;
|
||||
}
|
||||
} else {
|
||||
if (self.stickerButton.isHidden) {
|
||||
self.stickerButton.hidden = NO;
|
||||
}
|
||||
ensureViewHiddenState(self.stickerButton, hideStickerButton);
|
||||
if (!hideStickerButton) {
|
||||
self.stickerButton.imageView.tintColor
|
||||
= (self.isStickerKeyboardActive ? Theme.primaryColor : Theme.navbarIconColor);
|
||||
= (self.isStickerKeyboardActive ? UIColor.ows_signalBlueColor : Theme.navbarIconColor);
|
||||
}
|
||||
|
||||
[self updateSuggestedStickers];
|
||||
@ -1365,13 +1367,13 @@ const CGFloat kMaxTextViewHeight = 98;
|
||||
}
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
BOOL shouldReset = self.suggestedStickerView.isHidden;
|
||||
NSMutableArray<StickerHorizontalListViewItem *> *items = [NSMutableArray new];
|
||||
NSMutableArray<id<StickerHorizontalListViewItem>> *items = [NSMutableArray new];
|
||||
for (StickerInfo *stickerInfo in self.suggestedStickerInfos) {
|
||||
[items addObject:[[StickerHorizontalListViewItem alloc] initWithStickerInfo:stickerInfo
|
||||
selectedBlock:^{
|
||||
[weakSelf
|
||||
didSelectSuggestedSticker:stickerInfo];
|
||||
}]];
|
||||
[items addObject:[[StickerHorizontalListViewItemSticker alloc]
|
||||
initWithStickerInfo:stickerInfo
|
||||
didSelectBlock:^{
|
||||
[weakSelf didSelectSuggestedSticker:stickerInfo];
|
||||
}]];
|
||||
}
|
||||
self.suggestedStickerView.items = items;
|
||||
self.suggestedStickerView.hidden = NO;
|
||||
|
||||
@ -1270,6 +1270,11 @@ typedef enum : NSUInteger {
|
||||
break;
|
||||
case ConversationViewActionCompose:
|
||||
[self popKeyBoard];
|
||||
// When we programmatically pop the keyboard here,
|
||||
// the scroll position gets into a weird state and
|
||||
// content is hidden behind the keyboard so we restore
|
||||
// it to the default position.
|
||||
[self scrollToDefaultPosition:YES];
|
||||
break;
|
||||
case ConversationViewActionAudioCall:
|
||||
[self startAudioCall];
|
||||
@ -3617,15 +3622,12 @@ typedef enum : NSUInteger {
|
||||
- (void)markVisibleMessagesAsRead
|
||||
{
|
||||
if (self.presentedViewController) {
|
||||
OWSLogInfo(@"Not marking messages as read; another view is presented.");
|
||||
return;
|
||||
}
|
||||
if (OWSWindowManager.sharedManager.shouldShowCallView) {
|
||||
OWSLogInfo(@"Not marking messages as read; call view is presented.");
|
||||
return;
|
||||
}
|
||||
if (self.navigationController.topViewController != self) {
|
||||
OWSLogInfo(@"Not marking messages as read; another view is pushed.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -25,6 +25,9 @@ protocol PhotoCaptureDelegate: AnyObject {
|
||||
func photoCaptureDidTryToCaptureTooMany(_ photoCapture: PhotoCapture)
|
||||
var zoomScaleReferenceHeight: CGFloat? { get }
|
||||
var captureOrientation: AVCaptureVideoOrientation { get }
|
||||
|
||||
func beginCaptureButtonAnimation(_ duration: TimeInterval)
|
||||
func endCaptureButtonAnimation(_ duration: TimeInterval)
|
||||
}
|
||||
|
||||
class PhotoCapture: NSObject {
|
||||
@ -332,15 +335,10 @@ class PhotoCapture: NSObject {
|
||||
private func clampZoom(_ factor: CGFloat, device: AVCaptureDevice) -> CGFloat {
|
||||
return min(factor.clamp(minimumZoom, maximumZoom), device.activeFormat.videoMaxZoomFactor)
|
||||
}
|
||||
}
|
||||
|
||||
extension PhotoCapture: CaptureButtonDelegate {
|
||||
|
||||
// MARK: - Photo
|
||||
|
||||
func didTapCaptureButton(_ captureButton: CaptureButton) {
|
||||
private func handleTap() {
|
||||
Logger.verbose("")
|
||||
|
||||
guard let delegate = delegate else { return }
|
||||
guard delegate.photoCaptureCanCaptureMoreItems(self) else {
|
||||
delegate.photoCaptureDidTryToCaptureTooMany(self)
|
||||
@ -355,7 +353,7 @@ extension PhotoCapture: CaptureButtonDelegate {
|
||||
|
||||
// MARK: - Video
|
||||
|
||||
func didBeginLongPressCaptureButton(_ captureButton: CaptureButton) {
|
||||
private func handleLongPressBegin() {
|
||||
AssertIsOnMainThread()
|
||||
Logger.verbose("")
|
||||
|
||||
@ -375,7 +373,7 @@ extension PhotoCapture: CaptureButtonDelegate {
|
||||
}.retainUntilComplete()
|
||||
}
|
||||
|
||||
func didCompleteLongPressCaptureButton(_ captureButton: CaptureButton) {
|
||||
private func handleLongPressComplete() {
|
||||
Logger.verbose("")
|
||||
sessionQueue.async {
|
||||
self.captureOutput.completeVideo(delegate: self)
|
||||
@ -386,7 +384,7 @@ extension PhotoCapture: CaptureButtonDelegate {
|
||||
delegate?.photoCaptureDidCompleteVideo(self)
|
||||
}
|
||||
|
||||
func didCancelLongPressCaptureButton(_ captureButton: CaptureButton) {
|
||||
private func handleLongPressCancel() {
|
||||
Logger.verbose("")
|
||||
AssertIsOnMainThread()
|
||||
sessionQueue.async {
|
||||
@ -394,6 +392,50 @@ extension PhotoCapture: CaptureButtonDelegate {
|
||||
}
|
||||
delegate?.photoCaptureDidCancelVideo(self)
|
||||
}
|
||||
}
|
||||
|
||||
extension PhotoCapture: VolumeButtonObserver {
|
||||
func didPressVolumeButton(with identifier: VolumeButtons.Identifier) {
|
||||
delegate?.beginCaptureButtonAnimation(0.5)
|
||||
}
|
||||
|
||||
func didReleaseVolumeButton(with identifier: VolumeButtons.Identifier) {
|
||||
delegate?.endCaptureButtonAnimation(0.2)
|
||||
}
|
||||
|
||||
func didTapVolumeButton(with identifier: VolumeButtons.Identifier) {
|
||||
handleTap()
|
||||
}
|
||||
|
||||
func didBeginLongPressVolumeButton(with identifier: VolumeButtons.Identifier) {
|
||||
handleLongPressBegin()
|
||||
}
|
||||
|
||||
func didCompleteLongPressVolumeButton(with identifier: VolumeButtons.Identifier) {
|
||||
handleLongPressComplete()
|
||||
}
|
||||
|
||||
func didCancelLongPressVolumeButton(with identifier: VolumeButtons.Identifier) {
|
||||
handleLongPressCancel()
|
||||
}
|
||||
}
|
||||
|
||||
extension PhotoCapture: CaptureButtonDelegate {
|
||||
func didTapCaptureButton(_ captureButton: CaptureButton) {
|
||||
handleTap()
|
||||
}
|
||||
|
||||
func didBeginLongPressCaptureButton(_ captureButton: CaptureButton) {
|
||||
handleLongPressBegin()
|
||||
}
|
||||
|
||||
func didCompleteLongPressCaptureButton(_ captureButton: CaptureButton) {
|
||||
handleLongPressComplete()
|
||||
}
|
||||
|
||||
func didCancelLongPressCaptureButton(_ captureButton: CaptureButton) {
|
||||
handleLongPressCancel()
|
||||
}
|
||||
|
||||
var zoomScaleReferenceHeight: CGFloat? {
|
||||
return delegate?.zoomScaleReferenceHeight
|
||||
@ -449,8 +491,11 @@ extension PhotoCapture: CaptureOutputDelegate {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
if let error = error {
|
||||
delegate?.photoCapture(self, processingDidError: error)
|
||||
return
|
||||
guard didSucceedDespiteError(error) else {
|
||||
delegate?.photoCapture(self, processingDidError: error)
|
||||
return
|
||||
}
|
||||
Logger.info("Ignoring error, since capture succeeded.")
|
||||
}
|
||||
|
||||
guard let dataSource = DataSourcePath.dataSource(with: outputFileURL, shouldDeleteOnDeallocation: true) else {
|
||||
@ -467,6 +512,19 @@ extension PhotoCapture: CaptureOutputDelegate {
|
||||
self.delegate?.photoCapture(self, didFinishProcessingAttachment: attachment)
|
||||
}.retainUntilComplete()
|
||||
}
|
||||
|
||||
/// The AVCaptureFileOutput can return an error even though recording succeeds.
|
||||
/// I can't find useful documentation on this, but Apple's example AVCam app silently
|
||||
/// discards these errors, so we do the same.
|
||||
/// These spurious errors can be reproduced 1/3 of the time when making a series of short videos.
|
||||
private func didSucceedDespiteError(_ error: Error) -> Bool {
|
||||
let nsError = error as NSError
|
||||
guard let successfullyFinished = nsError.userInfo[AVErrorRecordingSuccessfullyFinishedKey] as? Bool else {
|
||||
return false
|
||||
}
|
||||
|
||||
return successfullyFinished
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Capture Adapter
|
||||
|
||||
@ -70,7 +70,6 @@ class PhotoCaptureViewController: OWSViewController {
|
||||
view.addGestureRecognizer(doubleTapToSwitchCameraGesture)
|
||||
|
||||
tapToFocusGesture.require(toFail: doubleTapToSwitchCameraGesture)
|
||||
doubleTapToSwitchCameraGesture.require(toFail: captureButton.tapGesture)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
@ -82,8 +81,14 @@ class PhotoCaptureViewController: OWSViewController {
|
||||
super.viewDidAppear(animated)
|
||||
if hasCaptureStarted {
|
||||
BenchEventComplete(eventId: "Show-Camera")
|
||||
VolumeButtons.shared?.addObserver(observer: photoCapture)
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
VolumeButtons.shared?.removeObserver(photoCapture)
|
||||
}
|
||||
|
||||
override var prefersStatusBarHidden: Bool {
|
||||
guard !OWSWindowManager.shared().hasCall() else {
|
||||
@ -333,6 +338,8 @@ class PhotoCaptureViewController: OWSViewController {
|
||||
view.addSubview(captureButton)
|
||||
captureButton.autoHCenterInSuperview()
|
||||
captureButton.centerYAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: SendMediaNavigationController.bottomButtonsCenterOffset).isActive = true
|
||||
|
||||
VolumeButtons.shared?.addObserver(observer: photoCapture)
|
||||
}
|
||||
|
||||
private func showFailureUI(error: Error) {
|
||||
@ -368,6 +375,13 @@ extension PhotoCaptureViewController: PhotoCaptureDelegate {
|
||||
captureFeedbackView.backgroundColor = .black
|
||||
view.insertSubview(captureFeedbackView, aboveSubview: previewView)
|
||||
captureFeedbackView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
// Ensure the capture feedback is laid out before we remove it,
|
||||
// depending on where we're coming from a layout pass might not
|
||||
// trigger in 0.05 seconds otherwise.
|
||||
view.setNeedsLayout()
|
||||
view.layoutIfNeeded()
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||
captureFeedbackView.removeFromSuperview()
|
||||
}
|
||||
@ -419,6 +433,14 @@ extension PhotoCaptureViewController: PhotoCaptureDelegate {
|
||||
var captureOrientation: AVCaptureVideoOrientation {
|
||||
return lastKnownCaptureOrientation
|
||||
}
|
||||
|
||||
func beginCaptureButtonAnimation(_ duration: TimeInterval) {
|
||||
captureButton.beginRecordingAnimation(duration: duration)
|
||||
}
|
||||
|
||||
func endCaptureButtonAnimation(_ duration: TimeInterval) {
|
||||
captureButton.endRecordingAnimation(duration: duration)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Views
|
||||
@ -440,8 +462,6 @@ class CaptureButton: UIView {
|
||||
|
||||
let innerButton = CircleView()
|
||||
|
||||
var tapGesture: UITapGestureRecognizer!
|
||||
|
||||
var longPressGesture: UILongPressGestureRecognizer!
|
||||
let longPressDuration = 0.5
|
||||
|
||||
@ -457,11 +477,10 @@ class CaptureButton: UIView {
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap))
|
||||
innerButton.addGestureRecognizer(tapGesture)
|
||||
|
||||
// The long press handles both the tap and the hold interaction, as well as the animation
|
||||
// the presents as the user begins to hold (and the button begins to grow prior to recording)
|
||||
longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(didLongPress))
|
||||
longPressGesture.minimumPressDuration = longPressDuration
|
||||
longPressGesture.minimumPressDuration = 0
|
||||
innerButton.addGestureRecognizer(longPressGesture)
|
||||
|
||||
addSubview(innerButton)
|
||||
@ -485,14 +504,39 @@ class CaptureButton: UIView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - Gestures
|
||||
|
||||
@objc
|
||||
func didTap(_ gesture: UITapGestureRecognizer) {
|
||||
delegate?.didTapCaptureButton(self)
|
||||
func beginRecordingAnimation(duration: TimeInterval, delay: TimeInterval = 0) {
|
||||
UIView.animate(
|
||||
withDuration: duration,
|
||||
delay: delay,
|
||||
options: [.beginFromCurrentState, .curveLinear],
|
||||
animations: {
|
||||
self.innerButtonSizeConstraints.forEach { $0.constant = type(of: self).recordingDiameter }
|
||||
self.zoomIndicatorSizeConstraints.forEach { $0.constant = type(of: self).recordingDiameter }
|
||||
self.superview?.layoutIfNeeded()
|
||||
},
|
||||
completion: nil
|
||||
)
|
||||
}
|
||||
|
||||
func endRecordingAnimation(duration: TimeInterval, delay: TimeInterval = 0) {
|
||||
UIView.animate(
|
||||
withDuration: duration,
|
||||
delay: delay,
|
||||
options: [.beginFromCurrentState, .curveEaseIn],
|
||||
animations: {
|
||||
self.innerButtonSizeConstraints.forEach { $0.constant = self.defaultDiameter }
|
||||
self.zoomIndicatorSizeConstraints.forEach { $0.constant = self.defaultDiameter }
|
||||
self.superview?.layoutIfNeeded()
|
||||
},
|
||||
completion: nil
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Gestures
|
||||
|
||||
var initialTouchLocation: CGPoint?
|
||||
var touchTimer: Timer?
|
||||
var isLongPressing = false
|
||||
|
||||
@objc
|
||||
func didLongPress(_ gesture: UILongPressGestureRecognizer) {
|
||||
@ -507,13 +551,24 @@ class CaptureButton: UIView {
|
||||
case .possible: break
|
||||
case .began:
|
||||
initialTouchLocation = gesture.location(in: gesture.view)
|
||||
delegate?.didBeginLongPressCaptureButton(self)
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
self.innerButtonSizeConstraints.forEach { $0.constant = type(of: self).recordingDiameter }
|
||||
self.zoomIndicatorSizeConstraints.forEach { $0.constant = type(of: self).recordingDiameter }
|
||||
self.superview?.layoutIfNeeded()
|
||||
beginRecordingAnimation(duration: 0.4, delay: 0.1)
|
||||
|
||||
isLongPressing = false
|
||||
|
||||
touchTimer?.invalidate()
|
||||
touchTimer = WeakTimer.scheduledTimer(
|
||||
timeInterval: longPressDuration,
|
||||
target: self,
|
||||
userInfo: nil,
|
||||
repeats: false
|
||||
) { [weak self] _ in
|
||||
guard let `self` = self else { return }
|
||||
self.isLongPressing = true
|
||||
self.delegate?.didBeginLongPressCaptureButton(self)
|
||||
}
|
||||
case .changed:
|
||||
guard isLongPressing else { break }
|
||||
|
||||
guard let referenceHeight = delegate?.zoomScaleReferenceHeight else {
|
||||
owsFailDebug("referenceHeight was unexpectedly nil")
|
||||
return
|
||||
@ -545,22 +600,25 @@ class CaptureButton: UIView {
|
||||
|
||||
delegate?.longPressCaptureButton(self, didUpdateZoomAlpha: alpha)
|
||||
case .ended:
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
self.innerButtonSizeConstraints.forEach { $0.constant = self.defaultDiameter }
|
||||
self.zoomIndicatorSizeConstraints.forEach { $0.constant = self.defaultDiameter }
|
||||
endRecordingAnimation(duration: 0.2)
|
||||
|
||||
self.superview?.layoutIfNeeded()
|
||||
if isLongPressing {
|
||||
delegate?.didCompleteLongPressCaptureButton(self)
|
||||
} else {
|
||||
delegate?.didTapCaptureButton(self)
|
||||
}
|
||||
delegate?.didCompleteLongPressCaptureButton(self)
|
||||
|
||||
touchTimer?.invalidate()
|
||||
touchTimer = nil
|
||||
case .cancelled, .failed:
|
||||
endRecordingAnimation(duration: 0.2)
|
||||
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
self.innerButtonSizeConstraints.forEach { $0.constant = self.defaultDiameter }
|
||||
self.zoomIndicatorSizeConstraints.forEach { $0.constant = self.defaultDiameter }
|
||||
|
||||
self.superview?.layoutIfNeeded()
|
||||
if isLongPressing {
|
||||
delegate?.didCancelLongPressCaptureButton(self)
|
||||
}
|
||||
delegate?.didCancelLongPressCaptureButton(self)
|
||||
|
||||
touchTimer?.invalidate()
|
||||
touchTimer = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -672,6 +730,7 @@ class RecordingTimerView: UIView {
|
||||
UIView.animate(withDuration: 0.4) {
|
||||
self.icon.alpha = 0
|
||||
}
|
||||
label.text = nil
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
247
Signal/src/util/VolumeButtons.swift
Normal file
247
Signal/src/util/VolumeButtons.swift
Normal file
@ -0,0 +1,247 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol VolumeButtonObserver: class {
|
||||
func didPressVolumeButton(with identifier: VolumeButtons.Identifier)
|
||||
func didReleaseVolumeButton(with identifier: VolumeButtons.Identifier)
|
||||
|
||||
func didTapVolumeButton(with identifier: VolumeButtons.Identifier)
|
||||
|
||||
func didBeginLongPressVolumeButton(with identifier: VolumeButtons.Identifier)
|
||||
func didCompleteLongPressVolumeButton(with identifier: VolumeButtons.Identifier)
|
||||
func didCancelLongPressVolumeButton(with identifier: VolumeButtons.Identifier)
|
||||
}
|
||||
|
||||
class VolumeButtons {
|
||||
static let shared = VolumeButtons()
|
||||
|
||||
enum Identifier {
|
||||
case up, down
|
||||
}
|
||||
|
||||
private init?() {
|
||||
// If for some reason the API we’re using goes away (for example, in
|
||||
// a future iOS version) this class will never instantiate.
|
||||
guard VolumeButtons.supportsListeningToEvents else { return nil }
|
||||
}
|
||||
|
||||
deinit {
|
||||
stopObservation()
|
||||
}
|
||||
|
||||
// MARK: Observer Management
|
||||
|
||||
private var observers: [Weak<VolumeButtonObserver>] = []
|
||||
func addObserver(observer: VolumeButtonObserver) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
if observers.firstIndex(where: { $0.value === observer }) == nil {
|
||||
observers.append(Weak(value: observer))
|
||||
}
|
||||
|
||||
guard !observers.isEmpty else { return }
|
||||
startObservation()
|
||||
}
|
||||
|
||||
func removeObserver(_ observer: VolumeButtonObserver) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
observers = observers.filter { $0.value !== observer }
|
||||
|
||||
guard observers.isEmpty else { return }
|
||||
stopObservation()
|
||||
}
|
||||
|
||||
func removeAllObservers() {
|
||||
AssertIsOnMainThread()
|
||||
observers = []
|
||||
stopObservation()
|
||||
}
|
||||
|
||||
private func startObservation() {
|
||||
guard !VolumeButtons.isRegisteredForEvents else { return }
|
||||
VolumeButtons.isRegisteredForEvents = true
|
||||
registerForNotifications()
|
||||
}
|
||||
|
||||
private func stopObservation() {
|
||||
VolumeButtons.isRegisteredForEvents = false
|
||||
unregisterForNotifications()
|
||||
|
||||
defer { resetLongPress() }
|
||||
guard let longPressingButton = longPressingButton else { return }
|
||||
notifyObserversOfCancelLongPress(with: longPressingButton)
|
||||
}
|
||||
|
||||
private func notifyObserversOfTap(with identifier: Identifier) {
|
||||
observers.forEach { observer in
|
||||
observer.value?.didTapVolumeButton(with: identifier)
|
||||
}
|
||||
}
|
||||
|
||||
private func notifyObserversOfBeginLongPress(with identifier: Identifier) {
|
||||
observers.forEach { observer in
|
||||
observer.value?.didBeginLongPressVolumeButton(with: identifier)
|
||||
}
|
||||
}
|
||||
|
||||
private func notifyObserversOfCompleteLongPress(with identifier: Identifier) {
|
||||
observers.forEach { observer in
|
||||
observer.value?.didCompleteLongPressVolumeButton(with: identifier)
|
||||
}
|
||||
}
|
||||
|
||||
private func notifyObserversOfCancelLongPress(with identifier: Identifier) {
|
||||
observers.forEach { observer in
|
||||
observer.value?.didCancelLongPressVolumeButton(with: identifier)
|
||||
}
|
||||
}
|
||||
|
||||
private func notifyObserversOfPress(with identifier: Identifier) {
|
||||
observers.forEach { observer in
|
||||
observer.value?.didPressVolumeButton(with: identifier)
|
||||
}
|
||||
}
|
||||
|
||||
private func notifyObserversOfRelease(with identifier: Identifier) {
|
||||
observers.forEach { observer in
|
||||
observer.value?.didReleaseVolumeButton(with: identifier)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Tap / long press handling
|
||||
|
||||
private var longPressTimer: Timer?
|
||||
private var longPressingButton: Identifier?
|
||||
|
||||
// It's not possible for up and down to be pressed simulataneously
|
||||
// (if you press the second button, the OS will end the press on
|
||||
// the first), so it allows for simplified handling here.
|
||||
private func didPressButton(with identifier: Identifier) {
|
||||
longPressingButton = nil
|
||||
|
||||
longPressTimer?.invalidate()
|
||||
longPressTimer = WeakTimer.scheduledTimer(
|
||||
timeInterval: longPressDuration,
|
||||
target: self,
|
||||
userInfo: nil,
|
||||
repeats: false
|
||||
) { [weak self] _ in
|
||||
self?.longPressingButton = identifier
|
||||
self?.notifyObserversOfBeginLongPress(with: identifier)
|
||||
self?.longPressTimer?.invalidate()
|
||||
self?.longPressTimer = nil
|
||||
}
|
||||
|
||||
notifyObserversOfPress(with: identifier)
|
||||
}
|
||||
|
||||
private func didReleaseButton(with identifier: Identifier) {
|
||||
if longPressingButton == identifier {
|
||||
notifyObserversOfCompleteLongPress(with: identifier)
|
||||
} else {
|
||||
notifyObserversOfTap(with: identifier)
|
||||
}
|
||||
|
||||
resetLongPress()
|
||||
|
||||
notifyObserversOfRelease(with: identifier)
|
||||
}
|
||||
|
||||
private func resetLongPress() {
|
||||
longPressTimer?.invalidate()
|
||||
longPressTimer = nil
|
||||
longPressingButton = nil
|
||||
}
|
||||
|
||||
// MARK: Volume Event Registration
|
||||
|
||||
// let encodedSelectorString = "setWantsVolumeButtonEvents:".encodedForSelector
|
||||
private static let volumeEventsSelector = Selector("BXYGaHIABgVnAX0HfnZTBwYGAQBWCHYABgVL".decodedForSelector!)
|
||||
|
||||
private(set) static var isRegisteredForEvents = false {
|
||||
didSet {
|
||||
setEventRegistration(isRegisteredForEvents)
|
||||
}
|
||||
}
|
||||
|
||||
private static func setEventRegistration(_ active: Bool) {
|
||||
typealias Type = @convention(c) (AnyObject, Selector, Bool) -> Void
|
||||
let implementation = class_getMethodImplementation(UIApplication.self, volumeEventsSelector)
|
||||
let setRegistration = unsafeBitCast(implementation, to: Type.self)
|
||||
setRegistration(UIApplication.shared, volumeEventsSelector, active)
|
||||
}
|
||||
|
||||
private static var supportsListeningToEvents: Bool {
|
||||
return UIApplication.shared.responds(to: volumeEventsSelector)
|
||||
}
|
||||
|
||||
// MARK: Notification Handling
|
||||
|
||||
// let encodedDownDownNotificationName = "_UIApplicationVolumeDownButtonDownNotification".encodedForSelector
|
||||
private let downDownNotificationName = Notification.Name("cGZaUgICfXp0cgZ6AQBnAX0HfnZVAQkAUwcGBgEAVQEJAF8BBnp3enRyBnoBAA==".decodedForSelector!)
|
||||
|
||||
// let encodedDownUpNotificationName = "_UIApplicationVolumeDownButtonUpNotification".encodedForSelector
|
||||
private let downUpNotificationName = Notification.Name("cGZaUgICfXp0cgZ6AQBnAX0HfnZVAQkAUwcGBgEAZgJfAQZ6d3p0cgZ6AQA=".decodedForSelector!)
|
||||
|
||||
// let encodedUpDownNotificationName = "_UIApplicationVolumeUpButtonDownNotification".encodedForSelector
|
||||
private let upDownNotificationName = Notification.Name("cGZaUgICfXp0cgZ6AQBnAX0HfnZmAlMHBgYBAFUBCQBfAQZ6d3p0cgZ6AQA=".decodedForSelector!)
|
||||
|
||||
// let encodedUpUpNotificationName = "_UIApplicationVolumeUpButtonUpNotification".encodedForSelector
|
||||
private let upUpNotificationName = Notification.Name("cGZaUgICfXp0cgZ6AQBnAX0HfnZmAlMHBgYBAGYCXwEGend6dHIGegEA".decodedForSelector!)
|
||||
|
||||
private let longPressDuration: TimeInterval = 0.5
|
||||
|
||||
private func registerForNotifications() {
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(didPressVolumeUp),
|
||||
name: upDownNotificationName,
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(didReleaseVolumeUp),
|
||||
name: upUpNotificationName,
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(didPressVolumeDown),
|
||||
name: downDownNotificationName,
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(didReleaseVolumeDown),
|
||||
name: downUpNotificationName,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
private func unregisterForNotifications() {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
@objc func didPressVolumeUp(_ notification: Notification) {
|
||||
didPressButton(with: .up)
|
||||
}
|
||||
|
||||
@objc func didReleaseVolumeUp(_ notification: Notification) {
|
||||
didReleaseButton(with: .up)
|
||||
}
|
||||
|
||||
@objc func didPressVolumeDown(_ notification: Notification) {
|
||||
didPressButton(with: .down)
|
||||
}
|
||||
|
||||
@objc func didReleaseVolumeDown(_ notification: Notification) {
|
||||
didReleaseButton(with: .down)
|
||||
}
|
||||
}
|
||||
@ -103,7 +103,7 @@ public class LinkPreviewDraft: NSObject, LinkPreviewState {
|
||||
}
|
||||
|
||||
public func imageState() -> LinkPreviewImageState {
|
||||
if linkPreviewDraft.jpegImageData != nil {
|
||||
if linkPreviewDraft.imageData != nil {
|
||||
return .loaded
|
||||
} else {
|
||||
return .none
|
||||
@ -113,11 +113,11 @@ public class LinkPreviewDraft: NSObject, LinkPreviewState {
|
||||
public func image() -> UIImage? {
|
||||
assert(imageState() == .loaded)
|
||||
|
||||
guard let jpegImageData = linkPreviewDraft.jpegImageData else {
|
||||
guard let imageData = linkPreviewDraft.imageData else {
|
||||
return nil
|
||||
}
|
||||
guard let image = UIImage(data: jpegImageData) else {
|
||||
owsFailDebug("Could not load image: \(jpegImageData.count)")
|
||||
guard let image = UIImage(data: imageData) else {
|
||||
owsFailDebug("Could not load image: \(imageData.count)")
|
||||
return nil
|
||||
}
|
||||
return image
|
||||
@ -843,7 +843,7 @@ public class LinkPreviewView: UIStackView {
|
||||
}
|
||||
if let sentBodyView = self.sentBodyView {
|
||||
let borderView = OWSBubbleShapeView(draw: ())
|
||||
let borderColor = UIColor(rgbHex: Theme.isDarkThemeEnabled ? 0x0F1012 : 0xD5D6D6)
|
||||
let borderColor = (Theme.isDarkThemeEnabled ? UIColor.ows_gray60 : UIColor.ows_gray15)
|
||||
borderView.strokeColor = borderColor
|
||||
borderView.strokeThickness = CGHairlineWidth()
|
||||
sentBodyView.addSubview(borderView)
|
||||
|
||||
@ -5,14 +5,67 @@
|
||||
import Foundation
|
||||
|
||||
@objc
|
||||
public class StickerHorizontalListViewItem: NSObject {
|
||||
let stickerInfo: StickerInfo
|
||||
let selectedBlock: () -> Void
|
||||
public protocol StickerHorizontalListViewItem {
|
||||
var view: UIView { get }
|
||||
var didSelectBlock: () -> Void { get }
|
||||
var isSelected: Bool { get }
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
@objc
|
||||
public class StickerHorizontalListViewItemSticker: NSObject, StickerHorizontalListViewItem {
|
||||
private let stickerInfo: StickerInfo
|
||||
public let didSelectBlock: () -> Void
|
||||
public let isSelectedBlock: () -> Bool
|
||||
|
||||
// This initializer can be used for cells which are never selected.
|
||||
@objc
|
||||
public init(stickerInfo: StickerInfo, didSelectBlock: @escaping () -> Void) {
|
||||
self.stickerInfo = stickerInfo
|
||||
self.didSelectBlock = didSelectBlock
|
||||
self.isSelectedBlock = {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public init(stickerInfo: StickerInfo, selectedBlock: @escaping () -> Void) {
|
||||
public init(stickerInfo: StickerInfo, didSelectBlock: @escaping () -> Void, isSelectedBlock: @escaping () -> Bool) {
|
||||
self.stickerInfo = stickerInfo
|
||||
self.selectedBlock = selectedBlock
|
||||
self.didSelectBlock = didSelectBlock
|
||||
self.isSelectedBlock = isSelectedBlock
|
||||
}
|
||||
|
||||
public var view: UIView {
|
||||
return StickerView(stickerInfo: stickerInfo)
|
||||
}
|
||||
|
||||
public var isSelected: Bool {
|
||||
return isSelectedBlock()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
@objc
|
||||
public class StickerHorizontalListViewItemRecents: NSObject, StickerHorizontalListViewItem {
|
||||
public let didSelectBlock: () -> Void
|
||||
public let isSelectedBlock: () -> Bool
|
||||
|
||||
@objc
|
||||
public init(didSelectBlock: @escaping () -> Void, isSelectedBlock: @escaping () -> Bool) {
|
||||
self.didSelectBlock = didSelectBlock
|
||||
self.isSelectedBlock = isSelectedBlock
|
||||
}
|
||||
|
||||
public var view: UIView {
|
||||
let imageView = UIImageView()
|
||||
imageView.setTemplateImageName("recent-outline-24", tintColor: Theme.secondaryColor)
|
||||
return imageView
|
||||
}
|
||||
|
||||
public var isSelected: Bool {
|
||||
return isSelectedBlock()
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,6 +75,7 @@ public class StickerHorizontalListViewItem: NSObject {
|
||||
public class StickerHorizontalListView: UICollectionView {
|
||||
|
||||
private let cellSize: CGFloat
|
||||
private let cellInset: CGFloat
|
||||
|
||||
public typealias Item = StickerHorizontalListViewItem
|
||||
|
||||
@ -47,8 +101,9 @@ public class StickerHorizontalListView: UICollectionView {
|
||||
private var heightConstraint: NSLayoutConstraint?
|
||||
|
||||
@objc
|
||||
public required init(cellSize: CGFloat, spacing: CGFloat = 0) {
|
||||
public required init(cellSize: CGFloat, cellInset: CGFloat, spacing: CGFloat = 0) {
|
||||
self.cellSize = cellSize
|
||||
self.cellInset = cellInset
|
||||
let layout = LinearHorizontalLayout(itemSize: CGSize(width: cellSize, height: cellSize), spacing: spacing)
|
||||
|
||||
super.init(frame: .zero, collectionViewLayout: layout)
|
||||
@ -92,7 +147,10 @@ extension StickerHorizontalListView: UICollectionViewDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
item.selectedBlock()
|
||||
item.didSelectBlock()
|
||||
|
||||
// Selection has changed; update cells to reflect that.
|
||||
self.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,10 +180,19 @@ extension StickerHorizontalListView: UICollectionViewDataSource {
|
||||
return cell
|
||||
}
|
||||
|
||||
let stickerView = StickerView(stickerInfo: item.stickerInfo)
|
||||
cell.contentView.addSubview(stickerView)
|
||||
stickerView.autoPinEdgesToSuperviewEdges()
|
||||
if item.isSelected {
|
||||
let selectionView = UIView()
|
||||
selectionView.backgroundColor = (Theme.isDarkThemeEnabled
|
||||
? UIColor(rgbHex: 0x1e1d1c)
|
||||
: UIColor(rgbHex: 0xe1e2e3))
|
||||
selectionView.layer.cornerRadius = 8
|
||||
cell.contentView.addSubview(selectionView)
|
||||
selectionView.autoPinEdgesToSuperviewEdges()
|
||||
}
|
||||
|
||||
let itemView = item.view
|
||||
cell.contentView.addSubview(itemView)
|
||||
itemView.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(top: cellInset, leading: cellInset, bottom: cellInset, trailing: cellInset))
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,14 +81,14 @@ public class StickerKeyboard: UIStackView {
|
||||
autoresizingMask = .flexibleHeight
|
||||
alignment = .fill
|
||||
|
||||
addBackgroundView(withBackgroundColor: Theme.offBackgroundColor)
|
||||
addBackgroundView(withBackgroundColor: keyboardBackgroundColor)
|
||||
|
||||
addArrangedSubview(headerView)
|
||||
headerView.setContentHuggingVerticalHigh()
|
||||
headerView.setCompressionResistanceVerticalHigh()
|
||||
|
||||
stickerCollectionView.stickerDelegate = self
|
||||
stickerCollectionView.backgroundColor = Theme.offBackgroundColor
|
||||
stickerCollectionView.backgroundColor = keyboardBackgroundColor
|
||||
addArrangedSubview(stickerCollectionView)
|
||||
stickerCollectionView.setContentHuggingVerticalLow()
|
||||
stickerCollectionView.setCompressionResistanceVerticalLow()
|
||||
@ -96,6 +96,12 @@ public class StickerKeyboard: UIStackView {
|
||||
populateHeaderView()
|
||||
}
|
||||
|
||||
private var keyboardBackgroundColor: UIColor {
|
||||
return (Theme.isDarkThemeEnabled
|
||||
? UIColor.ows_gray90
|
||||
: UIColor.ows_gray02)
|
||||
}
|
||||
|
||||
@objc
|
||||
public func wasPresented() {
|
||||
// If there are no recents, default to showing the first sticker pack.
|
||||
@ -117,12 +123,21 @@ public class StickerKeyboard: UIStackView {
|
||||
}
|
||||
}
|
||||
|
||||
let packItems = stickerPacks.map { (stickerPack) in
|
||||
StickerHorizontalListView.Item(stickerInfo: stickerPack.coverInfo) { [weak self] in
|
||||
self?.stickerPack = stickerPack
|
||||
}
|
||||
var items = [StickerHorizontalListViewItem]()
|
||||
items.append(StickerHorizontalListViewItemRecents(didSelectBlock: { [weak self] in
|
||||
self?.recentsButtonWasTapped()
|
||||
}, isSelectedBlock: { [weak self] in
|
||||
self?.stickerPack == nil
|
||||
}))
|
||||
items += stickerPacks.map { (stickerPack) in
|
||||
StickerHorizontalListViewItemSticker(stickerInfo: stickerPack.coverInfo,
|
||||
didSelectBlock: { [weak self] in
|
||||
self?.stickerPack = stickerPack
|
||||
}, isSelectedBlock: { [weak self] in
|
||||
self?.stickerPack?.info == stickerPack.info
|
||||
})
|
||||
}
|
||||
packsCollectionView.items = packItems
|
||||
packsCollectionView.items = items
|
||||
|
||||
guard stickerPacks.count > 0 else {
|
||||
stickerPack = nil
|
||||
@ -130,18 +145,19 @@ public class StickerKeyboard: UIStackView {
|
||||
}
|
||||
}
|
||||
|
||||
private static let packCoverSize: CGFloat = 24
|
||||
private static let packCoverSpacing: CGFloat = 12
|
||||
private let packsCollectionView = StickerHorizontalListView(cellSize: StickerKeyboard.packCoverSize, spacing: StickerKeyboard.packCoverSpacing)
|
||||
private static let packCoverSize: CGFloat = 32
|
||||
private static let packCoverInset: CGFloat = 4
|
||||
private static let packCoverSpacing: CGFloat = 4
|
||||
private let packsCollectionView = StickerHorizontalListView(cellSize: StickerKeyboard.packCoverSize,
|
||||
cellInset: StickerKeyboard.packCoverInset,
|
||||
spacing: StickerKeyboard.packCoverSpacing)
|
||||
|
||||
private func populateHeaderView() {
|
||||
backgroundColor = Theme.offBackgroundColor
|
||||
|
||||
headerView.spacing = StickerKeyboard.packCoverSpacing
|
||||
headerView.axis = .horizontal
|
||||
headerView.alignment = .center
|
||||
headerView.backgroundColor = Theme.offBackgroundColor
|
||||
headerView.layoutMargins = UIEdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12)
|
||||
headerView.backgroundColor = keyboardBackgroundColor
|
||||
headerView.layoutMargins = UIEdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 6)
|
||||
headerView.isLayoutMarginsRelativeArrangement = true
|
||||
|
||||
if FeatureFlags.stickerSearch {
|
||||
@ -151,12 +167,7 @@ public class StickerKeyboard: UIStackView {
|
||||
headerView.addArrangedSubview(searchButton)
|
||||
}
|
||||
|
||||
let recentsButton = buildHeaderButton("recent-outline-24") { [weak self] in
|
||||
self?.recentsButtonWasTapped()
|
||||
}
|
||||
headerView.addArrangedSubview(recentsButton)
|
||||
|
||||
packsCollectionView.backgroundColor = Theme.offBackgroundColor
|
||||
packsCollectionView.backgroundColor = keyboardBackgroundColor
|
||||
headerView.addArrangedSubview(packsCollectionView)
|
||||
|
||||
let manageButton = buildHeaderButton("plus-24") { [weak self] in
|
||||
|
||||
@ -37,6 +37,8 @@ public class StickerPackViewController: OWSViewController {
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
self.modalPresentationStyle = .overFullScreen
|
||||
|
||||
stickerCollectionView.stickerDelegate = self
|
||||
stickerCollectionView.show(dataSource: dataSource)
|
||||
dataSource.add(delegate: self)
|
||||
@ -58,8 +60,9 @@ public class StickerPackViewController: OWSViewController {
|
||||
view.backgroundColor = Theme.darkThemeBackgroundColor
|
||||
} else {
|
||||
view.backgroundColor = .clear
|
||||
view.isOpaque = false
|
||||
|
||||
let blurEffect = Theme.darkThemeBarBlurEffect
|
||||
let blurEffect = Theme.barBlurEffect
|
||||
let blurEffectView = UIVisualEffectView(effect: blurEffect)
|
||||
view.addSubview(blurEffectView)
|
||||
blurEffectView.autoPinEdgesToSuperviewEdges()
|
||||
@ -250,6 +253,10 @@ public class StickerPackViewController: OWSViewController {
|
||||
return true
|
||||
}
|
||||
|
||||
override public var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
// - MARK: Events
|
||||
|
||||
@objc
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
@ -40,10 +40,13 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@property (class, readonly, nonatomic) UIColor *ows_whiteColor;
|
||||
@property (class, readonly, nonatomic) UIColor *ows_gray02Color;
|
||||
@property (class, readonly, nonatomic) UIColor *ows_gray05Color;
|
||||
@property (class, readonly, nonatomic) UIColor *ows_gray10Color;
|
||||
@property (class, readonly, nonatomic) UIColor *ows_gray15Color;
|
||||
@property (class, readonly, nonatomic) UIColor *ows_gray25Color;
|
||||
@property (class, readonly, nonatomic) UIColor *ows_gray45Color;
|
||||
@property (class, readonly, nonatomic) UIColor *ows_gray60Color;
|
||||
@property (class, readonly, nonatomic) UIColor *ows_gray75Color;
|
||||
@property (class, readonly, nonatomic) UIColor *ows_gray85Color;
|
||||
@property (class, readonly, nonatomic) UIColor *ows_gray90Color;
|
||||
@property (class, readonly, nonatomic) UIColor *ows_gray95Color;
|
||||
@property (class, readonly, nonatomic) UIColor *ows_blackColor;
|
||||
|
||||
@ -160,6 +160,16 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
return [UIColor colorWithRGBHex:0xEEEFEF];
|
||||
}
|
||||
|
||||
+ (UIColor *)ows_gray10Color
|
||||
{
|
||||
return [UIColor colorWithRGBHex:0xE1E2E3];
|
||||
}
|
||||
|
||||
+ (UIColor *)ows_gray15Color
|
||||
{
|
||||
return [UIColor colorWithRGBHex:0xD5D6D6];
|
||||
}
|
||||
|
||||
+ (UIColor *)ows_gray25Color
|
||||
{
|
||||
return [UIColor colorWithRGBHex:0xBBBDBE];
|
||||
@ -172,7 +182,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
+ (UIColor *)ows_gray60Color
|
||||
{
|
||||
return [UIColor colorWithRGBHex:0x636467];
|
||||
return [UIColor colorWithRGBHex:0x6B6D70];
|
||||
}
|
||||
|
||||
+ (UIColor *)ows_gray75Color
|
||||
@ -180,6 +190,11 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
return [UIColor colorWithRGBHex:0x3D3E44];
|
||||
}
|
||||
|
||||
+ (UIColor *)ows_gray85Color
|
||||
{
|
||||
return [UIColor colorWithRGBHex:0x23252A];
|
||||
}
|
||||
|
||||
+ (UIColor *)ows_gray90Color
|
||||
{
|
||||
return [UIColor colorWithRGBHex:0x17191D];
|
||||
|
||||
@ -138,6 +138,9 @@ private struct OWSThumbnailRequest {
|
||||
guard canThumbnailAttachment(attachment: attachment) else {
|
||||
throw OWSThumbnailError.failure(description: "Cannot thumbnail attachment.")
|
||||
}
|
||||
|
||||
let isWebp = attachment.contentType == OWSMimeTypeImageWebp
|
||||
|
||||
let thumbnailPath = attachment.path(forThumbnailDimensionPoints: thumbnailRequest.thumbnailDimensionPoints)
|
||||
if FileManager.default.fileExists(atPath: thumbnailPath) {
|
||||
guard let image = UIImage(contentsOfFile: thumbnailPath) else {
|
||||
@ -157,7 +160,7 @@ private struct OWSThumbnailRequest {
|
||||
}
|
||||
let maxDimension = CGFloat(thumbnailRequest.thumbnailDimensionPoints)
|
||||
let thumbnailImage: UIImage
|
||||
if attachment.contentType == OWSMimeTypeImageWebp {
|
||||
if isWebp {
|
||||
thumbnailImage = try OWSMediaUtils.thumbnail(forWebpAtPath: originalFilePath, maxDimension: maxDimension)
|
||||
} else if attachment.isImage || attachment.isAnimated {
|
||||
thumbnailImage = try OWSMediaUtils.thumbnail(forImageAtPath: originalFilePath, maxDimension: maxDimension)
|
||||
@ -166,8 +169,17 @@ private struct OWSThumbnailRequest {
|
||||
} else {
|
||||
throw OWSThumbnailError.assertionFailure(description: "Invalid attachment type.")
|
||||
}
|
||||
guard let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.85) else {
|
||||
throw OWSThumbnailError.failure(description: "Could not convert thumbnail to JPEG.")
|
||||
let thumbnailData: Data
|
||||
if isWebp {
|
||||
guard let pngThumbnailData = thumbnailImage.pngData() else {
|
||||
throw OWSThumbnailError.failure(description: "Could not convert thumbnail to PNG.")
|
||||
}
|
||||
thumbnailData = pngThumbnailData
|
||||
} else {
|
||||
guard let jpegThumbnailData = thumbnailImage.jpegData(compressionQuality: 0.85) else {
|
||||
throw OWSThumbnailError.failure(description: "Could not convert thumbnail to JPEG.")
|
||||
}
|
||||
thumbnailData = jpegThumbnailData
|
||||
}
|
||||
do {
|
||||
try thumbnailData.write(to: URL(fileURLWithPath: thumbnailPath), options: .atomic)
|
||||
@ -177,4 +189,10 @@ private struct OWSThumbnailRequest {
|
||||
OWSFileSystem.protectFileOrFolder(atPath: thumbnailPath)
|
||||
return OWSLoadedThumbnail(image: thumbnailImage, data: thumbnailData)
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func thumbnailFileExtension(forContentType contentType: String) -> String {
|
||||
let isWebp = contentType == OWSMimeTypeImageWebp
|
||||
return isWebp ? "png" : "jpg"
|
||||
}
|
||||
}
|
||||
|
||||
@ -345,7 +345,9 @@ typedef void (^OWSLoadedThumbnailSuccess)(OWSLoadedThumbnail *loadedThumbnail);
|
||||
|
||||
- (NSString *)pathForThumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints
|
||||
{
|
||||
NSString *filename = [NSString stringWithFormat:@"thumbnail-%lu.jpg", (unsigned long)thumbnailDimensionPoints];
|
||||
NSString *fileExtension = [OWSThumbnailService thumbnailFileExtensionForContentType:self.contentType];
|
||||
NSString *filename =
|
||||
[NSString stringWithFormat:@"thumbnail-%lu.%@", (unsigned long)thumbnailDimensionPoints, fileExtension];
|
||||
return [self.thumbnailsDirPath stringByAppendingPathComponent:filename];
|
||||
}
|
||||
|
||||
|
||||
@ -41,12 +41,16 @@ public class OWSLinkPreviewDraft: NSObject {
|
||||
public var title: String?
|
||||
|
||||
@objc
|
||||
public var jpegImageData: Data?
|
||||
public var imageData: Data?
|
||||
|
||||
public init(urlString: String, title: String?, jpegImageData: Data? = nil) {
|
||||
@objc
|
||||
public var imageMimeType: String?
|
||||
|
||||
public init(urlString: String, title: String?, imageData: Data? = nil, imageMimeType: String? = nil) {
|
||||
self.urlString = urlString
|
||||
self.title = title
|
||||
self.jpegImageData = jpegImageData
|
||||
self.imageData = imageData
|
||||
self.imageMimeType = imageMimeType
|
||||
|
||||
super.init()
|
||||
}
|
||||
@ -56,7 +60,7 @@ public class OWSLinkPreviewDraft: NSObject {
|
||||
if let titleValue = title {
|
||||
hasTitle = titleValue.count > 0
|
||||
}
|
||||
let hasImage = jpegImageData != nil
|
||||
let hasImage = imageData != nil && imageMimeType != nil
|
||||
return hasTitle || hasImage
|
||||
}
|
||||
|
||||
@ -181,8 +185,9 @@ public class OWSLinkPreview: MTLModel {
|
||||
guard SSKPreferences.areLinkPreviewsEnabled(transaction: transaction.asAnyRead) else {
|
||||
throw LinkPreviewError.noPreview
|
||||
}
|
||||
let imageAttachmentId = OWSLinkPreview.saveAttachmentIfPossible(jpegImageData: info.jpegImageData,
|
||||
transaction: transaction)
|
||||
let imageAttachmentId = OWSLinkPreview.saveAttachmentIfPossible(imageData: info.imageData,
|
||||
imageMimeType: info.imageMimeType,
|
||||
transaction: transaction)
|
||||
|
||||
let linkPreview = OWSLinkPreview(urlString: info.urlString, title: info.title, imageAttachmentId: imageAttachmentId)
|
||||
|
||||
@ -194,22 +199,28 @@ public class OWSLinkPreview: MTLModel {
|
||||
return linkPreview
|
||||
}
|
||||
|
||||
private class func saveAttachmentIfPossible(jpegImageData: Data?,
|
||||
private class func saveAttachmentIfPossible(imageData: Data?,
|
||||
imageMimeType: String?,
|
||||
transaction: YapDatabaseReadWriteTransaction) -> String? {
|
||||
guard let jpegImageData = jpegImageData else {
|
||||
guard let imageData = imageData else {
|
||||
return nil
|
||||
}
|
||||
let fileSize = jpegImageData.count
|
||||
guard let imageMimeType = imageMimeType else {
|
||||
return nil
|
||||
}
|
||||
guard let fileExtension = MIMETypeUtil.fileExtension(forMIMEType: imageMimeType) else {
|
||||
return nil
|
||||
}
|
||||
let fileSize = imageData.count
|
||||
guard fileSize > 0 else {
|
||||
owsFailDebug("Invalid file size for image data.")
|
||||
return nil
|
||||
}
|
||||
let fileExtension = "jpg"
|
||||
let contentType = OWSMimeTypeImageJpeg
|
||||
let contentType = imageMimeType
|
||||
|
||||
let filePath = OWSFileSystem.temporaryFilePath(withFileExtension: fileExtension)
|
||||
do {
|
||||
try jpegImageData.write(to: NSURL.fileURL(withPath: filePath))
|
||||
try imageData.write(to: NSURL.fileURL(withPath: filePath))
|
||||
} catch let error as NSError {
|
||||
owsFailDebug("file write failed: \(filePath), \(error)")
|
||||
return nil
|
||||
@ -807,7 +818,10 @@ public class OWSLinkPreviewManager: NSObject {
|
||||
return downloadImage(url: imageUrlString, imageMimeType: imageMimeType)
|
||||
.map(on: DispatchQueue.global()) { (imageData: Data) -> OWSLinkPreviewDraft in
|
||||
// We always recompress images to Jpeg.
|
||||
let linkPreviewDraft = OWSLinkPreviewDraft(urlString: linkUrlString, title: title, jpegImageData: imageData)
|
||||
let linkPreviewDraft = OWSLinkPreviewDraft(urlString: linkUrlString,
|
||||
title: title,
|
||||
imageData: imageData,
|
||||
imageMimeType: OWSMimeTypeImageJpeg)
|
||||
return linkPreviewDraft
|
||||
}
|
||||
.recover(on: DispatchQueue.global()) { (_) -> Promise<OWSLinkPreviewDraft> in
|
||||
@ -919,7 +933,7 @@ public class OWSLinkPreviewManager: NSObject {
|
||||
// tryToDownloadSticker will use locally saved data if possible.
|
||||
return StickerManager.tryToDownloadSticker(stickerPack: stickerPack, stickerInfo: coverInfo).map(on: DispatchQueue.global()) { (coverData) -> OWSLinkPreviewDraft in
|
||||
// Try to build thumbnail from cover webp.
|
||||
var jpegImageData: Data?
|
||||
var pngImageData: Data?
|
||||
if let stillImage = (coverData as NSData).stillForWebpData() {
|
||||
var stillThumbnail = stillImage
|
||||
let maxImageSize: CGFloat = 1024
|
||||
@ -933,8 +947,8 @@ public class OWSLinkPreviewManager: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
if let stillData = stillThumbnail.jpegData(compressionQuality: 0.85) {
|
||||
jpegImageData = stillData
|
||||
if let stillData = stillThumbnail.pngData() {
|
||||
pngImageData = stillData
|
||||
} else {
|
||||
owsFailDebug("Could not encode as JPEG.")
|
||||
}
|
||||
@ -942,7 +956,10 @@ public class OWSLinkPreviewManager: NSObject {
|
||||
owsFailDebug("Could not extract still.")
|
||||
}
|
||||
|
||||
return OWSLinkPreviewDraft(urlString: url.absoluteString, title: stickerPack.title, jpegImageData: jpegImageData)
|
||||
return OWSLinkPreviewDraft(urlString: url.absoluteString,
|
||||
title: stickerPack.title,
|
||||
imageData: pngImageData,
|
||||
imageMimeType: OWSMimeTypeImagePng)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,23 +19,7 @@ class DefaultStickerPack {
|
||||
}
|
||||
|
||||
private class func parseAll() -> [StickerPackInfo: DefaultStickerPack] {
|
||||
guard FeatureFlags.testBuiltInStickerPacks else {
|
||||
return [:]
|
||||
}
|
||||
|
||||
// TODO: Replace with production values.
|
||||
let packs = [
|
||||
DefaultStickerPack(packIdHex: "0123456789abcdef0123456789abcdef",
|
||||
packKeyHex: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
|
||||
shouldAutoInstall: true),
|
||||
DefaultStickerPack(packIdHex: "aaaaaaaabbbbbbbbcccccccc00000000",
|
||||
packKeyHex: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
|
||||
shouldAutoInstall: false),
|
||||
DefaultStickerPack(packIdHex: "aaaaaaaabbbbbbbbcccccccc11111111",
|
||||
packKeyHex: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
|
||||
shouldAutoInstall: true)
|
||||
].compactMap { $0 }
|
||||
|
||||
let packs = [DefaultStickerPack]()
|
||||
var result = [StickerPackInfo: DefaultStickerPack]()
|
||||
for pack in packs {
|
||||
result[pack.info] = pack
|
||||
|
||||
@ -65,9 +65,6 @@ public class FeatureFlags: NSObject {
|
||||
@objc
|
||||
public static let stickerPackOrdering = false
|
||||
|
||||
@objc
|
||||
public static let testBuiltInStickerPacks = false
|
||||
|
||||
// Don't enable this flag until the Desktop changes have been in production for a while.
|
||||
@objc
|
||||
public static let strictSyncTranscriptTimestamps = false
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.40.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2.40.0.11</string>
|
||||
<string>2.40.0.12</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user