Merge remote-tracking branch 'private/release/2.40.0'

This commit is contained in:
Matthew Chen 2019-05-24 11:17:02 -04:00
commit 0b4728ba3f
19 changed files with 658 additions and 165 deletions

View File

@ -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 */,

View File

@ -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>

View File

@ -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;

View File

@ -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;
}

View File

@ -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

View File

@ -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: -

View 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 were 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)
}
}

View File

@ -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)

View File

@ -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
}
}

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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];

View File

@ -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"
}
}

View File

@ -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];
}

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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>