Add full-screen view to render messages with per-message expiration in conversation view.

This commit is contained in:
Matthew Chen 2019-06-10 16:29:43 -04:00
parent 195f1ebec8
commit 69ada9acd8
6 changed files with 314 additions and 6 deletions

View File

@ -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 = "<group>"; };
34330AA11E79686200DF2FB9 /* OWSProgressView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSProgressView.h; sourceTree = "<group>"; };
34330AA21E79686200DF2FB9 /* OWSProgressView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSProgressView.m; sourceTree = "<group>"; };
3434AE1B22AEDE7D002EE04E /* PerMessageExpirationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PerMessageExpirationViewController.swift; sourceTree = "<group>"; };
34386A4D207D0C01009F5D9C /* HomeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HomeViewController.m; sourceTree = "<group>"; };
34386A4E207D0C01009F5D9C /* HomeViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HomeViewCell.h; sourceTree = "<group>"; };
34386A4F207D0C01009F5D9C /* HomeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HomeViewController.h; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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 */,

View File

@ -2669,7 +2669,7 @@ typedef enum : NSUInteger {
OWSAssertDebug(viewItem);
OWSAssertDebug(attachmentStream);
// TODO:
[PerMessageExpirationViewController tryToPresentWithInteraction:viewItem.interaction from:self];
}
#pragma mark - ContactEditingDelegate

View File

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

View File

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

View File

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

View File

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