From 29e0128a556ea7e129242c5a603e0132fb5804a9 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 6 Jan 2020 20:27:58 -0500 Subject: [PATCH 01/14] Fix broken file paths --- Tusker.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 9dd15d8b..90072160 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -485,8 +485,8 @@ 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; name = ContentWarningCopyMode.swift; path = ../../../../../../../System/Volumes/Data/Users/shadowfacts/Dev/iOS/Tusker/Tusker/Preferences/ContentWarningCopyMode.swift; sourceTree = ""; }; - D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Preferences+Notification.swift"; path = "../../../../../../../System/Volumes/Data/Users/shadowfacts/Dev/iOS/Tusker/Tusker/Preferences/Preferences+Notification.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 = ""; }; D6E6F26221603F8B006A8599 /* CharacterCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounter.swift; sourceTree = ""; }; D6E6F26421604242006A8599 /* CharacterCounterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounterTests.swift; sourceTree = ""; }; From 1e066ac28edd36b5b19493eeffc0c59392a87ba2 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 6 Jan 2020 20:35:11 -0500 Subject: [PATCH 02/14] Add installation instructions to readme --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index b9f4114d..fad1b1c1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,15 @@ # Tusker Tusker is a WIP iOS app for Mastodon and Pleroma. + +## Installing for Development + +Xcode 11 is required, macOS Mojave or later should work (only macOS Catalina is regularly tested). + +1. Clone the project: `git clone https://git.shadowfacts.net/shadowfacts/Tusker.git` +2. Change directory into the project: `cd Tusker` +3. Clone the submodules: `git submodule init && git submodule update` +4. Open `Tusker.xcworkspace` in Xcode. +5. Change the code signing identity to your own. +6. Change the bundle identifier to something unique. +7. Select a target in the Tusker scheme and build & run. From ec2062ad424968e5060d4abe93e309b49daa6164 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 6 Jan 2020 22:14:17 -0500 Subject: [PATCH 03/14] Fix not being able to sign into Mastodon instances not in the recommended list --- .../Onboarding/InstanceSelectorTableViewController.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift b/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift index 9d48ca6f..95635f01 100644 --- a/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift +++ b/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift @@ -159,7 +159,9 @@ class InstanceSelectorTableViewController: UITableViewController { } switch item { case let .selected(instance): - delegate.didSelectInstance(url: URL(string: instance.uri)!) + // we can't just turn the URI string from the API into a URL instance, because Mastodon only includes the domain in the "URI" + let components = parseURLComponents(input: instance.uri) + delegate.didSelectInstance(url: components.url!) case let .recommended(instance): var components = URLComponents() components.scheme = "https" From db4312ee34fea7fa8f5492617cf12f56695ed692 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 7 Jan 2020 21:54:19 -0500 Subject: [PATCH 04/14] Fix refreshing multiple times with no new data not working When the requested range has no results, no pagination data is returned, so the existing `newer` request range is replaced with nil. As there was no new data, the existing request range is still correct and should not be replaced. Fixes #75 --- .../Notifications/NotificationsTableViewController.swift | 6 ++++-- Tusker/Screens/Profile/ProfileTableViewController.swift | 4 +++- Tusker/Screens/Timeline/TimelineTableViewController.swift | 7 ++++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Tusker/Screens/Notifications/NotificationsTableViewController.swift b/Tusker/Screens/Notifications/NotificationsTableViewController.swift index c68ac440..0bd72ca5 100644 --- a/Tusker/Screens/Notifications/NotificationsTableViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsTableViewController.swift @@ -208,8 +208,10 @@ class NotificationsTableViewController: EnhancedTableViewController { MastodonCache.addAll(statuses: newNotifications.compactMap { $0.status }) MastodonCache.addAll(accounts: newNotifications.map { $0.account }) - self.newer = pagination?.newer - + if let newer = pagination?.newer { + self.newer = newer + } + DispatchQueue.main.async { self.refreshControl?.endRefreshing() diff --git a/Tusker/Screens/Profile/ProfileTableViewController.swift b/Tusker/Screens/Profile/ProfileTableViewController.swift index ecf12bfd..f969d781 100644 --- a/Tusker/Screens/Profile/ProfileTableViewController.swift +++ b/Tusker/Screens/Profile/ProfileTableViewController.swift @@ -218,7 +218,9 @@ class ProfileTableViewController: EnhancedTableViewController { MastodonCache.addAll(statuses: newStatuses) self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0) - self.newer = pagination?.newer + if let newer = pagination?.newer { + self.newer = newer + } DispatchQueue.main.async { self.refreshControl?.endRefreshing() diff --git a/Tusker/Screens/Timeline/TimelineTableViewController.swift b/Tusker/Screens/Timeline/TimelineTableViewController.swift index c5096da3..f7321f38 100644 --- a/Tusker/Screens/Timeline/TimelineTableViewController.swift +++ b/Tusker/Screens/Timeline/TimelineTableViewController.swift @@ -127,9 +127,14 @@ class TimelineTableViewController: EnhancedTableViewController { let request = MastodonController.client.getStatuses(timeline: timeline, range: newer) MastodonController.client.run(request) { response in guard case let .success(newStatuses, pagination) = response else { fatalError() } - self.newer = pagination?.newer + MastodonCache.addAll(statuses: newStatuses) self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0) + + if let newer = pagination?.newer { + self.newer = newer + } + DispatchQueue.main.async { self.refreshControl?.endRefreshing() From bb86e1aafdbdf657e66276607935d66f22319408 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 7 Jan 2020 22:19:38 -0500 Subject: [PATCH 05/14] Allow rotaion in the attachment gallery Closes #77 --- Tusker/Info.plist | 2 ++ Tusker/Screens/Gallery/GalleryViewController.swift | 8 ++++++++ Tusker/Screens/Main/MainTabBarViewController.swift | 8 ++++++++ .../Onboarding/InstanceSelectorTableViewController.swift | 8 ++++++++ 4 files changed, 26 insertions(+) diff --git a/Tusker/Info.plist b/Tusker/Info.plist index 937a979e..7a90b6e8 100644 --- a/Tusker/Info.plist +++ b/Tusker/Info.plist @@ -69,6 +69,8 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft UISupportedInterfaceOrientations~ipad diff --git a/Tusker/Screens/Gallery/GalleryViewController.swift b/Tusker/Screens/Gallery/GalleryViewController.swift index d582d7dd..81d76482 100644 --- a/Tusker/Screens/Gallery/GalleryViewController.swift +++ b/Tusker/Screens/Gallery/GalleryViewController.swift @@ -35,6 +35,14 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc return viewControllers?.first } + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + if UIDevice.current.userInterfaceIdiom == .phone { + return .allButUpsideDown + } else { + return .all + } + } + init(attachments: [Attachment], sourcesInfo: [LargeImageViewController.SourceInfo?], startIndex: Int) { self.attachments = attachments self.sourcesInfo = sourcesInfo diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index 02590765..50ca2284 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -10,6 +10,14 @@ import UIKit class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + if UIDevice.current.userInterfaceIdiom == .phone { + return .portrait + } else { + return .all + } + } + override func viewDidLoad() { super.viewDidLoad() diff --git a/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift b/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift index 95635f01..4c55262a 100644 --- a/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift +++ b/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift @@ -29,6 +29,14 @@ class InstanceSelectorTableViewController: UITableViewController { var urlHandler: AnyCancellable? var currentQuery: String? + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + if UIDevice.current.userInterfaceIdiom == .phone { + return .portrait + } else { + return .all + } + } + init() { super.init(style: .grouped) From 53702a8324ddcdfc1d99586d766e2e3baabf87ff Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Fri, 17 Jan 2020 21:13:17 -0500 Subject: [PATCH 06/14] Add pinned status refreshing Closes #82 --- .../Profile/ProfileTableViewController.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Tusker/Screens/Profile/ProfileTableViewController.swift b/Tusker/Screens/Profile/ProfileTableViewController.swift index f969d781..2dc0100b 100644 --- a/Tusker/Screens/Profile/ProfileTableViewController.swift +++ b/Tusker/Screens/Profile/ProfileTableViewController.swift @@ -226,6 +226,24 @@ class ProfileTableViewController: EnhancedTableViewController { self.refreshControl?.endRefreshing() } } + + getStatuses(onlyPinned: true) { (response) in + guard case let .success(newPinnedStatuses, _) = response else { fatalError() } + MastodonCache.addAll(statuses: newPinnedStatuses) + + let oldPinnedStatuses = self.pinnedStatuses + var pinnedStatuses = [(id: String, state: StatusState)]() + for status in newPinnedStatuses { + let state: StatusState + if let (_, oldState) = oldPinnedStatuses.first(where: { $0.id == status.id }) { + state = oldState + } else { + state = .unknown + } + pinnedStatuses.append((status.id, state)) + } + self.pinnedStatuses = pinnedStatuses + } } @objc func composePressed(_ sender: Any) { From 8178a1f339fbd24e1437a7435115778326b0fbae Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Fri, 17 Jan 2020 21:29:53 -0500 Subject: [PATCH 07/14] Fix crash when tapping more actions buttons on iPad Fixes #78 --- .../Profile/ProfileTableViewController.swift | 3 +- Tusker/Screens/Utilities/Previewing.swift | 16 +++++----- Tusker/TuskerNavigationDelegate.swift | 30 +++++++++++-------- .../Account Cell/AccountTableViewCell.swift | 5 +++- Tusker/Views/ContentLabel.swift | 2 +- .../ProfileHeaderTableViewCell.swift | 10 +++---- .../Status/BaseStatusTableViewCell.swift | 13 ++++---- .../Status/TimelineStatusTableViewCell.swift | 4 +-- 8 files changed, 48 insertions(+), 35 deletions(-) diff --git a/Tusker/Screens/Profile/ProfileTableViewController.swift b/Tusker/Screens/Profile/ProfileTableViewController.swift index 2dc0100b..3386dcab 100644 --- a/Tusker/Screens/Profile/ProfileTableViewController.swift +++ b/Tusker/Screens/Profile/ProfileTableViewController.swift @@ -261,7 +261,7 @@ extension ProfileTableViewController: StatusTableViewCellDelegate { } extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate { - func showMoreOptions() { + func showMoreOptions(cell: ProfileHeaderTableViewCell) { let account = MastodonCache.account(for: accountID)! MastodonCache.relationship(for: account.id) { [weak self] (relationship) in @@ -276,6 +276,7 @@ extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate { DispatchQueue.main.async { let activityController = UIActivityViewController(activityItems: [account.url, account], applicationActivities: customActivities) activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(viewController: self, url: account.url) + activityController.popoverPresentationController?.sourceView = cell.moreButtonVisualEffectView self.present(activityController, animated: true) } } diff --git a/Tusker/Screens/Utilities/Previewing.swift b/Tusker/Screens/Utilities/Previewing.swift index 2c20e4e7..46a3793f 100644 --- a/Tusker/Screens/Utilities/Previewing.swift +++ b/Tusker/Screens/Utilities/Previewing.swift @@ -22,7 +22,7 @@ protocol MenuPreviewProvider { extension MenuPreviewProvider { - func actionsForProfile(accountID: String) -> [UIAction] { + func actionsForProfile(accountID: String, sourceView: UIView?) -> [UIAction] { guard let account = MastodonCache.account(for: accountID) else { return [] } return [ createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { (_) in @@ -32,27 +32,27 @@ extension MenuPreviewProvider { self.navigationDelegate?.compose(mentioning: account.acct) }), createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { (_) in - self.navigationDelegate?.showMoreOptions(forAccount: accountID) + self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView) }) ] } - func actionsForURL(_ url: URL) -> [UIAction] { + func actionsForURL(_ url: URL, sourceView: UIView?) -> [UIAction] { return [ createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { (_) in self.navigationDelegate?.selected(url: url) }), createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { (_) in - self.navigationDelegate?.showMoreOptions(forURL: url) + self.navigationDelegate?.showMoreOptions(forURL: url, sourceView: sourceView) }) ] } - func actionsForHashtag(_ hashtag: Hashtag) -> [UIAction] { - return actionsForURL(hashtag.url) + func actionsForHashtag(_ hashtag: Hashtag, sourceView: UIView?) -> [UIAction] { + return actionsForURL(hashtag.url, sourceView: sourceView) } - func actionsForStatus(statusID: String) -> [UIAction] { + func actionsForStatus(statusID: String, sourceView: UIView?) -> [UIAction] { guard let status = MastodonCache.status(for: statusID) else { return [] } return [ createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { (_) in @@ -62,7 +62,7 @@ extension MenuPreviewProvider { self.navigationDelegate?.selected(url: status.url!) }), createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { (_) in - self.navigationDelegate?.showMoreOptions(forStatus: statusID) + self.navigationDelegate?.showMoreOptions(forStatus: statusID, sourceView: sourceView) }) ] } diff --git a/Tusker/TuskerNavigationDelegate.swift b/Tusker/TuskerNavigationDelegate.swift index a62072b2..9f342077 100644 --- a/Tusker/TuskerNavigationDelegate.swift +++ b/Tusker/TuskerNavigationDelegate.swift @@ -44,11 +44,11 @@ protocol TuskerNavigationDelegate { func showGallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) - func showMoreOptions(forStatus statusID: String) + func showMoreOptions(forStatus statusID: String, sourceView: UIView?) - func showMoreOptions(forAccount accountID: String) + func showMoreOptions(forAccount accountID: String, sourceView: UIView?) - func showMoreOptions(forURL url: URL) + func showMoreOptions(forURL url: URL, sourceView: UIView?) func showFollowedByList(accountIDs: [String]) @@ -182,7 +182,7 @@ extension TuskerNavigationDelegate where Self: UIViewController { present(gallery(attachments: attachments, sourceViews: sourceViews, startIndex: startIndex), animated: true) } - private func moreOptions(forURL url: URL) -> UIViewController { + private func moreOptions(forURL url: URL) -> UIActivityViewController { let customActivites: [UIActivity] = [ OpenInSafariActivity() ] @@ -191,7 +191,7 @@ extension TuskerNavigationDelegate where Self: UIViewController { return activityController } - private func moreOptions(forStatus statusID: String) -> UIViewController { + private func moreOptions(forStatus statusID: String) -> UIActivityViewController { guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") } guard let url = status.url else { fatalError("Missing url for status \(statusID)") } var customActivites: [UIActivity] = [OpenInSafariActivity()] @@ -210,21 +210,27 @@ extension TuskerNavigationDelegate where Self: UIViewController { return activityController } - private func moreOptions(forAccount accountID: String) -> UIViewController { + private func moreOptions(forAccount accountID: String) -> UIActivityViewController { guard let account = MastodonCache.account(for: accountID) else { fatalError("Missing cached account \(accountID)") } return moreOptions(forURL: account.url) } - func showMoreOptions(forStatus statusID: String) { - present(moreOptions(forStatus: statusID), animated: true) + func showMoreOptions(forStatus statusID: String, sourceView: UIView?) { + let vc = moreOptions(forStatus: statusID) + vc.popoverPresentationController?.sourceView = sourceView + present(vc, animated: true) } - func showMoreOptions(forURL url: URL) { - present(moreOptions(forURL: url), animated: true) + func showMoreOptions(forURL url: URL, sourceView: UIView?) { + let vc = moreOptions(forURL: url) + vc.popoverPresentationController?.sourceView = sourceView + present(vc, animated: true) } - func showMoreOptions(forAccount accountID: String) { - present(moreOptions(forAccount: accountID), animated: true) + func showMoreOptions(forAccount accountID: String, sourceView: UIView?) { + let vc = moreOptions(forAccount: accountID) + vc.popoverPresentationController?.sourceView = sourceView + present(vc, animated: true) } func showFollowedByList(accountIDs: [String]) { diff --git a/Tusker/Views/Account Cell/AccountTableViewCell.swift b/Tusker/Views/Account Cell/AccountTableViewCell.swift index e933d335..9dd1bc02 100644 --- a/Tusker/Views/Account Cell/AccountTableViewCell.swift +++ b/Tusker/Views/Account Cell/AccountTableViewCell.swift @@ -68,6 +68,9 @@ extension AccountTableViewCell: MenuPreviewProvider { var navigationDelegate: TuskerNavigationDelegate? { return delegate } func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { - return (content: { ProfileTableViewController(accountID: self.accountID) }, actions: { self.actionsForProfile(accountID: self.accountID) }) + return ( + content: { ProfileTableViewController(accountID: self.accountID) }, + actions: { self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) } + ) } } diff --git a/Tusker/Views/ContentLabel.swift b/Tusker/Views/ContentLabel.swift index 1cdced23..0a965d06 100644 --- a/Tusker/Views/ContentLabel.swift +++ b/Tusker/Views/ContentLabel.swift @@ -209,7 +209,7 @@ class ContentLabel: LinkLabel { } override func linkLongPressed(_ link: LinkLabel.Link) { - navigationDelegate?.showMoreOptions(forURL: link.url) + navigationDelegate?.showMoreOptions(forURL: link.url, sourceView: self) } // MARK: - Navigation diff --git a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift index a3517792..be52ae50 100644 --- a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift +++ b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift @@ -10,7 +10,7 @@ import UIKit import Pachyderm protocol ProfileHeaderTableViewCellDelegate: TuskerNavigationDelegate { - func showMoreOptions() + func showMoreOptions(cell: ProfileHeaderTableViewCell) } class ProfileHeaderTableViewCell: UITableViewCell { @@ -137,7 +137,7 @@ class ProfileHeaderTableViewCell: UITableViewCell { } @objc func morePressed() { - delegate?.showMoreOptions() + delegate?.showMoreOptions(cell: self) } @objc func avatarPressed() { @@ -161,11 +161,11 @@ extension ProfileHeaderTableViewCell: MenuPreviewProvider { 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) + return self.actionsForProfile(accountID: mention.id, sourceView: self) } else if let hashtag = self.noteLabel.getHashtag(for: link.url, text: text) { - return self.actionsForHashtag(hashtag) + return self.actionsForHashtag(hashtag, sourceView: self) } else { - return self.actionsForURL(link.url) + return self.actionsForURL(link.url, sourceView: self) } } ) diff --git a/Tusker/Views/Status/BaseStatusTableViewCell.swift b/Tusker/Views/Status/BaseStatusTableViewCell.swift index f17fe0ac..5e5bfe38 100644 --- a/Tusker/Views/Status/BaseStatusTableViewCell.swift +++ b/Tusker/Views/Status/BaseStatusTableViewCell.swift @@ -289,7 +289,7 @@ class BaseStatusTableViewCell: UITableViewCell { } @IBAction func morePressed() { - delegate?.showMoreOptions(forStatus: statusID) + delegate?.showMoreOptions(forStatus: statusID, sourceView: moreButton) } @objc func accountPressed() { @@ -314,7 +314,10 @@ extension BaseStatusTableViewCell: MenuPreviewProvider { func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { if avatarImageView.frame.contains(location) { - return (content: { ProfileTableViewController(accountID: self.accountID)}, actions: { self.actionsForProfile(accountID: self.accountID) }) + return ( + content: { ProfileTableViewController(accountID: self.accountID)}, + actions: { self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) } + ) } else if attachmentsView.frame.contains(location) { let attachmentsViewLocation = attachmentsView.convert(location, from: self) if let attachmentView = attachmentsView.attachmentViews.allObjects.first(where: { $0.frame.contains(attachmentsViewLocation) }), @@ -329,11 +332,11 @@ extension BaseStatusTableViewCell: MenuPreviewProvider { actions: { let text = (self.contentLabel.text! as NSString).substring(with: link.range) if let mention = self.contentLabel.getMention(for: link.url, text: text) { - return self.actionsForProfile(accountID: mention.id) + return self.actionsForProfile(accountID: mention.id, sourceView: self) } else if let hashtag = self.contentLabel.getHashtag(for: link.url, text: text) { - return self.actionsForHashtag(hashtag) + return self.actionsForHashtag(hashtag, sourceView: self) } else { - return self.actionsForURL(link.url) + return self.actionsForURL(link.url, sourceView: self) } } ) diff --git a/Tusker/Views/Status/TimelineStatusTableViewCell.swift b/Tusker/Views/Status/TimelineStatusTableViewCell.swift index 15be357b..0ce1aab8 100644 --- a/Tusker/Views/Status/TimelineStatusTableViewCell.swift +++ b/Tusker/Views/Status/TimelineStatusTableViewCell.swift @@ -127,7 +127,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell { override func getStatusCellPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> BaseStatusTableViewCell.PreviewProviders? { return ( content: { ConversationTableViewController(for: self.statusID, state: self.statusState.copy()) }, - actions: { self.actionsForStatus(statusID: self.statusID) } + actions: { self.actionsForStatus(statusID: self.statusID, sourceView: self) } ) } @@ -211,7 +211,7 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider { reply.backgroundColor = tintColor let more = UIContextualAction(style: .normal, title: "More") { (action, view, completion) in completion(true) - self.delegate?.showMoreOptions(forStatus: self.statusID) + self.delegate?.showMoreOptions(forStatus: self.statusID, sourceView: self) } more.image = UIImage(systemName: "ellipsis") more.backgroundColor = .gray From 23de1312909ff561906123ffe659dcad36a58724 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Fri, 17 Jan 2020 21:55:21 -0500 Subject: [PATCH 08/14] Add preference to require attachment descriptions before posting Closes #76 --- Tusker.xcodeproj/project.pbxproj | 2 + Tusker/Preferences/Preferences.swift | 4 ++ .../Compose/ComposeViewController.swift | 37 ++++++++++++++++--- Tusker/Screens/Compose/CompositionState.swift | 23 ++++++++++++ .../Preferences/BehaviorPrefsView.swift | 3 ++ .../Compose Media/ComposeMediaView.swift | 2 + 6 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 Tusker/Screens/Compose/CompositionState.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 90072160..8402fc82 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -73,6 +73,7 @@ D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */; }; 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 */; }; 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 */; }; @@ -344,6 +345,7 @@ D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelectorTableViewController.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; diff --git a/Tusker/Preferences/Preferences.swift b/Tusker/Preferences/Preferences.swift index a0e50b46..75fdaa97 100644 --- a/Tusker/Preferences/Preferences.swift +++ b/Tusker/Preferences/Preferences.swift @@ -45,6 +45,7 @@ class Preferences: Codable, ObservableObject { self.defaultPostVisibility = try container.decode(Status.Visibility.self, forKey: .defaultPostVisibility) self.automaticallySaveDrafts = try container.decode(Bool.self, forKey: .automaticallySaveDrafts) self.contentWarningCopyMode = try container.decode(ContentWarningCopyMode.self, forKey: .contentWarningCopyMode) + self.requireAttachmentDescriptions = try container.decode(Bool.self, forKey: .requireAttachmentDescriptions) self.blurAllMedia = try container.decode(Bool.self, forKey: .blurAllMedia) self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps) self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari) @@ -68,6 +69,7 @@ class Preferences: Codable, ObservableObject { try container.encode(defaultPostVisibility, forKey: .defaultPostVisibility) try container.encode(automaticallySaveDrafts, forKey: .automaticallySaveDrafts) try container.encode(contentWarningCopyMode, forKey: .contentWarningCopyMode) + try container.encode(requireAttachmentDescriptions, forKey: .requireAttachmentDescriptions) try container.encode(blurAllMedia, forKey: .blurAllMedia) try container.encode(openLinksInApps, forKey: .openLinksInApps) try container.encode(useInAppSafari, forKey: .useInAppSafari) @@ -90,6 +92,7 @@ class Preferences: Codable, ObservableObject { @Published var defaultPostVisibility = Status.Visibility.public @Published var automaticallySaveDrafts = true @Published var contentWarningCopyMode = ContentWarningCopyMode.asIs + @Published var requireAttachmentDescriptions = false @Published var blurAllMedia = false @Published var openLinksInApps = true @Published var useInAppSafari = true @@ -112,6 +115,7 @@ class Preferences: Codable, ObservableObject { case defaultPostVisibility case automaticallySaveDrafts case contentWarningCopyMode + case requireAttachmentDescriptions case blurAllMedia case openLinksInApps case useInAppSafari diff --git a/Tusker/Screens/Compose/ComposeViewController.swift b/Tusker/Screens/Compose/ComposeViewController.swift index 4caace12..165fa0bb 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -37,6 +37,12 @@ class ComposeViewController: UIViewController { weak var xcbSession: XCBSession? var postedStatus: Status? + var compositionState: CompositionState = .valid { + didSet { + postBarButtonItem.isEnabled = compositionState.isValid + } + } + weak var postBarButtonItem: UIBarButtonItem! var visibilityBarButtonItem: UIBarButtonItem! var contentWarningBarButtonItem: UIBarButtonItem! @@ -131,6 +137,7 @@ class ComposeViewController: UIViewController { // we have to set the font here, because the monospaced digit font is not available in IB charactersRemainingLabel.font = .monospacedDigitSystemFont(ofSize: 17, weight: .regular) updateCharactersRemaining() + updateAttachmentDescriptionsRequired() updatePlaceholder() NotificationCenter.default.addObserver(self, selector: #selector(contentWarningTextFieldDidChange), name: UITextField.textDidChangeNotification, object: contentWarningTextField) @@ -266,17 +273,29 @@ class ComposeViewController: UIViewController { scrollView.scrollIndicatorInsets = scrollView.contentInset } + func updateAttachmentDescriptionsRequired() { + if Preferences.shared.requireAttachmentDescriptions { + for case let mediaView as ComposeMediaView in attachmentsStackView.arrangedSubviews { + if mediaView.descriptionTextView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + compositionState.formUnion(.requiresAttachmentDescriptions) + return + } + } + + } + compositionState.subtract(.requiresAttachmentDescriptions) + } + func updateCharactersRemaining() { - // TODO: include CW char count let count = CharacterCounter.count(text: statusTextView.text) let cwCount = contentWarningEnabled ? (contentWarningTextField.text?.count ?? 0) : 0 let remaining = (MastodonController.instance.maxStatusCharacters ?? 500) - count - cwCount if remaining < 0 { charactersRemainingLabel.textColor = .red - postBarButtonItem.isEnabled = false + compositionState.formUnion(.tooManyCharacters) } else { charactersRemainingLabel.textColor = .darkGray - postBarButtonItem.isEnabled = true + compositionState.subtract(.tooManyCharacters) } charactersRemainingLabel.text = String(remaining) charactersRemainingLabel.accessibilityLabel = String(format: NSLocalizedString("%d characters remaining", comment: "compose characters remaining accessibility label"), remaining) @@ -454,7 +473,7 @@ class ComposeViewController: UIViewController { saveDraft() // disable post button while sending post request - postBarButtonItem.isEnabled = false + compositionState.formUnion(.currentlyPosting) let contentWarning: String? if contentWarningEnabled, let cwText = contentWarningTextField.text, !cwText.isEmpty { @@ -575,6 +594,7 @@ extension ComposeViewController: AssetPickerViewControllerDelegate { } func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachment]) { selectedAttachments.append(contentsOf: attachments) + updateAttachmentDescriptionsRequired() } } @@ -583,6 +603,11 @@ extension ComposeViewController: ComposeMediaViewDelegate { let index = attachmentsStackView.arrangedSubviews.firstIndex(of: mediaView)! selectedAttachments.remove(at: index) updateAddAttachmentButton() + updateAttachmentDescriptionsRequired() + } + + func descriptionTextViewDidChange(_ mediaView: ComposeMediaView) { + updateAttachmentDescriptionsRequired() } } @@ -620,7 +645,7 @@ extension ComposeViewController: DraftsTableViewControllerDelegate { updatePlaceholder() updateCharactersRemaining() - + selectedAttachments = draft.attachments.map { $0.attachment } updateAttachmentViews() @@ -631,6 +656,8 @@ extension ComposeViewController: DraftsTableViewControllerDelegate { // call the delegate method manually, since setting the text property doesn't call it mediaView.textViewDidChange(mediaView.descriptionTextView) } + + updateAttachmentDescriptionsRequired() } func draftSelectionCompleted() { diff --git a/Tusker/Screens/Compose/CompositionState.swift b/Tusker/Screens/Compose/CompositionState.swift new file mode 100644 index 00000000..950ff821 --- /dev/null +++ b/Tusker/Screens/Compose/CompositionState.swift @@ -0,0 +1,23 @@ +// +// CompositionState.swift +// Tusker +// +// Created by Shadowfacts on 1/17/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import Foundation + +struct CompositionState: OptionSet { + let rawValue: Int + + static let currentlyPosting = CompositionState(rawValue: 1 << 0) + static let tooManyCharacters = CompositionState(rawValue: 1 << 1) + static let requiresAttachmentDescriptions = CompositionState(rawValue: 1 << 2) + + static let valid: CompositionState = [] + + var isValid: Bool { + isEmpty + } +} diff --git a/Tusker/Screens/Preferences/BehaviorPrefsView.swift b/Tusker/Screens/Preferences/BehaviorPrefsView.swift index f7074cd8..bb07a322 100644 --- a/Tusker/Screens/Preferences/BehaviorPrefsView.swift +++ b/Tusker/Screens/Preferences/BehaviorPrefsView.swift @@ -40,6 +40,9 @@ struct BehaviorPrefsView: View { Text("Prepend 're: '").tag(ContentWarningCopyMode.prependRe) Text("Don't copy").tag(ContentWarningCopyMode.doNotCopy) } + Toggle(isOn: $preferences.requireAttachmentDescriptions) { + Text("Require Attachment Descriptions") + } } } diff --git a/Tusker/Views/Compose Media/ComposeMediaView.swift b/Tusker/Views/Compose Media/ComposeMediaView.swift index d11a418b..54c1efd7 100644 --- a/Tusker/Views/Compose Media/ComposeMediaView.swift +++ b/Tusker/Views/Compose Media/ComposeMediaView.swift @@ -12,6 +12,7 @@ import AVFoundation protocol ComposeMediaViewDelegate { func didRemoveMedia(_ mediaView: ComposeMediaView) + func descriptionTextViewDidChange(_ mediaView: ComposeMediaView) } class ComposeMediaView: UIView { @@ -69,5 +70,6 @@ class ComposeMediaView: UIView { extension ComposeMediaView: UITextViewDelegate { func textViewDidChange(_ textView: UITextView) { placeholderLabel.isHidden = !descriptionTextView.text.isEmpty + delegate?.descriptionTextViewDidChange(self) } } From b5a41badccb5e610666dd43a5774b3f172ccffef Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 18 Jan 2020 16:00:38 -0500 Subject: [PATCH 09/14] Replace content labels with text views UITextView uses TextKit internally, unlike UILabel, so no additional code is needed to keep the TextKit and view representations of the text in sync since they are one and the same. This means that detecting which character was tapped in a content text view is much more accurate, which means link handling is substantially imrpoved. Fixes #20 --- Tusker.xcodeproj/project.pbxproj | 26 +- Tusker/TuskerNavigationDelegate.swift | 6 +- .../ComposeStatusReplyView.swift | 4 +- .../ComposeStatusReplyView.xib | 25 +- Tusker/Views/ContentLabel.swift | 229 -------------- Tusker/Views/ContentTextView.swift | 283 ++++++++++++++++++ .../Instance Cell/InstanceTableViewCell.swift | 6 +- .../Instance Cell/InstanceTableViewCell.xib | 21 +- Tusker/Views/LinkLabel.swift | 185 ------------ Tusker/Views/LinkTextView.swift | 23 ++ .../ActionNotificationGroupTableViewCell.xib | 4 +- .../ProfileHeaderTableViewCell.swift | 74 ++--- .../ProfileHeaderTableViewCell.xib | 17 +- .../Status/BaseStatusTableViewCell.swift | 16 +- .../ConversationMainStatusTableViewCell.swift | 4 +- .../ConversationMainStatusTableViewCell.xib | 18 +- .../Status/TimelineStatusTableViewCell.xib | 33 +- ...abel.swift => StatusContentTextView.swift} | 16 +- 18 files changed, 448 insertions(+), 542 deletions(-) delete mode 100644 Tusker/Views/ContentLabel.swift create mode 100644 Tusker/Views/ContentTextView.swift delete mode 100644 Tusker/Views/LinkLabel.swift create mode 100644 Tusker/Views/LinkTextView.swift rename Tusker/Views/{StatusContentLabel.swift => StatusContentTextView.swift} (81%) 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 @@ - + + - + - + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. diff --git a/Tusker/Views/Status/TimelineStatusTableViewCell.xib b/Tusker/Views/Status/TimelineStatusTableViewCell.xib index 41d4aea6..87833823 100644 --- a/Tusker/Views/Status/TimelineStatusTableViewCell.xib +++ b/Tusker/Views/Status/TimelineStatusTableViewCell.xib @@ -105,7 +105,7 @@ - + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. From e19364abdf1c17d6a2aa4065f79d1e262e98a5e5 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 18 Jan 2020 18:21:01 -0500 Subject: [PATCH 13/14] Fix content text view text color in dark mode --- Tusker/Views/ContentTextView.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Tusker/Views/ContentTextView.swift b/Tusker/Views/ContentTextView.swift index bae5d7d9..e77198c5 100644 --- a/Tusker/Views/ContentTextView.swift +++ b/Tusker/Views/ContentTextView.swift @@ -19,6 +19,7 @@ class ContentTextView: LinkTextView { var navigationDelegate: TuskerNavigationDelegate? var defaultFont: UIFont = .systemFont(ofSize: 17) + var defaultColor: UIColor = .label override func awakeFromNib() { super.awakeFromNib() @@ -117,9 +118,9 @@ class ContentTextView: LinkTextView { } else { text = node.text() } - return NSAttributedString(string: text, attributes: [.font: defaultFont]) + return NSAttributedString(string: text, attributes: [.font: defaultFont, .foregroundColor: defaultColor]) case let node as Element: - let attributed = NSMutableAttributedString(string: "", attributes: [.font: defaultFont]) + let attributed = NSMutableAttributedString(string: "", attributes: [.font: defaultFont, .foregroundColor: defaultColor]) for child in node.getChildNodes() { attributed.append(attributedTextForHTMLNode(child, usePreformattedText: usePreformattedText || node.tagName() == "pre")) } From 38085eee379fc23a8c59ddfb81af79d775620ef9 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 18 Jan 2020 18:38:00 -0500 Subject: [PATCH 14/14] Fix not being able to collapse/expand statuses Instead of simply returning the content text view from hitTest(_:with:), we need to call the super method so that the system still performs its own checks. --- Tusker/Views/ContentTextView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tusker/Views/ContentTextView.swift b/Tusker/Views/ContentTextView.swift index e77198c5..696c52f8 100644 --- a/Tusker/Views/ContentTextView.swift +++ b/Tusker/Views/ContentTextView.swift @@ -181,7 +181,7 @@ class ContentTextView: LinkTextView { // only accept touches that are over a link override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if getLinkAtPoint(point) != nil || isSelectable { - return self + return super.hitTest(point, with: event) } else { return nil }