Compare commits

..

4 Commits

Author SHA1 Message Date
Shadowfacts 9b3cc61dcb Update WebURL to version with IDNA support
Closes #163
2022-07-09 11:45:27 -04:00
Shadowfacts 0c37b99a68 i don't even remember 2022-07-09 11:26:37 -04:00
Shadowfacts f96d1d780c Enable data detectors on main status text view
Tapping detected items doesn't work because it conflicts with our tap
gesture recognizer, but long pressing does
2022-07-09 11:25:23 -04:00
Shadowfacts 5a5364ad3b Use iOS 16 API for disabling compose attachment list scrolling 2022-07-09 11:02:01 -04:00
14 changed files with 67 additions and 48 deletions

View File

@ -16,7 +16,7 @@ let package = Package(
], ],
dependencies: [ dependencies: [
// Dependencies declare other packages that this package depends on. // Dependencies declare other packages that this package depends on.
.package(url: "https://github.com/karwa/swift-url.git", from: "0.3.1"), .package(url: "https://github.com/karwa/swift-url.git", branch: "main"),
], ],
targets: [ targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets are the basic building blocks of a package. A target can define a module or a test suite.

View File

@ -12,13 +12,13 @@ import WebURLFoundationExtras
public class Hashtag: Codable { public class Hashtag: Codable {
public let name: String public let name: String
public let url: URL public let url: WebURL
/// Only present when returned from the trending hashtags endpoint /// Only present when returned from the trending hashtags endpoint
public let history: [History]? public let history: [History]?
public init(name: String, url: URL) { public init(name: String, url: URL) {
self.name = name self.name = name
self.url = url self.url = WebURL(url)!
self.history = nil self.history = nil
} }
@ -26,24 +26,14 @@ public class Hashtag: Codable {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name) self.name = try container.decode(String.self, forKey: .name)
// pixelfed (possibly others) don't fully escape special characters in the hashtag url // pixelfed (possibly others) don't fully escape special characters in the hashtag url
do { self.url = try container.decode(WebURL.self, forKey: .url)
let webURL = try container.decode(WebURL.self, forKey: .url)
if let url = URL(webURL) {
self.url = url
} else {
let s = try? container.decode(String.self, forKey: .url)
throw DecodingError.dataCorruptedError(forKey: .url, in: container, debugDescription: "unable to convert WebURL \(s?.debugDescription ?? "nil") to URL")
}
} catch {
self.url = try container.decode(URL.self, forKey: .url)
}
self.history = try container.decodeIfPresent([History].self, forKey: .history) self.history = try container.decodeIfPresent([History].self, forKey: .history)
} }
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name) try container.encode(name, forKey: .name)
try container.encode(url.absoluteString, forKey: .url) try container.encode(url, forKey: .url)
try container.encodeIfPresent(history, forKey: .history) try container.encodeIfPresent(history, forKey: .history)
} }

View File

