Add full-screen view to render messages with per-message expiration in conversation view.
This commit is contained in:
parent
195f1ebec8
commit
69ada9acd8
@ -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 */,
|
||||
|
||||
@ -2669,7 +2669,7 @@ typedef enum : NSUInteger {
|
||||
OWSAssertDebug(viewItem);
|
||||
OWSAssertDebug(attachmentStream);
|
||||
|
||||
// TODO:
|
||||
[PerMessageExpirationViewController tryToPresentWithInteraction:viewItem.interaction from:self];
|
||||
}
|
||||
|
||||
#pragma mark - ContactEditingDelegate
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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 -
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user