Implement linkification
This commit is contained in:
parent
aa027a28c5
commit
401a29344d
|
@ -216,6 +216,7 @@
|
|||
B8041A9525C8FA1D003C2166 /* MediaLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8041A9425C8FA1D003C2166 /* MediaLoaderView.swift */; };
|
||||
B8041AA725C90927003C2166 /* TypingIndicatorCellV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8041AA625C90927003C2166 /* TypingIndicatorCellV2.swift */; };
|
||||
B80A579F23DFF1F300876683 /* NewClosedGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80A579E23DFF1F300876683 /* NewClosedGroupVC.swift */; };
|
||||
B821494625D4D6FF009C0F2A /* URLModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B821494525D4D6FF009C0F2A /* URLModal.swift */; };
|
||||
B8269D2925C7A4B400488AB4 /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D2825C7A4B400488AB4 /* InputView.swift */; };
|
||||
B8269D3325C7A8C600488AB4 /* InputViewButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D3225C7A8C600488AB4 /* InputViewButton.swift */; };
|
||||
B8269D3D25C7B34D00488AB4 /* InputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D3C25C7B34D00488AB4 /* InputTextView.swift */; };
|
||||
|
@ -279,7 +280,7 @@
|
|||
B88A1AC725C90A4700E6D421 /* TypingIndicatorInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B6A904218B4C90007C4606 /* TypingIndicatorInteraction.swift */; };
|
||||
B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */; };
|
||||
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0742339EDCF00B4D94D /* NukeDataModal.swift */; };
|
||||
B897621C25D201F7004F83B2 /* ConversationVC+ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B897621B25D201F7004F83B2 /* ConversationVC+ScrollToBottomButton.swift */; };
|
||||
B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */; };
|
||||
B8A14D702589CE9000E70D57 /* KeyPairMigrationSuccessSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A14D6F2589CE9000E70D57 /* KeyPairMigrationSuccessSheet.swift */; };
|
||||
B8AE75A425A6C6A6001A84D2 /* Data+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */; };
|
||||
B8AE760B25ABFB5A001A84D2 /* GeneralUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = B8AE760A25ABFB5A001A84D2 /* GeneralUtilities.m */; };
|
||||
|
@ -1261,6 +1262,7 @@
|
|||
B8041A9425C8FA1D003C2166 /* MediaLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoaderView.swift; sourceTree = "<group>"; };
|
||||
B8041AA625C90927003C2166 /* TypingIndicatorCellV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingIndicatorCellV2.swift; sourceTree = "<group>"; };
|
||||
B80A579E23DFF1F300876683 /* NewClosedGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewClosedGroupVC.swift; sourceTree = "<group>"; };
|
||||
B821494525D4D6FF009C0F2A /* URLModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLModal.swift; sourceTree = "<group>"; };
|
||||
B8269D2825C7A4B400488AB4 /* InputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputView.swift; sourceTree = "<group>"; };
|
||||
B8269D3225C7A8C600488AB4 /* InputViewButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputViewButton.swift; sourceTree = "<group>"; };
|
||||
B8269D3C25C7B34D00488AB4 /* InputTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputTextView.swift; sourceTree = "<group>"; };
|
||||
|
@ -1303,7 +1305,7 @@
|
|||
B88847BB23E10BC6009836D2 /* GroupMembersVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMembersVC.swift; sourceTree = "<group>"; };
|
||||
B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanQRCodeWrapperVC.swift; sourceTree = "<group>"; };
|
||||
B894D0742339EDCF00B4D94D /* NukeDataModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeDataModal.swift; sourceTree = "<group>"; };
|
||||
B897621B25D201F7004F83B2 /* ConversationVC+ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationVC+ScrollToBottomButton.swift"; sourceTree = "<group>"; };
|
||||
B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = "<group>"; };
|
||||
B8A14D6F2589CE9000E70D57 /* KeyPairMigrationSuccessSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyPairMigrationSuccessSheet.swift; sourceTree = "<group>"; };
|
||||
B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Trimming.swift"; sourceTree = "<group>"; };
|
||||
B8AE760925ABFB00001A84D2 /* GeneralUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneralUtilities.h; sourceTree = "<group>"; };
|
||||
|
@ -2229,15 +2231,24 @@
|
|||
path = "Content Views";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B821493625D4D6A7009C0F2A /* Views & Modals */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */,
|
||||
B821494525D4D6FF009C0F2A /* URLModal.swift */,
|
||||
);
|
||||
path = "Views & Modals";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B835246C25C38AA20089A44F /* Conversations V2 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B835246D25C38ABF0089A44F /* ConversationVC.swift */,
|
||||
B8569AC225CB5D2900DBA3DB /* ConversationVC+Interaction.swift */,
|
||||
B897621B25D201F7004F83B2 /* ConversationVC+ScrollToBottomButton.swift */,
|
||||
B887C38125C7C79700E11DAE /* Input View */,
|
||||
B835247725C38D190089A44F /* Message Cells */,
|
||||
C328252E25CA54F70062D0A7 /* Context Menu */,
|
||||
B821493625D4D6A7009C0F2A /* Views & Modals */,
|
||||
);
|
||||
path = "Conversations V2";
|
||||
sourceTree = "<group>";
|
||||
|
@ -5027,6 +5038,7 @@
|
|||
34D1F0881F8678AA0066283D /* ConversationViewLayout.m in Sources */,
|
||||
B82B408E239DC00D00A248E7 /* DisplayNameVC.swift in Sources */,
|
||||
B835246E25C38ABF0089A44F /* ConversationVC.swift in Sources */,
|
||||
B821494625D4D6FF009C0F2A /* URLModal.swift in Sources */,
|
||||
4CA485BB2232339F004B9E7D /* PhotoCaptureViewController.swift in Sources */,
|
||||
34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */,
|
||||
344825C6211390C800DB4BD8 /* OWSOrphanDataCleaner.m in Sources */,
|
||||
|
@ -5115,7 +5127,7 @@
|
|||
340FC8B0204DAC8D007AEB0F /* AddToBlockListViewController.m in Sources */,
|
||||
3496957321A301A100DCFE74 /* OWSBackupJob.m in Sources */,
|
||||
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */,
|
||||
B897621C25D201F7004F83B2 /* ConversationVC+ScrollToBottomButton.swift in Sources */,
|
||||
B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */,
|
||||
346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */,
|
||||
45E5A6991F61E6DE001E4A8A /* MarqueeLabel.swift in Sources */,
|
||||
34D1F0B01F867BFC0066283D /* OWSSystemMessageCell.m in Sources */,
|
||||
|
|
|
@ -201,4 +201,11 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
|
|||
func handleQuoteViewCancelButtonTapped() {
|
||||
snInputView.quoteDraftInfo = nil
|
||||
}
|
||||
|
||||
func openURL(_ url: URL) {
|
||||
let urlModal = URLModal(url: url)
|
||||
urlModal.modalPresentationStyle = .overFullScreen
|
||||
urlModal.modalTransitionStyle = .crossDissolve
|
||||
present(urlModal, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
|
||||
extension ConversationVC {
|
||||
|
||||
final class ScrollToBottomButton : UIView {
|
||||
private let delegate: ScrollToBottomButtonDelegate
|
||||
|
||||
// MARK: Settings
|
||||
private static let size: CGFloat = 40
|
||||
private static let iconSize: CGFloat = 16
|
||||
|
||||
// MARK: Lifecycle
|
||||
init(delegate: ScrollToBottomButtonDelegate) {
|
||||
self.delegate = delegate
|
||||
super.init(frame: CGRect.zero)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
preconditionFailure("Use init(delegate:) instead.")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure("Use init(delegate:) instead.")
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
// Background & blur
|
||||
let backgroundView = UIView()
|
||||
backgroundView.backgroundColor = isLightMode ? .white : .black
|
||||
backgroundView.alpha = Values.lowOpacity
|
||||
addSubview(backgroundView)
|
||||
backgroundView.pin(to: self)
|
||||
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .regular))
|
||||
addSubview(blurView)
|
||||
blurView.pin(to: self)
|
||||
// Size & shape
|
||||
let size = ScrollToBottomButton.size
|
||||
set(.width, to: size)
|
||||
set(.height, to: size)
|
||||
layer.cornerRadius = size / 2
|
||||
layer.masksToBounds = true
|
||||
// Border
|
||||
layer.borderWidth = 1
|
||||
let borderColor = (isLightMode ? UIColor.black : UIColor.white).withAlphaComponent(Values.veryLowOpacity)
|
||||
layer.borderColor = borderColor.cgColor
|
||||
// Icon
|
||||
let tint = isLightMode ? UIColor.black : UIColor.white
|
||||
let icon = UIImage(named: "ic_chevron_down")!.withTint(tint)
|
||||
let iconImageView = UIImageView(image: icon)
|
||||
iconImageView.set(.width, to: ScrollToBottomButton.iconSize)
|
||||
iconImageView.set(.height, to: ScrollToBottomButton.iconSize)
|
||||
iconImageView.contentMode = .scaleAspectFit
|
||||
addSubview(iconImageView)
|
||||
iconImageView.center(in: self)
|
||||
// Gesture recognizer
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
||||
addGestureRecognizer(tapGestureRecognizer)
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func handleTap() {
|
||||
delegate.handleScrollToBottomButtonTapped()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protocol ScrollToBottomButtonDelegate {
|
||||
|
||||
func handleScrollToBottomButtonTapped()
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
|
||||
final class LinkView : UIView {
|
||||
private let viewItem: ConversationViewItem
|
||||
private let maxWidth: CGFloat
|
||||
private let delegate: UITextViewDelegate
|
||||
|
||||
private var textColor: UIColor {
|
||||
let isOutgoing = (viewItem.interaction.interactionType() == .outgoingMessage)
|
||||
|
@ -12,18 +14,20 @@ final class LinkView : UIView {
|
|||
|
||||
private static let imageSize: CGFloat = 100
|
||||
|
||||
init(for viewItem: ConversationViewItem) {
|
||||
init(for viewItem: ConversationViewItem, maxWidth: CGFloat, delegate: UITextViewDelegate) {
|
||||
self.viewItem = viewItem
|
||||
self.maxWidth = maxWidth
|
||||
self.delegate = delegate
|
||||
super.init(frame: CGRect.zero)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
preconditionFailure("Use init(for:) instead.")
|
||||
preconditionFailure("Use init(for:maxWidth:delegate:) instead.")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure("Use init(for:) instead.")
|
||||
preconditionFailure("Use init(for:maxWidth:delegate:) instead.")
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
|
@ -72,12 +76,12 @@ final class LinkView : UIView {
|
|||
separator.set(.height, to: 1 / UIScreen.main.scale)
|
||||
vStackView.addArrangedSubview(separator)
|
||||
|
||||
let bodyLabelContainer = UIView()
|
||||
let bodyTextViewContainer = UIView()
|
||||
|
||||
let bodyLabel = VisibleMessageCell.getBodyLabel(for: viewItem, with: textColor)
|
||||
bodyLabelContainer.addSubview(bodyLabel)
|
||||
bodyLabel.pin(to: bodyLabelContainer, withInset: 12)
|
||||
vStackView.addArrangedSubview(bodyLabelContainer)
|
||||
let bodyTextView = VisibleMessageCell.getBodyTextView(for: viewItem, with: maxWidth, textColor: textColor, delegate: delegate)
|
||||
bodyTextViewContainer.addSubview(bodyTextView)
|
||||
bodyTextView.pin(to: bodyTextViewContainer, withInset: 12)
|
||||
vStackView.addArrangedSubview(bodyTextViewContainer)
|
||||
|
||||
addSubview(vStackView)
|
||||
vStackView.pin(to: self)
|
||||
|
|
|
@ -55,4 +55,5 @@ protocol MessageCellDelegate {
|
|||
func handleViewItemTapped(_ viewItem: ConversationViewItem, gestureRecognizer: UITapGestureRecognizer)
|
||||
func handleViewItemDoubleTapped(_ viewItem: ConversationViewItem)
|
||||
func showFullText(_ viewItem: ConversationViewItem)
|
||||
func openURL(_ url: URL)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
|
||||
final class VisibleMessageCell : MessageCell {
|
||||
final class VisibleMessageCell : MessageCell, UITextViewDelegate {
|
||||
private var unloadContent: (() -> Void)?
|
||||
var albumView: MediaAlbumView?
|
||||
var bodyTextView: UITextView?
|
||||
var mediaTextOverlayView: MediaTextOverlayView?
|
||||
// Constraints
|
||||
private lazy var headerViewTopConstraint = headerView.pin(.top, to: .top, of: self, withInset: 1)
|
||||
|
@ -103,7 +104,6 @@ final class VisibleMessageCell : MessageCell {
|
|||
// MARK: Lifecycle
|
||||
override func setUpViewHierarchy() {
|
||||
super.setUpViewHierarchy()
|
||||
isUserInteractionEnabled = true
|
||||
// Header view
|
||||
addSubview(headerView)
|
||||
headerViewTopConstraint.isActive = true
|
||||
|
@ -224,32 +224,34 @@ final class VisibleMessageCell : MessageCell {
|
|||
private func populateContentView(for viewItem: ConversationViewItem) {
|
||||
snContentView.subviews.forEach { $0.removeFromSuperview() }
|
||||
albumView = nil
|
||||
bodyTextView = nil
|
||||
mediaTextOverlayView = nil
|
||||
let isOutgoing = (viewItem.interaction.interactionType() == .outgoingMessage)
|
||||
switch viewItem.messageCellType {
|
||||
case .textOnlyMessage:
|
||||
let inset: CGFloat = 12
|
||||
let maxWidth = VisibleMessageCell.getMaxWidth(for: viewItem) - 2 * inset
|
||||
if viewItem.linkPreview != nil {
|
||||
let linkView = LinkView(for: viewItem)
|
||||
let linkView = LinkView(for: viewItem, maxWidth: maxWidth, delegate: self)
|
||||
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)
|
||||
// Body text view
|
||||
let bodyTextView = VisibleMessageCell.getBodyTextView(for: viewItem, with: maxWidth, textColor: bodyLabelTextColor, delegate: self)
|
||||
self.bodyTextView = bodyTextView
|
||||
stackView.addArrangedSubview(bodyTextView)
|
||||
// Constraints
|
||||
snContentView.addSubview(stackView)
|
||||
stackView.pin(to: snContentView, withInset: inset)
|
||||
|
@ -305,6 +307,16 @@ final class VisibleMessageCell : MessageCell {
|
|||
}
|
||||
|
||||
// MARK: Interaction
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let bodyTextView = bodyTextView {
|
||||
let pointInBodyTextViewCoordinates = convert(point, to: bodyTextView)
|
||||
if bodyTextView.bounds.contains(pointInBodyTextViewCoordinates) {
|
||||
return bodyTextView
|
||||
}
|
||||
}
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
@objc private func handleLongPress() {
|
||||
guard let viewItem = viewItem else { return }
|
||||
delegate?.handleViewItemLongPressed(viewItem)
|
||||
|
@ -320,6 +332,11 @@ final class VisibleMessageCell : MessageCell {
|
|||
delegate?.handleViewItemDoubleTapped(viewItem)
|
||||
}
|
||||
|
||||
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
||||
delegate?.openURL(URL)
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: Convenience
|
||||
private func getCornersToRound() -> UIRectCorner {
|
||||
guard !isOnlyMessageInCluster else { return .allCorners }
|
||||
|
@ -409,15 +426,29 @@ final class VisibleMessageCell : MessageCell {
|
|||
return isGroupThread && viewItem.shouldShowSenderProfilePicture && senderSessionID != nil
|
||||
}
|
||||
|
||||
static func getBodyLabel(for viewItem: ConversationViewItem, with textColor: UIColor) -> UILabel {
|
||||
static func getBodyTextView(for viewItem: ConversationViewItem, with availableWidth: CGFloat, textColor: UIColor, delegate: UITextViewDelegate) -> UITextView {
|
||||
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
|
||||
let result = UITextView()
|
||||
result.isEditable = false
|
||||
let attributes: [NSAttributedString.Key:Any] = [
|
||||
.foregroundColor : textColor,
|
||||
.font : UIFont.systemFont(ofSize: getFontSize(for: viewItem))
|
||||
]
|
||||
result.attributedText = given(message.body) { MentionUtilities.highlightMentions(in: $0, isOutgoingMessage: isOutgoing, threadID: viewItem.interaction.uniqueThreadId, attributes: attributes) }
|
||||
result.dataDetectorTypes = .link
|
||||
result.backgroundColor = .clear
|
||||
result.isOpaque = false
|
||||
result.textContainerInset = UIEdgeInsets.zero
|
||||
result.contentInset = UIEdgeInsets.zero
|
||||
result.textContainer.lineFragmentPadding = 0
|
||||
result.isScrollEnabled = false
|
||||
result.isUserInteractionEnabled = true
|
||||
result.delegate = delegate
|
||||
result.linkTextAttributes = [ .foregroundColor : textColor, .underlineStyle : NSUnderlineStyle.single.rawValue ]
|
||||
let availableSpace = CGSize(width: availableWidth, height: .greatestFiniteMagnitude)
|
||||
let size = result.sizeThatFits(availableSpace)
|
||||
result.set(.height, to: size.height)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
|
||||
final class ScrollToBottomButton : UIView {
|
||||
private let delegate: ScrollToBottomButtonDelegate
|
||||
|
||||
// MARK: Settings
|
||||
private static let size: CGFloat = 40
|
||||
private static let iconSize: CGFloat = 16
|
||||
|
||||
// MARK: Lifecycle
|
||||
init(delegate: ScrollToBottomButtonDelegate) {
|
||||
self.delegate = delegate
|
||||
super.init(frame: CGRect.zero)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
preconditionFailure("Use init(delegate:) instead.")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure("Use init(delegate:) instead.")
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
// Background & blur
|
||||
let backgroundView = UIView()
|
||||
backgroundView.backgroundColor = isLightMode ? .white : .black
|
||||
backgroundView.alpha = Values.lowOpacity
|
||||
addSubview(backgroundView)
|
||||
backgroundView.pin(to: self)
|
||||
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .regular))
|
||||
addSubview(blurView)
|
||||
blurView.pin(to: self)
|
||||
// Size & shape
|
||||
let size = ScrollToBottomButton.size
|
||||
set(.width, to: size)
|
||||
set(.height, to: size)
|
||||
layer.cornerRadius = size / 2
|
||||
layer.masksToBounds = true
|
||||
// Border
|
||||
layer.borderWidth = 1
|
||||
let borderColor = (isLightMode ? UIColor.black : UIColor.white).withAlphaComponent(Values.veryLowOpacity)
|
||||
layer.borderColor = borderColor.cgColor
|
||||
// Icon
|
||||
let tint = isLightMode ? UIColor.black : UIColor.white
|
||||
let icon = UIImage(named: "ic_chevron_down")!.withTint(tint)
|
||||
let iconImageView = UIImageView(image: icon)
|
||||
iconImageView.set(.width, to: ScrollToBottomButton.iconSize)
|
||||
iconImageView.set(.height, to: ScrollToBottomButton.iconSize)
|
||||
iconImageView.contentMode = .scaleAspectFit
|
||||
addSubview(iconImageView)
|
||||
iconImageView.center(in: self)
|
||||
// Gesture recognizer
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
||||
addGestureRecognizer(tapGestureRecognizer)
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func handleTap() {
|
||||
delegate.handleScrollToBottomButtonTapped()
|
||||
}
|
||||
}
|
||||
|
||||
protocol ScrollToBottomButtonDelegate {
|
||||
|
||||
func handleScrollToBottomButtonTapped()
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
|
||||
final class URLModal : Modal {
|
||||
private let url: URL
|
||||
|
||||
// MARK: Lifecycle
|
||||
init(url: URL) {
|
||||
self.url = url
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
override init(nibName: String?, bundle: Bundle?) {
|
||||
preconditionFailure("Use init(url:) instead.")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure("Use init(url:) instead.")
|
||||
}
|
||||
|
||||
override func populateContentView() {
|
||||
// Title
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.largeFontSize)
|
||||
titleLabel.text = "Open URL?"
|
||||
titleLabel.textAlignment = .center
|
||||
// Message
|
||||
let messageLabel = UILabel()
|
||||
messageLabel.textColor = Colors.text
|
||||
messageLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
let message = "Are you sure you want to open \(url.absoluteString)?"
|
||||
let attributedMessage = NSMutableAttributedString(string: message)
|
||||
attributedMessage.addAttributes([ .font : UIFont.boldSystemFont(ofSize: Values.smallFontSize) ], range: (message as NSString).range(of: url.absoluteString))
|
||||
messageLabel.attributedText = attributedMessage
|
||||
messageLabel.numberOfLines = 0
|
||||
messageLabel.lineBreakMode = .byWordWrapping
|
||||
messageLabel.textAlignment = .center
|
||||
// Open button
|
||||
let openButton = UIButton()
|
||||
openButton.set(.height, to: Values.mediumButtonHeight)
|
||||
openButton.layer.cornerRadius = Values.modalButtonCornerRadius
|
||||
openButton.backgroundColor = Colors.buttonBackground
|
||||
openButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
openButton.setTitleColor(Colors.text, for: UIControl.State.normal)
|
||||
openButton.setTitle("Open", for: UIControl.State.normal)
|
||||
openButton.addTarget(self, action: #selector(openURL), for: UIControl.Event.touchUpInside)
|
||||
// Button stack view
|
||||
let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, openButton ])
|
||||
buttonStackView.axis = .horizontal
|
||||
buttonStackView.spacing = Values.mediumSpacing
|
||||
buttonStackView.distribution = .fillEqually
|
||||
// Main stack view
|
||||
let mainStackView = UIStackView(arrangedSubviews: [ titleLabel, messageLabel, buttonStackView ])
|
||||
mainStackView.axis = .vertical
|
||||
mainStackView.spacing = Values.largeSpacing
|
||||
contentView.addSubview(mainStackView)
|
||||
mainStackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
|
||||
mainStackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.trailing, to: .trailing, of: mainStackView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.bottom, to: .bottom, of: mainStackView, withInset: Values.largeSpacing)
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func openURL() {
|
||||
let url = self.url
|
||||
presentingViewController?.dismiss(animated: true, completion: {
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue