diff --git a/Session/Meta/MainAppContext.m b/Session/Meta/MainAppContext.m index af13c0326..897281679 100644 --- a/Session/Meta/MainAppContext.m +++ b/Session/Meta/MainAppContext.m @@ -160,6 +160,10 @@ NSString *const ReportedApplicationStateDidChangeNotification = @"ReportedApplic return [UIApplication sharedApplication].applicationState == UIApplicationStateActive; } +- (BOOL)isShareExtension { + return NO; +} + - (BOOL)isRTL { static BOOL isRTL = NO; diff --git a/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift b/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift index 6ef9d9598..f65d8640c 100644 --- a/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift +++ b/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift @@ -114,6 +114,9 @@ public class SignalAttachment: NSObject { @objc public var captionText: String? + + @objc + public var linkPreviewDraft: OWSLinkPreviewDraft? @objc public var data: Data { @@ -292,6 +295,15 @@ public class SignalAttachment: NSObject { return nil } } + + @objc + public func text() -> String? { + guard let text = String(data: dataSource.data(), encoding: .utf8) else { + return nil + } + + return text + } // Returns the MIME type for this attachment or nil if no MIME type // can be identified. diff --git a/SessionNotificationServiceExtension/NotificationServiceExtensionContext.swift b/SessionNotificationServiceExtension/NotificationServiceExtensionContext.swift index b5b4dfe50..469f280e1 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtensionContext.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtensionContext.swift @@ -9,6 +9,7 @@ final class NotificationServiceExtensionContext : NSObject, AppContext { let appLaunchTime = Date() let isMainApp = false let isMainAppAndActive = false + var isShareExtension: Bool = false var openSystemSettingsAction: UIAlertAction? var wasWokenUpByPushNotification = true diff --git a/SessionShareExtension/ShareAppExtensionContext.swift b/SessionShareExtension/ShareAppExtensionContext.swift index 51c80bd95..e25e91f47 100644 --- a/SessionShareExtension/ShareAppExtensionContext.swift +++ b/SessionShareExtension/ShareAppExtensionContext.swift @@ -13,6 +13,7 @@ final class ShareAppExtensionContext: NSObject, AppContext { let appLaunchTime = Date() let isMainApp = false let isMainAppAndActive = false + var isShareExtension: Bool = true var mainWindow: UIWindow? var wasWokenUpByPushNotification: Bool = false diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index e11a2d6e0..0eef72d5f 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -147,19 +147,44 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView } func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) { + // Sharing a URL or plain text will populate the 'messageText' field so in those + // cases we should ignore the attachments + let isSharingUrl: Bool = (attachments.count == 1 && attachments[0].isUrl) + let isSharingText: Bool = (attachments.count == 1 && attachments[0].isText) + let finalAttachments: [SignalAttachment] = (isSharingUrl || isSharingText ? [] : attachments) + let message = VisibleMessage() message.sentTimestamp = NSDate.millisecondTimestamp() message.text = messageText let tsMessage = TSOutgoingMessage.from(message, associatedWith: selectedThread!) - Storage.write { transaction in - tsMessage.save(with: transaction) - } + Storage.write( + with: { transaction in + if isSharingUrl { + message.linkPreview = VisibleMessage.LinkPreview.from( + attachments[0].linkPreviewDraft, + using: transaction + ) + } + else { + tsMessage.save(with: transaction) + } + }, + completion: { + if isSharingUrl { + tsMessage.linkPreview = OWSLinkPreview.from(message.linkPreview) + + Storage.write { transaction in + tsMessage.save(with: transaction) + } + } + } + ) shareVC!.dismiss(animated: true, completion: nil) ModalActivityIndicatorViewController.present(fromViewController: shareVC!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in - MessageSender.sendNonDurably(message, with: attachments, in: self.selectedThread!) + MessageSender.sendNonDurably(message, with: finalAttachments, in: self.selectedThread!) .done { [weak self] _ in activityIndicator.dismiss { } self?.shareVC?.shareViewWasCompleted() diff --git a/SessionUtilitiesKit/General/AppContext.h b/SessionUtilitiesKit/General/AppContext.h index 50305e99e..77d6ee718 100755 --- a/SessionUtilitiesKit/General/AppContext.h +++ b/SessionUtilitiesKit/General/AppContext.h @@ -35,6 +35,7 @@ NSString *NSStringForUIApplicationState(UIApplicationState value); @property (nonatomic, readonly) BOOL isMainApp; @property (nonatomic, readonly) BOOL isMainAppAndActive; +@property (nonatomic, readonly) BOOL isShareExtension; /// Whether the app was woken up by a silent push notification. This is important for determining whether attachments should be downloaded or not. @property (nonatomic) BOOL wasWokenUpByPushNotification; diff --git a/SessionUtilitiesKit/Networking/ProxiedContentDownloader.swift b/SessionUtilitiesKit/Networking/ProxiedContentDownloader.swift index 307efcdc5..326760296 100644 --- a/SessionUtilitiesKit/Networking/ProxiedContentDownloader.swift +++ b/SessionUtilitiesKit/Networking/ProxiedContentDownloader.swift @@ -594,7 +594,7 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio removeAssetRequestFromQueue(assetRequest: assetRequest) return } - guard CurrentAppContext().isMainAppAndActive else { + guard CurrentAppContext().isMainAppAndActive || CurrentAppContext().isShareExtension else { // If app is not active, fail the asset request. assetRequest.state = .failed assetRequestDidFail(assetRequest: assetRequest) diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift index bb72c1cc4..04ce2726a 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift @@ -241,6 +241,12 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC UIView.performWithoutAnimation { self.currentPageViewController?.view.layoutIfNeeded() } + + // If the first item is just text, or is a URL and LinkPreviews are disabled + // then just fill the 'message' box with it + if firstItem.attachment.isText || (firstItem.attachment.isUrl && OWSLinkPreview.previewURL(forRawBodyText: firstItem.attachment.text()) == nil) { + bottomToolView.attachmentTextToolbar.messageText = firstItem.attachment.text() + } setupLayout() } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift index 75e41f937..9c294582c 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift @@ -5,6 +5,7 @@ import Foundation import MediaPlayer import YYImage +import NVActivityIndicatorView import SessionUIKit @@ -52,6 +53,8 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { @objc public var contentView: UIView? + + private var linkPreviewInfo: (url: String, draft: OWSLinkPreviewDraft?)? // MARK: Initializers @@ -89,6 +92,10 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { createVideoPreview() } else if attachment.isAudio { createAudioPreview() + } else if attachment.isUrl { + createUrlPreview() + } else if attachment.isText { + // Do nothing as we will just put the text in the 'message' input } else { createGenericPreview() } @@ -118,6 +125,31 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { return stackView } + + private func wrapViewsInHorizontalStack(subviews: [UIView]) -> UIView { + assert(subviews.count > 0) + + let stackView = UIView() + + var lastView: UIView? + for subview in subviews { + + stackView.addSubview(subview) + subview.autoVCenterInSuperview() + + if lastView == nil { + subview.autoPinEdge(toSuperviewEdge: .left) + } else { + subview.autoPinEdge(.left, to: .right, of: lastView!, withOffset: stackSpacing()) + } + + lastView = subview + } + + lastView?.autoPinEdge(toSuperviewEdge: .right) + + return stackView + } private func stackSpacing() -> CGFloat { switch mode { @@ -265,6 +297,120 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { videoPlayButton.autoSetDimension(.height, toSize: 72) } } + + private func createUrlPreview() { + // If link previews aren't enabled then use a fallback state + guard let linkPreviewURL: String = OWSLinkPreview.previewURL(forRawBodyText: attachment.text()) else { + createGenericPreview() + return + } + + linkPreviewInfo = (url: linkPreviewURL, draft: nil) + + var subviews = [UIView]() + + let color: UIColor = isLightMode ? .black : .white + let loadingView = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: color, padding: nil) + loadingView.set(.width, to: 24) + loadingView.set(.height, to: 24) + loadingView.startAnimating() + subviews.append(loadingView) + + let imageViewContainer = UIView() + imageViewContainer.clipsToBounds = true + imageViewContainer.contentMode = .center + imageViewContainer.alpha = 0 + imageViewContainer.layer.cornerRadius = 8 + subviews.append(imageViewContainer) + + let imageView = createHeroImageView(imageName: "FileLarge") + imageViewContainer.addSubview(imageView) + imageView.pin(to: imageViewContainer) + + let titleLabel = UILabel() + titleLabel.text = linkPreviewURL + titleLabel.textColor = controlTintColor + titleLabel.font = labelFont() + titleLabel.textAlignment = .center + titleLabel.lineBreakMode = .byTruncatingMiddle + subviews.append(titleLabel) + + let stackView = wrapViewsInVerticalStack(subviews: subviews) + self.addSubview(stackView) + + titleLabel.autoPinWidthToSuperview(withMargin: 32) + + NSLayoutConstraint.activate([ + imageView.widthAnchor.constraint(equalToConstant: 80), + imageView.heightAnchor.constraint(equalToConstant: 80) + ]) + + // Build the link preview + OWSLinkPreview.tryToBuildPreviewInfo(previewUrl: linkPreviewURL).done { [weak self] draft in + // Loader + loadingView.alpha = 0 + loadingView.stopAnimating() + + self?.linkPreviewInfo = (url: linkPreviewURL, draft: draft) + + // TODO: Look at refactoring this behaviour to consolidate attachment mutations + self?.attachment.linkPreviewDraft = draft + + let image: UIImage? + + if let jpegImageData: Data = draft.jpegImageData, let loadedImage: UIImage = UIImage(data: jpegImageData) { + image = loadedImage + imageView.contentMode = .scaleAspectFill + } + else { + image = UIImage(named: "Link")?.withTint(isLightMode ? .black : .white) + imageView.contentMode = .center + } + + // Image view + (imageView as? UIImageView)?.image = image + imageViewContainer.alpha = 1 + imageViewContainer.backgroundColor = isDarkMode ? .black : UIColor.black.withAlphaComponent(0.06) + + // Title + if let title = draft.title { + titleLabel.font = .boldSystemFont(ofSize: Values.smallFontSize) + titleLabel.text = title + titleLabel.textAlignment = .left + titleLabel.numberOfLines = 2 + } + + guard let hStackView = self?.wrapViewsInHorizontalStack(subviews: subviews) else { + // TODO: Fallback + return + } + stackView.removeFromSuperview() + self?.addSubview(hStackView) + + // We want to center the stackView in it's superview while also ensuring + // it's superview is big enough to contain it. + hStackView.autoPinWidthToSuperview(withMargin: 32) + hStackView.autoVCenterInSuperview() + NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultLow) { + hStackView.autoPinHeightToSuperview() + } + hStackView.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual) + hStackView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0, relation: .greaterThanOrEqual) + }.catch { _ in + // TODO: Fallback + loadingView.stopAnimating() + }.retainUntilComplete() + + // We want to center the stackView in it's superview while also ensuring + // it's superview is big enough to contain it. + stackView.autoPinWidthToSuperview() + stackView.autoVCenterInSuperview() + NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultLow) { + stackView.autoPinHeightToSuperview() + } + stackView.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual) + stackView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0, relation: .greaterThanOrEqual) + } private func createGenericPreview() { var subviews = [UIView]()