Added initial support for sharing URLs and text
Updated the share extension to load URL previews. Updated the ThreadPickerVC to send plain text & URLs in the same way they are sent for normal messages.
This commit is contained in:
parent
3c32ed7cc1
commit
dd9eeb5d61
|
@ -160,6 +160,10 @@ NSString *const ReportedApplicationStateDidChangeNotification = @"ReportedApplic
|
||||||
return [UIApplication sharedApplication].applicationState == UIApplicationStateActive;
|
return [UIApplication sharedApplication].applicationState == UIApplicationStateActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (BOOL)isShareExtension {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
- (BOOL)isRTL
|
- (BOOL)isRTL
|
||||||
{
|
{
|
||||||
static BOOL isRTL = NO;
|
static BOOL isRTL = NO;
|
||||||
|
|
|
@ -114,6 +114,9 @@ public class SignalAttachment: NSObject {
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
public var captionText: String?
|
public var captionText: String?
|
||||||
|
|
||||||
|
@objc
|
||||||
|
public var linkPreviewDraft: OWSLinkPreviewDraft?
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
public var data: Data {
|
public var data: Data {
|
||||||
|
@ -292,6 +295,15 @@ public class SignalAttachment: NSObject {
|
||||||
return nil
|
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
|
// Returns the MIME type for this attachment or nil if no MIME type
|
||||||
// can be identified.
|
// can be identified.
|
||||||
|
|
|
@ -9,6 +9,7 @@ final class NotificationServiceExtensionContext : NSObject, AppContext {
|
||||||
let appLaunchTime = Date()
|
let appLaunchTime = Date()
|
||||||
let isMainApp = false
|
let isMainApp = false
|
||||||
let isMainAppAndActive = false
|
let isMainAppAndActive = false
|
||||||
|
var isShareExtension: Bool = false
|
||||||
|
|
||||||
var openSystemSettingsAction: UIAlertAction?
|
var openSystemSettingsAction: UIAlertAction?
|
||||||
var wasWokenUpByPushNotification = true
|
var wasWokenUpByPushNotification = true
|
||||||
|
|
|
@ -13,6 +13,7 @@ final class ShareAppExtensionContext: NSObject, AppContext {
|
||||||
let appLaunchTime = Date()
|
let appLaunchTime = Date()
|
||||||
let isMainApp = false
|
let isMainApp = false
|
||||||
let isMainAppAndActive = false
|
let isMainAppAndActive = false
|
||||||
|
var isShareExtension: Bool = true
|
||||||
|
|
||||||
var mainWindow: UIWindow?
|
var mainWindow: UIWindow?
|
||||||
var wasWokenUpByPushNotification: Bool = false
|
var wasWokenUpByPushNotification: Bool = false
|
||||||
|
|
|
@ -147,19 +147,44 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
||||||
}
|
}
|
||||||
|
|
||||||
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) {
|
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()
|
let message = VisibleMessage()
|
||||||
message.sentTimestamp = NSDate.millisecondTimestamp()
|
message.sentTimestamp = NSDate.millisecondTimestamp()
|
||||||
message.text = messageText
|
message.text = messageText
|
||||||
|
|
||||||
let tsMessage = TSOutgoingMessage.from(message, associatedWith: selectedThread!)
|
let tsMessage = TSOutgoingMessage.from(message, associatedWith: selectedThread!)
|
||||||
Storage.write { transaction in
|
Storage.write(
|
||||||
tsMessage.save(with: transaction)
|
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)
|
shareVC!.dismiss(animated: true, completion: nil)
|
||||||
|
|
||||||
ModalActivityIndicatorViewController.present(fromViewController: shareVC!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in
|
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
|
.done { [weak self] _ in
|
||||||
activityIndicator.dismiss { }
|
activityIndicator.dismiss { }
|
||||||
self?.shareVC?.shareViewWasCompleted()
|
self?.shareVC?.shareViewWasCompleted()
|
||||||
|
|
|
@ -35,6 +35,7 @@ NSString *NSStringForUIApplicationState(UIApplicationState value);
|
||||||
|
|
||||||
@property (nonatomic, readonly) BOOL isMainApp;
|
@property (nonatomic, readonly) BOOL isMainApp;
|
||||||
@property (nonatomic, readonly) BOOL isMainAppAndActive;
|
@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.
|
/// 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;
|
@property (nonatomic) BOOL wasWokenUpByPushNotification;
|
||||||
|
|
||||||
|
|
|
@ -594,7 +594,7 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio
|
||||||
removeAssetRequestFromQueue(assetRequest: assetRequest)
|
removeAssetRequestFromQueue(assetRequest: assetRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard CurrentAppContext().isMainAppAndActive else {
|
guard CurrentAppContext().isMainAppAndActive || CurrentAppContext().isShareExtension else {
|
||||||
// If app is not active, fail the asset request.
|
// If app is not active, fail the asset request.
|
||||||
assetRequest.state = .failed
|
assetRequest.state = .failed
|
||||||
assetRequestDidFail(assetRequest: assetRequest)
|
assetRequestDidFail(assetRequest: assetRequest)
|
||||||
|
|
|
@ -241,6 +241,12 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
|
||||||
UIView.performWithoutAnimation {
|
UIView.performWithoutAnimation {
|
||||||
self.currentPageViewController?.view.layoutIfNeeded()
|
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()
|
setupLayout()
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import MediaPlayer
|
import MediaPlayer
|
||||||
import YYImage
|
import YYImage
|
||||||
|
import NVActivityIndicatorView
|
||||||
|
|
||||||
import SessionUIKit
|
import SessionUIKit
|
||||||
|
|
||||||
|
@ -52,6 +53,8 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
public var contentView: UIView?
|
public var contentView: UIView?
|
||||||
|
|
||||||
|
private var linkPreviewInfo: (url: String, draft: OWSLinkPreviewDraft?)?
|
||||||
|
|
||||||
// MARK: Initializers
|
// MARK: Initializers
|
||||||
|
|
||||||
|
@ -89,6 +92,10 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
|
||||||
createVideoPreview()
|
createVideoPreview()
|
||||||
} else if attachment.isAudio {
|
} else if attachment.isAudio {
|
||||||
createAudioPreview()
|
createAudioPreview()
|
||||||
|
} else if attachment.isUrl {
|
||||||
|
createUrlPreview()
|
||||||
|
} else if attachment.isText {
|
||||||
|
// Do nothing as we will just put the text in the 'message' input
|
||||||
} else {
|
} else {
|
||||||
createGenericPreview()
|
createGenericPreview()
|
||||||
}
|
}
|
||||||
|
@ -118,6 +125,31 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
|
||||||
|
|
||||||
return stackView
|
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 {
|
private func stackSpacing() -> CGFloat {
|
||||||
switch mode {
|
switch mode {
|
||||||
|
@ -265,6 +297,120 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
|
||||||
videoPlayButton.autoSetDimension(.height, toSize: 72)
|
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() {
|
private func createGenericPreview() {
|
||||||
var subviews = [UIView]()
|
var subviews = [UIView]()
|
||||||
|
|
Loading…
Reference in New Issue