From 3308209d9567407e85e32415b1a25fff1f14d106 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Thu, 11 Jun 2020 14:07:04 +0300 Subject: [PATCH 1/2] Highlight & open links --- Stepic.xcodeproj/project.pbxproj | 4 + .../HTMLToAttributedStringConverter.swift | 73 +++++++++++++++++++ .../CourseInfoTabInfoViewController.swift | 16 +++- .../CourseInfoTabInfoTextBlockView.swift | 39 ++++++++-- .../Views/CourseInfoTabInfoView.swift | 18 +++-- 5 files changed, 136 insertions(+), 14 deletions(-) create mode 100644 Stepic/Sources/Helpers/HTMLToAttributedStringConverter.swift diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index fc38d740ca..e0e7d8b529 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -588,6 +588,7 @@ 2CB62BE72019FD8500B5E336 /* step2.html in Resources */ = {isa = PBXBuildFile; fileRef = 2CB62BE22019FD8400B5E336 /* step2.html */; }; 2CB62BEE2019FDB100B5E336 /* arrow_right.svg in Resources */ = {isa = PBXBuildFile; fileRef = 2CB62BEC2019FDB000B5E336 /* arrow_right.svg */; }; 2CB62BEF2019FDB100B5E336 /* arrow_left.svg in Resources */ = {isa = PBXBuildFile; fileRef = 2CB62BED2019FDB100B5E336 /* arrow_left.svg */; }; + 2CB718DC249240FC00AEDF84 /* HTMLToAttributedStringConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB718DB249240FC00AEDF84 /* HTMLToAttributedStringConverter.swift */; }; 2CB9529C229F29F000A6117A /* ViewsNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB9529B229F29F000A6117A /* ViewsNetworkService.swift */; }; 2CB9529E22A0352000A6117A /* AssignmentsPersistenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB9529D22A0351F00A6117A /* AssignmentsPersistenceService.swift */; }; 2CB9E56223D54BE10050A7F3 /* FileLocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB9E56123D54BE10050A7F3 /* FileLocationManager.swift */; }; @@ -1862,6 +1863,7 @@ 2CB62BEA2019FD9300B5E336 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = en; path = OnboardingContent/en.lproj/step3.html; sourceTree = ""; }; 2CB62BEC2019FDB000B5E336 /* arrow_right.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = arrow_right.svg; sourceTree = ""; }; 2CB62BED2019FDB100B5E336 /* arrow_left.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = arrow_left.svg; sourceTree = ""; }; + 2CB718DB249240FC00AEDF84 /* HTMLToAttributedStringConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLToAttributedStringConverter.swift; sourceTree = ""; }; 2CB9529B229F29F000A6117A /* ViewsNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewsNetworkService.swift; sourceTree = ""; }; 2CB9529D22A0351F00A6117A /* AssignmentsPersistenceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssignmentsPersistenceService.swift; sourceTree = ""; }; 2CB9E56123D54BE10050A7F3 /* FileLocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileLocationManager.swift; sourceTree = ""; }; @@ -5705,6 +5707,7 @@ isa = PBXGroup; children = ( 62E981CAF11255059CB09BF1 /* FormatterHelper.swift */, + 2CB718DB249240FC00AEDF84 /* HTMLToAttributedStringConverter.swift */, 2C01BB67233CD92C00C8DCF0 /* Require.swift */, ); path = Helpers; @@ -7861,6 +7864,7 @@ 62E983D50569783CDB719D65 /* ContentLanguageSwitchInteractor.swift in Sources */, 62E98F674E6B10451E7397F9 /* ContentLanguageSwitchPresenter.swift in Sources */, 62E98273401D1360B07BAD0A /* ContentLanguageSwitchProvider.swift in Sources */, + 2CB718DC249240FC00AEDF84 /* HTMLToAttributedStringConverter.swift in Sources */, 62E9889E935597A0ED849B0C /* ContentLanguageSwitchViewController.swift in Sources */, 62E982DE03DC1E9967B8B43B /* ContentLanguageSwitchViewModel.swift in Sources */, 62E98561DFB8CBB54B0CA2F7 /* ContentLanguageSwitchButton.swift in Sources */, diff --git a/Stepic/Sources/Helpers/HTMLToAttributedStringConverter.swift b/Stepic/Sources/Helpers/HTMLToAttributedStringConverter.swift new file mode 100644 index 0000000000..c6913570b6 --- /dev/null +++ b/Stepic/Sources/Helpers/HTMLToAttributedStringConverter.swift @@ -0,0 +1,73 @@ +import Atributika +import Foundation + +protocol HTMLToAttributedStringConverterProtocol { + func convertToAttributedText(htmlString: String) -> AttributedTextProtocol + func convertToAttributedString(htmlString: String) -> NSAttributedString +} + +final class HTMLToAttributedStringConverter: HTMLToAttributedStringConverterProtocol { + static let defaultTagTransformers: [TagTransformer] = [ + TagTransformer.brTransformer, + TagTransformer(tagName: "p", tagType: .start, replaceValue: "\n"), + TagTransformer(tagName: "p", tagType: .end, replaceValue: "\n") + ] + + static let defaultLinkStyle = Style("a") + .foregroundColor(.stepikLightBlue, .normal) + .foregroundColor(.stepikPrimaryText, .highlighted) + + static func defaultTagStyles(fontSize: CGFloat) -> [Style] { + [ + Style("b").font(.boldSystemFont(ofSize: fontSize)), + Style("strong").font(.boldSystemFont(ofSize: fontSize)), + Style("i").font(.italicSystemFont(ofSize: fontSize)), + Style("em").font(.italicSystemFont(ofSize: fontSize)), + Style("strike").strikethroughStyle(NSUnderlineStyle.single), + Style("p").font(.systemFont(ofSize: fontSize)), + Self.defaultLinkStyle + ] + } + + private let font: UIFont + private let tagStyles: [Style] + private let tagTransformers: [TagTransformer] + + private var allStyle: Style { Style.font(self.font) } + + init( + font: UIFont, + tagStyles: [Style] = [], + tagTransformers: [TagTransformer] = HTMLToAttributedStringConverter.defaultTagTransformers + ) { + let defaultStyles = Self.defaultTagStyles(fontSize: font.pointSize) + let finalStyles = tagStyles.isEmpty + ? defaultStyles + : ( + tagStyles + defaultStyles.filter { defaultStyle in + !tagStyles.contains { $0.name == defaultStyle.name } + } + ) + + self.font = font + self.tagStyles = finalStyles + self.tagTransformers = tagTransformers + } + + func convertToAttributedText(htmlString: String) -> AttributedTextProtocol { + htmlString + .style(tags: self.tagStyles, transformers: self.tagTransformers) + .styleLinks(Self.defaultLinkStyle) + .styleAll(self.allStyle) + } + + func convertToAttributedString(htmlString: String) -> NSAttributedString { + guard let attributedText = self.convertToAttributedText(htmlString: htmlString) as? AttributedText else { + return NSAttributedString() + } + + return attributedText + .attributedString + .trimmingCharacters(in: .whitespacesAndNewlines) + } +} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabInfo/CourseInfoTabInfoViewController.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabInfo/CourseInfoTabInfoViewController.swift index 5931fafc9e..3a9a448058 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabInfo/CourseInfoTabInfoViewController.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabInfo/CourseInfoTabInfoViewController.swift @@ -127,11 +127,21 @@ extension CourseInfoTabInfoViewController: CourseInfoTabInfoIntroVideoBlockViewD // MARK: - CourseInfoTabInfoViewController: CourseInfoTabInfoViewDelegate - extension CourseInfoTabInfoViewController: CourseInfoTabInfoViewDelegate { - func courseInfoTabInfoViewDidClickInstructor( - _ courseInfoTabInfoView: CourseInfoTabInfoView, - instructor: CourseInfoTabInfoInstructorViewModel + func courseInfoTabInfoView( + _ view: CourseInfoTabInfoView, + didClickInstructor instructor: CourseInfoTabInfoInstructorViewModel ) { let assembly = ProfileAssembly(userID: instructor.id) self.navigationController?.pushViewController(assembly.makeModule(), animated: true) } + + func courseInfoTabInfoView(_ view: CourseInfoTabInfoView, didOpenURL url: URL) { + WebControllerManager.shared.presentWebControllerWithURL( + url, + inController: self, + withKey: .externalLink, + allowsSafari: true, + backButtonStyle: .done + ) + } } diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabInfo/Views/Blocks/CourseInfoTabInfoTextBlockView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabInfo/Views/Blocks/CourseInfoTabInfoTextBlockView.swift index e768ab5016..7562f247c3 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabInfo/Views/Blocks/CourseInfoTabInfoTextBlockView.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabInfo/Views/Blocks/CourseInfoTabInfoTextBlockView.swift @@ -1,3 +1,4 @@ +import Atributika import SnapKit import UIKit @@ -18,14 +19,34 @@ final class CourseInfoTabInfoTextBlockView: UIView { private lazy var headerView = CourseInfoTabInfoHeaderBlockView() - private lazy var messageLabel: UILabel = { - let label = UILabel() + private lazy var messageLabel: AttributedLabel = { + let label = AttributedLabel() label.numberOfLines = 0 label.font = self.appearance.messageLabelFont label.textColor = self.appearance.messageLabelTextColor + label.onClick = { [weak self] label, detection in + guard let strongSelf = self else { + return + } + + switch detection.type { + case .link(let url): + strongSelf.onOpenURL?(url) + case .tag(let tag): + if tag.name == "a", + let href = tag.attributes["href"], + let url = URL(string: href) { + strongSelf.onOpenURL?(url) + } + default: + break + } + } return label }() + private let htmlToAttributedStringConverter: HTMLToAttributedStringConverterProtocol + var icon: UIImage? { didSet { self.headerView.icon = self.icon @@ -40,15 +61,21 @@ final class CourseInfoTabInfoTextBlockView: UIView { var message: String? { didSet { - self.messageLabel.setTextWithHTMLString( - self.message ?? "", - lineSpacing: self.appearance.messageLabelLineSpacing - ) + if let trimmedMessage = self.message?.trimmingCharacters(in: .whitespacesAndNewlines) { + self.messageLabel.attributedText = self.htmlToAttributedStringConverter.convertToAttributedText( + htmlString: trimmedMessage + ) as? AttributedText + } else { + self.messageLabel.attributedText = nil + } } } + var onOpenURL: ((URL) -> Void)? + init(frame: CGRect = .zero, appearance: Appearance = Appearance()) { self.appearance = appearance + self.htmlToAttributedStringConverter = HTMLToAttributedStringConverter(font: appearance.messageLabelFont) super.init(frame: frame) self.setupView() diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabInfo/Views/CourseInfoTabInfoView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabInfo/Views/CourseInfoTabInfoView.swift index 092f2b3ee7..3afb484823 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabInfo/Views/CourseInfoTabInfoView.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoTabInfo/Views/CourseInfoTabInfoView.swift @@ -3,10 +3,11 @@ import SnapKit import UIKit protocol CourseInfoTabInfoViewDelegate: AnyObject { - func courseInfoTabInfoViewDidClickInstructor( - _ courseInfoTabInfoView: CourseInfoTabInfoView, - instructor: CourseInfoTabInfoInstructorViewModel + func courseInfoTabInfoView( + _ view: CourseInfoTabInfoView, + didClickInstructor instructor: CourseInfoTabInfoInstructorViewModel ) + func courseInfoTabInfoView(_ view: CourseInfoTabInfoView, didOpenURL url: URL) } extension CourseInfoTabInfoView { @@ -149,6 +150,13 @@ final class CourseInfoTabInfoView: UIView { textBlockView.icon = block.icon textBlockView.title = block.title textBlockView.message = message + textBlockView.onOpenURL = { [weak self] url in + guard let strongSelf = self else { + return + } + + strongSelf.delegate?.courseInfoTabInfoView(strongSelf, didOpenURL: url) + } self.scrollableStackView.addArrangedView(textBlockView) } @@ -160,12 +168,12 @@ final class CourseInfoTabInfoView: UIView { let instructorsView = CourseInfoTabInfoInstructorsBlockView() instructorsView.configure(instructors: instructors) - instructorsView.onInstructorClick = { [weak self] in + instructorsView.onInstructorClick = { [weak self] instructor in guard let strongSelf = self else { return } - strongSelf.delegate?.courseInfoTabInfoViewDidClickInstructor(strongSelf, instructor: $0) + strongSelf.delegate?.courseInfoTabInfoView(strongSelf, didClickInstructor: instructor) } self.scrollableStackView.addArrangedView(instructorsView) From cbb0f489a85151da691236752bcca3a8b8fb1ca8 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Thu, 11 Jun 2020 14:32:55 +0300 Subject: [PATCH 2/2] Add mobile_internal_deeplink query parameter --- Stepic/Legacy/Model/Managers/WebControllerManager.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Stepic/Legacy/Model/Managers/WebControllerManager.swift b/Stepic/Legacy/Model/Managers/WebControllerManager.swift index 1d2d39030a..277c38cc57 100644 --- a/Stepic/Legacy/Model/Managers/WebControllerManager.swift +++ b/Stepic/Legacy/Model/Managers/WebControllerManager.swift @@ -99,7 +99,10 @@ final class WebControllerManager: NSObject { return } - guard let url = url.appendingQueryParameters(["from_mobile_app": "true"]) else { + let queryParameters = key == .externalLink + ? ["from_mobile_app": "true", "mobile_internal_deeplink": "true"] + : ["from_mobile_app": "true"] + guard let url = url.appendingQueryParameters(queryParameters) else { return }