@ -8,10 +8,9 @@
import Foundation import Foundation
import WebURL import WebURL
import WebURLFoundationExtras
public class Mention: Codable { public class Mention: Codable {
public let url: URL public let url: WebURL
public let username: String public let username: String
public let acct: String public let acct: String
/// The instance-local ID of the user being mentioned. /// The instance-local ID of the user being mentioned.
@ -22,17 +21,7 @@ public class Mention: Codable {
self.username = try container.decode(String.self, forKey: .username) self.username = try container.decode(String.self, forKey: .username)
self.acct = try container.decode(String.self, forKey: .acct) self.acct = try container.decode(String.self, forKey: .acct)
self.id = try container.decode(String.self, forKey: .id) self.id = try container.decode(String.self, forKey: .id)
do { self.url = try container.decode(WebURL.self, forKey: .url)
let webURL = try container.decode(WebURL.self, forKey: .url)
if let url = URL(webURL) {
self.url = url
} else {
let s = try? container.decode(String.self, forKey: .url)
throw DecodingError.dataCorruptedError(forKey: .url, in: container, debugDescription: "unable to convert WebURL \(s?.debugDescription ?? "nil") to URL")
}
} catch {
self.url = try container.decode(URL.self, forKey: .url)
}
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {

View File

@ -12,9 +12,14 @@ import WebURLFoundationExtras
class URLTests: XCTestCase { class URLTests: XCTestCase {
func testDecodeURL() { func testDecodeURL() {
print(WebURL(URL(string: "https://xn--baw-joa.social/@unituebingen")!)) XCTAssertNotNil(WebURL(URL(string: "https://xn--baw-joa.social/@unituebingen")!))
let url = WebURL("https://xn--baw-joa.social/@unituebingen") XCTAssertNotNil(WebURL("https://xn--baw-joa.social/@unituebingen"))
print(url) XCTAssertNotNil(URLComponents(string: "https://xn--baw-joa.social/test/é"))
XCTAssertNotNil(WebURL("https://xn--baw-joa.social/test/é"))
if #available(iOS 16.0, *) {
XCTAssertNotNil(try? URL.ParseStrategy().parse("https://xn--baw-joa.social/test/é"))
XCTAssertNotNil(try? URL.ParseStrategy().parse("http://見.香港/热狗/🌭"))
}
} }
} }

View File

@ -9,6 +9,7 @@
import Foundation import Foundation
import CoreData import CoreData
import Pachyderm import Pachyderm
import WebURLFoundationExtras
@objc(SavedHashtag) @objc(SavedHashtag)
public final class SavedHashtag: NSManagedObject { public final class SavedHashtag: NSManagedObject {
@ -32,6 +33,6 @@ extension SavedHashtag {
convenience init(hashtag: Hashtag, context: NSManagedObjectContext) { convenience init(hashtag: Hashtag, context: NSManagedObjectContext) {
self.init(context: context) self.init(context: context)
self.name = hashtag.name self.name = hashtag.name
self.url = hashtag.url self.url = URL(hashtag.url)!
} }
} }

View File

@ -70,7 +70,9 @@ struct ComposeAttachmentsList: View {
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2)) .listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
} }
.listStyle(PlainListStyle()) .listStyle(PlainListStyle())
// todo: scrollDisabled doesn't remove the need for manually calculating the frame height
.frame(height: totalListHeight) .frame(height: totalListHeight)
.scrollDisabledIfAvailable(totalHeight: totalListHeight)
.onAppear(perform: self.didAppear) .onAppear(perform: self.didAppear)
.onReceive(draft.$attachments, perform: self.attachmentsChanged) .onReceive(draft.$attachments, perform: self.attachmentsChanged)
} }
@ -212,6 +214,16 @@ fileprivate extension View {
self self
} }
} }
@available(iOS, obsoleted: 16.0)
@ViewBuilder
func scrollDisabledIfAvailable(totalHeight: CGFloat) -> some View {
if #available(iOS 16.0, *) {
self.scrollDisabled(true)
} else {
self.frame(height: totalHeight)
}
}
} }
@available(iOS 16.0, *) @available(iOS 16.0, *)

View File

