Add rough draft of link preview view to composer.
This commit is contained in:
parent
977ee9ffe9
commit
416aa2b347
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
||||
|
|
Loading…
Reference in New Issue