Add rough draft of link preview view to composer.

This commit is contained in:
Matthew Chen 2019-01-17 11:56:52 -05:00
parent 977ee9ffe9
commit 416aa2b347
6 changed files with 608 additions and 9 deletions

View File

@ -36,6 +36,7 @@
340FC8BC204DAC8D007AEB0F /* FingerprintViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC8A2204DAC8D007AEB0F /* FingerprintViewController.m */; };
340FC8BD204DAC8D007AEB0F /* ShowGroupMembersViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC8A6204DAC8D007AEB0F /* ShowGroupMembersViewController.m */; };
340FC8C5204DE223007AEB0F /* DebugUIBackup.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC8C4204DE223007AEB0F /* DebugUIBackup.m */; };
34129B8621EF877A005457A8 /* LinkPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34129B8521EF8779005457A8 /* LinkPreviewView.swift */; };
341341EF2187467A00192D59 /* ConversationViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 341341EE2187467900192D59 /* ConversationViewModel.m */; };
341F2C0F1F2B8AE700D07D6B /* DebugUIMisc.m in Sources */ = {isa = PBXBuildFile; fileRef = 341F2C0E1F2B8AE700D07D6B /* DebugUIMisc.m */; };
3421981C21061D2E00C57195 /* ByteParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3421981B21061D2E00C57195 /* ByteParserTest.swift */; };
@ -671,6 +672,7 @@
340FC8A6204DAC8D007AEB0F /* ShowGroupMembersViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ShowGroupMembersViewController.m; sourceTree = "<group>"; };
340FC8C3204DE223007AEB0F /* DebugUIBackup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIBackup.h; sourceTree = "<group>"; };
340FC8C4204DE223007AEB0F /* DebugUIBackup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIBackup.m; sourceTree = "<group>"; };
34129B8521EF8779005457A8 /* LinkPreviewView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkPreviewView.swift; sourceTree = "<group>"; };
341341ED2187467900192D59 /* ConversationViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConversationViewModel.h; sourceTree = "<group>"; };
341341EE2187467900192D59 /* ConversationViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationViewModel.m; sourceTree = "<group>"; };
341458471FBE11C4005ABCF9 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = translations/fa.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -2330,10 +2332,12 @@
452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */,
34E3E5671EC4B19400495BAC /* AudioProgressView.swift */,
4C2F454E214C00E1004871FF /* AvatarTableViewCell.swift */,
4CA46F4B219CCC630038ABDE /* CaptionView.swift */,
451764291DE939FD00EDB8B9 /* ContactCell.swift */,
4523149F1F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift */,
4C4AEC4420EC343B0020E72B /* DismissableTextField.swift */,
45A663C41F92EC760027B59E /* GroupTableViewCell.swift */,
34129B8521EF8779005457A8 /* LinkPreviewView.swift */,
45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */,
34386A53207D271C009F5D9C /* NeverClearView.swift */,
34F308A01ECB469700BB7697 /* OWSBezierPathView.h */,
@ -2342,6 +2346,7 @@
459311FB1D75C948008DD4F0 /* OWSDeviceTableViewCell.m */,
34330AA11E79686200DF2FB9 /* OWSProgressView.h */,
34330AA21E79686200DF2FB9 /* OWSProgressView.m */,
4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */,
45D308AB2049A439000189E4 /* PinEntryView.h */,
45D308AC2049A439000189E4 /* PinEntryView.m */,
457F671A20746193000EABCD /* QuotedReplyPreview.swift */,
@ -2350,8 +2355,6 @@
450D19121F85236600970622 /* RemoteVideoView.m */,
4CA5F792211E1F06008C2708 /* Toast.swift */,
34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */,
4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */,
4CA46F4B219CCC630038ABDE /* CaptionView.swift */,
);
name = Views;
path = views;
@ -3460,6 +3463,7 @@
4C20B2B920CA10DE001BAC90 /* ConversationSearchViewController.swift in Sources */,
450D19131F85236600970622 /* RemoteVideoView.m in Sources */,
B6B9ECFC198B31BA00C620D3 /* PushManager.m in Sources */,
34129B8621EF877A005457A8 /* LinkPreviewView.swift in Sources */,
34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */,
45DF5DF21DDB843F00C936C7 /* CompareSafetyNumbersActivity.swift in Sources */,
451166C01FD86B98000739BA /* AccountManager.swift in Sources */,

