diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index f43e4348a2..70aedc6c57 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -80,6 +80,7 @@ 34330A5C1E787A9800DF2FB9 /* dripicons-v2.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A5B1E787A9800DF2FB9 /* dripicons-v2.ttf */; }; 34330A5E1E787BD800DF2FB9 /* ElegantIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A5D1E787BD800DF2FB9 /* ElegantIcons.ttf */; }; 34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34330AA21E79686200DF2FB9 /* OWSProgressView.m */; }; + 3434AE1C22AEDE7D002EE04E /* PerMessageExpirationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3434AE1B22AEDE7D002EE04E /* PerMessageExpirationViewController.swift */; }; 34386A51207D0C01009F5D9C /* HomeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34386A4D207D0C01009F5D9C /* HomeViewController.m */; }; 34386A52207D0C01009F5D9C /* HomeViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34386A50207D0C01009F5D9C /* HomeViewCell.m */; }; 34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34386A53207D271C009F5D9C /* NeverClearView.swift */; }; @@ -777,6 +778,7 @@ 34330A5D1E787BD800DF2FB9 /* ElegantIcons.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = ElegantIcons.ttf; sourceTree = ""; }; 34330AA11E79686200DF2FB9 /* OWSProgressView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSProgressView.h; sourceTree = ""; }; 34330AA21E79686200DF2FB9 /* OWSProgressView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSProgressView.m; sourceTree = ""; }; + 3434AE1B22AEDE7D002EE04E /* PerMessageExpirationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PerMessageExpirationViewController.swift; sourceTree = ""; }; 34386A4D207D0C01009F5D9C /* HomeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HomeViewController.m; sourceTree = ""; }; 34386A4E207D0C01009F5D9C /* HomeViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HomeViewCell.h; sourceTree = ""; }; 34386A4F207D0C01009F5D9C /* HomeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HomeViewController.h; sourceTree = ""; }; @@ -1999,6 +2001,7 @@ 345BC30B2047030600257B7C /* OWS2FASettingsViewController.m */, 34A6C27F21E503E600B5B12E /* OWSImagePickerController.swift */, 34BE2F5722A7371D002C94AC /* PdfViewController.swift */, + 3434AE1B22AEDE7D002EE04E /* PerMessageExpirationViewController.swift */, 34969558219B605E00DCFE74 /* Photos */, 34CE88E51F2FB9A10098030F /* ProfileViewController.h */, 34CE88E61F2FB9A10098030F /* ProfileViewController.m */, @@ -2219,11 +2222,11 @@ 450DF2061E0DD28D003D14BE /* UserInterface */ = { isa = PBXGroup; children = ( - 450DF2071E0DD29E003D14BE /* Notifications */, + 4CC613352227A00400E21A3A /* ConversationSearch.swift */, 4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */, + 450DF2071E0DD29E003D14BE /* Notifications */, 34B3F8331E8DF1700035BE1A /* ViewControllers */, 76EB052B18170B33006006FC /* Views */, - 4CC613352227A00400E21A3A /* ConversationSearch.swift */, ); name = UserInterface; sourceTree = ""; @@ -3676,6 +3679,7 @@ buildActionMask = 2147483647; files = ( 4CC0B59C20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift in Sources */, + 3434AE1C22AEDE7D002EE04E /* PerMessageExpirationViewController.swift in Sources */, 3461293E1FD1D72B00532771 /* ExperienceUpgradeFinder.swift in Sources */, 34C4E2582118957600BEA353 /* WebRTCProto.swift in Sources */, 34D1F0BD1F8D108C0066283D /* AttachmentUploadView.m in Sources */, diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index a48a7a4c34..604d627b0e 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -2669,7 +2669,7 @@ typedef enum : NSUInteger { OWSAssertDebug(viewItem); OWSAssertDebug(attachmentStream); - // TODO: + [PerMessageExpirationViewController tryToPresentWithInteraction:viewItem.interaction from:self]; } #pragma mark - ContactEditingDelegate diff --git a/Signal/src/ViewControllers/MessageDetailViewController.swift b/Signal/src/ViewControllers/MessageDetailViewController.swift index 58f396e573..b47e741748 100644 --- a/Signal/src/ViewControllers/MessageDetailViewController.swift +++ b/Signal/src/ViewControllers/MessageDetailViewController.swift @@ -850,7 +850,8 @@ extension MessageDetailViewController: OWSMessageStickerViewDelegate { extension MessageDetailViewController: OWSMessageHiddenViewDelegate { public func didTapAttachment(withPerMessageExpiration viewItem: ConversationViewItem, attachmentStream: TSAttachmentStream) { - // TODO: + PerMessageExpirationViewController.tryToPresent(interaction: viewItem.interaction, + from: self) } } diff --git a/Signal/src/ViewControllers/PerMessageExpirationViewController.swift b/Signal/src/ViewControllers/PerMessageExpirationViewController.swift new file mode 100644 index 0000000000..4089bf2db5 --- /dev/null +++ b/Signal/src/ViewControllers/PerMessageExpirationViewController.swift @@ -0,0 +1,298 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation +import UIKit + +@objc +class PerMessageExpirationViewController: OWSViewController { + + // MARK: - Dependencies + + static var databaseStorage: SDSDatabaseStorage { + return SDSDatabaseStorage.shared + } + + var databaseStorage: SDSDatabaseStorage { + return SDSDatabaseStorage.shared + } + + // MARK: - Properties + + private let interaction: TSInteraction + private let attachmentStream: TSAttachmentStream + + // MARK: - Initializers + + required init(interaction: TSInteraction, attachmentStream: TSAttachmentStream) { + self.interaction = interaction + self.attachmentStream = attachmentStream + + super.init(nibName: nil, bundle: nil) + } + + public required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - + + private typealias Presentation = (interaction: TSInteraction, attachmentStream: TSAttachmentStream) + + @objc + public class func tryToPresent(interaction: TSInteraction, + from fromViewController: UIViewController) { + AssertIsOnMainThread() + + guard let presentation = loadPresentation(interaction: interaction) else { + owsFailDebug("Could not present interaction") + return + } + + let view = PerMessageExpirationViewController(interaction: presentation.interaction, attachmentStream: presentation.attachmentStream) + fromViewController.present(view, animated: true) + } + + private class func loadPresentation(interaction: TSInteraction) -> Presentation? { + AssertIsOnMainThread() + + return databaseStorage.uiReadReturningResult { transaction in + guard let interactionId = interaction.uniqueId else { + return nil + } + guard let message = TSInteraction.anyFetch(uniqueId: interactionId, transaction: transaction) as? TSMessage else { + return nil + } + guard !message.perMessageExpirationHasExpired else { + return nil + } + guard let attachmentId = message.attachmentIds.firstObject as? String else { + return nil + } + guard let attachmentStream = TSAttachment.anyFetch(uniqueId: attachmentId, transaction: transaction) as? TSAttachmentStream else { + return nil + } + return (interaction: message, attachmentStream: attachmentStream) + } + } + + // MARK: - View Lifecycle + + override public func loadView() { + self.view = UIView() + view.backgroundColor = UIColor.ows_black + + self.modalPresentationStyle = .overFullScreen + + let contentView = UIView() + view.addSubview(contentView) + contentView.autoPinWidthToSuperview() + contentView.autoPin(toTopLayoutGuideOf: self, withInset: 0) + contentView.autoPin(toBottomLayoutGuideOf: self, withInset: 0) + + var mediaAspectRatio: CGFloat + var mediaView: UIView + if let mediaTuple = buildMediaView() { + mediaAspectRatio = mediaTuple.aspectRatio + mediaView = mediaTuple.mediaView + } else { + mediaAspectRatio = 1 + mediaView = UIView() + mediaView.backgroundColor = Theme.darkThemeOffBackgroundColor + } + + contentView.addSubview(mediaView) + _ = contentView.applyScaleAspectFitLayout(subview: mediaView, aspectRatio: mediaAspectRatio) + + let hMargin: CGFloat = 16 + + let dismissButton = OWSButton(imageName: "x-24", tintColor: Theme.darkThemePrimaryColor) + dismissButton.addTarget(self, action: #selector(dismissButtonPressed(sender:)), for: .touchUpInside) + dismissButton.contentEdgeInsets = UIEdgeInsets(top: 20, leading: hMargin, bottom: 20, trailing: hMargin) + view.addSubview(dismissButton) + dismissButton.autoPinEdge(.leading, to: .leading, of: mediaView) + dismissButton.autoPinEdge(.top, to: .top, of: mediaView) + + view.addGestureRecognizer(UITapGestureRecognizer(target: self, + action: #selector(rootViewWasTapped))) + + setupDatabaseObservation() + } + + private func buildMediaView() -> (mediaView: UIView, aspectRatio: CGFloat)? { + guard attachmentStream.isValidVisualMedia else { + return nil + } + guard let filePath = attachmentStream.originalFilePath else { + owsFailDebug("Attachment missing file path.") + return nil + } + + if attachmentStream.isAnimated || attachmentStream.contentType == OWSMimeTypeImageWebp { + guard let image = YYImage(contentsOfFile: filePath) else { + owsFailDebug("Could not load attachment.") + return nil + } + guard image.size.width > 0, + image.size.height > 0 else { + owsFailDebug("Attachment has invalid size.") + return nil + } + let aspectRatio = image.size.width / image.size.height + + let animatedImageView = YYAnimatedImageView() + // We need to specify a contentMode since the size of the image + // might not match the aspect ratio of the view. + animatedImageView.contentMode = .scaleAspectFit + // Use trilinear filters for better scaling quality at + // some performance cost. + animatedImageView.layer.minificationFilter = .trilinear + animatedImageView.layer.magnificationFilter = .trilinear + animatedImageView.image = image + return (mediaView: animatedImageView, aspectRatio: aspectRatio) + } else if attachmentStream.isImage { + guard let image = UIImage(contentsOfFile: filePath) else { + owsFailDebug("Could not load attachment.") + return nil + } + guard image.size.width > 0, + image.size.height > 0 else { + owsFailDebug("Attachment has invalid size.") + return nil + } + let aspectRatio = image.size.width / image.size.height + + let imageView = UIImageView() + // We need to specify a contentMode since the size of the image + // might not match the aspect ratio of the view. + imageView.contentMode = .scaleAspectFit + // Use trilinear filters for better scaling quality at + // some performance cost. + imageView.layer.minificationFilter = .trilinear + imageView.layer.magnificationFilter = .trilinear + imageView.image = image + return (mediaView: imageView, aspectRatio: aspectRatio) + } else { + owsFailDebug("Unexpected content type: \(attachmentStream.contentType).") + return nil + } + } + + func setupDatabaseObservation() { + if FeatureFlags.useGRDB { + guard let observer = databaseStorage.grdbStorage.conversationViewDatabaseObserver else { + owsFailDebug("observer was unexpectedly nil") + return + } + observer.appendSnapshotDelegate(self) + } else { + NotificationCenter.default.addObserver(self, + selector: #selector(uiDatabaseDidUpdate), + name: .OWSUIDatabaseConnectionDidUpdateExternally, + object: OWSPrimaryStorage.shared().dbNotificationObject) + NotificationCenter.default.addObserver(self, + selector: #selector(uiDatabaseDidUpdate), + name: .OWSUIDatabaseConnectionDidUpdate, + object: OWSPrimaryStorage.shared().dbNotificationObject) + } + NotificationCenter.default.addObserver(self, + selector: #selector(applicationWillEnterForeground), + name: .OWSApplicationWillEnterForeground, + object: nil) + } + + public override var canBecomeFirstResponder: Bool { + return true + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.becomeFirstResponder() + } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.becomeFirstResponder() + } + + public override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + // MARK: - Video + + private func dismissIfExpired() { + AssertIsOnMainThread() + + let shouldDismiss: Bool = databaseStorage.uiReadReturningResult { transaction in + guard let uniqueId = self.interaction.uniqueId else { + return true + } + guard let message = TSInteraction.anyFetch(uniqueId: uniqueId, transaction: transaction) as? TSMessage else { + return true + } + return message.perMessageExpirationHasExpired + } + + if shouldDismiss { + self.dismiss(animated: true) + } + } + + // MARK: - Events + + @objc internal func uiDatabaseDidUpdate(notification: NSNotification) { + AssertIsOnMainThread() + + Logger.debug("") + + dismissIfExpired() + } + + @objc + func applicationWillEnterForeground() throws { + AssertIsOnMainThread() + + Logger.debug("") + + dismissIfExpired() + } + + @objc + private func dismissButtonPressed(sender: UIButton) { + AssertIsOnMainThread() + + dismiss(animated: true) + } + + @objc + func rootViewWasTapped(sender: UIGestureRecognizer) { + AssertIsOnMainThread() + + dismiss(animated: true) + } + +} + +// MARK: - + +extension PerMessageExpirationViewController: ConversationViewDatabaseSnapshotDelegate { + public func conversationViewDatabaseSnapshotWillUpdate() { + dismissIfExpired() + } + + public func conversationViewDatabaseSnapshotDidUpdate(transactionChanges: ConversationViewDatabaseTransactionChanges) { + dismissIfExpired() + } + + public func conversationViewDatabaseSnapshotDidUpdateExternally() { + dismissIfExpired() + } + + public func conversationViewDatabaseSnapshotDidReset() { + dismissIfExpired() + } +} diff --git a/SignalMessaging/appearance/Theme.h b/SignalMessaging/appearance/Theme.h index 0d2851232b..59c7c242cd 100644 --- a/SignalMessaging/appearance/Theme.h +++ b/SignalMessaging/appearance/Theme.h @@ -47,6 +47,7 @@ extern NSString *const ThemeDidChangeNotification; @property (class, readonly, nonatomic) UIColor *darkThemePrimaryColor; @property (class, readonly, nonatomic) UIBlurEffect *darkThemeBarBlurEffect; @property (class, readonly, nonatomic) UIColor *galleryHighlightColor; +@property (class, readonly, nonatomic) UIColor *darkThemeOffBackgroundColor; #pragma mark - diff --git a/SignalMessaging/appearance/Theme.m b/SignalMessaging/appearance/Theme.m index e93b541c13..4a57fdc7c7 100644 --- a/SignalMessaging/appearance/Theme.m +++ b/SignalMessaging/appearance/Theme.m @@ -86,10 +86,14 @@ NSString *const ThemeKeyThemeEnabled = @"ThemeKeyThemeEnabled"; return (Theme.isDarkThemeEnabled ? Theme.darkThemeBackgroundColor : UIColor.ows_whiteColor); } ++ (UIColor *)darkThemeOffBackgroundColor +{ + return [UIColor colorWithWhite:0.2f alpha:1.f]; +} + + (UIColor *)offBackgroundColor { - return ( - Theme.isDarkThemeEnabled ? [UIColor colorWithWhite:0.2f alpha:1.f] : [UIColor colorWithWhite:0.94f alpha:1.f]); + return (Theme.isDarkThemeEnabled ? self.darkThemeOffBackgroundColor : [UIColor colorWithWhite:0.94f alpha:1.f]); } + (UIColor *)primaryColor