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:
Morgan Pretty 2022-01-12 09:40:53 +11:00
parent 3c32ed7cc1
commit dd9eeb5d61
9 changed files with 201 additions and 5 deletions

View File

@ -160,6 +160,10 @@ NSString *const ReportedApplicationStateDidChangeNotification = @"ReportedApplic
return [UIApplication sharedApplication].applicationState == UIApplicationStateActive;
}
- (BOOL)isShareExtension {
return NO;
}
- (BOOL)isRTL
{
static BOOL isRTL = NO;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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