diff --git a/Podfile.lock b/Podfile.lock index 503cc3094..3fc70a96b 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -218,4 +218,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 2fca3f32c171e1324c9e3809b96a32d4a929d05c -COCOAPODS: 1.10.1 +COCOAPODS: 1.10.0.rc.1 diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 776651f11..a973a0498 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -232,6 +232,7 @@ B83786802586D296003CE78E /* KeyPairMigrationSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B837867F2586D296003CE78E /* KeyPairMigrationSheet.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 */; }; + B849789625D4A2F500D0D0B3 /* LinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B849789525D4A2F500D0D0B3 /* LinkView.swift */; }; B85357BF23A1AE0800AAF6CD /* SeedReminderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85357BE23A1AE0800AAF6CD /* SeedReminderView.swift */; }; B85357C323A1BD1200AAF6CD /* SeedVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85357C223A1BD1200AAF6CD /* SeedVC.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 = ""; }; B84664F4235022F30083A1CD /* MentionUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionUtilities.swift; sourceTree = ""; }; 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 = ""; }; B85357BE23A1AE0800AAF6CD /* SeedReminderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedReminderView.swift; sourceTree = ""; }; B85357C223A1BD1200AAF6CD /* SeedVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedVC.swift; sourceTree = ""; }; B8544E3023D16CA500299F14 /* DeviceUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceUtilities.swift; sourceTree = ""; }; @@ -2222,6 +2224,7 @@ 34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */, C328250E25CA06020062D0A7 /* VoiceMessageViewV2.swift */, B8569AE225CBB19A00DBA3DB /* DocumentView.swift */, + B849789525D4A2F500D0D0B3 /* LinkView.swift */, ); path = "Content Views"; sourceTree = ""; @@ -4998,6 +5001,7 @@ B85A68B12587141A008CC492 /* Storage+Resetting.swift in Sources */, 3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */, 340FC8A9204DAC8D007AEB0F /* NotificationSettingsOptionsViewController.m in Sources */, + B849789625D4A2F500D0D0B3 /* LinkView.swift in Sources */, C3D0972B2510499C00F6E3E4 /* BackgroundPoller.swift in Sources */, C3548F0624456447009433A8 /* PNModeVC.swift in Sources */, B80A579F23DFF1F300876683 /* NewClosedGroupVC.swift in Sources */, diff --git a/Session/Conversations V2/ConversationVC.swift b/Session/Conversations V2/ConversationVC.swift index bbffc9b03..3a3dee2eb 100644 --- a/Session/Conversations V2/ConversationVC.swift +++ b/Session/Conversations V2/ConversationVC.swift @@ -3,12 +3,13 @@ // • Tapping replies // • Mentions // • Remaining send logic -// • Paging glitch // • Blocking // • Subtitle // • Resending failed messages // • Linkification // • Link previews +// • Animation glitch when leaving conversation (probably because vc is resigning first responder) +// • Timestamps final class ConversationVC : BaseVC, ConversationViewModelDelegate, UITableViewDataSource, UITableViewDelegate { let thread: TSThread @@ -43,7 +44,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, UITableViewD private lazy var mediaCache: NSCache = { let result = NSCache() - result.countLimit = 24 + result.countLimit = 40 return result }() diff --git a/Session/Conversations V2/Message Cells/Content Views/LinkView.swift b/Session/Conversations V2/Message Cells/Content Views/LinkView.swift new file mode 100644 index 000000000..df77487de --- /dev/null +++ b/Session/Conversations V2/Message Cells/Content Views/LinkView.swift @@ -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) + } +} diff --git a/Session/Conversations V2/Message Cells/VisibleMessageCell.swift b/Session/Conversations V2/Message Cells/VisibleMessageCell.swift index 9af8ae482..9b210b838 100644 --- a/Session/Conversations V2/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations V2/Message Cells/VisibleMessageCell.swift @@ -215,6 +215,10 @@ final class VisibleMessageCell : MessageCell { let additionalBottomInset = shouldInsetHeader ? Values.mediumSpacing : 1 headerView.pin(.bottom, to: .bottom, of: dateBreakLabel, withInset: Values.smallSpacing + additionalBottomInset) 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) { @@ -224,32 +228,32 @@ final class VisibleMessageCell : MessageCell { let isOutgoing = (viewItem.interaction.interactionType() == .outgoingMessage) switch viewItem.messageCellType { case .textOnlyMessage: - guard let message = viewItem.interaction as? TSMessage else { return } - let inset: CGFloat = 12 - // Stack view - let stackView = UIStackView(arrangedSubviews: []) - stackView.axis = .vertical - stackView.spacing = 2 - // Quote label - if viewItem.quotedReply != nil { - let maxWidth = VisibleMessageCell.getMaxWidth(for: viewItem) - 2 * inset - 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) + if viewItem.linkPreview != nil { + let linkView = LinkView(for: viewItem) + snContentView.addSubview(linkView) + linkView.pin(to: snContentView) + } else { + let inset: CGFloat = 12 + // Stack view + let stackView = UIStackView(arrangedSubviews: []) + stackView.axis = .vertical + stackView.spacing = 2 + // Quote view + if viewItem.quotedReply != nil { + let maxWidth = VisibleMessageCell.getMaxWidth(for: viewItem) - 2 * inset + 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: guard let cache = delegate?.getMediaCache() else { preconditionFailure() } let maxMessageWidth = VisibleMessageCell.getMaxWidth(for: viewItem) @@ -332,7 +336,7 @@ final class VisibleMessageCell : MessageCell { return result } - private func getFontSize(for viewItem: ConversationViewItem) -> CGFloat { + private static func getFontSize(for viewItem: ConversationViewItem) -> CGFloat { let baselineFontSize = Values.mediumFontSize switch viewItem.displayableBodyText?.jumbomojiCount { case 1: return baselineFontSize + 30 @@ -404,4 +408,16 @@ final class VisibleMessageCell : MessageCell { let senderSessionID = (message as? TSIncomingMessage)?.authorId 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 + } } diff --git a/Session/Conversations/ConversationViewModel.m b/Session/Conversations/ConversationViewModel.m index 880f90e39..021ad6b13 100644 --- a/Session/Conversations/ConversationViewModel.m +++ b/Session/Conversations/ConversationViewModel.m @@ -171,7 +171,7 @@ NS_ASSUME_NONNULL_BEGIN // // 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. -static const int kYapDatabasePageSize = 24; +static const int kYapDatabasePageSize = 100; // Never show more than n messages in conversation view when user arrives. static const int kConversationInitialMaxRangeSize = 300;