@ -10,6 +10,7 @@ import UIKit
import Combine import Combine
import Pachyderm import Pachyderm
import CoreData import CoreData
import WebURLFoundationExtras
class ExploreViewController: UIViewController, UICollectionViewDelegate { class ExploreViewController: UIViewController, UICollectionViewDelegate {
@ -582,7 +583,10 @@ extension ExploreViewController: UICollectionViewDragDelegate {
activity.displaysAuxiliaryScene = true activity.displaysAuxiliaryScene = true
provider = NSItemProvider(object: activity) provider = NSItemProvider(object: activity)
case let .savedHashtag(hashtag): case let .savedHashtag(hashtag):
provider = NSItemProvider(object: hashtag.url as NSURL) guard let url = URL(hashtag.url) else {
return []
}
provider = NSItemProvider(object: url as NSURL)
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: accountID) { if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: accountID) {
activity.displaysAuxiliaryScene = true activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all) provider.registerObject(activity, visibility: .all)

View File

@ -8,6 +8,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
import WebURLFoundationExtras
class TrendingHashtagsViewController: UIViewController { class TrendingHashtagsViewController: UIViewController {
@ -105,10 +106,11 @@ extension TrendingHashtagsViewController: UICollectionViewDelegate {
extension TrendingHashtagsViewController: UICollectionViewDragDelegate { extension TrendingHashtagsViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard let item = dataSource.itemIdentifier(for: indexPath), guard let item = dataSource.itemIdentifier(for: indexPath),
case let .tag(hashtag) = item else { case let .tag(hashtag) = item,
let url = URL(hashtag.url) else {
return [] return []
} }
let provider = NSItemProvider(object: hashtag.url as NSURL) let provider = NSItemProvider(object: url as NSURL)
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) { if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
activity.displaysAuxiliaryScene = true activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all) provider.registerObject(activity, visibility: .all)

View File

@ -9,6 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
import SafariServices import SafariServices
import WebURLFoundationExtras
class SearchViewController: UIViewController { class SearchViewController: UIViewController {
@ -295,7 +296,10 @@ extension SearchViewController: UICollectionViewDragDelegate {
} }
switch item { switch item {
case let .tag(hashtag): case let .tag(hashtag):
let provider = NSItemProvider(object: hashtag.url as NSURL) guard let url = URL(hashtag.url) else {
return []
}
let provider = NSItemProvider(object: url as NSURL)
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) { if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
activity.displaysAuxiliaryScene = true activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all) provider.registerObject(activity, visibility: .all)

View File

@ -9,6 +9,7 @@
import UIKit import UIKit
import SafariServices import SafariServices
import Pachyderm import Pachyderm
import WebURLFoundationExtras
protocol MenuActionProvider: AnyObject { protocol MenuActionProvider: AnyObject {
var navigationDelegate: TuskerNavigationDelegate? { get } var navigationDelegate: TuskerNavigationDelegate? { get }
@ -116,7 +117,12 @@ extension MenuActionProvider {
actionsSection = [] actionsSection = []
} }
let shareSection = actionsForURL(hashtag.url, sourceView: sourceView) let shareSection: [UIMenuElement]
if let url = URL(hashtag.url) {
shareSection = actionsForURL(url, sourceView: sourceView)
} else {
shareSection = []
}
return [ return [
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection), UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),

View File

@ -270,8 +270,9 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
extension ContentTextView: UITextViewDelegate { extension ContentTextView: UITextViewDelegate {
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
// disable the text view's link interactions, we handle tapping links ourself with a gesture recognizer // generally disable the text view's link interactions, we handle tapping links ourself with a gesture recognizer
return false // the builtin data detectors use the x-apple-data-detectors scheme, and we allow the text view to handle those itself
return URL.scheme == "x-apple-data-detectors"
} }
} }

View File

@ -50,6 +50,10 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
] ]
contentTextView.defaultFont = .systemFont(ofSize: 18) contentTextView.defaultFont = .systemFont(ofSize: 18)
contentTextView.dataDetectorTypes = [.flightNumber, .address, .shipmentTrackingNumber, .phoneNumber]
if #available(iOS 16.0, *) {
contentTextView.dataDetectorTypes.formUnion([.money, .physicalValue])
}
profileDetailContainerView.addInteraction(UIContextMenuInteraction(delegate: self)) profileDetailContainerView.addInteraction(UIContextMenuInteraction(delegate: self))

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21179.7" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21169.4"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -108,7 +108,7 @@
<action selector="collapseButtonPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="JaH-xX-UOD"/> <action selector="collapseButtonPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="JaH-xX-UOD"/>
</connections> </connections>
</button> </button>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="waJ-f5-LKv" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target"> <textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="waJ-f5-LKv" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="83" width="277" height="82.5"/> <rect key="frame" x="0.0" y="83" width="277" height="82.5"/>
<string key="text">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.</string> <string key="text">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.</string>
<color key="textColor" systemColor="labelColor"/> <color key="textColor" systemColor="labelColor"/>
@ -272,14 +272,14 @@
</view> </view>
</objects> </objects>
<resources> <resources>
<image name="arrowshape.turn.up.left.fill" catalog="system" width="128" height="106"/> <image name="arrowshape.turn.up.left.fill" catalog="system" width="128" height="104"/>
<image name="chevron.down" catalog="system" width="128" height="72"/> <image name="chevron.down" catalog="system" width="128" height="70"/>
<image name="ellipsis" catalog="system" width="128" height="37"/> <image name="ellipsis" catalog="system" width="128" height="37"/>
<image name="pin.fill" catalog="system" width="119" height="128"/> <image name="pin.fill" catalog="system" width="116" height="128"/>
<image name="repeat" catalog="system" width="128" height="98"/> <image name="repeat" catalog="system" width="128" height="98"/>
<image name="star.fill" catalog="system" width="128" height="116"/> <image name="star.fill" catalog="system" width="128" height="116"/>
<systemColor name="labelColor"> <systemColor name="labelColor">
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor> </systemColor>
<systemColor name="secondaryLabelColor"> <systemColor name="secondaryLabelColor">
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>

View File

@ -8,6 +8,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
import WebURLFoundationExtras
class StatusContentTextView: ContentTextView { class StatusContentTextView: ContentTextView {
@ -27,7 +28,7 @@ class StatusContentTextView: ContentTextView {
let status = mastodonController.persistentContainer.status(for: statusID) { let status = mastodonController.persistentContainer.status(for: statusID) {
mention = status.mentions.first { (mention) in mention = status.mentions.first { (mention) in
// Mastodon and Pleroma include the @ in the <a> text, GNU Social does not // Mastodon and Pleroma include the @ in the <a> text, GNU Social does not
(text.dropFirst() == mention.username || text == mention.username) && url.host == mention.url.host! (text.dropFirst() == mention.username || text == mention.username) && url.host == mention.url.host!.serialized
} }
} else { } else {
mention = nil mention = nil
@ -41,7 +42,7 @@ class StatusContentTextView: ContentTextView {
let mastodonController = mastodonController, let mastodonController = mastodonController,
let status = mastodonController.persistentContainer.status(for: statusID) { let status = mastodonController.persistentContainer.status(for: statusID) {
hashtag = status.hashtags.first { (hashtag) in hashtag = status.hashtags.first { (hashtag) in
hashtag.url == url URL(hashtag.url) == url
} }
} else { } else {
hashtag = nil hashtag = nil