diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 8402fc82..59fd6bd7 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -12,7 +12,6 @@ 0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */; }; 0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033922B31269000D31B6 /* AdvancedPrefsView.swift */; }; 0427037C22B316B9000D31B6 /* SilentActionPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427037B22B316B9000D31B6 /* SilentActionPrefs.swift */; }; - 04496BD721625361001F1B23 /* ContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04496BD621625361001F1B23 /* ContentLabel.swift */; }; 0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450531E22B0097E00100BA2 /* Timline+UI.swift */; }; 0454DDAF22B462EF00B8BB8E /* GalleryExpandAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0454DDAE22B462EF00B8BB8E /* GalleryExpandAnimationController.swift */; }; 0454DDB122B467AA00B8BB8E /* GalleryShrinkAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0454DDB022B467AA00B8BB8E /* GalleryShrinkAnimationController.swift */; }; @@ -74,6 +73,9 @@ D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */; }; D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */; }; D620483223D2A6A3008A63EF /* CompositionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483123D2A6A3008A63EF /* CompositionState.swift */; }; + D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; }; + D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; }; + D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; }; D626493323BD751600612E6E /* ShowCameraCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */; }; D626493523BD94CE00612E6E /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493423BD94CE00612E6E /* CompositionAttachment.swift */; }; D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */; }; @@ -199,7 +201,6 @@ D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; }; D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; }; - D6C693F92162E4DB007D6A6D /* StatusContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693F82162E4DB007D6A6D /* StatusContentLabel.swift */; }; D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; }; D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; }; D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */; }; @@ -210,7 +211,6 @@ D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */; }; D6D4DDE5212518A200E1C4BB /* TuskerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDE4212518A200E1C4BB /* TuskerTests.swift */; }; D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; }; - D6D58DF922074B74009C8DD9 /* LinkLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D58DF822074B74009C8DD9 /* LinkLabel.swift */; }; D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; }; D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */; }; D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; }; @@ -282,7 +282,6 @@ 0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BehaviorPrefsView.swift; sourceTree = ""; }; 0427033922B31269000D31B6 /* AdvancedPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedPrefsView.swift; sourceTree = ""; }; 0427037B22B316B9000D31B6 /* SilentActionPrefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SilentActionPrefs.swift; sourceTree = ""; }; - 04496BD621625361001F1B23 /* ContentLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLabel.swift; sourceTree = ""; }; 0450531E22B0097E00100BA2 /* Timline+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timline+UI.swift"; sourceTree = ""; }; 0454DDAE22B462EF00B8BB8E /* GalleryExpandAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryExpandAnimationController.swift; sourceTree = ""; }; 0454DDB022B467AA00B8BB8E /* GalleryShrinkAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryShrinkAnimationController.swift; sourceTree = ""; }; @@ -346,6 +345,9 @@ D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTableViewCell.swift; sourceTree = ""; }; D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InstanceTableViewCell.xib; sourceTree = ""; }; D620483123D2A6A3008A63EF /* CompositionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionState.swift; sourceTree = ""; }; + D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = ""; }; + D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = ""; }; + D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = ""; }; D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShowCameraCollectionViewCell.xib; sourceTree = ""; }; D626493423BD94CE00612E6E /* CompositionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachment.swift; sourceTree = ""; }; D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllPhotosTableViewCell.swift; sourceTree = ""; }; @@ -469,7 +471,6 @@ D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSoup.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = ""; }; D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = ""; }; - D6C693F82162E4DB007D6A6D /* StatusContentLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentLabel.swift; sourceTree = ""; }; D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = ""; }; D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = ""; }; D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentsContainerView.swift; sourceTree = ""; }; @@ -486,7 +487,6 @@ D6D4DDEB212518A200E1C4BB /* TuskerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TuskerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerUITests.swift; sourceTree = ""; }; D6D4DDF1212518A200E1C4BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - D6D58DF822074B74009C8DD9 /* LinkLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkLabel.swift; sourceTree = ""; }; D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = ""; }; D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = ""; }; D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = ""; }; @@ -856,6 +856,7 @@ D66362702136338600C9CBA2 /* ComposeViewController.swift */, D626493423BD94CE00612E6E /* CompositionAttachment.swift */, D6285B5221EA708700FE4B39 /* StatusFormat.swift */, + D620483123D2A6A3008A63EF /* CompositionState.swift */, ); path = Compose; sourceTree = ""; @@ -1099,9 +1100,9 @@ D6BED1722126661300F02DA0 /* Views */ = { isa = PBXGroup; children = ( - D6D58DF822074B74009C8DD9 /* LinkLabel.swift */, - 04496BD621625361001F1B23 /* ContentLabel.swift */, - D6C693F82162E4DB007D6A6D /* StatusContentLabel.swift */, + D620483323D3801D008A63EF /* LinkTextView.swift */, + D620483523D38075008A63EF /* ContentTextView.swift */, + D620483723D38190008A63EF /* StatusContentTextView.swift */, D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */, 04ED00B021481ED800567C53 /* SteppedProgressView.swift */, D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */, @@ -1619,9 +1620,7 @@ 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */, D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */, D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */, - D6C693F92162E4DB007D6A6D /* StatusContentLabel.swift in Sources */, D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */, - D6D58DF922074B74009C8DD9 /* LinkLabel.swift in Sources */, 0454DDAF22B462EF00B8BB8E /* GalleryExpandAnimationController.swift in Sources */, D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */, D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */, @@ -1631,6 +1630,7 @@ D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */, D6945C3623AC6C09005C403C /* SavedInstancesManager.swift in Sources */, D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */, + D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */, D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */, 0411610022B442870030A9B7 /* AttachmentViewController.swift in Sources */, D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */, @@ -1640,6 +1640,7 @@ D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */, D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */, D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */, + D620483623D38075008A63EF /* ContentTextView.swift in Sources */, D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */, D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */, D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */, @@ -1693,9 +1694,9 @@ D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */, D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */, D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */, + D620483423D3801D008A63EF /* LinkTextView.swift in Sources */, D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */, D627943523A5525100D38C68 /* StatusActivity.swift in Sources */, - 04496BD721625361001F1B23 /* ContentLabel.swift in Sources */, D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */, D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */, D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */, @@ -1715,6 +1716,7 @@ D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */, D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */, 04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */, + D620483223D2A6A3008A63EF /* CompositionState.swift in Sources */, D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */, D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */, 04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */, diff --git a/Tusker/TuskerNavigationDelegate.swift b/Tusker/TuskerNavigationDelegate.swift index 9f342077..f22b126d 100644 --- a/Tusker/TuskerNavigationDelegate.swift +++ b/Tusker/TuskerNavigationDelegate.swift @@ -58,7 +58,11 @@ protocol TuskerNavigationDelegate { extension TuskerNavigationDelegate where Self: UIViewController { func show(_ vc: UIViewController) { - show(vc, sender: self) + if vc is LargeImageViewController || vc is GalleryViewController || vc is SFSafariViewController { + present(vc, animated: true) + } else { + show(vc, sender: self) + } } func selected(account accountID: String) { diff --git a/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.swift b/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.swift index ccbcca3d..1242ae4b 100644 --- a/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.swift +++ b/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.swift @@ -14,7 +14,7 @@ class ComposeStatusReplyView: UIView { @IBOutlet weak var avatarImageView: UIImageView! @IBOutlet weak var displayNameLabel: UILabel! @IBOutlet weak var usernameLabel: UILabel! - @IBOutlet weak var contentLabel: StatusContentLabel! + @IBOutlet weak var statusContentTextView: StatusContentTextView! static func create() -> ComposeStatusReplyView { return UINib(nibName: "ComposeStatusReplyView", bundle: nil).instantiate(withOwner: nil, options: nil).first as! ComposeStatusReplyView @@ -34,7 +34,7 @@ class ComposeStatusReplyView: UIView { func updateUI(for status: Status) { displayNameLabel.text = status.account.realDisplayName usernameLabel.text = "@\(status.account.acct)" - contentLabel.statusID = status.id + statusContentTextView.statusID = status.id ImageCache.avatars.get(status.account.avatar) { (data) in guard let data = data else { return } diff --git a/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.xib b/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.xib index 43fe156c..dbebbf91 100644 --- a/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.xib +++ b/Tusker/Views/Compose Status Reply/ComposeStatusReplyView.xib @@ -1,8 +1,8 @@ - + - + @@ -39,23 +39,24 @@ - + + + - - - + - + + @@ -72,8 +73,8 @@ - + diff --git a/Tusker/Views/ContentLabel.swift b/Tusker/Views/ContentLabel.swift deleted file mode 100644 index 0a965d06..00000000 --- a/Tusker/Views/ContentLabel.swift +++ /dev/null @@ -1,229 +0,0 @@ -// -// ContentLabel.swift -// Tusker -// -// Created by Shadowfacts on 10/1/18. -// Copyright © 2018 Shadowfacts. All rights reserved. -// - -import UIKit -import SafariServices -import Pachyderm -import SwiftSoup - -class ContentLabel: LinkLabel { - - private static let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: []) - - var navigationDelegate: TuskerNavigationDelegate? - - // MARK: - Emojis - func setEmojis(_ emojis: [Emoji]) { - guard !emojis.isEmpty else { return } - - let group = DispatchGroup() - - let mutAttrString = NSMutableAttributedString(attributedString: self.attributedText!) - let string = mutAttrString.string - let matches = ContentLabel.emojiRegex.matches(in: string, options: [], range: NSRange(location: 0, length: mutAttrString.length)) - for match in matches.reversed() { - let shortcode = (string as NSString).substring(with: match.range(at: 1)) - guard let emoji = emojis.first(where: { $0.shortcode == shortcode }) else { - continue - } - - group.enter() - ImageCache.emojis.get(emoji.url) { (data) in - guard let data = data, let image = UIImage(data: data) else { - group.leave() - return - } - DispatchQueue.main.async { - let attachment = self.createEmojiTextAttachment(image: image, index: match.range.location) - mutAttrString.replaceCharacters(in: match.range, with: NSAttributedString(attachment: attachment)) - - group.leave() - } - } - } - - group.notify(queue: .main) { - self.attributedText = mutAttrString - self.setNeedsLayout() - self.setNeedsDisplay() - } - } - - // Based on https://github.com/ReticentJohn/Amaroq/blob/7c5b7088eb9fd1611dcb0f47d43bf8df093e142c/DireFloof/InlineImageHelpers.m - func createEmojiTextAttachment(image: UIImage, index: Int) -> NSTextAttachment { - let font = self.font! - - let adjustedCapHeight = font.capHeight - 1 - var imageSizeMatchingFontSize = CGSize(width: image.size.width * (adjustedCapHeight / image.size.height), height: adjustedCapHeight) - - let defaultScale: CGFloat = 1.4 - imageSizeMatchingFontSize = CGSize(width: imageSizeMatchingFontSize.width * defaultScale, height: imageSizeMatchingFontSize.height * defaultScale) - let textColor = self.textColor! - - UIGraphicsBeginImageContextWithOptions(imageSizeMatchingFontSize, false, 0.0) - textColor.set() - image.draw(in: CGRect(origin: .zero, size: imageSizeMatchingFontSize)) - let attachmentImage = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - - let attachment = NSTextAttachment() - attachment.image = attachmentImage - return attachment - } - - // MARK: - HTML Parsing - func setTextFromHtml(_ html: String) { - let doc = try! SwiftSoup.parse(html) - let body = doc.body()! - - let (attributedText, links) = attributedTextForHTMLNode(body) - let mutAttrString = NSMutableAttributedString(attributedString: attributedText) - - // only trailing whitespace can be trimmed here - // when posting an attachment without any text, pleromafe includes U+200B ZERO WIDTH SPACE at the beginning - // this would get trimmed and cause range out of bounds crashes - mutAttrString.trimTrailingCharactersInSet(.whitespacesAndNewlines) - - self.links = [] - let linkAttributes: [NSAttributedString.Key: Any] = [ - .foregroundColor: UIColor.systemBlue, - ] - for (range, url) in links { - mutAttrString.addAttributes(linkAttributes, range: range) - self.links.append(Link(range: range, url: url)) - } - - self.attributedText = mutAttrString - } - - private func attributedTextForHTMLNode(_ node: Node) -> (NSAttributedString, [NSRange: URL]) { - switch node { - case let node as TextNode: - return (NSAttributedString(string: node.text()), [:]) - case let node as Element: - var links = [NSRange: URL]() - let attributed = NSMutableAttributedString() - for child in node.getChildNodes() { - let (text, childLinks) = attributedTextForHTMLNode(child) - for (range, url) in childLinks { - let newRange = NSRange(location: range.location + attributed.length, length: range.length) - links[newRange] = url - } - attributed.append(text) - } - - switch node.tagName() { - case "br": - attributed.append(NSAttributedString(string: "\n")) - case "a": - if let link = try? node.attr("href"), - let url = URL(string: link) { - links[attributed.fullRange] = url - } - case "p": - attributed.append(NSAttributedString(string: "\n\n")) - case "em", "i": - let currentFont: UIFont = attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? self.font - attributed.addAttribute(.font, value: currentFont.addingTraits(.traitItalic)!, range: attributed.fullRange) - case "strong", "b": - let currentFont: UIFont = attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? self.font - attributed.addAttribute(.font, value: currentFont.addingTraits(.traitBold)!, range: attributed.fullRange) - case "del": - attributed.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: attributed.fullRange) - case "code": - attributed.addAttribute(.font, value: UIFont(name: "Menlo", size: font!.pointSize)!, range: attributed.fullRange) - case "pre": - attributed.addAttribute(.font, value: UIFont(name: "Menlo", size: font!.pointSize)!, range: attributed.fullRange) - attributed.append(NSAttributedString(string: "\n\n")) - case "ol", "ul": - attributed.trimLeadingCharactersInSet(.whitespacesAndNewlines) - attributed.append(NSAttributedString(string: "\n")) - break - case "li": - let parentEl = node.parent()! - let parentTag = parentEl.tagName() - let bullet: NSAttributedString - if parentTag == "ol" { - let index = (try? node.elementSiblingIndex()) ?? 0 - // we use the monospace digit font so that the periods of all the list items line up - bullet = NSAttributedString(string: "\(index + 1).\t", attributes: [.font: UIFont.monospacedDigitSystemFont(ofSize: font!.pointSize, weight: .regular)]) - } else if parentTag == "ul" { - bullet = NSAttributedString(string: "\u{2022}\t") - } else { - bullet = NSAttributedString(string: "") - } - // inserting bullets at the beginning of the string shifts all the links down, so we adjust the link ranges - for (range, url) in links { - let newRange = NSRange(location: range.location + bullet.length - 1, length: range.length) - links[newRange] = url - links.removeValue(forKey: range) - } - attributed.insert(bullet, at: 0) - attributed.append(NSAttributedString(string: "\n")) - default: - break - } - - return (attributed, links) - default: - fatalError("Unexpected node type: \(type(of: node))") - } - } - - func getViewController(forLink url: URL, inRange range: NSRange) -> UIViewController { - let text = (self.text! as NSString).substring(with: range) - - if let mention = getMention(for: url, text: text) { - return ProfileTableViewController(accountID: mention.id) - } else if let tag = getHashtag(for: url, text: text) { - return HashtagTimelineViewController(for: tag) - } else { - return SFSafariViewController(url: url) - } - } - - func getViewController(forLinkAt point: CGPoint) -> UIViewController? { - guard let link = getLink(atPoint: point) else { - return nil - } - return getViewController(forLink: link.url, inRange: link.range) - } - - // MARK: - Interaction - - override func linkTapped(_ link: LinkLabel.Link) { - let text = (self.text! as NSString).substring(with: link.range) - - if let mention = getMention(for: link.url, text: text) { - navigationDelegate?.selected(mention: mention) - } else if let tag = getHashtag(for: link.url, text: text) { - navigationDelegate?.selected(tag: tag) - } else { - navigationDelegate?.selected(url: link.url) - } - } - - override func linkLongPressed(_ link: LinkLabel.Link) { - navigationDelegate?.showMoreOptions(forURL: link.url, sourceView: self) - } - - // MARK: - Navigation - func getMention(for url: URL, text: String) -> Mention? { - return nil - } - - func getHashtag(for url: URL, text: String) -> Hashtag? { - if text.starts(with: "#") { - let tag = String(text.dropFirst()) - return Hashtag(name: tag, url: url) - } else { - return nil - } - } - -} diff --git a/Tusker/Views/ContentTextView.swift b/Tusker/Views/ContentTextView.swift new file mode 100644 index 00000000..18a5cf88 --- /dev/null +++ b/Tusker/Views/ContentTextView.swift @@ -0,0 +1,283 @@ +// +// ContentTextView.swift +// Tusker +// +// Created by Shadowfacts on 1/18/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit +import SwiftSoup +import Pachyderm +import SafariServices + +private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: []) + +class ContentTextView: LinkTextView { + + // todo: should be weak + var navigationDelegate: TuskerNavigationDelegate? + + var defaultFont: UIFont = .systemFont(ofSize: 17) + + override func awakeFromNib() { + super.awakeFromNib() + + delegate = self + + addInteraction(UIContextMenuInteraction(delegate: self)) + + textDragInteraction?.isEnabled = false + } + + // MARK: - Emojis + func setEmojis(_ emojis: [Emoji]) { + guard !emojis.isEmpty else { return } + + let emojiImages = CachedDictionary(name: "ContentTextView Emoji Images") + + let group = DispatchGroup() + + for emoji in emojis { + group.enter() + ImageCache.emojis.get(emoji.url) { (data) in + defer { group.leave() } + guard let data = data, let image = UIImage(data: data) else { + return + } + emojiImages[emoji.shortcode] = image + } + } + + group.notify(queue: .main) { + let mutAttrString = NSMutableAttributedString(attributedString: self.attributedText!) + let string = mutAttrString.string + let matches = emojiRegex.matches(in: string, options: [], range: mutAttrString.fullRange) + // replaces the emojis started from the end of the string as to not alter the indexes of the other emojis + for match in matches.reversed() { + let shortcode = (string as NSString).substring(with: match.range(at: 1)) + guard let emojiImage = emojiImages[shortcode] else { + continue + } + + let attachment = self.createEmojiTextAttachment(image: emojiImage, index: match.range.location) + let attachmentStr = NSAttributedString(attachment: attachment) + mutAttrString.replaceCharacters(in: match.range, with: attachmentStr) + } + + self.attributedText = mutAttrString + self.setNeedsLayout() + self.setNeedsDisplay() + } + } + + // Based on https://github.com/ReticentJohn/Amaroq/blob/7c5b7088eb9fd1611dcb0f47d43bf8df093e142c/DireFloof/InlineImageHelpers.m + private func createEmojiTextAttachment(image: UIImage, index: Int) -> NSTextAttachment { + let font = self.font! + + let adjustedCapHeight = font.capHeight - 1 + var imageSizeMatchingFontSize = CGSize(width: image.size.width * (adjustedCapHeight / image.size.height), height: adjustedCapHeight) + + let defaultScale: CGFloat = 1.4 + imageSizeMatchingFontSize = CGSize(width: imageSizeMatchingFontSize.width * defaultScale, height: imageSizeMatchingFontSize.height * defaultScale) + let textColor = self.textColor ?? UIColor.label + + UIGraphicsBeginImageContextWithOptions(imageSizeMatchingFontSize, false, 0.0) + textColor.set() + image.draw(in: CGRect(origin: .zero, size: imageSizeMatchingFontSize)) + let attachmentImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + let attachment = NSTextAttachment() + attachment.image = attachmentImage + return attachment + } + + // MARK: - HTML Parsing + func setTextFromHtml(_ html: String) { + let doc = try! SwiftSoup.parse(html) + let body = doc.body()! + + let attributedText = attributedTextForHTMLNode(body) + let mutAttrString = NSMutableAttributedString(attributedString: attributedText) + mutAttrString.trimTrailingCharactersInSet(.whitespacesAndNewlines) + + self.attributedText = mutAttrString + } + + func attributedTextForHTMLNode(_ node: Node) -> NSAttributedString { + switch node { + case let node as TextNode: + return NSAttributedString(string: node.text(), attributes: [.font: defaultFont]) + case let node as Element: + let attributed = NSMutableAttributedString(string: "", attributes: [.font: defaultFont]) + for child in node.getChildNodes() { + attributed.append(attributedTextForHTMLNode(child)) + } + + switch node.tagName() { + case "br": + attributed.append(NSAttributedString(string: "\n")) + case "a": + if let link = try? node.attr("href"), + let url = URL(string: link) { + attributed.addAttribute(.link, value: url, range: attributed.fullRange) + } + case "p": + attributed.append(NSAttributedString(string: "\n\n")) + case "em", "i": + let currentFont: UIFont = attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? self.font! + attributed.addAttribute(.font, value: currentFont.addingTraits(.traitItalic)!, range: attributed.fullRange) + case "strong", "b": + let currentFont: UIFont = attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? self.font! + attributed.addAttribute(.font, value: currentFont.addingTraits(.traitBold)!, range: attributed.fullRange) + case "del": + attributed.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: attributed.fullRange) + case "code": + attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: self.font!.pointSize, weight: .regular), range: attributed.fullRange) + case "pre": + attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: self.font!.pointSize, weight: .regular), range: attributed.fullRange) + attributed.append(NSAttributedString(string: "\n\n")) + case "ol", "ul": + attributed.trimLeadingCharactersInSet(.whitespacesAndNewlines) + attributed.append(NSAttributedString(string: "\n\n")) + case "li": + let parentEl = node.parent()! + let parentTag = parentEl.tagName() + let bullet: NSAttributedString + if parentTag == "ol" { + let index = (try? node.elementSiblingIndex()) ?? 0 + // we use the monospace digit font so that the periods of all the list items line up + bullet = NSAttributedString(string: "\(index + 1).\t", attributes: [.font: UIFont.monospacedDigitSystemFont(ofSize: self.font!.pointSize, weight: .regular)]) + } else if parentTag == "ul" { + bullet = NSAttributedString(string: "\u{2022}\t") + } else { + bullet = NSAttributedString() + } + attributed.insert(bullet, at: 0) + attributed.append(NSAttributedString(string: "\n")) + default: + break + } + + return attributed + default: + fatalError("Unexpected node type \(type(of: node))") + } + } + + // MARK: - Interaction + + // only accept touches that are over a link + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if getLinkAtPoint(point) != nil { + return self + } else { + return nil + } + } + + func getLinkAtPoint(_ point: CGPoint) -> (URL, NSRange)? { + let locationInTextContainer = CGPoint(x: point.x - textContainerInset.left, y: point.y - textContainerInset.top) + var partialFraction: CGFloat = 0 + let characterIndex = layoutManager.characterIndex(for: locationInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: &partialFraction) + if characterIndex < textStorage.length { + var range = NSRange() + if let link = textStorage.attribute(.link, at: characterIndex, longestEffectiveRange: &range, in: textStorage.fullRange) as? URL { + return (link, range) + } + } + return nil + } + + // MARK: - Navigation + + func getViewController(forLink url: URL, inRange range: NSRange) -> UIViewController { + let text = (self.text as NSString).substring(with: range) + + if let mention = getMention(for: url, text: text) { + return ProfileTableViewController(accountID: mention.id) + } else if let tag = getHashtag(for: url, text: text) { + return HashtagTimelineViewController(for: tag) + } else { + return SFSafariViewController(url: url) + } + } + + open func getMention(for url: URL, text: String) -> Mention? { + return nil + } + + open func getHashtag(for url: URL, text: String) -> Hashtag? { + if text.starts(with: "#") { + let tag = String(text.dropFirst()) + return Hashtag(name: tag, url: url) + } else { + return nil + } + } + +} + +extension ContentTextView: UITextViewDelegate { + func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { + let text = (self.text as NSString).substring(with: characterRange) + switch interaction { + case .invokeDefaultAction: + if let mention = getMention(for: URL, text: text) { + navigationDelegate?.selected(mention: mention) + } else if let tag = getHashtag(for: URL, text: text) { + navigationDelegate?.selected(tag: tag) + } else { + navigationDelegate?.selected(url: URL) + } + case .presentActions: + print("present actions") + case .preview: + print("preview") + @unknown default: + break + } + + return false + } +} + +extension ContentTextView: MenuPreviewProvider { + func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { + fatalError("unimplemented") + } +} + +extension ContentTextView: UIContextMenuInteractionDelegate { + func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { + if let (link, range) = getLinkAtPoint(location) { + let preview: UIContextMenuContentPreviewProvider = { + self.getViewController(forLink: link, inRange: range) + } + let actions: UIContextMenuActionProvider = { (_) in + let text = (self.text as NSString).substring(with: range) + let actions: [UIAction] + if let mention = self.getMention(for: link, text: text) { + actions = self.actionsForProfile(accountID: mention.id, sourceView: self) + } else if let tag = self.getHashtag(for: link, text: text) { + actions = self.actionsForHashtag(tag, sourceView: self) + } else { + actions = self.actionsForURL(link, sourceView: self) + } + return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions) + } + return UIContextMenuConfiguration(identifier: nil, previewProvider: preview, actionProvider: actions) + } else { + return nil + } + } + func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { + if let viewController = animator.previewViewController { + animator.preferredCommitStyle = .pop + animator.addCompletion { + self.navigationDelegate?.show(viewController) + } + } + } +} diff --git a/Tusker/Views/Instance Cell/InstanceTableViewCell.swift b/Tusker/Views/Instance Cell/InstanceTableViewCell.swift index 624caf15..920e068f 100644 --- a/Tusker/Views/Instance Cell/InstanceTableViewCell.swift +++ b/Tusker/Views/Instance Cell/InstanceTableViewCell.swift @@ -14,7 +14,7 @@ class InstanceTableViewCell: UITableViewCell { @IBOutlet weak var thumbnailImageView: UIImageView! @IBOutlet weak var domainLabel: UILabel! @IBOutlet weak var adultLabel: UILabel! - @IBOutlet weak var descriptionLabel: ContentLabel! + @IBOutlet weak var descriptionTextView: ContentTextView! var instance: Instance? var selectorInstance: InstanceSelector.Instance? @@ -37,7 +37,7 @@ class InstanceTableViewCell: UITableViewCell { domainLabel.text = instance.domain adultLabel.isHidden = instance.category != .adult - descriptionLabel.setTextFromHtml(instance.description) + descriptionTextView.setTextFromHtml(instance.description) updateThumbnail(url: instance.proxiedThumbnailURL) } @@ -47,7 +47,7 @@ class InstanceTableViewCell: UITableViewCell { domainLabel.text = URLComponents(string: instance.uri)?.host ?? instance.uri adultLabel.isHidden = true - descriptionLabel.setTextFromHtml(instance.description) + descriptionTextView.setTextFromHtml(instance.description) if let thumbnail = instance.thumbnail { updateThumbnail(url: thumbnail) diff --git a/Tusker/Views/Instance Cell/InstanceTableViewCell.xib b/Tusker/Views/Instance Cell/InstanceTableViewCell.xib index ed7702f0..e8a0881c 100644 --- a/Tusker/Views/Instance Cell/InstanceTableViewCell.xib +++ b/Tusker/Views/Instance Cell/InstanceTableViewCell.xib @@ -1,8 +1,8 @@ - + - + @@ -27,7 +27,7 @@ - + @@ -51,15 +51,12 @@ - + + @@ -75,7 +72,7 @@ - + diff --git a/Tusker/Views/LinkLabel.swift b/Tusker/Views/LinkLabel.swift deleted file mode 100644 index aff52783..00000000 --- a/Tusker/Views/LinkLabel.swift +++ /dev/null @@ -1,185 +0,0 @@ -// -// LinkLabel.swift -// Tusker -// -// Created by Shadowfacts on 2/3/19. -// Copyright © 2019 Shadowfacts. All rights reserved. -// - -import UIKit - -class LinkLabel: UILabel { - - typealias Link = (range: NSRange, url: URL) - - let layoutManager = NSLayoutManager() - let textContainer = NSTextContainer(size: .zero) - var textStorage: NSTextStorage! - - var links = [Link]() - - var selectedLinkAttributes: [NSAttributedString.Key: Any] = [ -// .backgroundColor: UIColor(hue: 0, saturation: 0, brightness: 0.9, alpha: 1) - .backgroundColor: UIColor.secondarySystemBackground - ] - - var selectedLinkRange: NSRange? { - didSet { - if let oldValue = oldValue { - removeSelectedLinkAttributes(oldValue) - } - if let newValue = selectedLinkRange { - addSelectedLinkAttributes(newValue) - } - } - } - - override var attributedText: NSAttributedString? { - didSet { - guard let attributedText = attributedText else { return } - - textStorage?.removeLayoutManager(layoutManager) - - textStorage = NSTextStorage(attributedString: attributedText) - textStorage.addLayoutManager(layoutManager) - } - } - - override var text: String? { - willSet { - fatalError("LinkLabel does not support non-attributed text") - } - } - - override func awakeFromNib() { - super.awakeFromNib() - - isUserInteractionEnabled = true - let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(labelTapped(_:))) - tapRecognizer.delegate = self - addGestureRecognizer(tapRecognizer) - let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(labelLongPressed(_:))) - longPressRecognizer.delegate = self - addGestureRecognizer(longPressRecognizer) - - layoutManager.addTextContainer(textContainer) - textContainer.lineFragmentPadding = 0.0 - textContainer.lineBreakMode = lineBreakMode - textContainer.maximumNumberOfLines = numberOfLines - } - - override func layoutSubviews() { - super.layoutSubviews() - - textContainer.size = bounds.size - } - - func getLink(atPoint point: CGPoint) -> Link? { - let labelSize = bounds.size - let textBoundingBox = layoutManager.usedRect(for: textContainer) - let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x, - y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y) - let locationOfTouchInTextContainer = CGPoint(x: point.x - textContainerOffset.x, - y: point.y - textContainerOffset.y) - // let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) - let indexOfCharacter = layoutManager.glyphIndex(for: locationOfTouchInTextContainer, in: textContainer) - - if let link = links.first(where: { $0.range.contains(indexOfCharacter) }) { - return link - } else { - return nil - } - } - - func addSelectedLinkAttributes(_ range: NSRange) { - let mutAttrString = NSMutableAttributedString(attributedString: attributedText!) - mutAttrString.addAttributes(selectedLinkAttributes, range: range) - self.attributedText = mutAttrString - setNeedsDisplay() - } - - func removeSelectedLinkAttributes(_ range: NSRange) { - let mutAttrString = NSMutableAttributedString(attributedString: attributedText!) - selectedLinkAttributes.keys.forEach { mutAttrString.removeAttribute($0, range: range) } - self.attributedText = mutAttrString - setNeedsDisplay() - } - - // MARK: - Interaction - override func touchesBegan(_ touches: Set, with event: UIEvent?) { - if let touch = touches.first, onTouch(touch) { - return - } - super.touchesBegan(touches, with: event) - } - - override func touchesMoved(_ touches: Set, with event: UIEvent?) { - if let touch = touches.first, onTouch(touch) { - return - } - super.touchesMoved(touches, with: event) - } - - override func touchesEnded(_ touches: Set, with event: UIEvent?) { - if let touch = touches.first, onTouch(touch) { - return - } - super.touchesEnded(touches, with: event) - } - - override func touchesCancelled(_ touches: Set, with event: UIEvent?) { - if let touch = touches.first, onTouch(touch) { - return - } - super.touchesCancelled(touches, with: event) - } - - func onTouch(_ touch: UITouch) -> Bool { - let location = touch.location(in: self) - let link = getLink(atPoint: location) - - switch touch.phase { - case .began, .moved: - selectedLinkRange = link?.range - case .cancelled, .ended: - selectedLinkRange = nil - default: - break - } - - return link != nil - } - - @objc func labelTapped(_ recognizer: UITapGestureRecognizer) { - let location = recognizer.location(in: self) - guard let link = getLink(atPoint: location) else { - return - } - - linkTapped(link) - } - - @objc func labelLongPressed(_ recognizer: UILongPressGestureRecognizer) { - let location = recognizer.location(in: self) - guard let link = getLink(atPoint: location) else { - return - } - - linkLongPressed(link) - } - - func linkTapped(_ link: Link) { - } - - func linkLongPressed(_ link: Link) { - } - -} - -extension LinkLabel: UIGestureRecognizerDelegate { - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { - let location = touch.location(in: self) - let link = getLink(atPoint: location) - return link != nil - } -} diff --git a/Tusker/Views/LinkTextView.swift b/Tusker/Views/LinkTextView.swift new file mode 100644 index 00000000..c85cad2a --- /dev/null +++ b/Tusker/Views/LinkTextView.swift @@ -0,0 +1,23 @@ +// +// LinkTextView.swift +// Tusker +// +// Created by Shadowfacts on 1/18/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit + +class LinkTextView: UITextView { + + override func awakeFromNib() { + super.awakeFromNib() + + delaysContentTouches = false + isScrollEnabled = false + isEditable = false + isUserInteractionEnabled = true + isSelectable = true // is this necessary? + } + +} diff --git a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.xib b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.xib index 77f25d9e..b9c06cdb 100644 --- a/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.xib +++ b/Tusker/Views/Notifications/ActionNotificationGroupTableViewCell.xib @@ -1,8 +1,8 @@ - + - + diff --git a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift index be52ae50..d9c64ebc 100644 --- a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift +++ b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift @@ -23,7 +23,7 @@ class ProfileHeaderTableViewCell: UITableViewCell { @IBOutlet weak var displayNameLabel: UILabel! @IBOutlet weak var usernameLabel: UILabel! @IBOutlet weak var followsYouLabel: UILabel! - @IBOutlet weak var noteLabel: StatusContentLabel! + @IBOutlet weak var noteTextView: StatusContentTextView! @IBOutlet weak var fieldsStackView: UIStackView! @IBOutlet weak var fieldNamesStackView: UIStackView! @IBOutlet weak var fieldValuesStack: UIStackView! @@ -78,9 +78,9 @@ class ProfileHeaderTableViewCell: UITableViewCell { } } - noteLabel.navigationDelegate = delegate - noteLabel.setTextFromHtml(account.note) - noteLabel.setEmojis(account.emojis) + noteTextView.navigationDelegate = delegate + noteTextView.setTextFromHtml(account.note) + noteTextView.setEmojis(account.emojis) if accountID != MastodonController.account.id { // don't show relationship label for the user's own account @@ -103,16 +103,18 @@ class ProfileHeaderTableViewCell: UITableViewCell { nameLabel.text = field.name nameLabel.font = .boldSystemFont(ofSize: 17) nameLabel.textAlignment = .right + nameLabel.numberOfLines = 0 fieldNamesStackView.addArrangedSubview(nameLabel) - let valueLabel = ContentLabel() - valueLabel.setTextFromHtml(field.value) - valueLabel.setEmojis(account.emojis) - valueLabel.font = .systemFont(ofSize: 17) - valueLabel.textAlignment = .left - valueLabel.awakeFromNib() // TODO: this shouldn't be necessary - valueLabel.navigationDelegate = delegate - fieldValuesStack.addArrangedSubview(valueLabel) + let valueTextView = ContentTextView() + valueTextView.isSelectable = false + valueTextView.font = .systemFont(ofSize: 17) + valueTextView.setTextFromHtml(field.value) + valueTextView.setEmojis(account.emojis) + valueTextView.textAlignment = .left + valueTextView.awakeFromNib() + valueTextView.navigationDelegate = delegate + fieldValuesStack.addArrangedSubview(valueTextView) } } else { fieldsStackView.isHidden = true @@ -150,27 +152,27 @@ class ProfileHeaderTableViewCell: UITableViewCell { } -extension ProfileHeaderTableViewCell: MenuPreviewProvider { - var navigationDelegate: TuskerNavigationDelegate? { return delegate } - func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { - let noteLabelPoint = noteLabel.convert(location, from: self) - if noteLabel.bounds.contains(noteLabelPoint), - let link = noteLabel.getLink(atPoint: noteLabelPoint) { - return ( - content: { self.noteLabel.getViewController(forLink: link.url, inRange: link.range) }, - actions: { - let text = (self.noteLabel.text! as NSString).substring(with: link.range) - if let mention = self.noteLabel.getMention(for: link.url, text: text) { - return self.actionsForProfile(accountID: mention.id, sourceView: self) - } else if let hashtag = self.noteLabel.getHashtag(for: link.url, text: text) { - return self.actionsForHashtag(hashtag, sourceView: self) - } else { - return self.actionsForURL(link.url, sourceView: self) - } - } - ) - } else { - return nil - } - } -} +//extension ProfileHeaderTableViewCell: MenuPreviewProvider { +// var navigationDelegate: TuskerNavigationDelegate? { return delegate } +// func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { +// let noteLabelPoint = noteLabel.convert(location, from: self) +// if noteLabel.bounds.contains(noteLabelPoint), +// let link = noteLabel.getLink(atPoint: noteLabelPoint) { +// return ( +// content: { self.noteLabel.getViewController(forLink: link.url, inRange: link.range) }, +// actions: { +// let text = (self.noteLabel.text! as NSString).substring(with: link.range) +// if let mention = self.noteLabel.getMention(for: link.url, text: text) { +// return self.actionsForProfile(accountID: mention.id, sourceView: self) +// } else if let hashtag = self.noteLabel.getHashtag(for: link.url, text: text) { +// return self.actionsForHashtag(hashtag, sourceView: self) +// } else { +// return self.actionsForURL(link.url, sourceView: self) +// } +// } +// ) +// } else { +// return nil +// } +// } +//} diff --git a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.xib b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.xib index a34c2b7a..2c4e2202 100644 --- a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.xib +++ b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.xib @@ -1,8 +1,8 @@ - + - + @@ -66,12 +66,13 @@ - + + @@ -165,7 +166,7 @@ - + diff --git a/Tusker/Views/Status/BaseStatusTableViewCell.swift b/Tusker/Views/Status/BaseStatusTableViewCell.swift index 5e5bfe38..a9118c31 100644 --- a/Tusker/Views/Status/BaseStatusTableViewCell.swift +++ b/Tusker/Views/Status/BaseStatusTableViewCell.swift @@ -17,7 +17,7 @@ protocol StatusTableViewCellDelegate: TuskerNavigationDelegate { class BaseStatusTableViewCell: UITableViewCell { var delegate: StatusTableViewCellDelegate? { didSet { - contentLabel.navigationDelegate = delegate + contentTextView.navigationDelegate = delegate } } @@ -26,7 +26,7 @@ class BaseStatusTableViewCell: UITableViewCell { @IBOutlet weak var usernameLabel: UILabel! @IBOutlet weak var contentWarningLabel: UILabel! @IBOutlet weak var collapseButton: UIButton! - @IBOutlet weak var contentLabel: StatusContentLabel! + @IBOutlet weak var contentTextView: StatusContentTextView! @IBOutlet weak var attachmentsView: AttachmentsContainerView! @IBOutlet weak var replyButton: UIButton! @IBOutlet weak var favoriteButton: UIButton! @@ -88,7 +88,7 @@ class BaseStatusTableViewCell: UITableViewCell { collapseButton.layer.masksToBounds = true collapseButton.layer.cornerRadius = 5 - accessibilityElements = [displayNameLabel!, contentWarningLabel!, collapseButton!, contentLabel!, attachmentsView!] + accessibilityElements = [displayNameLabel!, contentWarningLabel!, collapseButton!, contentTextView!, attachmentsView!] attachmentsView.isAccessibilityElement = true NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) @@ -123,7 +123,7 @@ class BaseStatusTableViewCell: UITableViewCell { updateStatusState(status: status) - contentLabel.statusID = statusID + contentTextView.statusID = statusID contentWarningLabel.text = status.spoilerText contentWarningLabel.isHidden = status.spoilerText.isEmpty @@ -132,7 +132,7 @@ class BaseStatusTableViewCell: UITableViewCell { collapsible = !status.spoilerText.isEmpty var shouldCollapse = collapsible if !shouldCollapse, - let text = contentLabel.text, + let text = contentTextView.text, text.count > 500 { collapsible = true shouldCollapse = true @@ -203,7 +203,7 @@ class BaseStatusTableViewCell: UITableViewCell { func setCollapsed(_ collapsed: Bool, animated: Bool) { self.collapsed = collapsed - contentLabel.isHidden = collapsed + contentTextView.isHidden = collapsed attachmentsView.isHidden = attachmentsView.attachments.count == 0 || collapsed let buttonImage = UIImage(systemName: collapsed ? "chevron.down" : "chevron.up")! @@ -325,7 +325,7 @@ extension BaseStatusTableViewCell: MenuPreviewProvider { let description = attachmentView.attachment.description return (content: { self.delegate?.largeImage(image, description: description, sourceView: attachmentView) }, actions: { [] }) } - } else if contentLabel.frame.contains(location), + }/* else if contentLabel.frame.contains(location), let link = contentLabel.getLink(atPoint: contentLabel.convert(location, from: self)) { return ( content: { self.contentLabel.getViewController(forLink: link.url, inRange: link.range) }, @@ -340,7 +340,7 @@ extension BaseStatusTableViewCell: MenuPreviewProvider { } } ) - } + }*/ return self.getStatusCellPreviewProviders(for: location, sourceViewController: sourceViewController) } } diff --git a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift index 184fdb5a..6432741f 100644 --- a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift +++ b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift @@ -33,7 +33,9 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell { profileAccessibilityElement = UIAccessibilityElement(accessibilityContainer: self) profileAccessibilityElement.accessibilityFrameInContainerSpace = profileDetailContainerView.convert(profileDetailContainerView.frame, to: self) - accessibilityElements = [profileAccessibilityElement!, contentWarningLabel!, collapseButton!, contentLabel!, totalFavoritesButton!, totalReblogsButton!, timestampAndClientLabel!, replyButton!, favoriteButton!, reblogButton!, moreButton!] + accessibilityElements = [profileAccessibilityElement!, contentWarningLabel!, collapseButton!, contentTextView!, totalFavoritesButton!, totalReblogsButton!, timestampAndClientLabel!, replyButton!, favoriteButton!, reblogButton!, moreButton!] + + contentTextView.defaultFont = .systemFont(ofSize: 20) } override func updateUI(statusID: String, state: StatusState) { diff --git a/Tusker/Views/Status/ConversationMainStatusTableViewCell.xib b/Tusker/Views/Status/ConversationMainStatusTableViewCell.xib index 93112ca2..dc3c40ac 100644 --- a/Tusker/Views/Status/ConversationMainStatusTableViewCell.xib +++ b/Tusker/Views/Status/ConversationMainStatusTableViewCell.xib @@ -1,8 +1,8 @@ - + - + @@ -75,12 +75,13 @@ - + + - +