View File

@ -5,6 +5,7 @@
NS_ASSUME_NONNULL_BEGIN
@class ConversationStyle;
@class OWSLinkPreviewDraft;
@class OWSQuotedReplyModel;
@class SignalAttachment;
@ -68,6 +69,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, nullable) OWSQuotedReplyModel *quotedReply;
@property (nonatomic, nullable, readonly) OWSLinkPreviewDraft *linkPreviewDraft;
@end
NS_ASSUME_NONNULL_END

View File

@ -26,7 +26,24 @@ const CGFloat kMaxTextViewHeight = 98;
#pragma mark -
@interface ConversationInputToolbar () <ConversationTextViewToolbarDelegate, QuotedReplyPreviewDelegate>
@interface InputLinkPreview : NSObject
@property (nonatomic) NSString *previewUrl;
@property (nonatomic, nullable) OWSLinkPreviewDraft *linkPreviewDraft;
@end
#pragma mark -
@implementation InputLinkPreview
@end
#pragma mark -
@interface ConversationInputToolbar () <ConversationTextViewToolbarDelegate,
QuotedReplyPreviewDelegate,
LinkPreviewViewDelegate>
@property (nonatomic, readonly) ConversationStyle *conversationStyle;
@ -55,12 +72,12 @@ const CGFloat kMaxTextViewHeight = 98;
@property (nonatomic) CGPoint voiceMemoGestureStartLocation;
@property (nonatomic, nullable) NSArray<NSLayoutConstraint *> *layoutContraints;
@property (nonatomic) UIEdgeInsets receivedSafeAreaInsets;
@property (nonatomic, nullable) InputLinkPreview *inputLinkPreview;
@property (nonatomic, nullable) LinkPreviewView *linkPreviewView;
@property (nonatomic) BOOL wasLinkPreviewCancelled;
@end
#pragma mark -
#pragma mark -
@implementation ConversationInputToolbar
@ -210,6 +227,7 @@ const CGFloat kMaxTextViewHeight = 98;
[self ensureShouldShowVoiceMemoButtonAnimated:isAnimated doLayout:YES];
[self ensureTextViewHeight];
[self updateInputLinkPreview];
}
- (void)ensureTextViewHeight
@ -221,6 +239,7 @@ const CGFloat kMaxTextViewHeight = 98;
{
[self setMessageText:nil animated:isAnimated];
[self.inputTextView.undoManager removeAllActions];
self.wasLinkPreviewCancelled = NO;
}
- (void)toggleDefaultKeyboard
@ -646,6 +665,7 @@ const CGFloat kMaxTextViewHeight = 98;
OWSAssertDebug(self.inputToolbarDelegate);
[self ensureShouldShowVoiceMemoButtonAnimated:YES doLayout:YES];
[self updateHeightWithTextView:textView];
[self updateInputLinkPreview];
}
- (void)updateHeightWithTextView:(UITextView *)textView
@ -676,6 +696,122 @@ const CGFloat kMaxTextViewHeight = 98;
self.quotedReply = nil;
}
#pragma mark - Link Preview
- (void)updateInputLinkPreview
{
OWSAssertIsOnMainThread();
if (self.wasLinkPreviewCancelled) {
self.inputLinkPreview = nil;
[self clearLinkPreviewView];
return;
}
NSString *body =
[[self messageText] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (body.length < 1) {
self.inputLinkPreview = nil;
[self clearLinkPreviewView];
self.wasLinkPreviewCancelled = NO;
return;
}
NSString *_Nullable previewUrl = [OWSLinkPreview previewUrlForMessageBodyText:body];
if (previewUrl.length < 1) {
self.inputLinkPreview = nil;
[self clearLinkPreviewView];
return;
}
if (self.inputLinkPreview && [self.inputLinkPreview.previewUrl isEqualToString:previewUrl]) {
// No need to update.
return;
}
InputLinkPreview *inputLinkPreview = [InputLinkPreview new];
self.inputLinkPreview = inputLinkPreview;
self.inputLinkPreview.previewUrl = previewUrl;
[self ensureLinkPreviewViewWithState:[LinkPreviewLoading new]];
__weak ConversationInputToolbar *weakSelf = self;
[OWSLinkPreview tryToBuildPreviewInfoWithPreviewUrl:previewUrl
callbackQueue:dispatch_get_main_queue()
completion:^(OWSLinkPreviewDraft *_Nullable linkPreviewDraft) {
ConversationInputToolbar *_Nullable strongSelf = weakSelf;
if (!strongSelf) {
return;
}
if (strongSelf.inputLinkPreview != inputLinkPreview) {
// Obsolete callback.
return;
}
if (!linkPreviewDraft) {
// The link preview could not be loaded.
[strongSelf clearLinkPreviewView];
return;
}
inputLinkPreview.linkPreviewDraft = linkPreviewDraft;
LinkPreviewDraft *viewState = [[LinkPreviewDraft alloc]
initWithLinkPreviewDraft:linkPreviewDraft];
[strongSelf ensureLinkPreviewViewWithState:viewState];
}];
}
- (void)ensureLinkPreviewViewWithState:(id<LinkPreviewState>)state
{
OWSAssertIsOnMainThread();
[self clearLinkPreviewView];
LinkPreviewView *linkPreviewView = [[LinkPreviewView alloc] initWithState:state delegate:self];
self.linkPreviewView = linkPreviewView;
// TODO: Revisit once we have a separate quoted reply view.
[self.contentRows insertArrangedSubview:linkPreviewView atIndex:0];
}
- (void)clearLinkPreviewView
{
OWSAssertIsOnMainThread();
// Clear old link preview state.
[self.linkPreviewView removeFromSuperview];
self.linkPreviewView = nil;
}
- (nullable OWSLinkPreviewDraft *)linkPreviewDraft
{
OWSAssertIsOnMainThread();
if (!self.inputLinkPreview) {
return nil;
}
if (self.wasLinkPreviewCancelled) {
return nil;
}
return self.inputLinkPreview.linkPreviewDraft;
}
#pragma mark - LinkPreviewViewDelegate
- (BOOL)linkPreviewCanCancel
{
OWSAssertIsOnMainThread();
return YES;
}
- (void)linkPreviewDidCancel
{
OWSAssertIsOnMainThread();
self.wasLinkPreviewCancelled = YES;
self.self.inputLinkPreview = nil;
[self clearLinkPreviewView];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -3579,12 +3579,15 @@ typedef enum : NSUInteger {
}
}
OWSLinkPreview *_Nullable linkPreview =
[self linkPreviewForLinkPreviewDraft:self.inputToolbar.linkPreviewDraft];
BOOL didAddToProfileWhitelist = [ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread];
TSOutgoingMessage *message = [ThreadUtil enqueueMessageWithAttachments:attachments
messageBody:messageText
inThread:self.thread
quotedReplyModel:self.inputToolbar.quotedReply
linkPreview:nil];
linkPreview:linkPreview];
[self messageWasSent:message];
@ -3916,6 +3919,7 @@ typedef enum : NSUInteger {
- (void)tryToSendTextMessage:(NSString *)text updateKeyboardState:(BOOL)updateKeyboardState
{
OWSAssertIsOnMainThread();
__weak ConversationViewController *weakSelf = self;
if ([self isBlockedConversation]) {
@ -3952,6 +3956,8 @@ typedef enum : NSUInteger {
BOOL didAddToProfileWhitelist = [ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread];
__block TSOutgoingMessage *message;
OWSLinkPreview *_Nullable linkPreview = [self linkPreviewForLinkPreviewDraft:self.inputToolbar.linkPreviewDraft];
if ([text lengthOfBytesUsingEncoding:NSUTF8StringEncoding] >= kOversizeTextMessageSizeThreshold) {
DataSource *_Nullable dataSource = [DataSourceValue dataSourceWithOversizeText:text];
SignalAttachment *attachment =
@ -3962,13 +3968,13 @@ typedef enum : NSUInteger {
message = [ThreadUtil enqueueMessageWithAttachment:attachment
inThread:self.thread
quotedReplyModel:self.inputToolbar.quotedReply
linkPreview:nil];
linkPreview:linkPreview];
} else {
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
message = [ThreadUtil enqueueMessageWithText:text
inThread:self.thread
quotedReplyModel:self.inputToolbar.quotedReply
linkPreview:nil
linkPreview:linkPreview
transaction:transaction];
}];
}
@ -3997,6 +4003,36 @@ typedef enum : NSUInteger {
}
}
- (nullable OWSLinkPreview *)linkPreviewForLinkPreviewDraft:(nullable OWSLinkPreviewDraft *)linkPreviewDraft
{
if (!linkPreviewDraft) {
return nil;
}
__block OWSLinkPreview *_Nullable linkPreview;
[self.editingDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
linkPreview = [self linkPreviewForLinkPreviewDraft:linkPreviewDraft transaction:transaction];
}];
return linkPreview;
}
- (nullable OWSLinkPreview *)linkPreviewForLinkPreviewDraft:(nullable OWSLinkPreviewDraft *)linkPreviewDraft
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssertDebug(transaction);
if (!linkPreviewDraft) {
return nil;
}
NSError *linkPreviewError;
OWSLinkPreview *_Nullable linkPreview = [OWSLinkPreview buildValidatedLinkPreviewFromInfo:linkPreviewDraft
transaction:transaction
error:&linkPreviewError];
if (linkPreviewError && ![OWSLinkPreview isNoPreviewError:linkPreviewError]) {
OWSLogError(@"linkPreviewError: %@", linkPreviewError);
}
return linkPreview;
}
- (void)voiceMemoGestureDidStart
{
OWSAssertIsOnMainThread();

View File

@ -0,0 +1,417 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
@objc
public enum LinkPreviewImageState: Int {
case none
case loading
case loaded
case invalid
}
// MARK: -
@objc
public protocol LinkPreviewState {
func isLoaded() -> Bool
func urlString() -> String?
func displayDomain() -> String?
func title() -> String?
func imageState() -> LinkPreviewImageState
func image() -> UIImage?
}
// MARK: -
@objc
public class LinkPreviewLoading: NSObject, LinkPreviewState {
override init() {
}
public func isLoaded() -> Bool {
return false
}
public func urlString() -> String? {
return nil
}
public func displayDomain() -> String? {
return nil
}
public func title() -> String? {
return nil
}
public func imageState() -> LinkPreviewImageState {
return .none
}
public func image() -> UIImage? {
return nil
}
}
// MARK: -
@objc
public class LinkPreviewDraft: NSObject, LinkPreviewState {
private let linkPreviewDraft: OWSLinkPreviewDraft
@objc
public required init(linkPreviewDraft: OWSLinkPreviewDraft) {
self.linkPreviewDraft = linkPreviewDraft
}
public func isLoaded() -> Bool {
return true
}
public func urlString() -> String? {
return linkPreviewDraft.urlString
}
public func displayDomain() -> String? {
guard let displayDomain = linkPreviewDraft.displayDomain() else {
owsFailDebug("Missing display domain")
return nil
}
return displayDomain
}
public func title() -> String? {
return linkPreviewDraft.title
}
public func imageState() -> LinkPreviewImageState {
if linkPreviewDraft.imageFilePath != nil {
return .loaded
} else {
return .none
}
}
public func image() -> UIImage? {
assert(imageState() == .loaded)
guard let imageFilepath = linkPreviewDraft.imageFilePath else {
return nil
}
guard let image = UIImage(contentsOfFile: imageFilepath) else {
owsFail("Could not load image: \(imageFilepath)")
}
return image
}
}
// MARK: -
@objc
public class LinkPreviewSent: NSObject, LinkPreviewState {
private let linkPreview: OWSLinkPreview
private let imageAttachment: TSAttachment?
@objc
public required init(linkPreview: OWSLinkPreview,
imageAttachment: TSAttachment?) {
self.linkPreview = linkPreview
self.imageAttachment = imageAttachment
}
public func isLoaded() -> Bool {
return true
}
public func urlString() -> String? {
guard let urlString = linkPreview.urlString else {
owsFailDebug("Missing url")
return nil
}
return urlString
}
public func displayDomain() -> String? {
guard let displayDomain = linkPreview.displayDomain() else {
owsFailDebug("Missing display domain")
return nil
}
return displayDomain
}
public func title() -> String? {
return linkPreview.title
}
public func imageState() -> LinkPreviewImageState {
guard linkPreview.imageAttachmentId != nil else {
return .none
}
guard let imageAttachment = imageAttachment else {
owsFailDebug("Missing imageAttachment.")
return .none
}
guard let attachmentStream = imageAttachment as? TSAttachmentStream else {
return .loading
}
guard attachmentStream.isValidImage else {
return .invalid
}
return .loaded
}
public func image() -> UIImage? {
assert(imageState() == .loaded)
guard let attachmentStream = imageAttachment as? TSAttachmentStream else {
owsFailDebug("Could not load image.")
return nil
}
guard attachmentStream.isValidImage else {
return nil
}
guard let imageFilepath = attachmentStream.originalFilePath else {
owsFailDebug("Attachment is missing file path.")
return nil
}
guard let image = UIImage(contentsOfFile: imageFilepath) else {
owsFail("Could not load image: \(imageFilepath)")
}
return image
}
}
// MARK: -
@objc
public protocol LinkPreviewViewDelegate {
func linkPreviewCanCancel() -> Bool
@objc optional func linkPreviewDidCancel()
@objc optional func linkPreviewDidTap(urlString: String?)
}
// MARK: -
@objc
public class LinkPreviewView: UIStackView {
private weak var delegate: LinkPreviewViewDelegate?
private let state: LinkPreviewState
@available(*, unavailable, message:"use other constructor instead.")
required init(coder aDecoder: NSCoder) {
notImplemented()
}
@available(*, unavailable, message:"use other constructor instead.")
override init(frame: CGRect) {
notImplemented()
}
private let imageView = UIImageView()
private let titleLabel = UILabel()
private let domainLabel = UILabel()
@objc
public init(state: LinkPreviewState,
delegate: LinkPreviewViewDelegate?) {
self.state = state
self.delegate = delegate
super.init(frame: .zero)
createContents()
}
private var isApproval: Bool {
return delegate != nil
}
private func createContents() {
self.isUserInteractionEnabled = true
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(wasTapped)))
guard state.isLoaded() else {
createLoadingContents()
return
}
guard isApproval else {
createMessageContents()
return
}
createApprovalContents()
}
private func createMessageContents() {
// guard state.isLoaded() else {
// createLoadingContents()
// return
// }
//
// if let imageView = createImageView() {
//
// }
//
// switch state.imageState() {
// case .loaded:
// guard
// let imageView = UIImageView()
//
// case .loading:
// default:
// break
// }
//
// let textStack = UIStackView()
// self.axis = .vertical
// self.alignment = .leading
// self.spacing = 5
}
private let approvalHeight: CGFloat = 76
private var cancelButton: UIImageView?
private func createApprovalContents() {
self.axis = .horizontal
self.alignment = .fill
self.distribution = .equalSpacing
self.spacing = 8
// Image
if let imageView = createImageView() {
imageView.contentMode = .scaleAspectFill
imageView.autoPinToSquareAspectRatio()
let imageSize = approvalHeight
imageView.autoSetDimensions(to: CGSize(width: imageSize, height: imageSize))
imageView.setContentHuggingHigh()
imageView.setCompressionResistanceHigh()
imageView.clipsToBounds = true
// TODO: Cropping, stroke.
addArrangedSubview(imageView)
}
// Right
let rightStack = UIStackView()
rightStack.axis = .horizontal
rightStack.alignment = .fill
rightStack.distribution = .equalSpacing
rightStack.spacing = 8
rightStack.setContentHuggingHorizontalLow()
rightStack.setCompressionResistanceHorizontalLow()
addArrangedSubview(rightStack)
// Text
let textStack = UIStackView()
textStack.axis = .vertical
textStack.alignment = .leading
textStack.spacing = 2
textStack.setContentHuggingHorizontalLow()
textStack.setCompressionResistanceHorizontalLow()
if let title = state.title(),
title.count > 0 {
let label = UILabel()
label.text = title
label.textColor = Theme.primaryColor
label.font = UIFont.ows_dynamicTypeBody
textStack.addArrangedSubview(label)
}
if let displayDomain = state.displayDomain(),
displayDomain.count > 0 {
let label = UILabel()
label.text = displayDomain.uppercased()
label.textColor = Theme.secondaryColor
label.font = UIFont.ows_dynamicTypeCaption1
textStack.addArrangedSubview(label)
}
let textWrapper = UIStackView(arrangedSubviews: [textStack])
textWrapper.axis = .horizontal
textWrapper.alignment = .center
textWrapper.setContentHuggingHorizontalLow()
textWrapper.setCompressionResistanceHorizontalLow()
rightStack.addArrangedSubview(textWrapper)
// Cancel
let cancelStack = UIStackView()
cancelStack.axis = .horizontal
cancelStack.alignment = .top
cancelStack.setContentHuggingHigh()
cancelStack.setCompressionResistanceHigh()
let cancelImage: UIImage = #imageLiteral(resourceName: "quoted-message-cancel").withRenderingMode(.alwaysTemplate)
let cancelButton = UIImageView(image: cancelImage)
self.cancelButton = cancelButton
cancelButton.tintColor = Theme.secondaryColor
cancelButton.setContentHuggingHigh()
cancelButton.setCompressionResistanceHigh()
cancelStack.addArrangedSubview(cancelButton)
rightStack.addArrangedSubview(cancelStack)
// Stroke
let strokeView = UIView()
strokeView.backgroundColor = Theme.secondaryColor
rightStack.addSubview(strokeView)
strokeView.autoPinWidthToSuperview()
strokeView.autoPinEdge(toSuperviewEdge: .bottom)
strokeView.autoSetDimension(.height, toSize: CGHairlineWidth())
}
private func createImageView() -> UIImageView? {
guard state.isLoaded() else {
owsFailDebug("State not loaded.")
return nil
}
guard state.imageState() == .loaded else {
return nil
}
guard let image = state.image() else {
owsFailDebug("Could not load image.")
return nil
}
let imageView = UIImageView()
imageView.image = image
return imageView
}
private func createLoadingContents() {
self.axis = .vertical
self.alignment = .center
self.autoSetDimension(.height, toSize: approvalHeight)
let label = UILabel()
label.text = NSLocalizedString("LINK_PREVIEW_LOADING", comment: "Indicates that the link preview is being loaded.")
label.textColor = Theme.secondaryColor
label.font = UIFont.ows_dynamicTypeBody
addArrangedSubview(label)
}
// MARK: Events
@objc func wasTapped(sender: UIGestureRecognizer) {
guard sender.state == .recognized else {
return
}
if let cancelButton = cancelButton {
let cancelLocation = sender.location(in: cancelButton)
// Permissive hot area to make it very easy to cancel the link preview.
let hotAreaInset: CGFloat = -20
let cancelButtonHotArea = cancelButton.bounds.insetBy(dx: hotAreaInset, dy: hotAreaInset)
if cancelButtonHotArea.contains(cancelLocation) {
self.delegate?.linkPreviewDidCancel?()
return
}
}
self.delegate?.linkPreviewDidTap?(urlString: self.state.urlString())
}
}

View File

@ -1182,6 +1182,9 @@
/* Navigation title when scanning QR code to add new device. */
"LINK_NEW_DEVICE_TITLE" = "Link New Device";
/* Indicates that the link preview is being loaded. */
"LINK_PREVIEW_LOADING" = "Loading…";
/* Menu item and navbar title for the device manager */
"LINKED_DEVICES_TITLE" = "Linked Devices";