Implement preliminary link preview view
This commit is contained in:
parent
074c1bf43f
commit
aa027a28c5
|
@ -218,4 +218,4 @@ SPEC CHECKSUMS:
|
||||||
|
|
||||||
PODFILE CHECKSUM: 2fca3f32c171e1324c9e3809b96a32d4a929d05c
|
PODFILE CHECKSUM: 2fca3f32c171e1324c9e3809b96a32d4a929d05c
|
||||||
|
|
||||||
COCOAPODS: 1.10.1
|
COCOAPODS: 1.10.0.rc.1
|
||||||
|
|
|
@ -232,6 +232,7 @@
|
||||||
B83786802586D296003CE78E /* KeyPairMigrationSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B837867F2586D296003CE78E /* KeyPairMigrationSheet.swift */; };
|
B83786802586D296003CE78E /* KeyPairMigrationSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B837867F2586D296003CE78E /* KeyPairMigrationSheet.swift */; };
|
||||||
B83F2B88240CB75A000A54AB /* UIImage+Scaling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83F2B87240CB75A000A54AB /* UIImage+Scaling.swift */; };
|
B83F2B88240CB75A000A54AB /* UIImage+Scaling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83F2B87240CB75A000A54AB /* UIImage+Scaling.swift */; };
|
||||||
B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B84664F4235022F30083A1CD /* MentionUtilities.swift */; };
|
B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B84664F4235022F30083A1CD /* MentionUtilities.swift */; };
|
||||||
|
B849789625D4A2F500D0D0B3 /* LinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B849789525D4A2F500D0D0B3 /* LinkView.swift */; };
|
||||||
B85357BF23A1AE0800AAF6CD /* SeedReminderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85357BE23A1AE0800AAF6CD /* SeedReminderView.swift */; };
|
B85357BF23A1AE0800AAF6CD /* SeedReminderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85357BE23A1AE0800AAF6CD /* SeedReminderView.swift */; };
|
||||||
B85357C323A1BD1200AAF6CD /* SeedVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85357C223A1BD1200AAF6CD /* SeedVC.swift */; };
|
B85357C323A1BD1200AAF6CD /* SeedVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85357C223A1BD1200AAF6CD /* SeedVC.swift */; };
|
||||||
B8544E3323D50E4900299F14 /* AppearanceUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8544E3223D50E4900299F14 /* AppearanceUtilities.swift */; };
|
B8544E3323D50E4900299F14 /* AppearanceUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8544E3223D50E4900299F14 /* AppearanceUtilities.swift */; };
|
||||||
|
@ -1280,6 +1281,7 @@
|
||||||
B840729F2565F1670037CB17 /* OWSQuotedReplyModel+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OWSQuotedReplyModel+Conversion.swift"; sourceTree = "<group>"; };
|
B840729F2565F1670037CB17 /* OWSQuotedReplyModel+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OWSQuotedReplyModel+Conversion.swift"; sourceTree = "<group>"; };
|
||||||
B84664F4235022F30083A1CD /* MentionUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionUtilities.swift; sourceTree = "<group>"; };
|
B84664F4235022F30083A1CD /* MentionUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionUtilities.swift; sourceTree = "<group>"; };
|
||||||
B847570023D568EB00759540 /* SignalServiceKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SignalServiceKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
B847570023D568EB00759540 /* SignalServiceKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SignalServiceKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
B849789525D4A2F500D0D0B3 /* LinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkView.swift; sourceTree = "<group>"; };
|
||||||
B85357BE23A1AE0800AAF6CD /* SeedReminderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedReminderView.swift; sourceTree = "<group>"; };
|
B85357BE23A1AE0800AAF6CD /* SeedReminderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedReminderView.swift; sourceTree = "<group>"; };
|
||||||
B85357C223A1BD1200AAF6CD /* SeedVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedVC.swift; sourceTree = "<group>"; };
|
B85357C223A1BD1200AAF6CD /* SeedVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedVC.swift; sourceTree = "<group>"; };
|
||||||
B8544E3023D16CA500299F14 /* DeviceUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceUtilities.swift; sourceTree = "<group>"; };
|
B8544E3023D16CA500299F14 /* DeviceUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceUtilities.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2222,6 +2224,7 @@
|
||||||
34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */,
|
34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */,
|
||||||
C328250E25CA06020062D0A7 /* VoiceMessageViewV2.swift */,
|
C328250E25CA06020062D0A7 /* VoiceMessageViewV2.swift */,
|
||||||
B8569AE225CBB19A00DBA3DB /* DocumentView.swift */,
|
B8569AE225CBB19A00DBA3DB /* DocumentView.swift */,
|
||||||
|
B849789525D4A2F500D0D0B3 /* LinkView.swift */,
|
||||||
);
|
);
|
||||||
path = "Content Views";
|
path = "Content Views";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -4998,6 +5001,7 @@
|
||||||
B85A68B12587141A008CC492 /* Storage+Resetting.swift in Sources */,
|
B85A68B12587141A008CC492 /* Storage+Resetting.swift in Sources */,
|
||||||
3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */,
|
3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */,
|
||||||
340FC8A9204DAC8D007AEB0F /* NotificationSettingsOptionsViewController.m in Sources */,
|
340FC8A9204DAC8D007AEB0F /* NotificationSettingsOptionsViewController.m in Sources */,
|
||||||
|
B849789625D4A2F500D0D0B3 /* LinkView.swift in Sources */,
|
||||||
C3D0972B2510499C00F6E3E4 /* BackgroundPoller.swift in Sources */,
|
C3D0972B2510499C00F6E3E4 /* BackgroundPoller.swift in Sources */,
|
||||||
C3548F0624456447009433A8 /* PNModeVC.swift in Sources */,
|
C3548F0624456447009433A8 /* PNModeVC.swift in Sources */,
|
||||||
B80A579F23DFF1F300876683 /* NewClosedGroupVC.swift in Sources */,
|
B80A579F23DFF1F300876683 /* NewClosedGroupVC.swift in Sources */,
|
||||||
|
|
|
@ -3,12 +3,13 @@
|
||||||
// • Tapping replies
|
// • Tapping replies
|
||||||
// • Mentions
|
// • Mentions
|
||||||
// • Remaining send logic
|
// • Remaining send logic
|
||||||
// • Paging glitch
|
|
||||||
// • Blocking
|
// • Blocking
|
||||||
// • Subtitle
|
// • Subtitle
|
||||||
// • Resending failed messages
|
// • Resending failed messages
|
||||||
// • Linkification
|
// • Linkification
|
||||||
// • Link previews
|
// • Link previews
|
||||||
|
// • Animation glitch when leaving conversation (probably because vc is resigning first responder)
|
||||||
|
// • Timestamps
|
||||||
|
|
||||||
final class ConversationVC : BaseVC, ConversationViewModelDelegate, UITableViewDataSource, UITableViewDelegate {
|
final class ConversationVC : BaseVC, ConversationViewModelDelegate, UITableViewDataSource, UITableViewDelegate {
|
||||||
let thread: TSThread
|
let thread: TSThread
|
||||||
|
@ -43,7 +44,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, UITableViewD
|
||||||
|
|
||||||
private lazy var mediaCache: NSCache<NSString, AnyObject> = {
|
private lazy var mediaCache: NSCache<NSString, AnyObject> = {
|
||||||
let result = NSCache<NSString, AnyObject>()
|
let result = NSCache<NSString, AnyObject>()
|
||||||
result.countLimit = 24
|
result.countLimit = 40
|
||||||
return result
|
return result
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
|
||||||
|
final class LinkView : UIView {
|
||||||
|
private let viewItem: ConversationViewItem
|
||||||
|
|
||||||
|
private var textColor: UIColor {
|
||||||
|
let isOutgoing = (viewItem.interaction.interactionType() == .outgoingMessage)
|
||||||
|
switch (isOutgoing, AppModeManager.shared.currentAppMode) {
|
||||||
|
case (true, .dark), (false, .light): return .black
|
||||||
|
default: return .white
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let imageSize: CGFloat = 100
|
||||||
|
|
||||||
|
init(for viewItem: ConversationViewItem) {
|
||||||
|
self.viewItem = viewItem
|
||||||
|
super.init(frame: CGRect.zero)
|
||||||
|
setUpViewHierarchy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
preconditionFailure("Use init(for:) instead.")
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
preconditionFailure("Use init(for:) instead.")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setUpViewHierarchy() {
|
||||||
|
guard let preview = viewItem.linkPreview else { return }
|
||||||
|
|
||||||
|
let hStackViewContainer = UIView()
|
||||||
|
hStackViewContainer.backgroundColor = .black
|
||||||
|
|
||||||
|
let hStackView = UIStackView()
|
||||||
|
hStackView.axis = .horizontal
|
||||||
|
hStackView.alignment = .center
|
||||||
|
|
||||||
|
hStackViewContainer.addSubview(hStackView)
|
||||||
|
hStackView.pin(to: hStackViewContainer)
|
||||||
|
|
||||||
|
let imageViewContainer = UIView()
|
||||||
|
imageViewContainer.set(.width, to: LinkView.imageSize)
|
||||||
|
imageViewContainer.set(.height, to: LinkView.imageSize)
|
||||||
|
imageViewContainer.clipsToBounds = true
|
||||||
|
|
||||||
|
let imageView = UIImageView()
|
||||||
|
let filePath = given(preview.imageAttachmentId) { TSAttachmentStream.fetch(uniqueId: $0)!.originalFilePath! }
|
||||||
|
imageView.image = given(filePath) { UIImage(contentsOfFile: $0)! }
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
imageViewContainer.addSubview(imageView)
|
||||||
|
imageView.pin(to: imageViewContainer)
|
||||||
|
hStackView.addArrangedSubview(imageViewContainer)
|
||||||
|
|
||||||
|
let titleLabelContainer = UIView()
|
||||||
|
|
||||||
|
let titleLabel = UILabel()
|
||||||
|
titleLabel.text = preview.title
|
||||||
|
titleLabel.textColor = textColor
|
||||||
|
titleLabel.font = .boldSystemFont(ofSize: Values.smallFontSize)
|
||||||
|
titleLabel.numberOfLines = 0
|
||||||
|
titleLabelContainer.addSubview(titleLabel)
|
||||||
|
titleLabel.pin(to: titleLabelContainer, withInset: Values.smallSpacing)
|
||||||
|
hStackView.addArrangedSubview(titleLabelContainer)
|
||||||
|
|
||||||
|
let vStackView = UIStackView()
|
||||||
|
vStackView.axis = .vertical
|
||||||
|
vStackView.addArrangedSubview(hStackViewContainer)
|
||||||
|
|
||||||
|
let separator = UIView()
|
||||||
|
separator.backgroundColor = Colors.separator
|
||||||
|
separator.set(.height, to: 1 / UIScreen.main.scale)
|
||||||
|
vStackView.addArrangedSubview(separator)
|
||||||
|
|
||||||
|
let bodyLabelContainer = UIView()
|
||||||
|
|
||||||
|
let bodyLabel = VisibleMessageCell.getBodyLabel(for: viewItem, with: textColor)
|
||||||
|
bodyLabelContainer.addSubview(bodyLabel)
|
||||||
|
bodyLabel.pin(to: bodyLabelContainer, withInset: 12)
|
||||||
|
vStackView.addArrangedSubview(bodyLabelContainer)
|
||||||
|
|
||||||
|
addSubview(vStackView)
|
||||||
|
vStackView.pin(to: self)
|
||||||
|
}
|
||||||
|
}
|
|
@ -215,6 +215,10 @@ final class VisibleMessageCell : MessageCell {
|
||||||
let additionalBottomInset = shouldInsetHeader ? Values.mediumSpacing : 1
|
let additionalBottomInset = shouldInsetHeader ? Values.mediumSpacing : 1
|
||||||
headerView.pin(.bottom, to: .bottom, of: dateBreakLabel, withInset: Values.smallSpacing + additionalBottomInset)
|
headerView.pin(.bottom, to: .bottom, of: dateBreakLabel, withInset: Values.smallSpacing + additionalBottomInset)
|
||||||
dateBreakLabel.center(.horizontal, in: headerView)
|
dateBreakLabel.center(.horizontal, in: headerView)
|
||||||
|
let availableWidth = VisibleMessageCell.getMaxWidth(for: viewItem)
|
||||||
|
let availableSpace = CGSize(width: availableWidth, height: .greatestFiniteMagnitude)
|
||||||
|
let dateBreakLabelSize = dateBreakLabel.sizeThatFits(availableSpace)
|
||||||
|
dateBreakLabel.set(.height, to: dateBreakLabelSize.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func populateContentView(for viewItem: ConversationViewItem) {
|
private func populateContentView(for viewItem: ConversationViewItem) {
|
||||||
|
@ -224,32 +228,32 @@ final class VisibleMessageCell : MessageCell {
|
||||||
let isOutgoing = (viewItem.interaction.interactionType() == .outgoingMessage)
|
let isOutgoing = (viewItem.interaction.interactionType() == .outgoingMessage)
|
||||||
switch viewItem.messageCellType {
|
switch viewItem.messageCellType {
|
||||||
case .textOnlyMessage:
|
case .textOnlyMessage:
|
||||||
guard let message = viewItem.interaction as? TSMessage else { return }
|
if viewItem.linkPreview != nil {
|
||||||
let inset: CGFloat = 12
|
let linkView = LinkView(for: viewItem)
|
||||||
// Stack view
|
snContentView.addSubview(linkView)
|
||||||
let stackView = UIStackView(arrangedSubviews: [])
|
linkView.pin(to: snContentView)
|
||||||
stackView.axis = .vertical
|
} else {
|
||||||
stackView.spacing = 2
|
let inset: CGFloat = 12
|
||||||
// Quote label
|
// Stack view
|
||||||
if viewItem.quotedReply != nil {
|
let stackView = UIStackView(arrangedSubviews: [])
|
||||||
let maxWidth = VisibleMessageCell.getMaxWidth(for: viewItem) - 2 * inset
|
stackView.axis = .vertical
|
||||||
let direction: QuoteView.Direction = isOutgoing ? .outgoing : .incoming
|
stackView.spacing = 2
|
||||||
let hInset: CGFloat = 2
|
// Quote view
|
||||||
let quoteView = QuoteView(for: viewItem, direction: direction, hInset: hInset, maxWidth: maxWidth)
|
if viewItem.quotedReply != nil {
|
||||||
let quoteViewContainer = UIView(wrapping: quoteView, withInsets: UIEdgeInsets(top: 0, leading: hInset, bottom: 0, trailing: hInset))
|
let maxWidth = VisibleMessageCell.getMaxWidth(for: viewItem) - 2 * inset
|
||||||
stackView.addArrangedSubview(quoteViewContainer)
|
let direction: QuoteView.Direction = isOutgoing ? .outgoing : .incoming
|
||||||
|
let hInset: CGFloat = 2
|
||||||
|
let quoteView = QuoteView(for: viewItem, direction: direction, hInset: hInset, maxWidth: maxWidth)
|
||||||
|
let quoteViewContainer = UIView(wrapping: quoteView, withInsets: UIEdgeInsets(top: 0, leading: hInset, bottom: 0, trailing: hInset))
|
||||||
|
stackView.addArrangedSubview(quoteViewContainer)
|
||||||
|
}
|
||||||
|
// Body label
|
||||||
|
let bodyLabel = VisibleMessageCell.getBodyLabel(for: viewItem, with: bodyLabelTextColor)
|
||||||
|
stackView.addArrangedSubview(bodyLabel)
|
||||||
|
// Constraints
|
||||||
|
snContentView.addSubview(stackView)
|
||||||
|
stackView.pin(to: snContentView, withInset: inset)
|
||||||
}
|
}
|
||||||
// Body label
|
|
||||||
let bodyLabel = UILabel()
|
|
||||||
bodyLabel.numberOfLines = 0
|
|
||||||
bodyLabel.lineBreakMode = .byWordWrapping
|
|
||||||
bodyLabel.textColor = bodyLabelTextColor
|
|
||||||
bodyLabel.font = .systemFont(ofSize: getFontSize(for: viewItem))
|
|
||||||
bodyLabel.attributedText = given(message.body) { MentionUtilities.highlightMentions(in: $0, isOutgoingMessage: isOutgoing, threadID: viewItem.interaction.uniqueThreadId, attributes: [:]) }
|
|
||||||
stackView.addArrangedSubview(bodyLabel)
|
|
||||||
// Constraints
|
|
||||||
snContentView.addSubview(stackView)
|
|
||||||
stackView.pin(to: snContentView, withInset: inset)
|
|
||||||
case .mediaMessage:
|
case .mediaMessage:
|
||||||
guard let cache = delegate?.getMediaCache() else { preconditionFailure() }
|
guard let cache = delegate?.getMediaCache() else { preconditionFailure() }
|
||||||
let maxMessageWidth = VisibleMessageCell.getMaxWidth(for: viewItem)
|
let maxMessageWidth = VisibleMessageCell.getMaxWidth(for: viewItem)
|
||||||
|
@ -332,7 +336,7 @@ final class VisibleMessageCell : MessageCell {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getFontSize(for viewItem: ConversationViewItem) -> CGFloat {
|
private static func getFontSize(for viewItem: ConversationViewItem) -> CGFloat {
|
||||||
let baselineFontSize = Values.mediumFontSize
|
let baselineFontSize = Values.mediumFontSize
|
||||||
switch viewItem.displayableBodyText?.jumbomojiCount {
|
switch viewItem.displayableBodyText?.jumbomojiCount {
|
||||||
case 1: return baselineFontSize + 30
|
case 1: return baselineFontSize + 30
|
||||||
|
@ -404,4 +408,16 @@ final class VisibleMessageCell : MessageCell {
|
||||||
let senderSessionID = (message as? TSIncomingMessage)?.authorId
|
let senderSessionID = (message as? TSIncomingMessage)?.authorId
|
||||||
return isGroupThread && viewItem.shouldShowSenderProfilePicture && senderSessionID != nil
|
return isGroupThread && viewItem.shouldShowSenderProfilePicture && senderSessionID != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func getBodyLabel(for viewItem: ConversationViewItem, with textColor: UIColor) -> UILabel {
|
||||||
|
guard let message = viewItem.interaction as? TSMessage else { preconditionFailure() }
|
||||||
|
let isOutgoing = (message.interactionType() == .outgoingMessage)
|
||||||
|
let bodyLabel = UILabel()
|
||||||
|
bodyLabel.numberOfLines = 0
|
||||||
|
bodyLabel.lineBreakMode = .byWordWrapping
|
||||||
|
bodyLabel.textColor = textColor
|
||||||
|
bodyLabel.font = .systemFont(ofSize: getFontSize(for: viewItem))
|
||||||
|
bodyLabel.attributedText = given(message.body) { MentionUtilities.highlightMentions(in: $0, isOutgoingMessage: isOutgoing, threadID: viewItem.interaction.uniqueThreadId, attributes: [:]) }
|
||||||
|
return bodyLabel
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,7 +171,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
//
|
//
|
||||||
// PERF: we could do less messages on shorter (older, slower) devices
|
// PERF: we could do less messages on shorter (older, slower) devices
|
||||||
// PERF: we could cache the cell height, since some messages will be much taller.
|
// PERF: we could cache the cell height, since some messages will be much taller.
|
||||||
static const int kYapDatabasePageSize = 24;
|
static const int kYapDatabasePageSize = 100;
|
||||||
|
|
||||||
// Never show more than n messages in conversation view when user arrives.
|
// Never show more than n messages in conversation view when user arrives.
|
||||||
static const int kConversationInitialMaxRangeSize = 300;
|
static const int kConversationInitialMaxRangeSize = 300;
|
||||||
|
|
Loading…
Reference in New Issue